#include "StarMainInterface.hpp" #include "StarJsonExtra.hpp" #include "StarLogging.hpp" #include "StarLexicalCast.hpp" #include "StarContainerInterface.hpp" #include "StarCraftingInterface.hpp" #include "StarMerchantInterface.hpp" #include "StarRoot.hpp" #include "StarUniverseClient.hpp" #include "StarCodexInterface.hpp" #include "StarSongbookInterface.hpp" #include "StarQuestInterface.hpp" #include "StarQuestManager.hpp" #include "StarPopupInterface.hpp" #include "StarConfirmationDialog.hpp" #include "StarJoinRequestDialog.hpp" #include "StarImageMetadataDatabase.hpp" #include "StarGuiReader.hpp" #include "StarPaneManager.hpp" #include "StarClientCommandProcessor.hpp" #include "StarChat.hpp" #include "StarOptionsMenu.hpp" #include "StarActionBar.hpp" #include "StarWireInterface.hpp" #include "StarTeamBar.hpp" #include "StarStatusPane.hpp" #include "StarCanvasWidget.hpp" #include "StarLabelWidget.hpp" #include "StarItemSlotWidget.hpp" #include "StarPlayer.hpp" #include "StarPlayerLog.hpp" #include "StarMonster.hpp" #include "StarItemDrop.hpp" #include "StarAssets.hpp" #include "StarPlayerInventory.hpp" #include "StarCelestialDatabase.hpp" #include "StarItem.hpp" #include "StarAiInterface.hpp" #include "StarDrawable.hpp" #include "StarFireableItem.hpp" #include "StarClientContext.hpp" #include "StarToolUserEntity.hpp" #include "StarTeleportDialog.hpp" #include "StarCinematic.hpp" #include "StarNameplatePainter.hpp" #include "StarQuestIndicatorPainter.hpp" #include "StarScriptPane.hpp" #include "StarContainerEntity.hpp" #include "StarWarpTargetEntity.hpp" #include "StarPlayerUniverseMap.hpp" #include "StarWorldTemplate.hpp" #include "StarRadioMessagePopup.hpp" #include "StarAiTypes.hpp" #include "StarActiveItem.hpp" #include "StarInspectionTool.hpp" #include "StarQuestTracker.hpp" #include "StarContainerInteractor.hpp" #include "StarChatBubbleManager.hpp" #include "StarNpc.hpp" namespace Star { GuiMessage::GuiMessage() : message(), cooldown(), springState() {} GuiMessage::GuiMessage(String const& message, float cooldown, float spring) : message(message), cooldown(cooldown), springState(spring) {} MainInterface::MainInterface(UniverseClientPtr client, WorldPainterPtr painter, CinematicPtr cinematicOverlay) : m_guiContext(GuiContext::singletonPtr()) , m_config(MainInterfaceConfig::loadFromAssets()) , m_client(std::move(client)) , m_worldPainter(std::move(painter)) , m_cinematicOverlay(std::move(cinematicOverlay)) , m_containerInteractor(make_shared()) { GuiReader itemSlotReader; m_cursorItem = convert(itemSlotReader.makeSingle("cursorItemSlot", m_config->cursorItemSlot)); m_planetNameTimer = GameTimer(m_config->planetNameTime); m_debugSpatialClearTimer = GameTimer(m_config->debugSpatialClearTime); m_debugMapClearTimer = GameTimer(m_config->debugMapClearTime); m_stickyTargetingTimer = GameTimer(m_config->monsterHealthBarTime); m_inventoryWindow = make_shared(this, m_client->mainPlayer(), m_containerInteractor); m_paneManager.registerPane(MainInterfacePanes::Inventory, PaneLayer::Window, m_inventoryWindow, [this](PanePtr const&) { if (auto player = m_client->mainPlayer()) player->clearSwap(); if (m_containerPane) { m_containerPane->dismiss(); m_containerPane = {}; m_containerInteractor->closeContainer(); } for (EntityId id : m_interactionScriptPanes.keys()) { if (m_paneManager.isDisplayed(m_interactionScriptPanes[id]) && as(m_interactionScriptPanes[id])->openWithInventory()) m_interactionScriptPanes[id]->dismiss(); } }); m_overflowMessage = make_shared("", 0); m_plainCraftingWindow = make_shared(m_client->worldClient(), m_client->mainPlayer(), JsonObject{{"filter", JsonArray{"plain"}}}, m_client->mainPlayer()->entityId()); m_paneManager.registerPane(MainInterfacePanes::CraftingPlain, PaneLayer::Window, m_plainCraftingWindow); m_paneManager.registerPane(MainInterfacePanes::EscapeDialog, PaneLayer::ModalWindow, createEscapeDialog()); auto songbookInterface = make_shared(m_client->mainPlayer()); m_paneManager.registerPane(MainInterfacePanes::Songbook, PaneLayer::Window, songbookInterface); m_questLogInterface = make_shared(m_client->questManager(), m_client->mainPlayer(), m_cinematicOverlay, m_client); m_paneManager.registerPane(MainInterfacePanes::QuestLog, PaneLayer::Window, m_questLogInterface); auto aiInterface = make_shared(m_client, m_cinematicOverlay, &m_paneManager); m_paneManager.registerPane(MainInterfacePanes::Ai, PaneLayer::Window, aiInterface); m_codexInterface = make_shared(m_client->mainPlayer()); m_paneManager.registerPane(MainInterfacePanes::Codex, PaneLayer::Window, m_codexInterface); m_optionsMenu = make_shared(&m_paneManager); m_paneManager.registerPane(MainInterfacePanes::Options, PaneLayer::ModalWindow, m_optionsMenu); m_popupInterface = make_shared(); m_paneManager.registerPane(MainInterfacePanes::Popup, PaneLayer::Window, m_popupInterface); m_confirmationDialog = make_shared(); m_paneManager.registerPane(MainInterfacePanes::Confirmation, PaneLayer::ModalWindow, m_confirmationDialog); m_joinRequestDialog = make_shared(); m_paneManager.registerPane(MainInterfacePanes::JoinRequest, PaneLayer::ModalWindow, m_joinRequestDialog); m_actionBar = make_shared(&m_paneManager, m_client->mainPlayer()); m_paneManager.registerPane(MainInterfacePanes::ActionBar, PaneLayer::Hud, m_actionBar); m_questTracker = make_shared(); m_paneManager.registerPane(MainInterfacePanes::QuestTracker, PaneLayer::Hud, m_questTracker); m_mmUpgrade = make_shared(m_client, "/interface/scripted/mmupgrade/mmupgradegui.config"); m_paneManager.registerPane(MainInterfacePanes::MmUpgrade, PaneLayer::Window, m_mmUpgrade); m_collections = make_shared(m_client, "/interface/scripted/collections/collectionsgui.config"); m_paneManager.registerPane(MainInterfacePanes::Collections, PaneLayer::Window, m_collections); m_chat = make_shared(m_client); m_paneManager.registerPane(MainInterfacePanes::Chat, PaneLayer::Hud, m_chat); m_clientCommandProcessor = make_shared(m_client, m_cinematicOverlay, &m_paneManager, m_config->macroCommands); m_radioMessagePopup = make_shared(); m_paneManager.registerPane(MainInterfacePanes::RadioMessagePopup, PaneLayer::Hud, m_radioMessagePopup); m_wireInterface = make_shared(m_client->worldClient(), m_client->mainPlayer(), m_worldPainter); m_paneManager.registerPane(MainInterfacePanes::WireInterface, PaneLayer::World, m_wireInterface); m_client->mainPlayer()->setWireConnector(m_wireInterface.get()); auto teamBar = make_shared(this, m_client); m_paneManager.registerPane(MainInterfacePanes::TeamBar, PaneLayer::Hud, teamBar); auto statusPane = make_shared(&m_paneManager, m_client); m_paneManager.registerPane(MainInterfacePanes::StatusPane, PaneLayer::Hud, statusPane); auto planetName = make_shared(); m_planetText = make_shared(); m_planetText->setFontSize(m_config->planetNameFontSize); m_planetText->setFontMode(FontMode::Normal); m_planetText->setAnchor(HorizontalAnchor::HMidAnchor, VerticalAnchor::VMidAnchor); m_planetText->setDirectives(m_config->planetNameDirectives); planetName->disableScissoring(); planetName->setPosition(m_config->planetNameOffset); planetName->setAnchor(PaneAnchor::Center); planetName->addChild("planetText", m_planetText); m_paneManager.registerPane(MainInterfacePanes::PlanetText, PaneLayer::Hud, planetName); m_nameplatePainter = make_shared(); m_questIndicatorPainter = make_shared(m_client); m_chatBubbleManager = make_shared(); m_paneManager.displayRegisteredPane(MainInterfacePanes::ActionBar); m_paneManager.displayRegisteredPane(MainInterfacePanes::Chat); m_paneManager.displayRegisteredPane(MainInterfacePanes::TeamBar); m_paneManager.displayRegisteredPane(MainInterfacePanes::StatusPane); } MainInterface::~MainInterface() { m_paneManager.dismissAllPanes(); } MainInterface::RunningState MainInterface::currentState() const { return m_state; } MainInterfacePaneManager* MainInterface::paneManager() { return &m_paneManager; } bool MainInterface::escapeDialogOpen() const { return m_paneManager.registeredPaneIsDisplayed(MainInterfacePanes::EscapeDialog) || m_paneManager.registeredPaneIsDisplayed(MainInterfacePanes::Options); } void MainInterface::openCraftingWindow(Json const& config, EntityId sourceEntityId) { if (m_craftingWindow && m_paneManager.isDisplayed(m_craftingWindow)) { m_paneManager.dismissPane(m_craftingWindow); if (m_craftingWindow->sourceEntityId() == sourceEntityId) { m_craftingWindow.reset(); return; } } m_craftingWindow = make_shared(m_client->worldClient(), m_client->mainPlayer(), config, sourceEntityId); m_paneManager.displayPane(PaneLayer::Window, m_craftingWindow, [this](PanePtr const&) { if (auto player = m_client->mainPlayer()) player->clearSwap(); }); } void MainInterface::openMerchantWindow(Json const& config, EntityId sourceEntityId) { if (m_merchantWindow && m_paneManager.isDisplayed(m_merchantWindow)) { m_paneManager.dismissPane(m_merchantWindow); if (m_merchantWindow->sourceEntityId() == sourceEntityId) { m_merchantWindow.reset(); return; } } m_merchantWindow = make_shared(m_client->worldClient(), m_client->mainPlayer(), config, sourceEntityId); m_paneManager.displayPane(PaneLayer::Window, m_merchantWindow, [this](PanePtr const&) { if (auto player = m_client->mainPlayer()) player->clearSwap(); m_paneManager.dismissRegisteredPane(MainInterfacePanes::Inventory); }); m_paneManager.displayRegisteredPane(MainInterfacePanes::Inventory); m_paneManager.bringPaneAdjacent(m_paneManager.registeredPane(MainInterfacePanes::Inventory), m_merchantWindow, Root::singleton().assets()->json("/interface.config:bringAdjacentWindowGap").toFloat()); } void MainInterface::togglePlainCraftingWindow() { m_paneManager.toggleRegisteredPane(MainInterfacePanes::CraftingPlain); if (m_craftingWindow && m_craftingWindow->isDisplayed() && m_craftingWindow != m_paneManager.registeredPane(MainInterfacePanes::CraftingPlain)) m_paneManager.dismissPane(m_craftingWindow); m_craftingWindow = m_plainCraftingWindow; } bool MainInterface::windowsOpen() const { return (bool)m_paneManager.topPane({PaneLayer::Window}); } MerchantPanePtr MainInterface::activeMerchantPane() const { if (m_paneManager.isDisplayed(m_merchantWindow)) return m_merchantWindow; else return {}; } bool MainInterface::handleInputEvent(InputEvent const& event) { auto player = m_client->mainPlayer(); auto inv = player->inventory(); auto& root = Root::singleton(); if (auto mouseMove = event.ptr()) m_cursorScreenPos = mouseMove->mousePosition; if (m_paneManager.sendInputEvent(event)) { if (!event.is() && !event.is()) return true; } if (event.is()) { if (m_chat->hasFocus()) { if (m_guiContext->actions(event).contains(InterfaceAction::ChatSendLine)) { doChat(m_chat->currentChat(), true); m_chat->clearCurrentChat(); m_chat->stopChat(); return true; } } else if (!m_paneManager.keyboardCapturedPane()) { Maybe swapSlot; for (auto action : m_guiContext->actions(event)) { switch (action) { default: break; case InterfaceAction::GuiShifting: m_guiContext->setShiftHeld(true); break; case InterfaceAction::ChatBegin: m_chat->startChat(); break; case InterfaceAction::InterfaceHideHud: m_disableHud = !m_disableHud; break; case InterfaceAction::InterfaceRepeatCommand: if (!m_lastCommand.empty()) doChat(m_lastCommand, false); break; case InterfaceAction::InterfaceToggleFullscreen: m_optionsMenu->toggleFullscreen(); break; case InterfaceAction::InterfaceReload: root.reload(); root.fullyLoad(); break; case InterfaceAction::ChatBeginCommand: m_chat->startCommand(); break; case InterfaceAction::InterfaceEscapeMenu: m_paneManager.toggleRegisteredPane(MainInterfacePanes::EscapeDialog); break; case InterfaceAction::InterfaceInventory: m_paneManager.toggleRegisteredPane(MainInterfacePanes::Inventory); break; case InterfaceAction::InterfaceCodex: m_paneManager.toggleRegisteredPane(MainInterfacePanes::Codex); break; case InterfaceAction::InterfaceQuest: m_paneManager.toggleRegisteredPane(MainInterfacePanes::QuestLog); break; case InterfaceAction::InterfaceCrafting: togglePlainCraftingWindow(); break; } } } return false; } else if (auto keyUp = event.ptr()) { if (m_guiContext->actionsForKey(keyUp->key).contains(InterfaceAction::GuiShifting)) m_guiContext->setShiftHeld(false); return false; } else if (auto mouseDown = event.ptr()) { auto mouseButton = mouseDown->mouseButton; if (mouseButton >= MouseButton::Left && mouseButton <= MouseButton::Right && overlayClick(mouseDown->mousePosition, mouseDown->mouseButton)) { return true; } else { if (mouseButton == MouseButton::Left) player->beginPrimaryFire(); if (mouseButton == MouseButton::Right) player->beginAltFire(); if (mouseButton == MouseButton::Middle) player->beginTrigger(); } } else if (auto mouseUp = event.ptr()) { if (mouseUp->mouseButton == MouseButton::Left) player->endPrimaryFire(); if (mouseUp->mouseButton == MouseButton::Right) player->endAltFire(); if (mouseUp->mouseButton == MouseButton::Middle) player->endTrigger(); } bool captured = false; for (auto& pair : m_canvases) captured |= pair.second->sendEvent(event); return captured; } bool MainInterface::inputFocus() const { return (bool)m_paneManager.keyboardCapturedPane(); } bool MainInterface::textInputActive() const { return m_paneManager.keyboardCapturedForTextInput(); } void MainInterface::handleInteractAction(InteractAction interactAction) { auto assets = Root::singleton().assets(); auto world = m_client->worldClient(); if (interactAction.type == InteractActionType::OpenContainer) { // If we're currently displaying this container, close it. if (m_containerPane && m_containerInteractor->openContainerId() == interactAction.entityId) { m_paneManager.dismissPane(m_containerPane); return; } // If we're currently displaying another container, close it before we open. if (m_containerPane) m_paneManager.dismissPane(m_containerPane); auto containerEntity = world->get(interactAction.entityId); if (!containerEntity) return; m_containerInteractor->openContainer(containerEntity); m_paneManager.displayRegisteredPane(MainInterfacePanes::Inventory); m_containerPane = make_shared(world, m_client->mainPlayer(), m_containerInteractor); m_paneManager.displayPane(PaneLayer::Window, m_containerPane, [this](PanePtr const&) { if (auto player = m_client->mainPlayer()) player->clearSwap(); m_paneManager.dismissRegisteredPane(MainInterfacePanes::Inventory); }); m_paneManager.bringPaneAdjacent(m_paneManager.registeredPane(MainInterfacePanes::Inventory), m_containerPane, Root::singleton().assets()->json("/interface.config:bringAdjacentWindowGap").toFloat()); } else if (interactAction.type == InteractActionType::SitDown) { m_client->mainPlayer()->lounge(interactAction.entityId, interactAction.data.toUInt()); } else if (interactAction.type == InteractActionType::OpenCraftingInterface) { if (!world->entity(interactAction.entityId)) return; openCraftingWindow(interactAction.data, interactAction.entityId); } else if (interactAction.type == InteractActionType::OpenSongbookInterface) { m_paneManager.displayRegisteredPane(MainInterfacePanes::Songbook); } else if (interactAction.type == InteractActionType::OpenNpcCraftingInterface) { if (!world->entity(interactAction.entityId)) return; openCraftingWindow(interactAction.data, interactAction.entityId); } else if (interactAction.type == InteractActionType::OpenMerchantInterface) { if (!world->entity(interactAction.entityId)) return; openMerchantWindow(interactAction.data, interactAction.entityId); } else if (interactAction.type == InteractActionType::OpenAiInterface) { as(m_paneManager.registeredPane(MainInterfacePanes::Ai))->setSourceEntityId(interactAction.entityId); m_paneManager.displayRegisteredPane(MainInterfacePanes::Ai); } else if (interactAction.type == InteractActionType::OpenTeleportDialog) { if (m_teleportDialog) m_teleportDialog->dismiss(); if (!m_client->canTeleport()) return; auto currentLocation = TeleportBookmark(); auto config = assets->fetchJson(interactAction.data); if (config.getBool("canBookmark", false)) { if (auto entity = world->entity(interactAction.entityId)) { if (auto uniqueEntityId = entity->uniqueId()) { auto worldTemplate = m_client->worldClient()->currentTemplate(); String icon, planetName; if (m_client->playerWorld().is()) { icon = "ship"; planetName = "Player Ship"; } else if (m_client->playerWorld().is()) { icon = worldTemplate->worldParameters()->typeName; planetName = worldTemplate->worldName(); } else if (m_client->playerWorld().is()) { icon = worldTemplate->worldParameters()->typeName; planetName = worldTemplate->worldName(); } else { icon = "default"; planetName = "???"; } currentLocation = TeleportBookmark { {m_client->playerWorld(), SpawnTargetUniqueEntity(*uniqueEntityId)}, planetName, config.getString("bookmarkName", ""), icon }; if (!m_client->mainPlayer()->universeMap()->teleportBookmarks().contains(currentLocation) || !config.getBool("canTeleport", true)) { auto editBookmarkDialog = make_shared(m_client->mainPlayer()->universeMap()); editBookmarkDialog->setBookmark(currentLocation); m_paneManager.displayPane(PaneLayer::ModalWindow, editBookmarkDialog); return; } } } } if (config.getBool("canTeleport", true)) { m_teleportDialog = make_shared(m_client, &m_paneManager, interactAction.data, interactAction.entityId, currentLocation); m_paneManager.displayPane(PaneLayer::ModalWindow, m_teleportDialog); } } else if (interactAction.type == InteractActionType::ShowPopup) { m_paneManager.displayRegisteredPane(MainInterfacePanes::Popup); m_popupInterface->displayMessage(interactAction.data.getString("message"), interactAction.data.getString("title", ""), interactAction.data.getString("subtitle", ""), interactAction.data.optString("sound")); } else if (interactAction.type == InteractActionType::ScriptPane) { auto sourceEntity = interactAction.entityId; // dismiss if there's already a scriptpane open for this source entity if (sourceEntity != NullEntityId && m_interactionScriptPanes.contains(sourceEntity) && m_paneManager.isDisplayed(m_interactionScriptPanes[sourceEntity])) m_paneManager.dismissPane(m_interactionScriptPanes[sourceEntity]); ScriptPanePtr scriptPane = make_shared(m_client, interactAction.data, sourceEntity); displayScriptPane(scriptPane, sourceEntity); } else if (interactAction.type == InteractActionType::Message) { m_client->mainPlayer()->receiveMessage(connectionForEntity(interactAction.entityId), interactAction.data.getString("messageType"), interactAction.data.getArray("messageArgs")); } } void MainInterface::preUpdate(float dt) { auto player = m_client->mainPlayer(); if (!m_client->paused()) player->aim(cursorWorldPosition()); } void MainInterface::update(float dt) { m_paneManager.update(dt); m_cursor.update(dt); m_questLogInterface->pollDialog(&m_paneManager); if (!m_paneManager.topPane({PaneLayer::ModalWindow}) && m_codexInterface->showNewCodex()) m_paneManager.displayRegisteredPane(MainInterfacePanes::Codex); auto player = m_client->mainPlayer(); auto cursorWorldPos = cursorWorldPosition(); if (player->wireToolInUse()) { m_paneManager.displayRegisteredPane(MainInterfacePanes::WireInterface); player->setWireConnector(m_wireInterface.get()); } else { m_paneManager.dismissRegisteredPane(MainInterfacePanes::WireInterface); } // update inventory pane items, to know if item slots changed m_inventoryWindow->updateItems(); // update mouseover target EntityId newMouseOverTarget = NullEntityId; m_stickyTargetingTimer.tick(dt); auto mouseoverEntities = m_client->worldClient()->query(RectF::withCenter(cursorWorldPos, Vec2F(1, 1)), [=](shared_ptr const& entity) { return entity != player && entity->damageBar() == DamageBarType::Default && (entity->getTeam().type == TeamType::Enemy || entity->getTeam().type == TeamType::PVP) && m_client->worldClient()->lightLevel(entity->position()) > 0; }); sortByComputedValue(mouseoverEntities, [&](DamageBarEntityPtr const& a) { return m_client->worldClient()->geometry().diff(a->position(), cursorWorldPos).magnitude(); }); if (mouseoverEntities.size() > 0) { newMouseOverTarget = mouseoverEntities[0]->entityId(); } else if (m_lastMouseoverTarget == NullEntityId && player->lastDamagedTarget() != NullEntityId && player->timeSinceLastGaveDamage() < m_stickyTargetingTimer.time / 2) { if (auto targetEntity = as(m_client->worldClient()->entity(player->lastDamagedTarget()))) { if (targetEntity->damageBar() == DamageBarType::Default && (targetEntity->getTeam().type == TeamType::Enemy || targetEntity->getTeam().type == TeamType::PVP)) { newMouseOverTarget = targetEntity->entityId(); } } } if (newMouseOverTarget != NullEntityId && newMouseOverTarget != m_lastMouseoverTarget) { m_lastMouseoverTarget = newMouseOverTarget; m_portraitScale = 0; m_stickyTargetingTimer.reset(); } if (m_stickyTargetingTimer.ready()) m_lastMouseoverTarget = NullEntityId; // special damage bar entity if (m_specialDamageBarTarget != NullEntityId) { auto damageBarEntity = as(m_client->worldClient()->entity(m_specialDamageBarTarget)); if (damageBarEntity && damageBarEntity->damageBar() == DamageBarType::Special) { float targetHealth = damageBarEntity->health() / damageBarEntity->maxHealth(); float fillSpeed = 1.0f / Root::singleton().assets()->json("/interface.config:specialDamageBar.fillTime").toFloat(); if (abs(targetHealth - m_specialDamageBarValue) < fillSpeed * dt) m_specialDamageBarValue = targetHealth; else m_specialDamageBarValue += copysign(1.0f, targetHealth - m_specialDamageBarValue) * fillSpeed * dt; } else { m_specialDamageBarTarget = NullEntityId; } } if (m_specialDamageBarTarget == NullEntityId) m_specialDamageBarValue = 0.0f; if (m_specialDamageBarTarget == NullEntityId && m_client->mainPlayer()->inWorld()) { List specialDamageTargets; m_client->worldClient()->forAllEntities([&specialDamageTargets](EntityPtr const& entity) { if (auto damageBarEntity = as(entity)) if (damageBarEntity->damageBar() == DamageBarType::Special) specialDamageTargets.append(damageBarEntity); }); sortByComputedValue(specialDamageTargets, [&](DamageBarEntityPtr entity) { return m_client->worldClient()->geometry().diff(entity->position(), m_client->mainPlayer()->position()); }); if (specialDamageTargets.size() > 0) m_specialDamageBarTarget = specialDamageTargets[0]->entityId(); } for (auto const& message : m_client->mainPlayer()->pullQueuedMessages()) queueMessage(message); auto chatHeight = (m_chat->active() && m_chat->visible() > 0.1) ? m_chat->size()[1] : 0; m_radioMessagePopup->setChatHeight(chatHeight); if (!m_cinematicOverlay || m_cinematicOverlay->completed()) { if (m_client->mainPlayer()->interruptRadioMessage()) m_radioMessagePopup->interrupt(); if (!m_radioMessagePopup->messageActive()) { if (auto radioMessage = m_client->mainPlayer()->pullPendingRadioMessage()) { m_radioMessagePopup->setMessage(*radioMessage); m_paneManager.displayRegisteredPane(MainInterfacePanes::RadioMessagePopup); ChatReceivedMessage message = { {MessageContext::RadioMessage}, ServerConnectionId, Text::stripEscapeCodes(radioMessage->senderName), Text::stripEscapeCodes(radioMessage->text), Text::stripEscapeCodes(radioMessage->portraitImage.replace("", "0")) }; m_chat->addMessages({message}, false); } else { m_paneManager.dismissRegisteredPane(MainInterfacePanes::RadioMessagePopup); } } m_client->mainPlayer()->setInCinematic(false); } else { m_client->mainPlayer()->setInCinematic(true); } for (auto const& drop : m_client->mainPlayer()->pullQueuedItemDrops()) queueItemPickupText(drop); m_chat->addMessages(m_client->pullChatMessages()); if (auto worldClient = m_client->worldClient()) { if (worldClient->inWorld()) { if (auto cinematic = m_client->mainPlayer()->pullPendingCinematic()) { if (*cinematic) m_cinematicOverlay->load(Root::singleton().assets()->fetchJson(cinematic.take())); else m_cinematicOverlay->stop(); } } } if (!m_confirmationDialog->isDisplayed()) { if (auto confirmation = m_client->mainPlayer()->pullPendingConfirmation()) { m_paneManager.displayRegisteredPane(MainInterfacePanes::Confirmation); m_confirmationDialog->displayConfirmation(confirmation->first, confirmation->second); } } else { auto confirmationSource = m_confirmationDialog->sourceEntityId(); if (confirmationSource && !m_client->worldClient()->playerCanReachEntity(*confirmationSource)) m_confirmationDialog->dismiss(); } if (!m_joinRequestDialog->isDisplayed()) { if (auto req = m_queuedJoinRequests.maybeTakeLast()) { m_paneManager.displayRegisteredPane(MainInterfacePanes::JoinRequest); m_joinRequestDialog->displayRequest(req->first, [req](P2PJoinRequestReply reply) mutable { req->second.fulfill(reply); }); } } for (EntityId id : m_interactionScriptPanes.keys()) { if (!m_paneManager.isDisplayed(m_interactionScriptPanes[id])) m_interactionScriptPanes.remove(id); } if (!m_messages.contains(m_overflowMessage)) m_messageOverflow = 0; unsigned maxMessages = m_messageOverflow == 0 ? m_config->maxMessageCount : m_config->maxMessageCount + 1; // exclude overflow message if (m_messages.size() > maxMessages) { if (m_messageOverflow == 0) { m_messages.prepend(m_overflowMessage); } m_messageOverflow++; m_overflowMessage->message = m_config->overflowMessageText.replace("", toString(m_messageOverflow)); m_overflowMessage->cooldown = m_config->messageTime; if (auto oldest = m_messages.sorted([](GuiMessagePtr a, GuiMessagePtr b) { return a->cooldown < b->cooldown; }).maybeFirst()) m_overflowMessage->cooldown = oldest.value()->cooldown; if (auto bottom = m_messages.filtered([this](GuiMessagePtr m) { return m != m_overflowMessage; }).maybeFirst()) bottom.value()->cooldown = 0; } for (auto it = m_messages.begin(); it != m_messages.end();) { auto& message = *it; message->cooldown -= dt; if (message->cooldown < 0) it = m_messages.erase(it); else it++; } for (auto it = m_itemDropMessages.begin(); it != m_itemDropMessages.end();) { auto& message = *it; if (message.second.second->cooldown < 0) it = m_itemDropMessages.erase(it); else it++; } bool playerInWorld = m_client->mainPlayer()->inWorld(); if (m_cinematicOverlay->completed()) { if (m_planetNameTimer.tick(dt)) { m_paneManager.dismissRegisteredPane(MainInterfacePanes::PlanetText); } else { if (playerInWorld) { String worldName; if (auto worldTemplate = m_client->worldClient()->currentTemplate()) worldName = worldTemplate->worldName(); if (!worldName.empty()) { m_planetText->setText(strf(m_config->planetNameFormatString.utf8Ptr(), worldName)); m_paneManager.displayRegisteredPane(MainInterfacePanes::PlanetText); } } Color textColor = Color::White; // probably need to make this jsonable float fadeTimer = m_planetNameTimer.timer; if (fadeTimer < m_config->planetNameFadeTime) textColor.setAlphaF(fadeTimer / m_config->planetNameFadeTime); m_planetText->setColor(textColor); } } else if (!playerInWorld) { m_planetNameTimer.reset(); m_paneManager.dismissRegisteredPane(MainInterfacePanes::PlanetText); } for (auto& containerResult : m_containerInteractor->pullContainerResults()) { if (!m_containerPane || !m_containerPane->giveContainerResult(containerResult)) { if (!m_inventoryWindow->giveContainerResult(containerResult)) { for (auto item : containerResult) { if (m_containerInteractor->containerOpen()) m_containerInteractor->addToContainer(m_client->mainPlayer()->pickupItems(item)); else m_client->mainPlayer()->giveItem(item); } } } } if (auto currentQuest = m_client->questManager()->currentQuest()) { m_paneManager.displayRegisteredPane(MainInterfacePanes::QuestTracker); m_questTracker->setQuest(*currentQuest); } else { m_paneManager.dismissRegisteredPane(MainInterfacePanes::QuestTracker); } updateCursor(); m_nameplatePainter->update(dt, m_client->worldClient(), m_worldPainter->camera(), m_client->worldClient()->interactiveHighlightMode()); m_questIndicatorPainter->update(dt, m_client->worldClient(), m_worldPainter->camera()); m_chatBubbleManager->setCamera(m_worldPainter->camera()); if (auto worldClient = m_client->worldClient()) { auto chatActions = worldClient->pullPendingChatActions(); auto portraitActions = chatActions.filtered([](ChatAction action) { return action.is(); }); for (auto action : portraitActions) { PortraitChatAction portraitAction = action.get(); String name; if (auto npc = as(worldClient->entity(portraitAction.entity))) name = npc->name(); ChatReceivedMessage message = { { MessageContext::World }, ServerConnectionId, Text::stripEscapeCodes(name), Text::stripEscapeCodes(portraitAction.text), Text::stripEscapeCodes(portraitAction.portrait.replace("", "0")) }; m_chat->addMessages({message}, false); } m_chatBubbleManager->addChatActions(chatActions); m_chatBubbleManager->update(dt, worldClient); } if (auto container = m_client->worldClient()->get(m_containerInteractor->openContainerId())) { if (!m_client->worldClient()->playerCanReachEntity(container->entityId()) || !container->isInteractive()) m_containerInteractor->closeContainer(); } if (m_paneManager.topPane({PaneLayer::Window, PaneLayer::ModalWindow})) m_client->mainPlayer()->setBusyState(PlayerBusyState::Menu); else if (m_chat->hasFocus()) m_client->mainPlayer()->setBusyState(PlayerBusyState::Chatting); else m_client->mainPlayer()->setBusyState(PlayerBusyState::None); for (auto& pair : m_canvases) { pair.second->setPosition(Vec2I()); if (pair.second->ignoreInterfaceScale()) pair.second->setSize(Vec2I(m_guiContext->windowSize())); else pair.second->setSize(Vec2I(m_guiContext->windowInterfaceSize())); pair.second->update(dt); } } void MainInterface::renderInWorldElements() { if (m_disableHud) return; m_guiContext->setDefaultFont(); m_guiContext->setFontProcessingDirectives(""); m_guiContext->setFontColor(Vec4B::filled(255)); m_questIndicatorPainter->render(); m_nameplatePainter->render(); m_chatBubbleManager->render(); } void MainInterface::render() { if (m_disableHud) return; m_guiContext->setDefaultFont(); m_guiContext->setFontProcessingDirectives(""); m_guiContext->setFontColor(Vec4B::filled(255)); renderBreath(); renderMessages(); renderMonsterHealthBar(); renderSpecialDamageBar(); renderMainBar(); renderDebug(); RectI screenRect = RectI::withSize(Vec2I(), Vec2I(m_guiContext->windowSize())); for (auto& pair : m_canvases) pair.second->render(screenRect); renderWindows(); renderCursor(); } Vec2F MainInterface::cursorWorldPosition() const { return m_worldPainter->camera().screenToWorld(Vec2F(m_cursorScreenPos)); } bool MainInterface::isDebugDisplayed() { return m_clientCommandProcessor->debugDisplayEnabled(); } void MainInterface::doChat(String const& chat, bool addToHistory) { if (chat.empty()) return; if (chat.beginsWith("/")) { m_lastCommand = chat; for (auto const& result : m_clientCommandProcessor->handleCommand(chat)) m_chat->addLine(result); } else { m_client->sendChat(chat, m_chat->sendMode()); } if (addToHistory) m_chat->addHistory(chat); } void MainInterface::queueMessage(String const& message, Maybe cooldown, float spring) { auto guiMessage = make_shared(message, cooldown.value(m_config->messageTime), spring); m_messages.append(guiMessage); } void MainInterface::queueMessage(String const& message) { queueMessage(message, m_config->messageTime, 0.0f); } void MainInterface::queueJoinRequest(pair> request) { m_queuedJoinRequests.push_back(request); } void MainInterface::queueItemPickupText(ItemPtr const& item) { auto descriptor = item->descriptor(); if (m_itemDropMessages.contains(descriptor.singular())) { auto countMessPair = m_itemDropMessages.get(descriptor.singular()); auto newCount = item->count() + countMessPair.first; auto message = countMessPair.second; message->message = strf("{} - {}", item->friendlyName(), newCount); message->cooldown = m_config->messageTime; m_itemDropMessages[descriptor.singular()] = {newCount, message}; } else { auto message = make_shared(strf("{} - {}", item->friendlyName(), item->count()), m_config->messageTime); m_messages.append(message); m_itemDropMessages[descriptor.singular()] = {item->count(), message}; } } bool MainInterface::fixedCamera() const { return m_clientCommandProcessor->fixedCameraEnabled(); } void MainInterface::warpToOrbitedWorld(bool deploy) { if (m_client->canBeamDown(deploy)) { if (deploy) m_client->warpPlayer(WarpAlias::OrbitedWorld, true, "deploy", true); else m_client->warpPlayer(WarpAlias::OrbitedWorld, true, "beam"); return; } m_guiContext->playAudio("/sfx/interface/clickon_error.ogg"); } void MainInterface::warpToOwnShip() { if (m_client->canBeamUp()) { warpTo(WarpAlias::OwnShip); } else { m_guiContext->playAudio("/sfx/interface/clickon_error.ogg"); } } void MainInterface::warpTo(WarpAction const& warpAction) { if (m_client->beamUpRule() == BeamUpRule::AnywhereWithWarning) { if (m_confirmationDialog->isDisplayed()) m_confirmationDialog->dismiss(); m_paneManager.displayRegisteredPane(MainInterfacePanes::Confirmation); m_confirmationDialog->displayConfirmation("/interface/windowconfig/beamupconfirmation.config", [this, warpAction] (Widget*) { m_client->warpPlayer(warpAction, true, "beam"); }, [](Widget*) {}); } else { m_client->warpPlayer(warpAction, true, "beam"); } } CanvasWidgetPtr MainInterface::fetchCanvas(String const& canvasName, bool ignoreInterfaceScale) { CanvasWidgetPtr canvas; if (auto canvasPtr = m_canvases.ptr(canvasName)) canvas = *canvasPtr; else { m_canvases.emplace(canvasName, canvas = make_shared()); canvas->setPosition(Vec2I()); if (ignoreInterfaceScale) canvas->setSize(Vec2I(m_guiContext->windowSize())); else canvas->setSize(Vec2I(m_guiContext->windowInterfaceSize())); } canvas->setIgnoreInterfaceScale(ignoreInterfaceScale); return canvas; } // For when the player swaps characters. We need to completely reload ScriptPanes, // because a lot of ScriptPanes do not expect the character to suddenly change and may break or spill data over. void MainInterface::takeScriptPanes(List& out) { m_paneManager.dismissWhere([&](PanePtr const& pane) { if (auto scriptPane = as(pane)) { if (scriptPane->isDismissed()) return false; auto sourceEntityId = scriptPane->sourceEntityId(); m_interactionScriptPanes.remove(sourceEntityId); auto& info = out.emplaceAppend(); info.scriptPane = scriptPane; info.config = scriptPane->rawConfig(); info.sourceEntityId = sourceEntityId; info.visible = scriptPane->visibility(); info.position = scriptPane->relativePosition(); return true; } return false; }); } void MainInterface::reviveScriptPanes(List& panes) { for (auto& info : panes) { // this is evil and stupid info.scriptPane->~ScriptPane(); new(info.scriptPane.get()) ScriptPane(m_client, info.config, info.sourceEntityId); info.scriptPane->setVisibility(info.visible); displayScriptPane(info.scriptPane, info.sourceEntityId); info.scriptPane->setPosition(info.position); } } PanePtr MainInterface::createEscapeDialog() { auto assets = Root::singleton().assets(); auto escapeDialog = make_shared(); auto escapeDialogPtr = escapeDialog.get(); GuiReader escapeDialogReader; escapeDialogReader.registerCallback("returnToGame", [escapeDialogPtr](Widget*) { escapeDialogPtr->dismiss(); }); escapeDialogReader.registerCallback("showOptions", [escapeDialogPtr, this](Widget*) { escapeDialogPtr->dismiss(); m_paneManager.displayRegisteredPane(MainInterfacePanes::Options); }); escapeDialogReader.registerCallback("saveAndQuit", [escapeDialogPtr, this](Widget*) { m_state = ReturnToTitle; escapeDialogPtr->dismiss(); }); escapeDialogReader.construct(assets->json("/interface.config:escapeDialog"), escapeDialogPtr); escapeDialog->fetchChild("lblversion")->setText(strf("OpenStarbound - {} ({})", StarVersionString, StarArchitectureString)); return escapeDialog; } float MainInterface::interfaceScale() const { return m_guiContext->interfaceScale(); } unsigned MainInterface::windowHeight() const { return m_guiContext->windowHeight(); } unsigned MainInterface::windowWidth() const { return m_guiContext->windowWidth(); } Vec2I MainInterface::mainBarPosition() const { return Vec2I(windowWidth(), windowHeight()) - m_config->mainBarSize * interfaceScale(); } void MainInterface::renderBreath() { auto assets = Root::singleton().assets(); auto imgMetadata = Root::singleton().imageMetadataDatabase(); Vec2I breathBarSize = Vec2I(Vec2F(m_guiContext->textureSize("/interface/breath/empty.png")) * interfaceScale()); Vec2I breathOffset = jsonToVec2I(assets->json("/interface.config:breathPos")); Vec2F breathBackgroundCenterPos(windowWidth() * 0.5f + breathOffset[0] * interfaceScale(), windowHeight() - breathOffset[1] * interfaceScale()); Vec2F breathBarPos = breathBackgroundCenterPos + Vec2F(jsonToVec2I(assets->json("/interface.config:breathBarPos")) * interfaceScale()); float breath = m_client->mainPlayer()->breath(); float breathMax = m_client->mainPlayer()->maxBreath(); size_t blocks = round((10 * breath) / breathMax); if (blocks < 10) { String breathPath = "/interface/breath/breath.png"; m_guiContext->drawQuad(breathPath, RectF::withCenter(breathBackgroundCenterPos, Vec2F(imgMetadata->imageSize(breathPath)) * interfaceScale())); for (size_t i = 0; i < 10; i++) { if (i >= blocks) { if (blocks == 0 && Time::monotonicMilliseconds() % 500 > 250) m_guiContext->drawQuad("/interface/breath/warning.png", breathBarPos + Vec2F(breathBarSize[0] * i, 0), interfaceScale()); else m_guiContext->drawQuad("/interface/breath/empty.png", breathBarPos + Vec2F(breathBarSize[0] * i, 0), interfaceScale()); } else { m_guiContext->drawQuad("/interface/breath/breathbar.png", breathBarPos + Vec2F(breathBarSize[0] * i, 0), interfaceScale()); } } } } void MainInterface::renderMessages() { if (m_messages.empty()) return; Vec2F totalOffset = {}; auto imgMetadata = Root::singleton().imageMetadataDatabase(); unsigned bottomOffset = Root::singleton().configuration()->getPath("inventory.bottomActionBar").optBool().value(false) ? 32 : 0; for (auto& message : m_messages) { Vec2F hiddenOffset = Vec2F(m_config->messageHiddenOffset); Vec2F activeOffset = Vec2F(m_config->messageActiveOffset); if (bottomOffset) { activeOffset[1] += bottomOffset; bottomOffset = 0; } Vec2F messageOffset = lerp(message->springState, Vec2F(), activeOffset - hiddenOffset); totalOffset += messageOffset; messageOffset = totalOffset + hiddenOffset; Vec2F backgroundCenterPos = Vec2F(windowWidth() * 0.5f + messageOffset[0] * interfaceScale(), messageOffset[1] * interfaceScale()); Vec2F backgroundTextCenterPos = backgroundCenterPos + Vec2F(m_config->messageTextContainerOffset * interfaceScale()); Vec2F messageTextOffset = backgroundTextCenterPos + Vec2F(m_config->messageTextOffset * interfaceScale()); if (message->cooldown > m_config->messageHideTime) message->springState = (message->springState * m_config->messageWindowSpring + 1.0f) / (m_config->messageWindowSpring + 1.0f); else message->springState = (message->springState * m_config->messageWindowSpring) / (m_config->messageWindowSpring + 1.0f); m_guiContext->drawQuad(m_config->messageTextContainer, RectF::withCenter(backgroundTextCenterPos, Vec2F(imgMetadata->imageSize(m_config->messageTextContainer) * interfaceScale()))); m_guiContext->setFont(m_config->font); m_guiContext->setFontSize(m_config->fontSize); m_guiContext->setFontColor(Color::White.toRgba()); m_guiContext->renderText(message->message, {messageTextOffset, HorizontalAnchor::HMidAnchor, VerticalAnchor::VMidAnchor}); } } void MainInterface::renderMonsterHealthBar() { auto assets = Root::singleton().assets(); auto imgMetadata = Root::singleton().imageMetadataDatabase(); if (m_lastMouseoverTarget != NullEntityId && !m_stickyTargetingTimer.ready()) { auto world = m_client->worldClient(); auto entity = world->entity(m_lastMouseoverTarget); auto showDamageEntity = as(entity); if (!showDamageEntity) { m_lastMouseoverTarget = NullEntityId; return; } Vec2F backgroundCenterPos = Vec2F(windowWidth() / 2.0f, windowHeight()); auto container = assets->json("/interface.config:monsterHealth.container").toString(); auto offset = jsonToVec2F(assets->json("/interface.config:monsterHealth.offset")) * interfaceScale(); m_guiContext->drawQuad(container, RectF::withCenter(backgroundCenterPos + offset, Vec2F(imgMetadata->imageSize(container) * interfaceScale()))); auto nameTextOffset = jsonToVec2F(assets->json("/interface.config:monsterHealth.nameTextOffset")) * interfaceScale(); m_guiContext->setFont(m_config->font); m_guiContext->setFontSize(m_config->fontSize); m_guiContext->setFontColor(Color::White.toRgba()); m_guiContext->renderText(showDamageEntity->name(), backgroundCenterPos + nameTextOffset); auto empty = assets->json("/interface.config:monsterHealth.progressEmpty").toString(); auto filled = assets->json("/interface.config:monsterHealth.progressFilled").toString(); auto progressBarOffset = jsonToVec2F(assets->json("/interface.config:monsterHealth.progressBarOffset")) * interfaceScale(); auto chunks = assets->json("/interface.config:monsterHealth.progressChunks").toInt(); int blocks = round(showDamageEntity->health() / showDamageEntity->maxHealth() * chunks); Vec2F barPos = backgroundCenterPos + progressBarOffset; Vec2F barItemOffset = Vec2F(imgMetadata->imageSize(filled)) * interfaceScale(); barItemOffset[1] = 0; m_guiContext->drawQuad(empty, RectF::withSize(backgroundCenterPos + barPos, Vec2F(imgMetadata->imageSize(empty) * interfaceScale()))); for (int i = 0; i < blocks; i++) m_guiContext->drawQuad(filled, barPos + barItemOffset * i, interfaceScale()); auto portraitOffset = jsonToVec2F(assets->json("/interface.config:monsterHealth.portraitOffset")) * interfaceScale(); auto portraitScale = assets->json("/interface.config:monsterHealth.portraitScale").toFloat() * interfaceScale(); auto portraitScissorRect = jsonToRectF(assets->json("/interface.config:monsterHealth.portraitScissorRect")).scaled(interfaceScale()); auto rect = portraitScissorRect.translated(backgroundCenterPos + portraitOffset); m_guiContext->setInterfaceScissorRect(RectI(RectF(rect).scaled(1.0f / interfaceScale()))); auto portraitMaxSize = jsonToVec2I(assets->json("/interface.config:monsterHealth.portraitMaxSize")); List portrait = showDamageEntity->portrait(PortraitMode::Full); auto bounds = Drawable::boundBoxAll(portrait, true); if (m_portraitScale == 0) m_portraitScale = max(1, ceil(max(bounds.size().x() / portraitMaxSize.x(), bounds.size().y() / portraitMaxSize.y()))); Drawable::translateAll(portrait, {-bounds.xMin() - (bounds.width() * 0.5f), -bounds.yMin() }); // crop out whitespace, align bottom center Drawable::scaleAll(portrait, 1.0f / m_portraitScale); for (auto drawable : portrait) m_guiContext->drawDrawable(std::move(drawable), backgroundCenterPos + portraitOffset, portraitScale); m_guiContext->resetInterfaceScissorRect(); } } void MainInterface::renderSpecialDamageBar() { if (m_specialDamageBarTarget == NullEntityId) return; auto assets = Root::singleton().assets(); auto imgMetadata = Root::singleton().imageMetadataDatabase(); if (auto target = as(m_client->worldClient()->entity(m_specialDamageBarTarget))) { Vec2F bottomCenter = Vec2F(windowWidth() / 2.0f, 0); auto barConfig = assets->json("/interface.config:specialDamageBar"); auto background = barConfig.getString("background"); auto backgroundOffset = jsonToVec2F(barConfig.get("backgroundOffset")) * interfaceScale(); auto screenPos = RectF::withSize(bottomCenter + backgroundOffset, Vec2F(imgMetadata->imageSize(background) * interfaceScale())); m_guiContext->drawQuad(background, screenPos); auto fill = barConfig.getString("fill"); auto fillOffset = jsonToVec2F(barConfig.get("fillOffset")) * interfaceScale(); Vec2F size = Vec2F(barConfig.getInt("fillWidth") * m_specialDamageBarValue, imgMetadata->imageSize(fill).y()); m_guiContext->drawQuad(fill, RectF::withSize(bottomCenter + fillOffset, size * interfaceScale())); auto nameOffset = jsonToVec2F(barConfig.get("nameOffset")) * interfaceScale(); m_guiContext->setFontColor(jsonToColor(barConfig.get("nameColor")).toRgba()); m_guiContext->setFontSize(barConfig.getUInt("nameSize")); m_guiContext->setFontProcessingDirectives(barConfig.getString("nameDirectives")); m_guiContext->renderText(target->name(), TextPositioning(bottomCenter + nameOffset, HorizontalAnchor::HMidAnchor, VerticalAnchor::BottomAnchor)); m_guiContext->setFontProcessingDirectives(""); } } void MainInterface::renderMainBar() { Vec2I barPos = mainBarPosition(); m_cursorTooltip = {}; auto assets = Root::singleton().assets(); Vec2I inventoryButtonPos = barPos + m_config->mainBarInventoryButtonOffset * interfaceScale(); if (m_paneManager.registeredPaneIsDisplayed(MainInterfacePanes::Inventory)) { if (overButton(m_config->mainBarInventoryButtonPoly, m_cursorScreenPos)) { m_guiContext->drawQuad(m_config->inventoryImageOpenHover, Vec2F(inventoryButtonPos), interfaceScale()); m_cursorTooltip = assets->json("/interface.config:cursorTooltip.inventoryText").toString(); } else { m_guiContext->drawQuad(m_config->inventoryImageOpen, Vec2F(inventoryButtonPos), interfaceScale()); } } else if (overButton(m_config->mainBarInventoryButtonPoly, m_cursorScreenPos)) { if (m_inventoryWindow->containsNewItems()) m_guiContext->drawQuad(m_config->inventoryImageGlowHover, Vec2F(inventoryButtonPos), interfaceScale()); else m_guiContext->drawQuad(m_config->inventoryImageHover, Vec2F(inventoryButtonPos), interfaceScale()); m_cursorTooltip = assets->json("/interface.config:cursorTooltip.inventoryText").toString(); } else { if (m_inventoryWindow->containsNewItems()) m_guiContext->drawQuad(m_config->inventoryImageGlow, Vec2F(inventoryButtonPos), interfaceScale()); else m_guiContext->drawQuad(m_config->inventoryImage, Vec2F(inventoryButtonPos), interfaceScale()); } auto drawStateButton = [this](MainInterfacePanes paneType, Vec2I pos, PolyI poly, String image, String hoverImage, String openImage, String hoverOpenImage, String toolTip) { if (m_paneManager.registeredPaneIsDisplayed(paneType)) { if (overButton(poly, m_cursorScreenPos)) { m_guiContext->drawQuad(hoverOpenImage, Vec2F(pos), interfaceScale()); m_cursorTooltip = toolTip; } else { m_guiContext->drawQuad(openImage, Vec2F(pos), interfaceScale()); } } else if (overButton(poly, m_cursorScreenPos)) { m_guiContext->drawQuad(hoverImage, Vec2F(pos), interfaceScale()); m_cursorTooltip = toolTip; } else { m_guiContext->drawQuad(image, Vec2F(pos), interfaceScale()); } }; Vec2I craftButtonPos = barPos + m_config->mainBarCraftButtonOffset * interfaceScale(); drawStateButton(MainInterfacePanes::CraftingPlain, craftButtonPos, m_config->mainBarCraftButtonPoly, m_config->craftImage, m_config->craftImageHover, m_config->craftImageOpen, m_config->craftImageOpenHover, assets->json("/interface.config:cursorTooltip.craftingText").toString()); Vec2I codexButtonPos = barPos + m_config->mainBarCodexButtonOffset * interfaceScale(); drawStateButton(MainInterfacePanes::Codex, codexButtonPos, m_config->mainBarCodexButtonPoly, m_config->codexImage, m_config->codexImageHover, m_config->codexImageOpen, m_config->codexImageHoverOpen, assets->json("/interface.config:cursorTooltip.codexText").toString()); Vec2I mmUpgradeButtonPos = barPos + m_config->mainBarMmUpgradeButtonOffset * interfaceScale(); if (m_client->mainPlayer()->inventory()->essentialItem(EssentialItem::BeamAxe)) { drawStateButton(MainInterfacePanes::MmUpgrade, mmUpgradeButtonPos, m_config->mainBarMmUpgradeButtonPoly, m_config->mmUpgradeImage, m_config->mmUpgradeImageHover, m_config->mmUpgradeImageOpen, m_config->mmUpgradeImageHoverOpen, assets->json("/interface.config:cursorTooltip.mmUpgradeText").toString()); } else { drawStateButton(MainInterfacePanes::MmUpgrade, mmUpgradeButtonPos, m_config->mainBarMmUpgradeButtonPoly, m_config->mmUpgradeImageDisabled, m_config->mmUpgradeImageDisabled, m_config->mmUpgradeImageDisabled, m_config->mmUpgradeImageDisabled, assets->json("/interface.config:cursorTooltip.disabledText").toString()); } Vec2I collectionsButtonPos = barPos + m_config->mainBarCollectionsButtonOffset * interfaceScale(); drawStateButton(MainInterfacePanes::Collections, collectionsButtonPos, m_config->mainBarCollectionsButtonPoly, m_config->collectionsImage, m_config->collectionsImageHover, m_config->collectionsImageOpen, m_config->collectionsImageHoverOpen, assets->json("/interface.config:cursorTooltip.collectionsText").toString()); // when the player can't deploy or beam, show the deploy button disabled // when the player can beam up they can't deploy down, show beaming up button in deploy button's place // when the player can only deploy, only show deploy button // when the player can deploy or beam down, show both buttons Vec2F deployButtonPos(barPos + m_config->mainBarDeployButtonOffset * interfaceScale()); if (m_client->canBeamUp()) { if (overButton(m_config->mainBarDeployButtonPoly, m_cursorScreenPos)) { m_guiContext->drawQuad(m_config->beamUpImageHover, deployButtonPos, interfaceScale()); m_cursorTooltip = assets->json("/interface.config:cursorTooltip.beamUpText").toString(); } else { m_guiContext->drawQuad(m_config->beamUpImage, deployButtonPos, interfaceScale()); } } else if (m_client->canBeamDown(true)) { if (overButton(m_config->mainBarDeployButtonPoly, m_cursorScreenPos)) { m_guiContext->drawQuad(m_config->deployImageHover, deployButtonPos, interfaceScale()); m_cursorTooltip = assets->json("/interface.config:cursorTooltip.deployText").toString(); } else { m_guiContext->drawQuad(m_config->deployImage, deployButtonPos, interfaceScale()); } } else { m_guiContext->drawQuad(m_config->deployImageDisabled, deployButtonPos, interfaceScale()); } Vec2F beamButtonPos(barPos + m_config->mainBarBeamButtonOffset * interfaceScale()); if (m_client->canBeamDown()) { if (overButton(m_config->mainBarBeamButtonPoly, m_cursorScreenPos)) { m_guiContext->drawQuad(m_config->beamDownImageHover, beamButtonPos, interfaceScale()); m_cursorTooltip = assets->json("/interface.config:cursorTooltip.beamDownText").toString(); } else { m_guiContext->drawQuad(m_config->beamDownImage, beamButtonPos, interfaceScale()); } } Vec2I questLogButtonPos = barPos + m_config->mainBarQuestLogButtonOffset * interfaceScale(); drawStateButton(MainInterfacePanes::QuestLog, questLogButtonPos, m_config->mainBarQuestLogButtonPoly, m_config->questLogImage, m_config->questLogImageHover, m_config->questLogImageOpen, m_config->questLogImageHoverOpen, assets->json("/interface.config:cursorTooltip.questsText").toString()); } void MainInterface::renderWindows() { m_paneManager.render(); } void MainInterface::renderDebug() { if (!isDebugDisplayed()) { SpatialLogger::clear(); m_debugTextRect = RectF::null(); LogMap::clear(); SpatialLogger::setObserved(false); return; } SpatialLogger::setObserved(true); if (m_clientCommandProcessor->debugHudEnabled()) { auto assets = Root::singleton().assets(); m_guiContext->setFontSize(m_config->debugFontSize); m_guiContext->setFont(m_config->debugFont); m_guiContext->setLineSpacing(0.5f); m_guiContext->setFontProcessingDirectives(m_config->debugFontDirectives); m_guiContext->setFontColor(Color::White.toRgba()); m_guiContext->setFontMode(FontMode::Normal); bool clearMap = m_debugMapClearTimer.wrapTick(); auto logMapValues = LogMap::getValues(); if (clearMap) LogMap::clear(); List formatted; formatted.reserve(logMapValues.size()); int counter = 0; for (auto const& pair : logMapValues) { TextPositioning positioning = { Vec2F(m_config->debugOffset[0], windowHeight() - m_config->debugOffset[1] - m_config->fontSize * interfaceScale() * counter++) }; String& text = formatted.emplace_back(strf("{}^lightgray;:^green,set; {}", pair.first, pair.second)); m_debugTextRect.combine(m_guiContext->determineTextSize(text, positioning).padded(m_config->debugBackgroundPad)); } if (!m_debugTextRect.isNull()) { RenderQuad& quad = m_guiContext->renderer()->immediatePrimitives() .emplace_back(std::in_place_type_t(), m_debugTextRect, m_config->debugBackgroundColor.toRgba(), 0.0f).get(); quad.b.color[3] = quad.c.color[3] = 0; }; m_debugTextRect = RectF::null(); for (size_t index = 0; index != formatted.size(); ++index) { TextPositioning positioning = { Vec2F(m_config->debugOffset[0], windowHeight() - m_config->debugOffset[1] - m_config->fontSize * interfaceScale() * index) }; m_guiContext->renderText(formatted[index], positioning); } m_guiContext->setFontSize(8); m_guiContext->setDefaultFont(); m_guiContext->setDefaultLineSpacing(); m_guiContext->setFontColor(Vec4B::filled(255)); m_guiContext->setFontProcessingDirectives(""); } auto const& camera = m_worldPainter->camera(); bool clearSpatial = m_debugSpatialClearTimer.wrapTick(); for (auto const& line : SpatialLogger::getLines("world", clearSpatial)) { Vec2F begin = camera.worldToScreen(line.begin); Vec2F end = camera.worldGeometry().diff(line.end, line.begin) * camera.pixelRatio() * TilePixels + begin; m_guiContext->drawLine(begin, end, line.color, 1); } for (auto const& line : SpatialLogger::getLines("screen", clearSpatial)) m_guiContext->drawLine(Vec2F(line.begin), Vec2F(line.end), line.color, 1); for (auto const& point : SpatialLogger::getPoints("world", clearSpatial)) { auto position = camera.worldToScreen(point.position); m_guiContext->drawLine(position + Vec2F(-2, -2), position + Vec2F(-2, 2), point.color, 1); m_guiContext->drawLine(position + Vec2F(-2, 2), position + Vec2F(2, 2), point.color, 1); m_guiContext->drawLine(position + Vec2F(2, 2), position + Vec2F(2, -2), point.color, 1); m_guiContext->drawLine(position + Vec2F(2, -2), position + Vec2F(-2, -2), point.color, 1); } for (auto const& point : SpatialLogger::getPoints("screen", clearSpatial)) { auto position = point.position; m_guiContext->drawLine(position + Vec2F(-2, -2), position + Vec2F(-2, 2), point.color, 1); m_guiContext->drawLine(position + Vec2F(-2, 2), position + Vec2F(2, 2), point.color, 1); m_guiContext->drawLine(position + Vec2F(2, 2), position + Vec2F(2, -2), point.color, 1); m_guiContext->drawLine(position + Vec2F(2, -2), position + Vec2F(-2, -2), point.color, 1); } m_guiContext->setFontSize(m_config->debugFontSize); for (auto const& logText : SpatialLogger::getText("world", clearSpatial)) { m_guiContext->setFontColor(logText.color); m_guiContext->renderText(logText.text.utf8Ptr(), camera.worldToScreen(logText.position)); } for (auto const& logText : SpatialLogger::getText("screen", clearSpatial)) { m_guiContext->setFontColor(logText.color); m_guiContext->renderText(logText.text.utf8Ptr(), logText.position); } m_guiContext->setFontColor(Vec4B::filled(255)); } void MainInterface::updateCursor() { Maybe cursorOverride = m_actionBar->cursorOverride(m_cursorScreenPos); if (!cursorOverride) { if (auto pane = m_paneManager.getPaneAt(m_cursorScreenPos / interfaceScale())) { cursorOverride = cursorOverride.orMaybe(pane->cursorOverride(m_cursorScreenPos / interfaceScale())); } else { auto player = m_client->mainPlayer(); if (auto anchorState = m_client->mainPlayer()->loungingIn()) { if (auto loungeable = m_client->worldClient()->get(anchorState->entityId)) { if (auto loungeAnchor = loungeable->loungeAnchor(anchorState->positionIndex)) cursorOverride = cursorOverride.orMaybe(loungeAnchor->cursorOverride); } } if (!cursorOverride) { for (auto item : {player->primaryHandItem(), player->altHandItem()}) { if (auto activeItem = as(item)) { if (auto cursor = activeItem->cursor()) { cursorOverride = cursor; break; } } else if (auto inspectionTool = as(item)) { cursorOverride = String("/cursors/inspect.cursor"); break; } } } } } if (cursorOverride) m_cursor.setCursor(cursorOverride.take()); else m_cursor.resetCursor(); } void MainInterface::renderCursor() { // if we're currently playing a cinematic, we should not render the mouse. if (m_cinematicOverlay && !m_cinematicOverlay->completed()) return m_guiContext->applicationController()->setCursorVisible(false); Vec2I cursorPos = m_cursorScreenPos; Vec2I cursorSize = m_cursor.size(); Vec2I cursorOffset = m_cursor.offset(); unsigned int cursorScale = m_cursor.scale(interfaceScale()); Drawable cursorDrawable = m_cursor.drawable(); cursorPos[0] -= cursorOffset[0] * cursorScale; cursorPos[1] -= (cursorSize[1] - cursorOffset[1]) * cursorScale; if (!m_guiContext->trySetCursor(cursorDrawable, cursorOffset, cursorScale)) m_guiContext->drawDrawable(cursorDrawable, Vec2F(cursorPos), cursorScale); if (m_cursorTooltip) { auto assets = Root::singleton().assets(); auto imgDb = Root::singleton().imageMetadataDatabase(); auto backgroundImage = assets->json("/interface.config:cursorTooltip.background").toString(); auto rawCursorOffset = jsonToVec2I(assets->json("/interface.config:cursorTooltip.offset")); Vec2I tooltipSize = Vec2I(imgDb->imageSize(backgroundImage)) * interfaceScale(); Vec2I cursorOffset = (Vec2I{0, -m_cursor.size().y()} + rawCursorOffset) * cursorScale; Vec2I tooltipOffset = m_cursorScreenPos + cursorOffset; size_t fontSize = assets->json("/interface.config:cursorTooltip.fontSize").toUInt(); String font = assets->json("/interface.config:cursorTooltip.font").toString(); Vec4B fontColor = jsonToColor(assets->json("/interface.config:cursorTooltip.color")).toRgba(); m_guiContext->drawQuad(backgroundImage, Vec2F(tooltipOffset) + Vec2F(-tooltipSize.x(), 0), interfaceScale()); m_guiContext->setFontSize(fontSize); m_guiContext->setFontColor(fontColor); m_guiContext->setFont(font); m_guiContext->renderText(*m_cursorTooltip, TextPositioning(Vec2F(tooltipOffset) + Vec2F(-tooltipSize.x(), tooltipSize.y()) / 2, HorizontalAnchor::HMidAnchor, VerticalAnchor::VMidAnchor)); } m_cursorItem->setPosition(m_cursorScreenPos / interfaceScale() + m_config->inventoryItemMouseOffset); if (auto swapItem = m_client->mainPlayer()->inventory()->swapSlotItem()) m_cursorItem->setItem(swapItem); else m_cursorItem->setItem({}); m_cursorItem->render(RectI::withSize({}, {(int)windowWidth(), (int)windowHeight()})); m_guiContext->resetInterfaceScissorRect(); } bool MainInterface::overButton(PolyI buttonPoly, Vec2I const& mousePos) const { Vec2I barPos = mainBarPosition(); buttonPoly.translate(barPos); buttonPoly.scale(interfaceScale(), barPos); return buttonPoly.contains(mousePos); } bool MainInterface::overlayClick(Vec2I const& mousePos, MouseButton) { PolyI mainBarPoly = m_config->mainBarPoly; Vec2I barPos = mainBarPosition(); mainBarPoly.translate(barPos); mainBarPoly.scale(interfaceScale(), barPos); if (overButton(m_config->mainBarInventoryButtonPoly, mousePos)) { m_paneManager.toggleRegisteredPane(MainInterfacePanes::Inventory); return true; } if (overButton(m_config->mainBarCraftButtonPoly, mousePos)) { togglePlainCraftingWindow(); return true; } if (overButton(m_config->mainBarCodexButtonPoly, mousePos)) { m_paneManager.toggleRegisteredPane(MainInterfacePanes::Codex); return true; } if (overButton(m_config->mainBarDeployButtonPoly, mousePos)) { if (m_client->canBeamDown(true)) warpToOrbitedWorld(true); else if (m_client->canBeamUp()) warpToOwnShip(); return true; } if (overButton(m_config->mainBarBeamButtonPoly, mousePos)) { if (m_client->canBeamDown()) warpToOrbitedWorld(); return true; } if (overButton(m_config->mainBarQuestLogButtonPoly, mousePos)) { m_paneManager.toggleRegisteredPane(MainInterfacePanes::QuestLog); return true; } if (overButton(m_config->mainBarMmUpgradeButtonPoly, mousePos)) { if (m_client->mainPlayer()->inventory()->essentialItem(EssentialItem::BeamAxe)) m_paneManager.toggleRegisteredPane(MainInterfacePanes::MmUpgrade); return true; } if (overButton(m_config->mainBarCollectionsButtonPoly, mousePos)) { m_paneManager.toggleRegisteredPane(MainInterfacePanes::Collections); return true; } return false; } void MainInterface::displayScriptPane(ScriptPanePtr& scriptPane, EntityId sourceEntity) { // keep any number of script panes open with null source entities if (sourceEntity != NullEntityId) m_interactionScriptPanes[sourceEntity] = scriptPane; PaneLayer layer = PaneLayer::Window; if (auto layerName = scriptPane->config().optString("paneLayer")) layer = PaneLayerNames.getLeft(*layerName); if (scriptPane->openWithInventory()) { m_paneManager.displayPane(layer, scriptPane, [this](PanePtr const&) { if (auto player = m_client->mainPlayer()) player->clearSwap(); m_paneManager.dismissRegisteredPane(MainInterfacePanes::Inventory); }); m_paneManager.displayRegisteredPane(MainInterfacePanes::Inventory); m_paneManager.bringPaneAdjacent(m_paneManager.registeredPane(MainInterfacePanes::Inventory), scriptPane, Root::singleton().assets()->json("/interface.config:bringAdjacentWindowGap").toFloat()); } else { m_paneManager.displayPane(layer, scriptPane); } } }