osb/source/client/StarClientApplication.cpp
Rohan 8f0c327ffa
Fix LeftStick movement StarClientApplication.cpp
It no longer has that drift. The drift is caused by the controller, and this simply turns up the threshold of where the game actually accepts the input as a movement.
2024-08-03 11:42:20 -07:00

1190 lines
48 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->reset();
}
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));
}
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");
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);