Add character swapping (no GUI yet)

This commit is contained in:
Kae 2023-07-22 22:31:04 +10:00
parent 4fbd67dacc
commit cb19eef701
35 changed files with 478 additions and 196 deletions

View File

@ -506,9 +506,17 @@ void ClientApplication::changeState(MainAppState newState) {
m_playerStorage = make_shared<PlayerStorage>(m_root->toStoragePath("player"));
m_statistics = make_shared<Statistics>(m_root->toStoragePath("player"), appController()->statisticsService());
m_universeClient = make_shared<UniverseClient>(m_playerStorage, m_statistics);
m_universeClient->setLuaCallbacks("input", LuaBindings::makeInputCallbacks());
m_universeClient->setLuaCallbacks("voice", LuaBindings::makeVoiceCallbacks());
m_universeClient->playerReloadCallback() = [&]() {
if (auto paneManager = m_mainInterface->paneManager()) {
if (auto inventory = paneManager->registeredPane<InventoryPane>(MainInterfacePanes::Inventory))
inventory->clearChangedSlots();
}
};
m_mainMixer->setUniverseClient(m_universeClient);
m_titleScreen = make_shared<TitleScreen>(m_playerStorage, m_mainMixer->mixer());
if (auto renderer = Application::renderer())

View File

@ -112,6 +112,7 @@ SET (star_core_HEADERS
StarStringView.hpp
StarStrongTypedef.hpp
StarTcp.hpp
StarText.hpp
StarThread.hpp
StarTickRateMonitor.hpp
StarTime.hpp
@ -171,6 +172,7 @@ SET (star_core_SOURCES
StarString.cpp
StarStringView.cpp
StarTcp.cpp
StarText.cpp
StarThread.cpp
StarTime.cpp
StarTickRateMonitor.cpp

89
source/core/StarText.cpp Normal file
View File

@ -0,0 +1,89 @@
#include "StarText.hpp"
#include <regex>
namespace Star {
namespace Text {
static auto stripEscapeRegex = std::regex(strf("\\{:c}[^;]*{:c}", CmdEsc, EndEsc));
String stripEscapeCodes(String const& s) {
return std::regex_replace(s.utf8(), stripEscapeRegex, "");
}
static std::string escapeChars = strf("{:c}{:c}", CmdEsc, StartEsc);
bool processText(StringView text, TextCallback textFunc, CommandsCallback commandsFunc, bool includeCommandSides) {
std::string_view escChars(escapeChars);
std::string_view str = text.utf8();
while (true) {
size_t escape = str.find_first_of(escChars);
if (escape != NPos) {
escape = str.find_first_not_of(escChars, escape) - 1; // jump to the last ^
size_t end = str.find_first_of(EndEsc, escape);
if (end != NPos) {
if (escape && !textFunc(str.substr(0, escape)))
return false;
if (commandsFunc) {
StringView commands = includeCommandSides
? str.substr(escape, end - escape + 1)
: str.substr(escape + 1, end - escape - 1);
if (!commands.empty() && !commandsFunc(commands))
return false;
}
str = str.substr(end + 1);
continue;
}
}
if (!str.empty())
return textFunc(str);
return true;
}
}
// The below two functions aren't used anymore, not bothering with StringView for them
String preprocessEscapeCodes(String const& s) {
bool escape = false;
std::string result = s.utf8();
size_t escapeStartIdx = 0;
for (size_t i = 0; i < result.size(); i++) {
auto& c = result[i];
if (isEscapeCode(c)) {
escape = true;
escapeStartIdx = i;
}
if ((c <= SpecialCharLimit) && !(c == StartEsc))
escape = false;
if ((c == EndEsc) && escape)
result[escapeStartIdx] = StartEsc;
}
return {result};
}
String extractCodes(String const& s) {
bool escape = false;
StringList result;
String escapeCode;
for (auto c : preprocessEscapeCodes(s)) {
if (c == StartEsc)
escape = true;
if (c == EndEsc) {
escape = false;
for (auto command : escapeCode.split(','))
result.append(command);
escapeCode = "";
}
if (escape && (c != StartEsc))
escapeCode.append(c);
}
if (!result.size())
return "";
return "^" + result.join(",") + ";";
}
}
}

26
source/core/StarText.hpp Normal file
View File

@ -0,0 +1,26 @@
#ifndef STAR_TEXT_HPP
#define STAR_TEXT_HPP
#include "StarString.hpp"
#include "StarStringView.hpp"
namespace Star {
namespace Text {
unsigned char const StartEsc = '\x1b';
unsigned char const EndEsc = ';';
unsigned char const CmdEsc = '^';
unsigned char const SpecialCharLimit = ' ';
String stripEscapeCodes(String const& s);
inline bool isEscapeCode(char c) { return c == CmdEsc || c == StartEsc; }
typedef function<bool(StringView text)> TextCallback;
typedef function<bool(StringView commands)> CommandsCallback;
bool processText(StringView text, TextCallback textFunc, CommandsCallback commandsFunc = CommandsCallback(), bool includeCommandSides = false);
String preprocessEscapeCodes(String const& s);
String extractCodes(String const& s);
}
}
#endif

View File

@ -43,7 +43,7 @@ String Time::printDuration(double time) {
seconds = strf("{} second{}", numSeconds, numSeconds == 1 ? "" : "s");
}
int numMilliseconds = round(time * 1000);
int numMilliseconds = round(fmod(time, 1.0) * 1000);
milliseconds = strf("{} millisecond{}", numMilliseconds, numMilliseconds == 1 ? "" : "s");
return String::joinWith(", ", hours, minutes, seconds, milliseconds);

View File

@ -50,7 +50,8 @@ ClientCommandProcessor::ClientCommandProcessor(UniverseClientPtr universeClient,
{"giveessentialitem", bind(&ClientCommandProcessor::giveEssentialItem, this, _1)},
{"maketechavailable", bind(&ClientCommandProcessor::makeTechAvailable, this, _1)},
{"enabletech", bind(&ClientCommandProcessor::enableTech, this, _1)},
{"upgradeship", bind(&ClientCommandProcessor::upgradeShip, this, _1)}
{"upgradeship", bind(&ClientCommandProcessor::upgradeShip, this, _1)},
{"swap", bind(&ClientCommandProcessor::swap, this, _1)}
};
}
@ -304,7 +305,7 @@ String ClientCommandProcessor::playTime() {
String ClientCommandProcessor::deathCount() {
auto deaths = m_universeClient->mainPlayer()->log()->deathCount();
return strf("Total deaths: {}{}", deaths, deaths == 0 ? ". Well done!" : "");
return deaths ? strf("Total deaths: {}", deaths) : "Total deaths: 0. Well done!";
}
String ClientCommandProcessor::cinema(String const& argumentsString) {
@ -408,4 +409,16 @@ String ClientCommandProcessor::upgradeShip(String const& argumentsString) {
return strf("Upgraded ship");
}
String ClientCommandProcessor::swap(String const& argumentsString) {
auto arguments = m_parser.tokenizeToStringList(argumentsString);
if (arguments.size() == 0)
return "Not enouch arguments to /swap";
if (m_universeClient->switchPlayer(arguments[0]))
return "Successfully swapped player";
else
return "Failed to swap player";
}
}

View File

@ -58,6 +58,7 @@ private:
String makeTechAvailable(String const& argumentsString);
String enableTech(String const& argumentsString);
String upgradeShip(String const& argumentsString);
String swap(String const& argumentsString);
UniverseClientPtr m_universeClient;
CinematicPtr m_cinematicOverlay;

View File

@ -210,7 +210,7 @@ bool InventoryPane::giveContainerResult(ContainerResult result) {
if (!m_expectingSwap)
return false;
for (auto item : result) {
for (auto& item : result) {
auto inv = m_player->inventory();
m_player->triggerPickupEvents(item);
@ -224,18 +224,25 @@ bool InventoryPane::giveContainerResult(ContainerResult result) {
}
void InventoryPane::updateItems() {
for (auto p : m_itemGrids)
for (auto& p : m_itemGrids)
p.second->updateItemState();
}
bool InventoryPane::containsNewItems() const {
for (auto p : m_itemGrids) {
for (auto& p : m_itemGrids) {
if (p.second->slotsChanged())
return true;
}
return false;
}
void InventoryPane::clearChangedSlots() {
for (auto& p : m_itemGrids) {
p.second->updateItemState();
p.second->clearChangedSlots();
}
}
void InventoryPane::update(float dt) {
auto inventory = m_player->inventory();
auto context = Widget::context();

View File

@ -33,6 +33,7 @@ public:
// this is a little hacky and should probably be checked in the player inventory instead
void updateItems();
bool containsNewItems() const;
void clearChangedSlots();
protected:
virtual void update(float dt) override;

View File

@ -40,7 +40,7 @@ TeamBar::TeamBar(MainInterface* mainInterface, UniverseClientPtr client) {
return;
auto position = jsonToVec2I(Root::singleton().assets()->json("/interface/windowconfig/teambar.config:selfMenuOffset"));
position[1] += windowHeight() / m_guiContext->interfaceScale();
showMemberMenu(m_client->mainPlayer()->clientContext()->serverUuid(), position);
showMemberMenu(m_client->mainPlayer()->clientContext()->playerUuid(), position);
});
reader.construct(assets->json("/interface/windowconfig/teambar.config:paneLayout"), this);
@ -155,7 +155,7 @@ void TeamBar::buildTeamBar() {
int memberSize = assets->json("/interface/windowconfig/teambar.config:memberSize").toInt();
int memberSpacing = assets->json("/interface/windowconfig/teambar.config:memberSpacing").toInt();
Uuid myUuid = player->clientContext()->serverUuid();
Uuid myUuid = player->clientContext()->playerUuid();
for (auto member : teamClient->members()) {
if (member.uuid == myUuid) {
memberIndex++;
@ -360,7 +360,7 @@ void TeamMemberMenu::update(float dt) {
void TeamMemberMenu::updateWidgets() {
bool isLeader = m_owner->m_client->teamClient()->isTeamLeader();
bool isSelf = m_owner->m_client->mainPlayer()->clientContext()->serverUuid() == m_memberUuid;
bool isSelf = m_owner->m_client->mainPlayer()->clientContext()->playerUuid() == m_memberUuid;
fetchChild<ButtonWidget>("beamToShip")->setEnabled(m_canBeam);
fetchChild<ButtonWidget>("makeLeader")->setEnabled(isLeader && !isSelf);

View File

@ -24,8 +24,9 @@ DataStream& operator<<(DataStream& ds, ShipUpgrades const& upgrades) {
return ds;
}
ClientContext::ClientContext(Uuid serverUuid) {
ClientContext::ClientContext(Uuid serverUuid, Uuid playerUuid) {
m_serverUuid = move(serverUuid);
m_playerUuid = move(playerUuid);
m_rpc = make_shared<JsonRpc>();
m_netGroup.addNetElement(&m_orbitWarpActionNetState);
@ -40,6 +41,10 @@ Uuid ClientContext::serverUuid() const {
return m_serverUuid;
}
Uuid ClientContext::playerUuid() const {
return m_playerUuid;
}
CelestialCoordinate ClientContext::shipCoordinate() const {
return m_shipCoordinate.get();
}

View File

@ -17,9 +17,12 @@ STAR_CLASS(ClientContext);
class ClientContext {
public:
ClientContext(Uuid serverUuid);
ClientContext(Uuid serverUuid, Uuid playerUuid);
Uuid serverUuid() const;
// The player Uuid can differ from the mainPlayer's Uuid
// if the player has swapped character - use this for ship saving.
Uuid playerUuid() const;
// The coordinate for the world which the player's ship is currently
// orbiting.
@ -43,6 +46,8 @@ public:
private:
Uuid m_serverUuid;
Uuid m_playerUuid;
JsonRpcPtr m_rpc;
NetElementTopGroup m_netGroup;

View File

@ -27,6 +27,25 @@ EntityId EntityMap::reserveEntityId() {
return id;
}
Maybe<EntityId> EntityMap::maybeReserveEntityId(EntityId entityId) {
if (m_spatialMap.size() >= (size_t)(m_endIdSpace - m_beginIdSpace))
throw EntityMapException("No more entity id space in EntityMap::reserveEntityId");
if (m_spatialMap.contains(entityId))
return {};
else
return entityId;
}
EntityId EntityMap::reserveEntityId(EntityId entityId) {
if (auto reserved = maybeReserveEntityId(entityId))
return *reserved;
m_nextId = entityId;
return reserveEntityId();
}
void EntityMap::addEntity(EntityPtr entity) {
auto position = entity->position();
auto boundBox = entity->metaBoundBox();

View File

@ -30,6 +30,10 @@ public:
// Get the next free id in the entity id space.
EntityId reserveEntityId();
// Or a specific one, can fail.
Maybe<EntityId> maybeReserveEntityId(EntityId entityId);
// If it doesn't matter that we don't get the one want
EntityId reserveEntityId(EntityId entityId);
// Add an entity to this EntityMap. The entity must already be initialized
// and have a unique EntityId returned by reserveEntityId.

View File

@ -56,6 +56,7 @@ Player::Player(PlayerConfigPtr config, Uuid uuid) {
auto assets = Root::singleton().assets();
m_config = config;
m_client = nullptr;
m_state = State::Idle;
m_emoteState = HumanoidEmote::Idle;
@ -186,13 +187,35 @@ Player::Player(PlayerConfigPtr config, Uuid uuid) {
m_netGroup.setNeedsStoreCallback(bind(&Player::setNetStates, this));
}
Player::Player(PlayerConfigPtr config, ByteArray const& netStore) : Player(config) {
DataStreamBuffer ds(netStore);
setUniqueId(ds.read<String>());
ds.read(m_description);
ds.read(m_modeType);
ds.read(m_identity);
m_humanoid = make_shared<Humanoid>(Root::singleton().speciesDatabase()->species(m_identity.species)->humanoidConfig());
m_humanoid->setIdentity(m_identity);
m_movementController->resetBaseParameters(ActorMovementParameters(jsonMerge(m_humanoid->defaultMovementParameters(), m_config->movementParameters)));
}
Player::Player(PlayerConfigPtr config, Json const& diskStore) : Player(config) {
diskLoad(diskStore);
}
void Player::diskLoad(Json const& diskStore) {
setUniqueId(diskStore.getString("uuid"));
m_description = diskStore.getString("description");
setModeType(PlayerModeNames.getLeft(diskStore.getString("modeType")));
m_shipUpgrades = ShipUpgrades(diskStore.get("shipUpgrades"));
m_blueprints = make_shared<PlayerBlueprints>(diskStore.get("blueprints"));
m_universeMap = make_shared<PlayerUniverseMap>(diskStore.get("universeMap"));
if (m_clientContext)
m_universeMap->setServerUuid(m_clientContext->serverUuid());
m_codexes = make_shared<PlayerCodexes>(diskStore.get("codexes"));
m_techs = make_shared<PlayerTech>(diskStore.get("techs"));
m_identity = HumanoidIdentity(diskStore.get("identity"));
@ -208,48 +231,39 @@ Player::Player(PlayerConfigPtr config, Json const& diskStore) : Player(config) {
m_log = make_shared<PlayerLog>(diskStore.get("log"));
m_codexes->learnInitialCodexes(species());
// Make sure to merge the stored player blueprints with what a new player
// would get as default.
for (auto const& descriptor : m_config->defaultBlueprints)
m_blueprints->add(descriptor);
for (auto const& descriptor : Root::singleton().speciesDatabase()->species(m_identity.species)->defaultBlueprints())
m_blueprints->add(descriptor);
auto speciesDef = Root::singleton().speciesDatabase()->species(m_identity.species);
m_questManager->diskLoad(diskStore.get("quests", JsonObject{}));
m_companions->diskLoad(diskStore.get("companions", JsonObject{}));
m_deployment->diskLoad(diskStore.get("deployment", JsonObject{}));
m_humanoid = make_shared<Humanoid>(Root::singleton().speciesDatabase()->species(m_identity.species)->humanoidConfig());
m_humanoid = make_shared<Humanoid>(speciesDef->humanoidConfig());
m_humanoid->setIdentity(m_identity);
m_movementController->resetBaseParameters(ActorMovementParameters(jsonMerge(m_humanoid->defaultMovementParameters(), m_config->movementParameters)));
m_effectsAnimator->setGlobalTag("effectDirectives", Root::singleton().speciesDatabase()->species(m_identity.species)->effectDirectives());
m_effectsAnimator->setGlobalTag("effectDirectives", speciesDef->effectDirectives());
m_genericProperties = diskStore.getObject("genericProperties");
refreshEquipment();
m_codexes->learnInitialCodexes(species());
m_aiState = AiState(diskStore.get("aiState", JsonObject{}));
for (auto& script : m_genericScriptContexts)
script.second->setScriptStorage({});
for (auto& p : diskStore.get("genericScriptStorage", JsonObject{}).toObject()) {
if (auto script = m_genericScriptContexts.maybe(p.first).value({})) {
script->setScriptStorage(p.second.toObject());
}
}
}
Player::Player(PlayerConfigPtr config, ByteArray const& netStore) : Player(config) {
DataStreamBuffer ds(netStore);
setUniqueId(ds.read<String>());
ds.read(m_description);
ds.read(m_modeType);
ds.read(m_identity);
m_humanoid = make_shared<Humanoid>(Root::singleton().speciesDatabase()->species(m_identity.species)->humanoidConfig());
m_humanoid->setIdentity(m_identity);
m_movementController->resetBaseParameters(ActorMovementParameters(jsonMerge(m_humanoid->defaultMovementParameters(), m_config->movementParameters)));
// Make sure to merge the stored player blueprints with what a new player
// would get as default.
for (auto const& descriptor : m_config->defaultBlueprints)
m_blueprints->add(descriptor);
for (auto const& descriptor : speciesDef->defaultBlueprints())
m_blueprints->add(descriptor);
}
ClientContextPtr Player::clientContext() const {
@ -279,6 +293,10 @@ EntityType Player::entityType() const {
return EntityType::Player;
}
ClientEntityMode Player::clientEntityMode() const {
return ClientEntityMode::ClientPresenceMaster;
}
void Player::init(World* world, EntityId entityId, EntityMode mode) {
Entity::init(world, entityId, mode);
@ -1961,6 +1979,13 @@ void Player::setShipUpgrades(ShipUpgrades shipUpgrades) {
m_shipUpgrades = move(shipUpgrades);
}
void Player::applyShipUpgrades(Json const& upgrades) {
if (m_clientContext->playerUuid() == uuid())
m_clientContext->rpcInterface()->invokeRemote("ship.applyShipUpgrades", upgrades);
else
m_shipUpgrades.apply(upgrades);
}
String Player::name() const {
return m_identity.name;
}
@ -2176,6 +2201,19 @@ List<PhysicsForceRegion> Player::forceRegions() const {
return m_tools->forceRegions();
}
StatusControllerPtr Player::statusControllerPtr() {
return m_statusController;
}
ActorMovementControllerPtr Player::movementControllerPtr() {
return m_movementController;
}
PlayerConfigPtr Player::config() {
return m_config;
}
SongbookPtr Player::songbook() const {
return m_songbook;
}

View File

@ -62,8 +62,10 @@ class Player :
public:
Player(PlayerConfigPtr config, Uuid uuid = Uuid());
Player(PlayerConfigPtr config, Json const& diskStore);
Player(PlayerConfigPtr config, ByteArray const& netStore);
Player(PlayerConfigPtr config, Json const& diskStore);
void diskLoad(Json const& diskStore);
ClientContextPtr clientContext() const;
void setClientContext(ClientContextPtr clientContext);
@ -79,6 +81,7 @@ public:
ByteArray netStore();
EntityType entityType() const override;
ClientEntityMode clientEntityMode() const override;
void init(World* world, EntityId entityId, EntityMode mode) override;
void uninit() override;
@ -283,6 +286,7 @@ public:
ShipUpgrades shipUpgrades();
void setShipUpgrades(ShipUpgrades shipUpgrades);
void applyShipUpgrades(Json const& upgrades);
String name() const override;
void setName(String const& name);
@ -395,6 +399,11 @@ public:
List<PhysicsForceRegion> forceRegions() const override;
StatusControllerPtr statusControllerPtr();
ActorMovementControllerPtr movementControllerPtr();
PlayerConfigPtr config();
SongbookPtr songbook() const;
void finalizeCreation();

View File

@ -755,9 +755,18 @@ void PlayerInventory::load(Json const& store) {
m_equipment[EquipmentSlot::LegsCosmetic] = itemDatabase->diskLoad(store.get("legsCosmeticSlot"));
m_equipment[EquipmentSlot::BackCosmetic] = itemDatabase->diskLoad(store.get("backCosmeticSlot"));
auto itemBags = store.get("itemBags");
for (String const& bagType : itemBags.toObject().keys())
m_bags[bagType] = make_shared<ItemBag>(ItemBag::loadStore(itemBags.get(bagType)));
//reuse ItemBags so the Inventory pane still works after load()'ing into the same PlayerInventory again (from swap)
auto itemBags = store.get("itemBags").toObject();
eraseWhere(m_bags, [&](auto const& p) { return !itemBags.contains(p.first); });
for (auto const& p : itemBags) {
auto& bagType = p.first;
auto newBag = ItemBag::loadStore(p.second);
auto& bagPtr = m_bags[bagType];
if (bagPtr)
*bagPtr = move(newBag);
else
bagPtr = make_shared<ItemBag>(move(newBag));
}
m_swapSlot = itemDatabase->diskLoad(store.get("swapSlot"));
m_trashSlot = itemDatabase->diskLoad(store.get("trashSlot"));
@ -778,6 +787,7 @@ void PlayerInventory::load(Json const& store) {
m_selectedActionBar = jsonToSelectedActionBarLocation(store.get("selectedActionBar"));
m_essential.clear();
m_essential[EssentialItem::BeamAxe] = itemDatabase->diskLoad(store.get("beamAxe"));
m_essential[EssentialItem::WireTool] = itemDatabase->diskLoad(store.get("wireTool"));
m_essential[EssentialItem::PaintTool] = itemDatabase->diskLoad(store.get("paintTool"));

View File

@ -8,6 +8,7 @@
#include "StarAssets.hpp"
#include "StarEntityFactory.hpp"
#include "StarRoot.hpp"
#include "StarText.hpp"
namespace Star {
@ -98,7 +99,28 @@ Maybe<Uuid> PlayerStorage::playerUuidAt(size_t index) {
return {};
}
void PlayerStorage::savePlayer(PlayerPtr const& player) {
Maybe<Uuid> PlayerStorage::playerUuidByName(String const& name) {
String cleanMatch = Text::stripEscapeCodes(name).toLower();
Maybe<Uuid> uuid;
RecursiveMutexLocker locker(m_mutex);
size_t longest = SIZE_MAX;
for (auto& cache : m_savedPlayersCache) {
if (auto name = cache.second.optQueryString("identity.name")) {
auto cleanName = Text::stripEscapeCodes(*name).toLower();
auto len = cleanName.size();
if (len < longest && cleanName.utf8().rfind(cleanMatch.utf8()) == 0) {
longest = len;
uuid = cache.first;
}
}
}
return uuid;
}
Json PlayerStorage::savePlayer(PlayerPtr const& player) {
auto entityFactory = Root::singleton().entityFactory();
auto versioningDatabase = Root::singleton().versioningDatabase();
@ -113,15 +135,28 @@ void PlayerStorage::savePlayer(PlayerPtr const& player) {
VersionedJson versionedJson = entityFactory->storeVersionedJson(EntityType::Player, playerCacheData);
VersionedJson::writeFile(versionedJson, File::relativeTo(m_storageDirectory, strf("{}.player", uuid.hex())));
}
return newPlayerData;
}
Maybe<Json> PlayerStorage::maybeGetPlayerData(Uuid const& uuid) {
RecursiveMutexLocker locker(m_mutex);
if (auto cache = m_savedPlayersCache.ptr(uuid))
return *cache;
else
return {};
}
Json PlayerStorage::getPlayerData(Uuid const& uuid) {
auto data = maybeGetPlayerData(uuid);
if (!data)
throw PlayerException(strf("No such stored player with uuid '{}'", uuid.hex()));
else
return *data;
}
PlayerPtr PlayerStorage::loadPlayer(Uuid const& uuid) {
RecursiveMutexLocker locker(m_mutex);
if (!m_savedPlayersCache.contains(uuid))
throw PlayerException(strf("No such stored player with uuid '{}'", uuid.hex()));
auto playerCacheData = getPlayerData(uuid);
auto entityFactory = Root::singleton().entityFactory();
auto const& playerCacheData = m_savedPlayersCache.get(uuid);
try {
auto player = convert<Player>(entityFactory->diskLoadEntity(EntityType::Player, playerCacheData));
if (player->uuid() != uuid)
@ -129,6 +164,7 @@ PlayerPtr PlayerStorage::loadPlayer(Uuid const& uuid) {
return player;
} catch (std::exception const& e) {
Logger::error("Error loading player file, ignoring! {}", outputException(e, false));
RecursiveMutexLocker locker(m_mutex);
m_savedPlayersCache.remove(uuid);
return {};
}

View File

@ -18,8 +18,14 @@ public:
size_t playerCount() const;
// Returns nothing if index is out of bounds.
Maybe<Uuid> playerUuidAt(size_t index);
// Returns nothing if name doesn't match a player.
Maybe<Uuid> playerUuidByName(String const& name);
void savePlayer(PlayerPtr const& player);
// Also returns the diskStore Json if needed.
Json savePlayer(PlayerPtr const& player);
Maybe<Json> maybeGetPlayerData(Uuid const& uuid);
Json getPlayerData(Uuid const& uuid);
PlayerPtr loadPlayer(Uuid const& uuid);
void deletePlayer(Uuid const& uuid);

View File

@ -110,7 +110,7 @@ bool QuestManager::canStart(QuestArcDescriptor const& questArc) const {
if (!m_player->inventory()->hasItem(item))
return false;
if (questTemplate->requiredShipLevel) {
if (m_player->clientContext()->shipUpgrades().shipLevel < *questTemplate->requiredShipLevel)
if (m_player->shipUpgrades().shipLevel < *questTemplate->requiredShipLevel)
return false;
}
}

View File

@ -94,6 +94,7 @@ public:
Maybe<WarpAction> objectWarpAction(Uuid const& uuid) const;
virtual List<SystemObjectPtr> objects() const = 0;
virtual List<Uuid> objectKeys() const = 0;
virtual SystemObjectPtr getObject(Uuid const& uuid) const = 0;
SystemObjectConfig systemObjectConfig(String const& name, Uuid const& uuid) const;

View File

@ -82,6 +82,10 @@ List<SystemObjectPtr> SystemWorldClient::objects() const {
return m_objects.values();
}
List<Uuid> SystemWorldClient::objectKeys() const {
return m_objects.keys();
}
SystemObjectPtr SystemWorldClient::getObject(Uuid const& uuid) const {
return m_objects.maybe(uuid).value({});
}

View File

@ -28,6 +28,7 @@ public:
void update(float dt);
List<SystemObjectPtr> objects() const override;
List<Uuid> objectKeys() const override;
SystemObjectPtr getObject(Uuid const& uuid) const override;
List<SystemClientShipPtr> ships() const;

View File

@ -247,6 +247,10 @@ List<SystemObjectPtr> SystemWorldServer::objects() const {
return m_objects.values();
}
List<Uuid> SystemWorldServer::objectKeys() const {
return m_objects.keys();
}
SystemObjectPtr SystemWorldServer::getObject(Uuid const& uuid) const {
return m_objects.maybe(uuid).value({});
}

View File

@ -40,6 +40,7 @@ public:
void update(float dt);
List<SystemObjectPtr> objects() const override;
List<Uuid> objectKeys() const override;
SystemObjectPtr getObject(Uuid const& uuid) const override;
List<ConnectionId> pullShipFlights();

View File

@ -109,7 +109,7 @@ Maybe<String> UniverseClient::connect(UniverseConnection connection, bool allowA
if (auto success = as<ConnectSuccessPacket>(packet)) {
m_universeClock = make_shared<Clock>();
m_clientContext = make_shared<ClientContext>(success->serverUuid);
m_clientContext = make_shared<ClientContext>(success->serverUuid, m_mainPlayer->uuid());
m_teamClient = make_shared<TeamClient>(m_mainPlayer, m_clientContext);
m_mainPlayer->setClientContext(m_clientContext);
m_mainPlayer->setStatistics(m_statistics);
@ -397,6 +397,10 @@ bool UniverseClient::playerOnOwnShip() const {
return playerWorld().is<ClientShipWorldId>() && playerWorld().get<ClientShipWorldId>() == mainPlayer()->uuid();
}
bool UniverseClient::playerIsOriginal() const {
return m_clientContext->playerUuid() == mainPlayer()->uuid();
}
WorldId UniverseClient::playerWorld() const {
return m_clientContext->playerWorldId();
}
@ -473,6 +477,80 @@ void UniverseClient::stopLua() {
m_scriptContexts.clear();
}
bool UniverseClient::reloadPlayer(Json const& data, Uuid const& uuid) {
auto player = mainPlayer();
auto world = worldClient();
bool inWorld = player->inWorld();
EntityId entityId = player->entityId();
if (m_playerReloadPreCallback)
m_playerReloadPreCallback();
if (inWorld)
world->removeEntity(entityId, false);
else {
m_respawning = false;
m_respawnTimer.reset();
}
Json originalData = m_playerStorage->savePlayer(player);
std::exception_ptr exception;
try {
auto newData = data.set("movementController", originalData.get("movementController"));
player->diskLoad(newData);
}
catch (std::exception const& e) {
player->diskLoad(originalData);
exception = std::current_exception();
}
world->addEntity(player);
CelestialCoordinate coordinate = m_systemWorldClient->location();
player->universeMap()->addMappedCoordinate(coordinate);
player->universeMap()->filterMappedObjects(coordinate, m_systemWorldClient->objectKeys());
if (m_playerReloadCallback)
m_playerReloadCallback();
if (exception)
std::rethrow_exception(exception);
return true;
}
bool UniverseClient::switchPlayer(Uuid const& uuid) {
if (uuid == mainPlayer()->uuid())
return false;
else if (auto data = m_playerStorage->maybeGetPlayerData(uuid))
return reloadPlayer(*data, uuid);
else
return false;
}
bool UniverseClient::switchPlayer(size_t index) {
if (auto uuid = m_playerStorage->playerUuidAt(index))
return switchPlayer(*uuid);
else
return false;
}
bool UniverseClient::switchPlayer(String const& name) {
if (auto uuid = m_playerStorage->playerUuidByName(name))
return switchPlayer(*uuid);
else
return false;
}
UniverseClient::Callback& UniverseClient::playerReloadPreCallback() {
return m_playerReloadPreCallback;
}
UniverseClient::Callback& UniverseClient::playerReloadCallback() {
return m_playerReloadCallback;
}
ClockConstPtr UniverseClient::universeClock() const {
return m_universeClock;
}
@ -518,10 +596,14 @@ void UniverseClient::handlePackets(List<PacketPtr> const& packets) {
for (auto const& packet : packets) {
if (auto clientContextUpdate = as<ClientContextUpdatePacket>(packet)) {
m_clientContext->readUpdate(clientContextUpdate->updateData);
m_playerStorage->applyShipUpdates(m_mainPlayer->uuid(), m_clientContext->newShipUpdates());
m_playerStorage->applyShipUpdates(m_clientContext->playerUuid(), m_clientContext->newShipUpdates());
if (playerIsOriginal())
m_mainPlayer->setShipUpgrades(m_clientContext->shipUpgrades());
m_mainPlayer->setAdmin(m_clientContext->isAdmin());
m_mainPlayer->setTeam(m_clientContext->team());
} else if (auto chatReceivePacket = as<ChatReceivePacket>(packet)) {
m_pendingMessages.append(chatReceivePacket->receivedMessage);

View File

@ -69,6 +69,7 @@ public:
CelestialCoordinate shipCoordinate() const;
bool playerOnOwnShip() const;
bool playerIsOriginal() const;
WorldId playerWorld() const;
bool isAdmin() const;
@ -90,6 +91,15 @@ public:
void startLua();
void stopLua();
bool reloadPlayer(Json const& data, Uuid const& uuid);
bool switchPlayer(Uuid const& uuid);
bool switchPlayer(size_t index);
bool switchPlayer(String const& name);
typedef std::function<void()> Callback;
Callback& playerReloadPreCallback();
Callback& playerReloadCallback();
ClockConstPtr universeClock() const;
CelestialLogConstPtr celestialLog() const;
JsonRpcInterfacePtr rpcInterface() const;
@ -150,6 +160,9 @@ private:
typedef LuaUpdatableComponent<LuaBaseComponent> ScriptComponent;
typedef shared_ptr<ScriptComponent> ScriptComponentPtr;
StringMap<ScriptComponentPtr> m_scriptContexts;
Callback m_playerReloadPreCallback;
Callback m_playerReloadCallback;
};
}

View File

@ -136,6 +136,40 @@ bool WorldClient::respawnInWorld() const {
return m_respawnInWorld;
}
void WorldClient::removeEntity(EntityId entityId, bool andDie) {
auto entity = m_entityMap->entity(entityId);
if (!entity)
return;
if (andDie) {
ClientRenderCallback renderCallback;
entity->destroy(&renderCallback);
const List<Directives>* directives = nullptr;
if (auto& worldTemplate = m_worldTemplate) {
if (const auto& parameters = worldTemplate->worldParameters())
if (auto& globalDirectives = m_worldTemplate->worldParameters()->globalDirectives)
directives = &globalDirectives.get();
}
if (directives) {
int directiveIndex = unsigned(entity->entityId()) % directives->size();
for (auto& p : renderCallback.particles)
p.directives.append(directives->get(directiveIndex));
}
m_particles->addParticles(move(renderCallback.particles));
m_samples.appendAll(move(renderCallback.audios));
}
if (auto version = m_masterEntitiesNetVersion.maybeTake(entity->entityId())) {
ByteArray finalNetState = entity->writeNetState(*version).first;
m_outgoingPackets.append(make_shared<EntityDestroyPacket>(entity->entityId(), move(finalNetState), andDie));
}
m_entityMap->removeEntity(entityId);
entity->uninit();
}
WorldTemplateConstPtr WorldClient::currentTemplate() const {
return m_worldTemplate;
}
@ -989,7 +1023,7 @@ void WorldClient::update(float dt) {
action(this);
List<EntityId> toRemove;
List<EntityId> clientPresenceEntities{m_mainPlayer->entityId()};
List<EntityId> clientPresenceEntities;
m_entityMap->updateAllEntities([&](EntityPtr const& entity) {
entity->update(dt, m_currentStep);
@ -1415,40 +1449,6 @@ void WorldClient::sparkDamagedBlocks() {
}
}
void WorldClient::removeEntity(EntityId entityId, bool andDie) {
auto entity = m_entityMap->entity(entityId);
if (!entity)
return;
if (andDie) {
ClientRenderCallback renderCallback;
entity->destroy(&renderCallback);
const List<Directives>* directives = nullptr;
if (auto& worldTemplate = m_worldTemplate) {
if (const auto& parameters = worldTemplate->worldParameters())
if (auto& globalDirectives = m_worldTemplate->worldParameters()->globalDirectives)
directives = &globalDirectives.get();
}
if (directives) {
int directiveIndex = unsigned(entity->entityId()) % directives->size();
for (auto& p : renderCallback.particles)
p.directives.append(directives->get(directiveIndex));
}
m_particles->addParticles(move(renderCallback.particles));
m_samples.appendAll(move(renderCallback.audios));
}
if (auto version = m_masterEntitiesNetVersion.maybeTake(entity->entityId())) {
ByteArray finalNetState = entity->writeNetState(*version).first;
m_outgoingPackets.append(make_shared<EntityDestroyPacket>(entity->entityId(), move(finalNetState), andDie));
}
m_entityMap->removeEntity(entityId);
entity->uninit();
}
InteractiveEntityPtr WorldClient::getInteractiveInRange(Vec2F const& targetPosition, Vec2F const& sourcePosition, float maxRange) const {
if (!inWorld())
return {};

View File

@ -108,6 +108,8 @@ public:
void reviveMainPlayer();
bool respawnInWorld() const;
void removeEntity(EntityId entityId, bool andDie);
WorldTemplateConstPtr currentTemplate() const;
SkyConstPtr currentSky() const;
@ -214,8 +216,6 @@ private:
void sparkDamagedBlocks();
void removeEntity(EntityId entityId, bool andDie);
Vec2I environmentBiomeTrackPosition() const;
AmbientNoisesDescriptionPtr currentAmbientNoises() const;
WeatherNoisesDescriptionPtr currentWeatherNoises() const;

View File

@ -40,7 +40,7 @@ void UnlockItem::fireTriggered() {
if (auto clientContext = player->clientContext()) {
if (m_shipUpgrade)
clientContext->rpcInterface()->invokeRemote("ship.applyShipUpgrades", JsonObject{{"shipLevel", *m_shipUpgrade}});
player->applyShipUpgrades(JsonObject{ {"shipLevel", *m_shipUpgrade} });
}
if (!m_unlockMessage.empty()) {

View File

@ -112,11 +112,8 @@ LuaCallbacks LuaBindings::makePlayerCallbacks(Player* player) {
player->interact(InteractAction(type, sourceEntityId.value(NullEntityId), configData));
});
callbacks.registerCallback("shipUpgrades", [player]() { return player->clientContext()->shipUpgrades().toJson(); });
callbacks.registerCallback("upgradeShip", [player](Json const& upgrades) {
player->clientContext()->rpcInterface()->invokeRemote("ship.applyShipUpgrades", upgrades);
});
callbacks.registerCallback("shipUpgrades", [player]() { return player->shipUpgrades().toJson(); });
callbacks.registerCallback("upgradeShip", [player](Json const& upgrades) { player->applyShipUpgrades(upgrades); });
callbacks.registerCallback("setUniverseFlag", [player](String const& flagName) {
player->clientContext()->rpcInterface()->invokeRemote("universe.setFlag", flagName);

View File

@ -1,98 +1,11 @@
#include "StarTextPainter.hpp"
#include "StarJsonExtra.hpp"
#include "StarText.hpp"
#include <regex>
namespace Star {
namespace Text {
static auto stripEscapeRegex = std::regex(strf("\\{:c}[^;]*{:c}", CmdEsc, EndEsc));
String stripEscapeCodes(String const& s) {
return std::regex_replace(s.utf8(), stripEscapeRegex, "");
}
inline bool isEscapeCode(char c) {
return c == CmdEsc || c == StartEsc;
}
static std::string escapeChars = strf("{:c}{:c}", CmdEsc, StartEsc);
typedef function<bool(StringView text)> TextCallback;
typedef function<bool(StringView commands)> CommandsCallback;
bool processText(StringView text, TextCallback textFunc, CommandsCallback commandsFunc = CommandsCallback(), bool includeCommandSides = false) {
std::string_view escChars(escapeChars);
std::string_view str = text.utf8();
while (true) {
size_t escape = str.find_first_of(escChars);
if (escape != NPos) {
escape = str.find_first_not_of(escChars, escape) - 1; // jump to the last ^
size_t end = str.find_first_of(EndEsc, escape);
if (end != NPos) {
if (escape && !textFunc(str.substr(0, escape)))
return false;
if (commandsFunc) {
StringView commands = includeCommandSides
? str.substr(escape, end - escape + 1)
: str.substr(escape + 1, end - escape - 1);
if (!commands.empty() && !commandsFunc(commands))
return false;
}
str = str.substr(end + 1);
continue;
}
}
if (!str.empty())
return textFunc(str);
return true;
}
}
// The below two functions aren't used anymore, not bothering with StringView for them
String preprocessEscapeCodes(String const& s) {
bool escape = false;
std::string result = s.utf8();
size_t escapeStartIdx = 0;
for (size_t i = 0; i < result.size(); i++) {
auto& c = result[i];
if (isEscapeCode(c)) {
escape = true;
escapeStartIdx = i;
}
if ((c <= SpecialCharLimit) && !(c == StartEsc))
escape = false;
if ((c == EndEsc) && escape)
result[escapeStartIdx] = StartEsc;
}
return {result};
}
String extractCodes(String const& s) {
bool escape = false;
StringList result;
String escapeCode;
for (auto c : preprocessEscapeCodes(s)) {
if (c == StartEsc)
escape = true;
if (c == EndEsc) {
escape = false;
for (auto command : escapeCode.split(','))
result.append(command);
escapeCode = "";
}
if (escape && (c != StartEsc))
escapeCode.append(c);
}
if (!result.size())
return "";
return "^" + result.join(",") + ";";
}
}
TextPositioning::TextPositioning() {
pos = Vec2F();
hAnchor = HorizontalAnchor::LeftAnchor;

View File

@ -10,17 +10,6 @@ namespace Star {
STAR_CLASS(TextPainter);
namespace Text {
unsigned char const StartEsc = '\x1b';
unsigned char const EndEsc = ';';
unsigned char const CmdEsc = '^';
unsigned char const SpecialCharLimit = ' ';
String stripEscapeCodes(String const& s);
String preprocessEscapeCodes(String const& s);
String extractCodes(String const& s);
}
enum class FontMode {
Normal,
Shadow

View File

@ -198,12 +198,9 @@ void ItemGridWidget::updateItemState() {
updateAllItemSlots();
auto newState = slotItemNames();
for (size_t i = 0; i < newState.size(); ++i) {
if (newState[i].empty()) {
if (newState[i].empty())
m_changedSlots.remove(i);
continue;
}
if (newState[i].compare(m_itemNames[i]) != 0)
else if (newState[i].compare(m_itemNames[i]) != 0)
m_changedSlots.insert(i);
}
m_itemNames = newState;

View File

@ -5,6 +5,7 @@
#include "StarCasting.hpp"
#include "StarInputEvent.hpp"
#include "StarGuiContext.hpp"
#include "StarText.hpp"
namespace Star {