From 6d3bf2fdc38d8ae2d2705af4ef9d6d36d1984b6e Mon Sep 17 00:00:00 2001 From: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> Date: Mon, 8 Sep 2025 20:50:22 -0700 Subject: [PATCH 1/3] fix: need to create account twice due to commit latency?? (#1873) idk fixes the issue --- dMasterServer/MasterServer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp index 38abdc71..2d108a2d 100644 --- a/dMasterServer/MasterServer.cpp +++ b/dMasterServer/MasterServer.cpp @@ -159,6 +159,7 @@ int main(int argc, char** argv) { } MigrationRunner::RunMigrations(); + Database::Get()->Commit(); const auto resServerPath = BinaryPathFinder::GetBinaryDir() / "resServer"; std::filesystem::create_directories(resServerPath); const bool cdServerExists = std::filesystem::exists(resServerPath / "CDServer.sqlite"); From 154112050f31a29b3a4b2f07d7f53a8b25ed797c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 8 Sep 2025 22:35:18 -0700 Subject: [PATCH 2/3] feat: Implement Minecraft-style execute command with relative positioning (#1864) * Initial plan * Implement Minecraft-style execute command Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com> * Add relative positioning support to execute command using ~ syntax Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com> * update the parsing and fix chat response --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aronwk-aaron <26027722+aronwk-aaron@users.noreply.github.com> Co-authored-by: Aaron Kimbrell --- dGame/dUtilities/SlashCommandHandler.cpp | 9 + .../SlashCommands/DEVGMCommands.cpp | 193 ++++++++++++++++-- .../dUtilities/SlashCommands/DEVGMCommands.h | 1 + docs/Commands.md | 1 + 4 files changed, 188 insertions(+), 16 deletions(-) diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index f65fae32..218ccfa8 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -808,6 +808,15 @@ void SlashCommandHandler::Startup() { }; RegisterCommand(DeleteInvenCommand); + Command ExecuteCommand{ + .help = "Execute commands with modified context (Minecraft-style)", + .info = "Execute commands as different entities or from different positions. Usage: /execute ... run . Subcommands: as , at , positioned ", + .aliases = { "execute", "exec" }, + .handle = DEVGMCommands::Execute, + .requiredLevel = eGameMasterLevel::DEVELOPER + }; + RegisterCommand(ExecuteCommand); + // Register Greater Than Zero Commands Command KickCommand{ diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index 6963ebff..447d5155 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -559,23 +559,25 @@ namespace DEVGMCommands { } } - std::optional ParseRelativeAxis(const float sourcePos, const std::string& toParse) { - if (toParse.empty()) return std::nullopt; - - // relative offset from current position - if (toParse[0] == '~') { - if (toParse.size() == 1) return sourcePos; - - if (toParse.size() < 3 || !(toParse[1] != '+' || toParse[1] != '-')) return std::nullopt; - - const auto offset = GeneralUtils::TryParse(toParse.substr(2)); - if (!offset.has_value()) return std::nullopt; - - bool isNegative = toParse[1] == '-'; - return isNegative ? sourcePos - offset.value() : sourcePos + offset.value(); + // Parse coordinates with support for relative positioning (~) + std::optional ParseRelativeAxis(const float currentValue, const std::string& rawCoord) { + if (rawCoord.empty()) return std::nullopt; + std::string coord = rawCoord; + // Remove any '+' characters to simplify parsing, since they don't affect the value + coord.erase(std::remove(coord.begin(), coord.end(), '+'), coord.end()); + if (coord[0] == '~') { + if (coord.length() == 1) { + return currentValue; + } else { + auto offsetOpt = GeneralUtils::TryParse(coord.substr(1)); + if (!offsetOpt) return std::nullopt; + return currentValue + offsetOpt.value(); + } + } else { + auto absoluteOpt = GeneralUtils::TryParse(coord); + if (!absoluteOpt) return std::nullopt; + return absoluteOpt.value(); } - - return GeneralUtils::TryParse(toParse); } void Teleport(Entity* entity, const SystemAddress& sysAddr, const std::string args) { @@ -1664,4 +1666,163 @@ namespace DEVGMCommands { LOG("Despawned entity (%llu)", target->GetObjectID()); ChatPackets::SendSystemMessage(sysAddr, u"Despawned entity: " + GeneralUtils::to_u16string(target->GetObjectID())); } + + void Execute(Entity* entity, const SystemAddress& sysAddr, const std::string args) { + if (args.empty()) { + ChatPackets::SendSystemMessage(sysAddr, + u"Usage: /execute ... run \n" + u"Subcommands:\n" + u" as - Execute as different player\n" + u" at - Execute from player's position\n" + u" positioned - Execute from coordinates (absolute or relative with ~)\n" + u"Examples:\n" + u" /execute as Player1 run pos\n" + u" /execute at Player2 positioned 100 200 300 run spawn 1234\n" + u" /execute positioned ~5 ~10 ~ run spawn 1234" + ); + return; + } + + const auto splitArgs = GeneralUtils::SplitString(args, ' '); + + // Prevent execute command recursion by checking if this is already an execute command + for (const auto& arg : splitArgs) { + if (arg == "execute" || arg == "exec") { + ChatPackets::SendSystemMessage(sysAddr, u"Error: Recursive execute commands are not allowed"); + return; + } + } + + // Context variables for execution + Entity* execEntity = entity; // Entity to execute as + NiPoint3 execPosition = entity->GetPosition(); // Position to execute from + bool positionOverridden = false; + std::string finalCommand; + + // Parse subcommands + size_t i = 0; + while (i < splitArgs.size()) { + const std::string& subcommand = splitArgs[i]; + + if (subcommand == "as") { + if (i + 1 >= splitArgs.size()) { + ChatPackets::SendSystemMessage(sysAddr, u"Error: 'as' requires a player name"); + return; + } + + const std::string& targetName = splitArgs[i + 1]; + auto* targetPlayer = PlayerManager::GetPlayer(targetName); + if (!targetPlayer) { + ChatPackets::SendSystemMessage(sysAddr, u"Error: Player '" + GeneralUtils::ASCIIToUTF16(targetName) + u"' not found"); + return; + } + + execEntity = targetPlayer; + i += 2; + + } else if (subcommand == "at") { + if (i + 1 >= splitArgs.size()) { + ChatPackets::SendSystemMessage(sysAddr, u"Error: 'at' requires a player name"); + return; + } + + const std::string& targetName = splitArgs[i + 1]; + auto* targetPlayer = PlayerManager::GetPlayer(targetName); + if (!targetPlayer) { + ChatPackets::SendSystemMessage(sysAddr, u"Error: Player '" + GeneralUtils::ASCIIToUTF16(targetName) + u"' not found"); + return; + } + + execPosition = targetPlayer->GetPosition(); + positionOverridden = true; + i += 2; + + } else if (subcommand == "positioned") { + if (i + 3 >= splitArgs.size()) { + ChatPackets::SendSystemMessage(sysAddr, u"Error: 'positioned' requires x, y, z coordinates"); + return; + } + + auto xOpt = ParseRelativeAxis(execPosition.x, splitArgs[i + 1]); + auto yOpt = ParseRelativeAxis(execPosition.y, splitArgs[i + 2]); + auto zOpt = ParseRelativeAxis(execPosition.z, splitArgs[i + 3]); + + if (!xOpt || !yOpt || !zOpt) { + ChatPackets::SendSystemMessage(sysAddr, u"Error: Invalid coordinates for 'positioned'. Use numeric values or relative coordinates with ~."); + return; + } + + execPosition = NiPoint3(xOpt.value(), yOpt.value(), zOpt.value()); + positionOverridden = true; + + i += 4; + + } else if (subcommand == "run") { + // Everything after "run" is the command to execute + if (i + 1 >= splitArgs.size()) { + ChatPackets::SendSystemMessage(sysAddr, u"Error: 'run' requires a command"); + return; + } + + // Reconstruct the command from remaining args + for (size_t j = i + 1; j < splitArgs.size(); ++j) { + if (!finalCommand.empty()) finalCommand += " "; + finalCommand += splitArgs[j]; + } + break; + + } else { + ChatPackets::SendSystemMessage(sysAddr, u"Error: Unknown subcommand '" + GeneralUtils::ASCIIToUTF16(subcommand) + u"'"); + ChatPackets::SendSystemMessage(sysAddr, u"Valid subcommands: as, at, positioned, run"); + return; + } + } + + if (finalCommand.empty()) { + ChatPackets::SendSystemMessage(sysAddr, u"Error: No command specified to run. Use 'run ' at the end."); + return; + } + + // Validate that the command starts with a valid character + if (finalCommand.empty() || finalCommand[0] == '/') { + ChatPackets::SendSystemMessage(sysAddr, u"Error: Command should not start with '/'. Just specify the command name."); + return; + } + + // Store original position if we need to restore it + NiPoint3 originalPosition; + bool needToRestore = false; + + if (positionOverridden && execEntity == entity) { + // If we're executing as ourselves but from a different position, + // temporarily move the entity + originalPosition = entity->GetPosition(); + needToRestore = true; + + // Set the position temporarily for the command execution + auto* controllable = entity->GetComponent(); + if (controllable) { + controllable->SetPosition(execPosition); + } + } + + // Provide feedback about what we're executing + std::string execAsName = execEntity->GetCharacter() ? execEntity->GetCharacter()->GetName() : "Unknown"; + ChatPackets::SendSystemMessage(sysAddr, u"[Execute] Running as '" + GeneralUtils::ASCIIToUTF16(execAsName) + + u"' from <" + GeneralUtils::to_u16string(execPosition.x) + u", " + + GeneralUtils::to_u16string(execPosition.y) + u", " + + GeneralUtils::to_u16string(execPosition.z) + u">: /" + + GeneralUtils::ASCIIToUTF16(finalCommand)); + + // Execute the command through the slash command handler + SlashCommandHandler::HandleChatCommand(GeneralUtils::ASCIIToUTF16("/" + finalCommand), execEntity, sysAddr); + + // Restore original position if needed + if (needToRestore) { + auto* controllable = entity->GetComponent(); + if (controllable) { + controllable->SetPosition(originalPosition); + } + } + } }; diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.h b/dGame/dUtilities/SlashCommands/DEVGMCommands.h index b1abb07e..64783e24 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.h +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.h @@ -76,6 +76,7 @@ namespace DEVGMCommands { void Shutdown(Entity* entity, const SystemAddress& sysAddr, const std::string args); void Barfight(Entity* entity, const SystemAddress& sysAddr, const std::string args); void Despawn(Entity* entity, const SystemAddress& sysAddr, const std::string args); + void Execute(Entity* entity, const SystemAddress& sysAddr, const std::string args); } #endif //!DEVGMCOMMANDS_H diff --git a/docs/Commands.md b/docs/Commands.md index 2a93e3de..54d5e531 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -116,6 +116,7 @@ These commands are primarily for development and testing. The usage of many of t |setrewardcode|`/setrewardcode `|Sets the rewardcode for the account you are logged into if it's a valid rewardcode, See cdclient table `RewardCodes`|8| |barfight|`/barfight start`|Starts a barfight (turns everyones pvp on)|8| |despawn|`/despawn `|Despawns the entity objectID IF it was spawned in through a slash command.|8| +|execute|`/execute ... run `|Execute commands with modified context (Minecraft-style). Subcommands: `as ` (execute as different player), `at ` (execute from player's position), `positioned ` (execute from coordinates - supports absolute coordinates like `100 200 300` or relative coordinates like `~5 ~10 ~` where `~` means current position). Example: `/execute as Player1 run pos`, `/execute positioned ~5 ~ ~-3 run spawn 1234`|8| |crash|`/crash`|Crashes the server.|9| |rollloot|`/rollloot `|Given a `loot matrix index`, look for `item id` in that matrix `amount` times and print to the chat box statistics of rolling that loot matrix.|9| |castskill|`/castskill `|Casts the skill as the player|9| From b798da8ef84e545ed7a909a17731100fc422ced3 Mon Sep 17 00:00:00 2001 From: HailStorm32 Date: Mon, 8 Sep 2025 23:07:08 -0700 Subject: [PATCH 3/3] fix: Update mute expiry from database (#1871) * Update mute expiry from database * Address review comments * Address review comment Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> --------- Co-authored-by: David Markowitz <39972741+EmosewaMC@users.noreply.github.com> --- dDatabase/GameDatabase/ITables/IAccounts.h | 1 + .../GameDatabase/MySQL/Tables/Accounts.cpp | 3 +- .../GameDatabase/SQLite/Tables/Accounts.cpp | 1 + dGame/User.cpp | 30 +++++++++++++++++-- dGame/User.h | 6 ++-- 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/dDatabase/GameDatabase/ITables/IAccounts.h b/dDatabase/GameDatabase/ITables/IAccounts.h index 13ecf29b..a58f3a25 100644 --- a/dDatabase/GameDatabase/ITables/IAccounts.h +++ b/dDatabase/GameDatabase/ITables/IAccounts.h @@ -14,6 +14,7 @@ public: std::string bcryptPassword; uint32_t id{}; uint32_t playKeyId{}; + uint64_t muteExpire{}; bool banned{}; bool locked{}; eGameMasterLevel maxGmLevel{}; diff --git a/dDatabase/GameDatabase/MySQL/Tables/Accounts.cpp b/dDatabase/GameDatabase/MySQL/Tables/Accounts.cpp index f4310dd8..b96c9c48 100644 --- a/dDatabase/GameDatabase/MySQL/Tables/Accounts.cpp +++ b/dDatabase/GameDatabase/MySQL/Tables/Accounts.cpp @@ -3,7 +3,7 @@ #include "eGameMasterLevel.h" std::optional MySQLDatabase::GetAccountInfo(const std::string_view username) { - auto result = ExecuteSelect("SELECT id, password, banned, locked, play_key_id, gm_level FROM accounts WHERE name = ? LIMIT 1;", username); + auto result = ExecuteSelect("SELECT id, password, banned, locked, play_key_id, gm_level, mute_expire FROM accounts WHERE name = ? LIMIT 1;", username); if (!result->next()) { return std::nullopt; @@ -16,6 +16,7 @@ std::optional MySQLDatabase::GetAccountInfo(const std::string_v toReturn.banned = result->getBoolean("banned"); toReturn.locked = result->getBoolean("locked"); toReturn.playKeyId = result->getUInt("play_key_id"); + toReturn.muteExpire = result->getUInt64("mute_expire"); return toReturn; } diff --git a/dDatabase/GameDatabase/SQLite/Tables/Accounts.cpp b/dDatabase/GameDatabase/SQLite/Tables/Accounts.cpp index 9431d407..72572f89 100644 --- a/dDatabase/GameDatabase/SQLite/Tables/Accounts.cpp +++ b/dDatabase/GameDatabase/SQLite/Tables/Accounts.cpp @@ -17,6 +17,7 @@ std::optional SQLiteDatabase::GetAccountInfo(const std::string_ toReturn.banned = result.getIntField("banned"); toReturn.locked = result.getIntField("locked"); toReturn.playKeyId = result.getIntField("play_key_id"); + toReturn.muteExpire = static_cast(result.getInt64Field("mute_expire")); return toReturn; } diff --git a/dGame/User.cpp b/dGame/User.cpp index 806d4611..7c03daa8 100644 --- a/dGame/User.cpp +++ b/dGame/User.cpp @@ -7,6 +7,10 @@ #include "dZoneManager.h" #include "eServerDisconnectIdentifiers.h" #include "eGameMasterLevel.h" +#include "BitStreamUtils.h" +#include "MessageType/Chat.h" +#include +#include User::User(const SystemAddress& sysAddr, const std::string& username, const std::string& sessionKey) { m_AccountID = 0; @@ -28,7 +32,7 @@ User::User(const SystemAddress& sysAddr, const std::string& username, const std: if (userInfo) { m_AccountID = userInfo->id; m_MaxGMLevel = userInfo->maxGmLevel; - m_MuteExpire = 0; //res->getUInt64(3); + m_MuteExpire = userInfo->muteExpire; } //If we're loading a zone, we'll load the last used (aka current) character: @@ -91,8 +95,28 @@ Character* User::GetLastUsedChar() { } } -bool User::GetIsMuted() const { - return m_MuteExpire == 1 || m_MuteExpire > time(NULL); +bool User::GetIsMuted() { + using namespace std::chrono; + constexpr auto refreshInterval = seconds{ 60 }; + const auto now = steady_clock::now(); + if (now - m_LastMuteCheck >= refreshInterval) { + m_LastMuteCheck = now; + if (const auto info = Database::Get()->GetAccountInfo(m_Username)) { + const auto expire = static_cast(info->muteExpire); + if (expire != m_MuteExpire) { + m_MuteExpire = expire; + + if (Game::chatServer && m_LoggedInCharID != 0) { + RakNet::BitStream bitStream; + BitStreamUtils::WriteHeader(bitStream, ServiceType::CHAT, MessageType::Chat::GM_MUTE); + bitStream.Write(m_LoggedInCharID); + bitStream.Write(m_MuteExpire); + Game::chatServer->Send(&bitStream, SYSTEM_PRIORITY, RELIABLE, 0, Game::chatSysAddr, false); + } + } + } + } + return m_MuteExpire == 1 || m_MuteExpire > std::time(nullptr); } time_t User::GetMuteExpire() const { diff --git a/dGame/User.h b/dGame/User.h index 662842a8..7fe8d335 100644 --- a/dGame/User.h +++ b/dGame/User.h @@ -3,6 +3,7 @@ #include #include +#include #include "RakNetTypes.h" #include "dCommonVars.h" @@ -46,7 +47,7 @@ public: const std::unordered_map& GetIsBestFriendMap() { return m_IsBestFriendMap; } void UpdateBestFriendValue(const std::string_view playerName, const bool newValue); - bool GetIsMuted() const; + bool GetIsMuted(); time_t GetMuteExpire() const; void SetMuteExpire(time_t value); @@ -72,7 +73,8 @@ private: bool m_LastChatMessageApproved = false; int m_AmountOfTimesOutOfSync = 0; const int m_MaxDesyncAllowed = 12; - time_t m_MuteExpire; + uint64_t m_MuteExpire; + std::chrono::steady_clock::time_point m_LastMuteCheck{}; }; #endif // USER_H