#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 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(audioFormat.sampleRate, audioFormat.channels); m_mainMixer->setVolume(0.5); m_worldPainter = make_shared(); m_guiContext = make_shared(m_mainMixer->mixer(), appController); m_input = make_shared(); m_voice = make_shared(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([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(); m_errorScreen = make_shared(); 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()) { m_heldKeyEvents.append(*keyDown); m_edgeKeyEvents.append(*keyDown); } else if (auto keyUp = event.ptr()) { eraseWhere(m_heldKeyEvents, [&](auto& keyEvent) { return keyEvent.key == keyUp->key; }); Maybe 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()) { 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 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(m_root->toStoragePath("player")); m_statistics = make_shared(m_root->toStoragePath("player"), appController()->statisticsService()); m_universeClient = make_shared(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>(); 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(MainInterfacePanes::Inventory)) inventory->clearChangedSlots(); if (resetInterface) { m_mainInterface->reviveScriptPanes(*heldScriptPanes); heldScriptPanes->clear(); } }; m_mainMixer->setUniverseClient(m_universeClient); m_titleScreen = make_shared(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()) { 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{{"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()) { 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(); 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()) m_currentRemoteJoin = *address; else m_currentRemoteJoin.reset(); } else { if (!m_universeServer) { try { m_universeServer = make_shared(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(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()) { 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(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> 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()) 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);