osb/source/client/StarClientApplication.cpp
2024-08-22 12:52:59 +10:00

1211 lines
49 KiB
C++

#include "StarClientApplication.hpp"
#include "StarConfiguration.hpp"
#include "StarJsonExtra.hpp"
#include "StarFile.hpp"
#include "StarEncode.hpp"
#include "StarLogging.hpp"
#include "StarJsonExtra.hpp"
#include "StarRoot.hpp"
#include "StarVersion.hpp"
#include "StarPlayer.hpp"
#include "StarPlayerStorage.hpp"
#include "StarPlayerLog.hpp"
#include "StarAssets.hpp"
#include "StarWorldTemplate.hpp"
#include "StarWorldClient.hpp"
#include "StarRootLoader.hpp"
#include "StarInput.hpp"
#include "StarVoice.hpp"
#include "StarCurve25519.hpp"
#include "StarInterpolation.hpp"
#include "StarInterfaceLuaBindings.hpp"
#include "StarInputLuaBindings.hpp"
#include "StarVoiceLuaBindings.hpp"
#include "StarClipboardLuaBindings.hpp"
#if defined STAR_SYSTEM_WINDOWS
#include <windows.h>
extern "C" __declspec(dllexport) DWORD NvOptimusEnablement = 1;
extern "C" __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1;
#endif // graphics driver is told by these exports to default to the dedicated GPU
namespace Star {
Json const AdditionalAssetsSettings = Json::parseJson(R"JSON(
{
"missingImage" : "/assetmissing.png",
"missingAudio" : "/assetmissing.wav"
}
)JSON");
Json const AdditionalDefaultConfiguration = Json::parseJson(R"JSON(
{
"configurationVersion" : {
"client" : 8
},
"allowAssetsMismatch" : false,
"vsync" : true,
"limitTextureAtlasSize" : false,
"useMultiTexturing" : true,
"audioChannelSeparation" : [-25, 25],
"sfxVol" : 100,
"instrumentVol" : 100,
"musicVol" : 70,
"hardwareCursor" : true,
"windowedResolution" : [1000, 600],
"fullscreenResolution" : [1920, 1080],
"fullscreen" : false,
"borderless" : false,
"maximized" : true,
"antiAliasing" : false,
"zoomLevel" : 3.0,
"cameraSpeedFactor" : 1.0,
"interfaceScale" : 0,
"speechBubbles" : true,
"title" : {
"multiPlayerAddress" : "",
"multiPlayerPort" : "",
"multiPlayerAccount" : ""
},
"bindings" : {
"PlayerUp" : [ { "type" : "key", "value" : "W", "mods" : [] } ],
"PlayerDown" : [ { "type" : "key", "value" : "S", "mods" : [] } ],
"PlayerLeft" : [ { "type" : "key", "value" : "A", "mods" : [] } ],
"PlayerRight" : [ { "type" : "key", "value" : "D", "mods" : [] } ],
"PlayerJump" : [ { "type" : "key", "value" : "Space", "mods" : [] } ],
"PlayerDropItem" : [ { "type" : "key", "value" : "Q", "mods" : [] } ],
"PlayerInteract" : [ { "type" : "key", "value" : "E", "mods" : [] } ],
"PlayerShifting" : [ { "type" : "key", "value" : "RShift", "mods" : [] }, { "type" : "key", "value" : "LShift", "mods" : [] } ],
"PlayerTechAction1" : [ { "type" : "key", "value" : "F", "mods" : [] } ],
"PlayerTechAction2" : [],
"PlayerTechAction3" : [],
"EmoteBlabbering" : [ { "type" : "key", "value" : "Right", "mods" : ["LCtrl", "LShift"] } ],
"EmoteShouting" : [ { "type" : "key", "value" : "Up", "mods" : ["LCtrl", "LAlt"] } ],
"EmoteHappy" : [ { "type" : "key", "value" : "Up", "mods" : [] } ],
"EmoteSad" : [ { "type" : "key", "value" : "Down", "mods" : [] } ],
"EmoteNeutral" : [ { "type" : "key", "value" : "Left", "mods" : [] } ],
"EmoteLaugh" : [ { "type" : "key", "value" : "Left", "mods" : [ "LCtrl" ] } ],
"EmoteAnnoyed" : [ { "type" : "key", "value" : "Right", "mods" : [] } ],
"EmoteOh" : [ { "type" : "key", "value" : "Right", "mods" : [ "LCtrl" ] } ],
"EmoteOooh" : [ { "type" : "key", "value" : "Down", "mods" : [ "LCtrl" ] } ],
"EmoteBlink" : [ { "type" : "key", "value" : "Up", "mods" : [ "LCtrl" ] } ],
"EmoteWink" : [ { "type" : "key", "value" : "Up", "mods" : ["LCtrl", "LShift"] } ],
"EmoteEat" : [ { "type" : "key", "value" : "Down", "mods" : ["LCtrl", "LShift"] } ],
"EmoteSleep" : [ { "type" : "key", "value" : "Left", "mods" : ["LCtrl", "LShift"] } ],
"ShowLabels" : [ { "type" : "key", "value" : "RAlt", "mods" : [] }, { "type" : "key", "value" : "LAlt", "mods" : [] } ],
"CameraShift" : [ { "type" : "key", "value" : "RCtrl", "mods" : [] }, { "type" : "key", "value" : "LCtrl", "mods" : [] } ],
"TitleBack" : [ { "type" : "key", "value" : "Esc", "mods" : [] } ],
"CinematicSkip" : [ { "type" : "key", "value" : "Esc", "mods" : [] } ],
"CinematicNext" : [ { "type" : "key", "value" : "Right", "mods" : [] }, { "type" : "key", "value" : "Return", "mods" : [] } ],
"GuiClose" : [ { "type" : "key", "value" : "Esc", "mods" : [] } ],
"GuiShifting" : [ { "type" : "key", "value" : "RShift", "mods" : [] }, { "type" : "key", "value" : "LShift", "mods" : [] } ],
"KeybindingCancel" : [ { "type" : "key", "value" : "Esc", "mods" : [] } ],
"KeybindingClear" : [ { "type" : "key", "value" : "Del", "mods" : [] }, { "type" : "key", "value" : "Backspace", "mods" : [] } ],
"ChatPageUp" : [ { "type" : "key", "value" : "PageUp", "mods" : [] } ],
"ChatPageDown" : [ { "type" : "key", "value" : "PageDown", "mods" : [] } ],
"ChatPreviousLine" : [ { "type" : "key", "value" : "Up", "mods" : [] } ],
"ChatNextLine" : [ { "type" : "key", "value" : "Down", "mods" : [] } ],
"ChatSendLine" : [ { "type" : "key", "value" : "Return", "mods" : [] } ],
"ChatBegin" : [ { "type" : "key", "value" : "Return", "mods" : [] } ],
"ChatBeginCommand" : [ { "type" : "key", "value" : "/", "mods" : [] } ],
"ChatStop" : [ { "type" : "key", "value" : "Esc", "mods" : [] } ],
"InterfaceHideHud" : [ { "type" : "key", "value" : "F1", "mods" : [] } ],
"InterfaceChangeBarGroup" : [ { "type" : "key", "value" : "X", "mods" : [] } ],
"InterfaceDeselectHands" : [ { "type" : "key", "value" : "Z", "mods" : [] } ],
"InterfaceBar1" : [ { "type" : "key", "value" : "1", "mods" : [] } ],
"InterfaceBar2" : [ { "type" : "key", "value" : "2", "mods" : [] } ],
"InterfaceBar3" : [ { "type" : "key", "value" : "3", "mods" : [] } ],
"InterfaceBar4" : [ { "type" : "key", "value" : "4", "mods" : [] } ],
"InterfaceBar5" : [ { "type" : "key", "value" : "5", "mods" : [] } ],
"InterfaceBar6" : [ { "type" : "key", "value" : "6", "mods" : [] } ],
"InterfaceBar7" : [],
"InterfaceBar8" : [],
"InterfaceBar9" : [],
"InterfaceBar10" : [],
"EssentialBar1" : [ { "type" : "key", "value" : "R", "mods" : [] } ],
"EssentialBar2" : [ { "type" : "key", "value" : "T", "mods" : [] } ],
"EssentialBar3" : [ { "type" : "key", "value" : "Y", "mods" : [] } ],
"EssentialBar4" : [ { "type" : "key", "value" : "N", "mods" : [] } ],
"InterfaceRepeatCommand" : [ { "type" : "key", "value" : "P", "mods" : [] } ],
"InterfaceToggleFullscreen" : [ { "type" : "key", "value" : "F11", "mods" : [] } ],
"InterfaceReload" : [],
"InterfaceEscapeMenu" : [ { "type" : "key", "value" : "Esc", "mods" : [] } ],
"InterfaceInventory" : [ { "type" : "key", "value" : "I", "mods" : [] } ],
"InterfaceCodex" : [ { "type" : "key", "value" : "L", "mods" : [] } ],
"InterfaceQuest" : [ { "type" : "key", "value" : "J", "mods" : [] } ],
"InterfaceCrafting" : [ { "type" : "key", "value" : "C", "mods" : [] } ]
}
}
)JSON");
void ClientApplication::startup(StringList const& cmdLineArgs) {
RootLoader rootLoader({AdditionalAssetsSettings, AdditionalDefaultConfiguration, String("starbound.log"), LogLevel::Info, false, String("starbound.config")});
m_root = rootLoader.initOrDie(cmdLineArgs).first;
Logger::info("Client Version {} ({}) Source ID: {} Protocol: {}", StarVersionString, StarArchitectureString, StarSourceIdentifierString, StarProtocolVersion);
}
void ClientApplication::shutdown() {
m_mainInterface.reset();
if (m_universeClient)
m_universeClient->disconnect();
if (m_universeServer) {
m_universeServer->stop();
m_universeServer->join();
m_universeServer.reset();
}
if (m_statistics) {
m_statistics->writeStatistics();
m_statistics.reset();
}
m_universeClient.reset();
m_statistics.reset();
}
void ClientApplication::applicationInit(ApplicationControllerPtr appController) {
Application::applicationInit(appController);
auto assets = m_root->assets();
m_minInterfaceScale = assets->json("/interface.config:minInterfaceScale").toInt();
m_maxInterfaceScale = assets->json("/interface.config:maxInterfaceScale").toInt();
m_crossoverRes = jsonToVec2F(assets->json("/interface.config:interfaceCrossoverRes"));
appController->setCursorVisible(true);
AudioFormat audioFormat = appController->enableAudio();
m_mainMixer = make_shared<MainMixer>(audioFormat.sampleRate, audioFormat.channels);
m_mainMixer->setVolume(0.5);
m_worldPainter = make_shared<WorldPainter>();
m_guiContext = make_shared<GuiContext>(m_mainMixer->mixer(), appController);
m_input = make_shared<Input>();
m_voice = make_shared<Voice>(appController);
auto configuration = m_root->configuration();
bool vsync = configuration->get("vsync").toBool();
Vec2U windowedSize = jsonToVec2U(configuration->get("windowedResolution"));
Vec2U fullscreenSize = jsonToVec2U(configuration->get("fullscreenResolution"));
bool fullscreen = configuration->get("fullscreen").toBool();
bool borderless = configuration->get("borderless").toBool();
bool maximized = configuration->get("maximized").toBool();
float updateRate = 1.0f / GlobalTimestep;
if (auto jUpdateRate = configuration->get("updateRate")) {
updateRate = jUpdateRate.toFloat();
GlobalTimestep = 1.0f / updateRate;
}
if (auto jServerUpdateRate = configuration->get("serverUpdateRate"))
ServerGlobalTimestep = 1.0f / jServerUpdateRate.toFloat();
appController->setTargetUpdateRate(updateRate);
appController->setApplicationTitle(assets->json("/client.config:windowTitle").toString());
appController->setVSyncEnabled(vsync);
appController->setCursorHardware(configuration->get("hardwareCursor").optBool().value(true));
if (fullscreen)
appController->setFullscreenWindow(fullscreenSize);
else if (borderless)
appController->setBorderlessWindow();
else if (maximized)
appController->setMaximizedWindow();
else
appController->setNormalWindow(windowedSize);
appController->setMaxFrameSkip(assets->json("/client.config:maxFrameSkip").toUInt());
appController->setUpdateTrackWindow(assets->json("/client.config:updateTrackWindow").toFloat());
if (auto jVoice = configuration->get("voice"))
m_voice->loadJson(jVoice.toObject(), true);
m_voice->init();
m_voice->setLocalSpeaker(0);
}
void ClientApplication::renderInit(RendererPtr renderer) {
Application::renderInit(renderer);
renderReload();
m_root->registerReloadListener(m_reloadListener = make_shared<CallbackListener>([this]() { renderReload(); }));
if (m_root->configuration()->get("limitTextureAtlasSize").optBool().value(false))
renderer->setSizeLimitEnabled(true);
renderer->setMultiTexturingEnabled(m_root->configuration()->get("useMultiTexturing").optBool().value(true));
m_guiContext->renderInit(renderer);
m_cinematicOverlay = make_shared<Cinematic>();
m_errorScreen = make_shared<ErrorScreen>();
if (m_titleScreen)
m_titleScreen->renderInit(renderer);
if (m_worldPainter)
m_worldPainter->renderInit(renderer);
changeState(MainAppState::Mods);
}
void ClientApplication::windowChanged(WindowMode windowMode, Vec2U screenSize) {
auto config = m_root->configuration();
if (windowMode == WindowMode::Fullscreen) {
config->set("fullscreenResolution", jsonFromVec2U(screenSize));
config->set("fullscreen", true);
config->set("borderless", false);
} else if (windowMode == WindowMode::Borderless) {
config->set("borderless", true);
config->set("fullscreen", false);
} else if (windowMode == WindowMode::Maximized) {
config->set("maximized", true);
config->set("fullscreen", false);
config->set("borderless", false);
} else {
config->set("maximized", false);
config->set("fullscreen", false);
config->set("borderless", false);
config->set("windowedResolution", jsonFromVec2U(screenSize));
}
}
void ClientApplication::processInput(InputEvent const& event) {
if (auto keyDown = event.ptr<KeyDownEvent>()) {
m_heldKeyEvents.append(*keyDown);
m_edgeKeyEvents.append(*keyDown);
} else if (auto keyUp = event.ptr<KeyUpEvent>()) {
eraseWhere(m_heldKeyEvents, [&](auto& keyEvent) {
return keyEvent.key == keyUp->key;
});
Maybe<KeyMod> modKey = KeyModNames.maybeLeft(KeyNames.getRight(keyUp->key));
if (modKey)
m_heldKeyEvents.transform([&](auto& keyEvent) {
return KeyDownEvent{keyEvent.key, keyEvent.mods & ~*modKey};
});
}
else if (auto cAxis = event.ptr<ControllerAxisEvent>()) {
if (cAxis->controllerAxis == ControllerAxis::LeftX)
m_controllerLeftStick[0] = cAxis->controllerAxisValue;
else if (cAxis->controllerAxis == ControllerAxis::LeftY)
m_controllerLeftStick[1] = cAxis->controllerAxisValue;
else if (cAxis->controllerAxis == ControllerAxis::RightX)
m_controllerRightStick[0] = cAxis->controllerAxisValue;
else if (cAxis->controllerAxis == ControllerAxis::RightY)
m_controllerRightStick[1] = cAxis->controllerAxisValue;
}
bool processed = !m_errorScreen->accepted() && m_errorScreen->handleInputEvent(event);
if (!processed) {
if (m_state == MainAppState::Splash) {
processed = m_cinematicOverlay->handleInputEvent(event);
} else if (m_state == MainAppState::Title) {
if (!(processed = m_cinematicOverlay->handleInputEvent(event)))
processed = m_titleScreen->handleInputEvent(event);
} else if (m_state == MainAppState::SinglePlayer || m_state == MainAppState::MultiPlayer) {
if (!(processed = m_cinematicOverlay->handleInputEvent(event)))
processed = m_mainInterface->handleInputEvent(event);
}
}
m_input->handleInput(event, processed);
}
void ClientApplication::update() {
float dt = GlobalTimestep * GlobalTimescale;
if (m_state >= MainAppState::Title) {
if (auto p2pNetworkingService = appController()->p2pNetworkingService()) {
if (auto join = p2pNetworkingService->pullPendingJoin()) {
m_pendingMultiPlayerConnection = PendingMultiPlayerConnection{join.takeValue(), {}, {}};
changeState(MainAppState::Title);
}
if (auto req = p2pNetworkingService->pullJoinRequest())
m_mainInterface->queueJoinRequest(*req);
p2pNetworkingService->update();
}
}
if (!m_errorScreen->accepted())
m_errorScreen->update(dt);
if (m_state == MainAppState::Mods)
updateMods(dt);
else if (m_state == MainAppState::ModsWarning)
updateModsWarning(dt);
if (m_state == MainAppState::Splash)
updateSplash(dt);
else if (m_state == MainAppState::Error)
updateError(dt);
else if (m_state == MainAppState::Title)
updateTitle(dt);
else if (m_state > MainAppState::Title)
updateRunning(dt);
// Swallow leftover encoded voice data if we aren't in-game to allow mic read to continue for settings.
if (m_state <= MainAppState::Title) {
DataStreamBuffer ext;
m_voice->send(ext);
} // TODO: directly disable encoding at menu so we don't have to do this
m_guiContext->cleanup();
m_edgeKeyEvents.clear();
m_input->update();
}
void ClientApplication::render() {
auto config = m_root->configuration();
auto assets = m_root->assets();
auto& renderer = Application::renderer();
renderer->setMultiSampling(config->get("antiAliasing").optBool().value(false) ? 4 : 0);
renderer->switchEffectConfig("interface");
if (auto interfaceScale = config->get("interfaceScale").optUInt().value())
m_guiContext->setInterfaceScale(interfaceScale);
else if (m_guiContext->windowWidth() >= m_crossoverRes[0] && m_guiContext->windowHeight() >= m_crossoverRes[1])
m_guiContext->setInterfaceScale(m_maxInterfaceScale);
else
m_guiContext->setInterfaceScale(m_minInterfaceScale);
if (m_state == MainAppState::Mods || m_state == MainAppState::Splash) {
m_cinematicOverlay->render();
} else if (m_state == MainAppState::Title) {
m_titleScreen->render();
m_cinematicOverlay->render();
} else if (m_state > MainAppState::Title) {
WorldClientPtr worldClient = m_universeClient->worldClient();
if (worldClient) {
auto totalStart = Time::monotonicMicroseconds();
renderer->switchEffectConfig("world");
auto clientStart = totalStart;
worldClient->render(m_renderData, TilePainter::BorderTileSize);
LogMap::set("client_render_world_client", strf(u8"{:05d}\u00b5s", Time::monotonicMicroseconds() - clientStart));
auto paintStart = Time::monotonicMicroseconds();
m_worldPainter->render(m_renderData, [&]() -> bool {
return worldClient->waitForLighting(&m_renderData);
});
LogMap::set("client_render_world_painter", strf(u8"{:05d}\u00b5s", Time::monotonicMicroseconds() - paintStart));
LogMap::set("client_render_world_total", strf(u8"{:05d}\u00b5s", Time::monotonicMicroseconds() - totalStart));
auto size = Vec2F(renderer->screenSize());
auto quad = renderFlatRect(RectF::withSize(size / -2, size), Vec4B::filled(0), 0.0f);
for (auto& layer : m_postProcessLayers) {
for (unsigned i = 0; i < layer.passes; i++) {
for (auto& effect : layer.effects) {
renderer->switchEffectConfig(effect);
renderer->render(quad);
}
}
}
}
renderer->switchEffectConfig("interface");
auto start = Time::monotonicMicroseconds();
m_mainInterface->renderInWorldElements();
m_mainInterface->render();
m_cinematicOverlay->render();
LogMap::set("client_render_interface", strf(u8"{:05d}\u00b5s", Time::monotonicMicroseconds() - start));
}
if (!m_errorScreen->accepted())
m_errorScreen->render(m_state == MainAppState::ModsWarning || m_state == MainAppState::Error);
}
void ClientApplication::getAudioData(int16_t* sampleData, size_t frameCount) {
if (m_mainMixer) {
m_mainMixer->read(sampleData, frameCount, [&](int16_t* buffer, size_t frames, unsigned channels) {
if (m_voice)
m_voice->mix(buffer, frames, channels);
});
}
}
void ClientApplication::renderReload() {
auto assets = m_root->assets();
auto renderer = Application::renderer();
auto loadEffectConfig = [&](String const& name) {
String path = strf("/rendering/effects/{}.config", name);
if (assets->assetExists(path)) {
StringMap<String> shaders;
auto config = assets->json(path);
auto shaderConfig = config.getObject("effectShaders");
for (auto& entry : shaderConfig) {
if (entry.second.isType(Json::Type::String)) {
String shader = entry.second.toString();
if (!shader.hasChar('\n')) {
auto shaderBytes = assets->bytes(AssetPath::relativeTo(path, shader));
shader = std::string(shaderBytes->ptr(), shaderBytes->size());
}
shaders[entry.first] = shader;
}
}
renderer->loadEffectConfig(name, config, shaders);
} else
Logger::warn("No rendering config found for renderer with id '{}'", renderer->rendererId());
};
renderer->loadConfig(assets->json("/rendering/opengl.config"));
loadEffectConfig("world");
m_postProcessLayers.clear();
auto postProcessLayers = assets->json("/client.config:postProcessLayers").toArray();
for (auto& layer : postProcessLayers) {
auto effects = jsonToStringList(layer.getArray("effects"));
for (auto& effect : effects)
loadEffectConfig(effect);
m_postProcessLayers.append(PostProcessLayer{ std::move(effects), (unsigned)layer.getUInt("passes", 1) });
}
loadEffectConfig("interface");
}
void ClientApplication::changeState(MainAppState newState) {
MainAppState oldState = m_state;
m_state = newState;
if (m_state == MainAppState::Quit)
appController()->quit();
if (newState == MainAppState::Mods)
m_cinematicOverlay->load(m_root->assets()->json("/cinematics/mods/modloading.cinematic"));
if (newState == MainAppState::Splash) {
m_cinematicOverlay->load(m_root->assets()->json("/cinematics/splash.cinematic"));
m_rootLoader = Thread::invoke("Async root loader", [this]() {
m_root->fullyLoad();
});
}
if (oldState > MainAppState::Title && m_state <= MainAppState::Title) {
if (m_universeClient)
m_universeClient->disconnect();
if (m_universeServer) {
m_universeServer->stop();
m_universeServer->join();
m_universeServer.reset();
}
m_cinematicOverlay->stop();
m_mainInterface.reset();
m_voice->clearSpeakers();
if (auto p2pNetworkingService = appController()->p2pNetworkingService()) {
p2pNetworkingService->setJoinUnavailable();
p2pNetworkingService->setAcceptingP2PConnections(false);
}
}
if (oldState > MainAppState::Title && m_state == MainAppState::Title) {
m_titleScreen->resetState();
m_mainMixer->setUniverseClient({});
}
if (oldState >= MainAppState::Title && m_state < MainAppState::Title) {
m_playerStorage.reset();
if (m_statistics) {
m_statistics->writeStatistics();
m_statistics.reset();
}
m_universeClient.reset();
m_mainMixer->setUniverseClient({});
m_titleScreen.reset();
}
if (oldState < MainAppState::Title && m_state >= MainAppState::Title) {
if (m_rootLoader)
m_rootLoader.finish();
m_cinematicOverlay->stop();
m_playerStorage = make_shared<PlayerStorage>(m_root->toStoragePath("player"));
m_statistics = make_shared<Statistics>(m_root->toStoragePath("player"), appController()->statisticsService());
m_universeClient = make_shared<UniverseClient>(m_playerStorage, m_statistics);
m_universeClient->setLuaCallbacks("input", LuaBindings::makeInputCallbacks());
m_universeClient->setLuaCallbacks("voice", LuaBindings::makeVoiceCallbacks());
if (!m_root->configuration()->get("safeScripts").toBool())
m_universeClient->setLuaCallbacks("clipboard", LuaBindings::makeClipboardCallbacks(appController()));
auto heldScriptPanes = make_shared<List<MainInterface::ScriptPaneInfo>>();
m_universeClient->playerReloadPreCallback() = [&, heldScriptPanes](bool resetInterface) {
if (!resetInterface)
return;
m_mainInterface->takeScriptPanes(*heldScriptPanes);
};
m_universeClient->playerReloadCallback() = [&, heldScriptPanes](bool resetInterface) {
auto paneManager = m_mainInterface->paneManager();
if (auto inventory = paneManager->registeredPane<InventoryPane>(MainInterfacePanes::Inventory))
inventory->clearChangedSlots();
if (resetInterface) {
m_mainInterface->reviveScriptPanes(*heldScriptPanes);
heldScriptPanes->clear();
}
};
m_mainMixer->setUniverseClient(m_universeClient);
m_titleScreen = make_shared<TitleScreen>(m_playerStorage, m_mainMixer->mixer());
if (auto renderer = Application::renderer())
m_titleScreen->renderInit(renderer);
}
if (m_state == MainAppState::Title) {
auto configuration = m_root->configuration();
if (m_pendingMultiPlayerConnection) {
if (auto address = m_pendingMultiPlayerConnection->server.ptr<HostAddressWithPort>()) {
m_titleScreen->setMultiPlayerAddress(toString(address->address()));
m_titleScreen->setMultiPlayerPort(toString(address->port()));
m_titleScreen->setMultiPlayerAccount(configuration->getPath("title.multiPlayerAccount").toString());
m_titleScreen->goToMultiPlayerSelectCharacter(false);
} else {
m_titleScreen->goToMultiPlayerSelectCharacter(true);
}
} else {
m_titleScreen->setMultiPlayerAddress(configuration->getPath("title.multiPlayerAddress").toString());
m_titleScreen->setMultiPlayerPort(configuration->getPath("title.multiPlayerPort").toString());
m_titleScreen->setMultiPlayerAccount(configuration->getPath("title.multiPlayerAccount").toString());
}
}
if (m_state > MainAppState::Title) {
if (m_titleScreen->currentlySelectedPlayer()) {
m_player = m_titleScreen->currentlySelectedPlayer();
} else {
if (auto uuid = m_playerStorage->playerUuidAt(0))
m_player = m_playerStorage->loadPlayer(*uuid);
if (!m_player) {
setError("Error loading player!");
return;
}
}
m_mainMixer->setUniverseClient(m_universeClient);
m_universeClient->setMainPlayer(m_player);
m_cinematicOverlay->setPlayer(m_player);
m_timeSinceJoin = (int64_t)Time::millisecondsSinceEpoch() / 1000;
auto assets = m_root->assets();
String loadingCinematic = assets->json("/client.config:loadingCinematic").toString();
m_cinematicOverlay->load(assets->json(loadingCinematic));
if (!m_player->log()->introComplete()) {
String introCinematic = assets->json("/client.config:introCinematic").toString();
introCinematic = introCinematic.replaceTags(StringMap<String>{{"species", m_player->species()}});
m_player->setPendingCinematic(Json(introCinematic));
} else {
m_player->setPendingCinematic(Json());
}
if (m_state == MainAppState::MultiPlayer) {
PacketSocketUPtr packetSocket;
auto multiPlayerConnection = m_pendingMultiPlayerConnection.take();
if (auto address = multiPlayerConnection.server.ptr<HostAddressWithPort>()) {
try {
packetSocket = TcpPacketSocket::open(TcpSocket::connectTo(*address));
} catch (StarException const& e) {
setError(strf("Join failed! Error connecting to '{}'", *address), e);
return;
}
} else {
auto p2pPeerId = multiPlayerConnection.server.ptr<P2PNetworkingPeerId>();
if (auto p2pNetworkingService = appController()->p2pNetworkingService()) {
auto result = p2pNetworkingService->connectToPeer(*p2pPeerId);
if (result.isLeft()) {
setError(strf("Cannot join peer: {}", result.left()));
return;
} else {
packetSocket = P2PPacketSocket::open(std::move(result.right()));
}
} else {
setError("Internal error, no p2p networking service when joining p2p networking peer");
return;
}
}
bool allowAssetsMismatch = m_root->configuration()->get("allowAssetsMismatch").toBool();
if (auto errorMessage = m_universeClient->connect(UniverseConnection(std::move(packetSocket)), allowAssetsMismatch,
multiPlayerConnection.account, multiPlayerConnection.password)) {
setError(*errorMessage);
return;
}
if (auto address = multiPlayerConnection.server.ptr<HostAddressWithPort>())
m_currentRemoteJoin = *address;
else
m_currentRemoteJoin.reset();
} else {
if (!m_universeServer) {
try {
m_universeServer = make_shared<UniverseServer>(m_root->toStoragePath("universe"));
m_universeServer->start();
} catch (StarException const& e) {
setError("Unable to start local server", e);
return;
}
}
if (auto errorMessage = m_universeClient->connect(m_universeServer->addLocalClient(), "", "")) {
setError(strf("Error connecting locally: {}", *errorMessage));
return;
}
}
m_titleScreen->stopMusic();
m_mainInterface = make_shared<MainInterface>(m_universeClient, m_worldPainter, m_cinematicOverlay);
m_universeClient->setLuaCallbacks("interface", LuaBindings::makeInterfaceCallbacks(m_mainInterface.get()));
m_universeClient->setLuaCallbacks("chat", LuaBindings::makeChatCallbacks(m_mainInterface.get(), m_universeClient.get()));
m_universeClient->startLua();
m_mainMixer->setWorldPainter(m_worldPainter);
if (auto renderer = Application::renderer()) {
m_worldPainter->renderInit(renderer);
}
}
}
void ClientApplication::setError(String const& error) {
Logger::error(error.utf8Ptr());
m_errorScreen->setMessage(error);
changeState(MainAppState::Title);
}
void ClientApplication::setError(String const& error, std::exception const& e) {
Logger::error("{}\n{}", error, outputException(e, true));
m_errorScreen->setMessage(strf("{}\n{}", error, outputException(e, false)));
changeState(MainAppState::Title);
}
void ClientApplication::updateMods(float dt) {
m_cinematicOverlay->update(dt);
auto ugcService = appController()->userGeneratedContentService();
if (ugcService && m_root->settings().includeUGC) {
Logger::info("Checking for user generated content...");
if (ugcService->triggerContentDownload()) {
StringList modDirectories;
for (auto& contentId : ugcService->subscribedContentIds()) {
if (auto contentDirectory = ugcService->contentDownloadDirectory(contentId)) {
Logger::info("Loading mods from user generated content with id '{}' from directory '{}'", contentId, *contentDirectory);
modDirectories.append(*contentDirectory);
} else {
Logger::warn("User generated content with id '{}' is not available", contentId);
}
}
if (modDirectories.empty()) {
Logger::info("No subscribed user generated content");
changeState(MainAppState::Splash);
} else {
Logger::info("Reloading to include all user generated content");
Root::singleton().reloadWithMods(modDirectories);
auto configuration = m_root->configuration();
auto assets = m_root->assets();
if (configuration->get("modsWarningShown").optBool().value()) {
changeState(MainAppState::Splash);
} else {
configuration->set("modsWarningShown", true);
m_errorScreen->setMessage(assets->json("/interface.config:modsWarningMessage").toString());
changeState(MainAppState::ModsWarning);
}
}
}
} else {
changeState(MainAppState::Splash);
}
}
void ClientApplication::updateModsWarning(float) {
if (m_errorScreen->accepted())
changeState(MainAppState::Splash);
}
void ClientApplication::updateSplash(float dt) {
m_cinematicOverlay->update(dt);
if (!m_rootLoader.isRunning() && (m_cinematicOverlay->completable() || m_cinematicOverlay->completed()))
changeState(MainAppState::Title);
}
void ClientApplication::updateError(float) {
if (m_errorScreen->accepted())
changeState(MainAppState::Title);
}
void ClientApplication::updateTitle(float dt) {
m_cinematicOverlay->update(dt);
m_titleScreen->update(dt);
m_mainMixer->update(dt);
m_mainMixer->setSpeed(GlobalTimescale);
appController()->setAcceptingTextInput(m_titleScreen->textInputActive());
auto p2pNetworkingService = appController()->p2pNetworkingService();
if (p2pNetworkingService) {
auto getStateString = [](TitleState state) -> const char* {
switch (state) {
case TitleState::Main:
return "In Main Menu";
case TitleState::Options:
return "In Options";
case TitleState::Mods:
return "In Mods";
case TitleState::SinglePlayerSelectCharacter:
return "Selecting a character for singleplayer";
case TitleState::SinglePlayerCreateCharacter:
return "Creating a character for singleplayer";
case TitleState::MultiPlayerSelectCharacter:
return "Selecting a character for multiplayer";
case TitleState::MultiPlayerCreateCharacter:
return "Creating a character for multiplayer";
case TitleState::MultiPlayerConnect:
return "Awaiting multiplayer connection info";
case TitleState::StartSinglePlayer:
return "Loading Singleplayer";
case TitleState::StartMultiPlayer:
return "Connecting to Multiplayer";
default:
return "";
}
};
p2pNetworkingService->setActivityData("Not In Game", getStateString(m_titleScreen->currentState()), 0, {});
}
if (m_titleScreen->currentState() == TitleState::StartSinglePlayer) {
changeState(MainAppState::SinglePlayer);
} else if (m_titleScreen->currentState() == TitleState::StartMultiPlayer) {
if (!m_pendingMultiPlayerConnection || m_pendingMultiPlayerConnection->server.is<HostAddressWithPort>()) {
auto addressString = m_titleScreen->multiPlayerAddress().trim();
auto portString = m_titleScreen->multiPlayerPort().trim();
portString = portString.empty() ? toString(m_root->configuration()->get("gameServerPort").toUInt()) : portString;
if (auto port = maybeLexicalCast<uint16_t>(portString)) {
auto address = HostAddressWithPort::lookup(addressString, *port);
if (address.isLeft()) {
setError(address.left());
} else {
m_pendingMultiPlayerConnection = PendingMultiPlayerConnection{
address.right(),
m_titleScreen->multiPlayerAccount(),
m_titleScreen->multiPlayerPassword()
};
auto configuration = m_root->configuration();
configuration->setPath("title.multiPlayerAddress", m_titleScreen->multiPlayerAddress());
configuration->setPath("title.multiPlayerPort", m_titleScreen->multiPlayerPort());
configuration->setPath("title.multiPlayerAccount", m_titleScreen->multiPlayerAccount());
changeState(MainAppState::MultiPlayer);
}
} else {
setError(strf("invalid port: {}", portString));
}
} else {
changeState(MainAppState::MultiPlayer);
}
} else if (m_titleScreen->currentState() == TitleState::Quit) {
changeState(MainAppState::Quit);
}
}
void ClientApplication::updateRunning(float dt) {
try {
auto worldClient = m_universeClient->worldClient();
auto p2pNetworkingService = appController()->p2pNetworkingService();
bool clientIPJoinable = m_root->configuration()->get("clientIPJoinable").toBool();
bool clientP2PJoinable = m_root->configuration()->get("clientP2PJoinable").toBool();
Maybe<pair<uint16_t, uint16_t>> party = make_pair(m_universeClient->players(), m_universeClient->maxPlayers());
if (m_state == MainAppState::MultiPlayer) {
if (p2pNetworkingService) {
p2pNetworkingService->setAcceptingP2PConnections(false);
if (clientP2PJoinable && m_currentRemoteJoin)
p2pNetworkingService->setJoinRemote(*m_currentRemoteJoin);
else
p2pNetworkingService->setJoinUnavailable();
}
} else {
m_universeServer->setListeningTcp(clientIPJoinable);
if (p2pNetworkingService) {
p2pNetworkingService->setAcceptingP2PConnections(clientP2PJoinable);
if (clientP2PJoinable) {
p2pNetworkingService->setJoinLocal(m_universeServer->maxClients());
} else {
p2pNetworkingService->setJoinUnavailable();
party = {};
}
}
}
if (p2pNetworkingService) {
auto getActivityDetail = [&](String const& tag) -> String {
if (tag == "playerName")
return Text::stripEscapeCodes(m_player->name());
if (tag == "playerHealth")
return toString(m_player->health());
if (tag == "playerMaxHealth")
return toString(m_player->maxHealth());
if (tag == "playerEnergy")
return toString(m_player->energy());
if (tag == "playerMaxEnergy")
return toString(m_player->maxEnergy());
if (tag == "playerBreath")
return toString(m_player->breath());
if (tag == "playerMaxBreath")
return toString(m_player->maxBreath());
if (tag == "playerXPos")
return toString(round(m_player->position().x()));
if (tag == "playerYPos")
return toString(round(m_player->position().y()));
if (tag == "worldName") {
if (m_universeClient->clientContext()->playerWorldId().is<ClientShipWorldId>())
return "Player Ship";
else if (WorldTemplate const* worldTemplate = worldClient ? worldClient->currentTemplate().get() : nullptr) {
auto worldName = worldTemplate->worldName();
if (worldName.empty())
return "In World";
else
return Text::stripEscapeCodes(worldName);
}
else
return "Nowhere";
}
return "";
};
String finalDetails = "";
Json activityDetails = m_root->configuration()->getPath("discord.activityDetails");
if (activityDetails.isType(Json::Type::Array)) {
StringList detailsList;
for (auto& detail : activityDetails.iterateArray())
detailsList.append(getActivityDetail(*detail.stringPtr()));
finalDetails = detailsList.join("\n");
} else if (activityDetails.isType(Json::Type::String))
finalDetails = activityDetails.toString().lookupTags(getActivityDetail);
p2pNetworkingService->setActivityData("In Game", finalDetails.utf8Ptr(), m_timeSinceJoin, party);
}
if (!m_mainInterface->inputFocus() && !m_cinematicOverlay->suppressInput()) {
m_player->setShifting(isActionTaken(InterfaceAction::PlayerShifting));
if (isActionTaken(InterfaceAction::PlayerRight))
m_player->moveRight();
if (isActionTaken(InterfaceAction::PlayerLeft))
m_player->moveLeft();
if (isActionTaken(InterfaceAction::PlayerUp))
m_player->moveUp();
if (isActionTaken(InterfaceAction::PlayerDown))
m_player->moveDown();
if (isActionTaken(InterfaceAction::PlayerJump))
m_player->jump();
if (isActionTaken(InterfaceAction::PlayerTechAction1))
m_player->special(1);
if (isActionTaken(InterfaceAction::PlayerTechAction2))
m_player->special(2);
if (isActionTaken(InterfaceAction::PlayerTechAction3))
m_player->special(3);
if (isActionTakenEdge(InterfaceAction::PlayerInteract))
m_player->beginTrigger();
else if (!isActionTaken(InterfaceAction::PlayerInteract))
m_player->endTrigger();
if (isActionTakenEdge(InterfaceAction::PlayerDropItem))
m_player->dropItem();
if (isActionTakenEdge(InterfaceAction::EmoteBlabbering))
m_player->addEmote(HumanoidEmote::Blabbering);
if (isActionTakenEdge(InterfaceAction::EmoteShouting))
m_player->addEmote(HumanoidEmote::Shouting);
if (isActionTakenEdge(InterfaceAction::EmoteHappy))
m_player->addEmote(HumanoidEmote::Happy);
if (isActionTakenEdge(InterfaceAction::EmoteSad))
m_player->addEmote(HumanoidEmote::Sad);
if (isActionTakenEdge(InterfaceAction::EmoteNeutral))
m_player->addEmote(HumanoidEmote::NEUTRAL);
if (isActionTakenEdge(InterfaceAction::EmoteLaugh))
m_player->addEmote(HumanoidEmote::Laugh);
if (isActionTakenEdge(InterfaceAction::EmoteAnnoyed))
m_player->addEmote(HumanoidEmote::Annoyed);
if (isActionTakenEdge(InterfaceAction::EmoteOh))
m_player->addEmote(HumanoidEmote::Oh);
if (isActionTakenEdge(InterfaceAction::EmoteOooh))
m_player->addEmote(HumanoidEmote::OOOH);
if (isActionTakenEdge(InterfaceAction::EmoteBlink))
m_player->addEmote(HumanoidEmote::Blink);
if (isActionTakenEdge(InterfaceAction::EmoteWink))
m_player->addEmote(HumanoidEmote::Wink);
if (isActionTakenEdge(InterfaceAction::EmoteEat))
m_player->addEmote(HumanoidEmote::Eat);
if (isActionTakenEdge(InterfaceAction::EmoteSleep))
m_player->addEmote(HumanoidEmote::Sleep);
if (int newZoomDirection = (int)m_input->bindHeld("opensb", "zoomIn") - (int)m_input->bindHeld("opensb", "zoomOut"))
m_cameraZoomDirection = newZoomDirection;
}
if (m_cameraZoomDirection != 0) {
const float threshold = 0.01f;
bool goingIn = m_cameraZoomDirection == 1;
auto config = m_root->configuration();
float curZoom = config->get("zoomLevel").toFloat(),
newZoom = max(1.f, curZoom * powf(1.f + (float)m_cameraZoomDirection * 0.5f, min(1.f, dt * 5.f))),
intZoom = max(1.f, (goingIn ? floor(curZoom) : ceil(curZoom)) + m_cameraZoomDirection);
bool pastInt = goingIn ? newZoom + threshold > intZoom
: newZoom - threshold < intZoom;
if (pastInt) {
float intNewZoom = goingIn ? ceil(newZoom) : floor(newZoom);
newZoom = lerp(clamp(abs(intZoom - newZoom) - 1.f, 0.f, 1.f), intZoom, intNewZoom);
m_cameraZoomDirection = 0;
}
config->set("zoomLevel", newZoom);
}
if (m_controllerLeftStick.magnitudeSquared() > 0.01f)
m_player->setMoveVector(m_controllerLeftStick);
else
m_player->setMoveVector(Vec2F());
m_voice->setInput(m_input->bindHeld("opensb", "pushToTalk"));
DataStreamBuffer voiceData;
voiceData.setByteOrder(ByteOrder::LittleEndian);
//voiceData.writeBytes(VoiceBroadcastPrefix.utf8Bytes()); transmitting with SE compat for now
bool needstoSendVoice = m_voice->send(voiceData, 5000);
auto checkDisconnection = [this]() {
if (!m_universeClient->isConnected()) {
m_cinematicOverlay->stop();
String errMessage;
if (auto disconnectReason = m_universeClient->disconnectReason())
errMessage = strf("You were disconnected from the server for the following reason:\n{}", *disconnectReason);
else
errMessage = "Client-server connection no longer valid!";
setError(errMessage);
changeState(MainAppState::Title);
return true;
}
return false;
};
if (checkDisconnection())
return;
m_mainInterface->preUpdate(dt);
m_universeClient->update(dt);
if (checkDisconnection())
return;
if (worldClient) {
m_worldPainter->update(dt);
auto& broadcastCallback = worldClient->broadcastCallback();
if (!broadcastCallback) {
broadcastCallback = [&](PlayerPtr player, StringView broadcast) -> bool {
auto& view = broadcast.utf8();
if (view.rfind(VoiceBroadcastPrefix.utf8(), 0) != NPos) {
auto entityId = player->entityId();
auto speaker = m_voice->speaker(connectionForEntity(entityId));
speaker->entityId = entityId;
speaker->name = player->name();
speaker->position = player->mouthPosition();
m_voice->receive(speaker, view.substr(VoiceBroadcastPrefix.utf8Size()));
}
return true;
};
}
if (worldClient->inWorld()) {
if (needstoSendVoice) {
auto signature = Curve25519::sign(voiceData.ptr(), voiceData.size());
std::string_view signatureView((char*)signature.data(), signature.size());
std::string_view audioDataView(voiceData.ptr(), voiceData.size());
auto broadcast = strf("data\0voice\0{}{}"s, signatureView, audioDataView);
worldClient->sendSecretBroadcast(broadcast, true, false); // Already compressed by Opus.
}
if (auto mainPlayer = m_universeClient->mainPlayer()) {
auto localSpeaker = m_voice->localSpeaker();
localSpeaker->position = mainPlayer->position();
localSpeaker->entityId = mainPlayer->entityId();
localSpeaker->name = mainPlayer->name();
}
m_voice->setLocalSpeaker(worldClient->connection());
}
worldClient->setInteractiveHighlightMode(isActionTaken(InterfaceAction::ShowLabels));
}
updateCamera(dt);
m_cinematicOverlay->update(dt);
m_mainInterface->update(dt);
m_mainMixer->update(dt, m_cinematicOverlay->muteSfx(), m_cinematicOverlay->muteMusic());
m_mainMixer->setSpeed(GlobalTimescale);
bool inputActive = m_mainInterface->textInputActive();
appController()->setAcceptingTextInput(inputActive);
m_input->setTextInputActive(inputActive);
for (auto const& interactAction : m_player->pullInteractActions())
m_mainInterface->handleInteractAction(interactAction);
if (m_universeServer) {
if (auto p2pNetworkingService = appController()->p2pNetworkingService()) {
for (auto& p2pClient : p2pNetworkingService->acceptP2PConnections())
m_universeServer->addClient(UniverseConnection(P2PPacketSocket::open(std::move(p2pClient))));
}
m_universeServer->setPause(m_mainInterface->escapeDialogOpen());
}
Vec2F aimPosition = m_player->aimPosition();
float fps = appController()->renderFps();
LogMap::set("client_render_rate", strf("{:4.2f} FPS ({:4.2f}ms)", fps, (1.0f / appController()->renderFps()) * 1000.0f));
LogMap::set("client_update_rate", strf("{:4.2f}Hz", appController()->updateRate()));
LogMap::set("player_pos", strf("[ ^#f45;{:4.2f}^reset;, ^#49f;{:4.2f}^reset; ]", m_player->position()[0], m_player->position()[1]));
LogMap::set("player_vel", strf("[ ^#f45;{:4.2f}^reset;, ^#49f;{:4.2f}^reset; ]", m_player->velocity()[0], m_player->velocity()[1]));
LogMap::set("player_aim", strf("[ ^#f45;{:4.2f}^reset;, ^#49f;{:4.2f}^reset; ]", aimPosition[0], aimPosition[1]));
if (auto world = m_universeClient->worldClient()) {
auto aim = Vec2I::floor(aimPosition);
LogMap::set("tile_liquid_level", toString(world->liquidLevel(aim).level));
LogMap::set("tile_dungeon_id", world->isTileProtected(aim) ? strf("^red;{}", world->dungeonId(aim)) : toString(world->dungeonId(aim)));
}
if (m_mainInterface->currentState() == MainInterface::ReturnToTitle)
changeState(MainAppState::Title);
} catch (std::exception& e) {
setError("Exception caught in client main-loop", e);
}
}
bool ClientApplication::isActionTaken(InterfaceAction action) const {
for (auto keyEvent : m_heldKeyEvents) {
if (m_guiContext->actions(keyEvent).contains(action))
return true;
}
return false;
}
bool ClientApplication::isActionTakenEdge(InterfaceAction action) const {
for (auto keyEvent : m_edgeKeyEvents) {
if (m_guiContext->actions(keyEvent).contains(action))
return true;
}
return false;
}
void ClientApplication::updateCamera(float dt) {
if (!m_universeClient->worldClient())
return;
WorldCamera& camera = m_worldPainter->camera();
camera.update(dt);
if (m_mainInterface->fixedCamera())
return;
auto assets = m_root->assets();
const float triggerRadius = 100.0f;
const float deadzone = 0.1f;
const float panFactor = 1.5f;
float cameraSpeedFactor = 30.0f / m_root->configuration()->get("cameraSpeedFactor").toFloat();
auto playerCameraPosition = m_player->cameraPosition();
if (isActionTaken(InterfaceAction::CameraShift)) {
m_snapBackCameraOffset = false;
m_cameraOffsetDownTicks++;
Vec2F aim = m_universeClient->worldClient()->geometry().diff(m_mainInterface->cursorWorldPosition(), playerCameraPosition);
float magnitude = aim.magnitude() / (triggerRadius / camera.pixelRatio());
if (magnitude > deadzone) {
float cameraXOffset = aim.x() / magnitude;
float cameraYOffset = aim.y() / magnitude;
magnitude = (magnitude - deadzone) / (1.0 - deadzone);
if (magnitude > 1)
magnitude = 1;
cameraXOffset *= magnitude * 0.5f * camera.pixelRatio() * panFactor;
cameraYOffset *= magnitude * 0.5f * camera.pixelRatio() * panFactor;
m_cameraXOffset = (m_cameraXOffset * (cameraSpeedFactor - 1.0) + cameraXOffset) / cameraSpeedFactor;
m_cameraYOffset = (m_cameraYOffset * (cameraSpeedFactor - 1.0) + cameraYOffset) / cameraSpeedFactor;
}
} else {
if ((m_cameraOffsetDownTicks > 0) && (m_cameraOffsetDownTicks < 20))
m_snapBackCameraOffset = true;
if (m_snapBackCameraOffset) {
m_cameraXOffset = (m_cameraXOffset * (cameraSpeedFactor - 1.0)) / cameraSpeedFactor;
m_cameraYOffset = (m_cameraYOffset * (cameraSpeedFactor - 1.0)) / cameraSpeedFactor;
}
m_cameraOffsetDownTicks = 0;
}
Vec2F newCameraPosition;
newCameraPosition.setX(playerCameraPosition.x());
newCameraPosition.setY(playerCameraPosition.y());
auto baseCamera = newCameraPosition;
const float cameraSmoothRadius = assets->json("/interface.config:cameraSmoothRadius").toFloat();
const float cameraSmoothFactor = assets->json("/interface.config:cameraSmoothFactor").toFloat();
auto cameraSmoothDistance = m_universeClient->worldClient()->geometry().diff(m_cameraPositionSmoother, newCameraPosition).magnitude();
if (cameraSmoothDistance > cameraSmoothRadius) {
auto cameraDelta = m_universeClient->worldClient()->geometry().diff(m_cameraPositionSmoother, newCameraPosition);
m_cameraPositionSmoother = newCameraPosition + cameraDelta.normalized() * cameraSmoothRadius;
m_cameraSmoothDelta = {};
}
auto cameraDelta = m_universeClient->worldClient()->geometry().diff(m_cameraPositionSmoother, newCameraPosition);
if (cameraDelta.magnitude() > assets->json("/interface.config:cameraSmoothDeadzone").toFloat())
newCameraPosition = newCameraPosition + cameraDelta * (cameraSmoothFactor - 1.0) / cameraSmoothFactor;
m_cameraPositionSmoother = newCameraPosition;
newCameraPosition.setX(newCameraPosition.x() + m_cameraXOffset / camera.pixelRatio());
newCameraPosition.setY(newCameraPosition.y() + m_cameraYOffset / camera.pixelRatio());
auto smoothDelta = newCameraPosition - baseCamera;
m_worldPainter->setCameraPosition(m_universeClient->worldClient()->geometry(), baseCamera + (smoothDelta + m_cameraSmoothDelta) * 0.5f);
m_cameraSmoothDelta = smoothDelta;
m_universeClient->worldClient()->setClientWindow(camera.worldTileRect());
}
}
STAR_MAIN_APPLICATION(Star::ClientApplication);