diff --git a/YNOCOMMANDS.md b/YNOCOMMANDS.md new file mode 100644 index 000000000..3cafdaddb --- /dev/null +++ b/YNOCOMMANDS.md @@ -0,0 +1,30 @@ +# YNO-specific commands + +ynoengine provides the following commands: + +###### `YnoAsyncRpc` + +```js +// sends (42, 69, "frobnicate") to server and continue execution +@raw 5000, 0, 42, 0, 69, "frobnicate" +// when response is received, assigns the status code to V[25] +@raw 5000, 0, 42, 0, 69, + 0, 25, "..." +// assigns the server's response to Str[27] +// and repeat this command until response is received +// parallel events execution recommended +@raw 5000, 0, 42, 0, 69, 0, 0, + 0, 7, 0, 27, "..." +// sends (42, 69, Str[29]) to server +@raw 5000, 0, 42, 0, 69, 0, 0, 0, 0, 0, 0, + 0, 29, "" +``` + +## Usage + +Add this to your EasyRPG.ini, which will also enable Maniacs: + +```ini +[Patch] +YNO = 1 +``` diff --git a/src/game_config_game.h b/src/game_config_game.h index fe9e3ec0c..6603fb0da 100644 --- a/src/game_config_game.h +++ b/src/game_config_game.h @@ -48,6 +48,7 @@ struct Game_ConfigGame { BoolConfigParam patch_rpg2k3_commands{ "RPG2k3 Event Commands", "Enable support for RPG2k3 event commands", "Patch", "RPG2k3Commands", false }; ConfigParam patch_anti_lag_switch{ "Anti-Lag Switch", "Disable event page refreshes when switch is set", "Patch", "AntiLagSwitch", 0 }; ConfigParam patch_direct_menu{ "Direct Menu", " Allows direct access to subscreens of the default menu", "Patch", "DirectMenu", 0 }; + BoolConfigParam patch_yno{ "YNOproject Extensions", "", "Patch", "YNO", false }; // Command line only BoolConfigParam patch_support{ "Support patches", "When OFF all patch support is disabled", "", "", true }; diff --git a/src/game_interpreter.cpp b/src/game_interpreter.cpp index fd390e037..d4667fa1a 100644 --- a/src/game_interpreter.cpp +++ b/src/game_interpreter.cpp @@ -26,10 +26,12 @@ #include #include "game_interpreter.h" #include "async_handler.h" +#include "async_op.h" #include "audio.h" #include "game_dynrpg.h" #include "filefinder.h" #include "game_destiny.h" +#include "game_interpreter_shared.h" #include "game_map.h" #include "game_event.h" #include "game_enemyparty.h" @@ -92,6 +94,7 @@ void Game_Interpreter::Clear() { _state = {}; _keyinput = {}; _async_op = {}; + pending_request = 0; } // Is interpreter running. @@ -794,6 +797,8 @@ bool Game_Interpreter::ExecuteCommand(lcf::rpg::EventCommand const& com) { return CmdSetup<&Game_Interpreter::CommandEasyRpgCloneMapEvent, 10>(com); case Cmd::EasyRpg_DestroyMapEvent: return CmdSetup<&Game_Interpreter::CommandEasyRpgDestroyMapEvent, 2>(com); + case static_cast(5000): + return CmdSetup<&Game_Interpreter::CommandYnoAsyncRpc, 4>(com); default: return true; } @@ -5406,6 +5411,11 @@ bool Game_Interpreter::CommandEasyRpgSetInterpreterFlag(lcf::rpg::EventCommand c Player::game_config.patch_rpg2k3_commands.Set(flag_value); if (flag_name == "rpg2k-battle") lcf::Data::system.easyrpg_use_rpg2k_battle_system = flag_value; + if (flag_name == "yno") { + Player::game_config.patch_yno.Set(flag_value); + if (flag_value) + Player::game_config.patch_maniac.Set(flag_value); + } return true; } @@ -5685,6 +5695,88 @@ bool Game_Interpreter::CommandEasyRpgDestroyMapEvent(lcf::rpg::EventCommand cons return true; } +bool Game_Interpreter::CommandYnoAsyncRpc(lcf::rpg::EventCommand const& com) { + // sends a string and two integers to the remote server + // + // @0, @1: RPC parameter 1 + // @2, @3: RPC parameter 2 + // @4, @5 (optional): variable ID to receive server-defined status code + // @6, @7 (optional): RPC return behavior + // 0: do nothing + // 1: assign to switch ID at parameter 3 return value as boolean (0 or non=0) + // 2: variable " integer + // 3: string " string + // 4: repeat this command until response received (block-in-place), applied as bitflag + // @8, @9 (optional): string/variable/switch ID to receive the response + // @10, @11 (optional): get string from string var + // @10 = 0: get from string ID at @11 + // @10 = 1: get from string ID at V[@11] + + if (pending_request) { + // each Interpreter is unique to an event page, so it can afford to block here. + if (auto entry = GMI().rpc_requests.find(pending_request); entry != GMI().rpc_requests.end()) { + auto [_1, _2, rpc_mode, target_var_id, rpc_status_var_id] = entry->second.params; + bool rpc_blocking = (rpc_mode & 4) >> 2; + if (entry->second.response.has_value()) { + if (rpc_status_var_id) + Main_Data::game_variables->Set(rpc_status_var_id, entry->second.code); + + std::string_view response(entry->second.response.value()); + int target_type = rpc_mode & 3; + switch (target_type) { + case 0: break; + case 1: // switch + Main_Data::game_switches->Set(target_var_id, atoi(response.data()) != 0); + break; + case 2: // Variable + Main_Data::game_variables->Set(target_var_id, atoi(response.data())); + break; + case 3: // String + Main_Data::game_strings->Asg({ target_var_id }, response); + break; + } + + GMI().rpc_requests.erase(entry); + pending_request = 0; + return true; + } + + // if (rpc_blocking) { + // Output::Warning("Blocking on request {}", pending_request); + // _async_op = AsyncOp::MakeYieldRepeat(); + // } + return !rpc_blocking; + } + } + + int param1 = ValueOrVariable(com.parameters[0], com.parameters[1]); + int param2 = ValueOrVariable(com.parameters[2], com.parameters[3]); + + int rpc_mode, rpc_var_id, rpc_status_var_id; + std::string payload; + + if (com.parameters.size() >= 6) + rpc_status_var_id = ValueOrVariable(com.parameters[4], com.parameters[5]); + if (com.parameters.size() >= 8) + rpc_mode = ValueOrVariable(com.parameters[6], com.parameters[7]); + if (com.parameters.size() >= 10) + rpc_var_id = ValueOrVariable(com.parameters[8], com.parameters[9]); + if (com.parameters.size() >= 12) { + payload = ToString(CommandStringOrVariable(com, 10, 11)); + } else { + payload = com.string; + } + + // if (rpc_blocking) { + // _async_op = AsyncOp::MakeYieldRepeat(); + // } + + pending_request = GMI().MakeRpcRequest(payload, param1, param2, rpc_mode, rpc_var_id, rpc_status_var_id); + + bool rpc_blocking = (rpc_mode & 4) >> 2; + return !rpc_blocking; +} + Game_Interpreter& Game_Interpreter::GetForegroundInterpreter() { return Game_Battle::IsBattleRunning() ? Game_Battle::GetInterpreter() diff --git a/src/game_interpreter.h b/src/game_interpreter.h index 8f5b40af3..6fa11dcc0 100644 --- a/src/game_interpreter.h +++ b/src/game_interpreter.h @@ -29,6 +29,7 @@ #include #include #include +#include "multiplayer/game_multiplayer.h" #include "system.h" #include #include @@ -303,6 +304,10 @@ class Game_Interpreter : public Game_BaseInterpreterContext bool CommandEasyRpgDestroyMapEvent(lcf::rpg::EventCommand const& com); bool CommandManiacGetGameInfo(lcf::rpg::EventCommand const& com); + // Online extensions + bool CommandYnoAsyncRpc(lcf::rpg::EventCommand const& com); + Game_Multiplayer::RequestId pending_request {0}; + void SetSubcommandIndex(int indent, int idx); uint8_t& ReserveSubcommandIndex(int indent); int GetSubcommandIndex(int indent) const; diff --git a/src/multiplayer/game_multiplayer.cpp b/src/multiplayer/game_multiplayer.cpp index ccc57ea33..de86dfe55 100644 --- a/src/multiplayer/game_multiplayer.cpp +++ b/src/multiplayer/game_multiplayer.cpp @@ -10,6 +10,7 @@ #include #include +#include #include "game_multiplayer.h" #include "../output.h" @@ -346,7 +347,7 @@ void Game_Multiplayer::InitConnection() { int rx; int ry; - + if (Game_Map::LoopHorizontal() && px - ox >= hmw) { rx = Game_Map::GetTilesX() - (px - ox); } else if (Game_Map::LoopHorizontal() && px - ox < hmw * -1) { @@ -441,6 +442,12 @@ void Game_Multiplayer::InitConnection() { Web_API::OnPlayerNameUpdated(p.name, p.id); }); + connection.RegisterHandler("rpc", [this] (RpcResponsePacket& p) { + if (auto req = rpc_requests.find(p.id); req != rpc_requests.end()) { + req->second.response = p.payload; + req->second.code = p.code; + } + }); } using namespace Messages::C2S; @@ -816,7 +823,7 @@ void Game_Multiplayer::Update() { auto old_list = &DrawableMgr::GetLocalList(); DrawableMgr::SetLocalList(&scene_map->GetDrawableList()); - + for (auto dcpi = dc_players.rbegin(); dcpi != dc_players.rend(); ++dcpi) { auto& ch = dcpi->ch; if (ch->GetBaseOpacity() > 0) { @@ -835,3 +842,14 @@ void Game_Multiplayer::Update() { if (session_connected) connection.FlushQueue(); } + +extern "C" EMSCRIPTEN_KEEPALIVE +void update_rpc_requests(int id) { + // Output::Warning("update_rpc_requests id={}", id); + if (auto entry = GMI().rpc_requests.find(id); entry != GMI().rpc_requests.end()) { + entry->second.response = "123"; + entry->second.code = 123; + } else { + // Output::Warning("Request {} already cancelled", id); + } +} diff --git a/src/multiplayer/game_multiplayer.h b/src/multiplayer/game_multiplayer.h index c537295e0..2d4cdce98 100644 --- a/src/multiplayer/game_multiplayer.h +++ b/src/multiplayer/game_multiplayer.h @@ -1,6 +1,7 @@ #ifndef EP_GAME_MULTIPLAYER_H #define EP_GAME_MULTIPLAYER_H +#include #include #include #include "../string_view.h" @@ -8,6 +9,9 @@ #include "../tone.h" #include #include "yno_connection.h" +#include +#include +#include "messages.h" class PlayerOther; @@ -48,6 +52,12 @@ class Game_Multiplayer { void SwitchSet(int switch_id, int value); void VariableSet(int var_id, int value); + using RequestId = uint32_t; + + // Allows up to five ints + template && ...)>> + RequestId MakeRpcRequest(std::string method, Args... args); + struct { bool enable_sounds{ true }; bool mute_audio{ false }; @@ -92,6 +102,17 @@ class Game_Multiplayer { std::unique_ptr> last_frame_flash; std::map> repeating_flashes; + struct RequestContext { + std::array params; + std::string method; + // if code is not 0, response is error message + std::optional response; + int code = 0; + explicit RequestContext(std::array _params, std::string _method) + : params(_params), method(std::move(_method)) {} + }; + std::map rpc_requests; + void SpawnOtherPlayer(int id); void ResetRepeatingFlash(); void InitConnection(); @@ -99,4 +120,23 @@ class Game_Multiplayer { inline Game_Multiplayer& GMI() { return Game_Multiplayer::Instance(); } +template +inline auto Game_Multiplayer::MakeRpcRequest(std::string method, Args... args) -> Game_Multiplayer::RequestId { + using Messages::C2S::RpcRequestPacket; + std::vector params {std::to_string(args)...}; + connection.SendPacketAsync(method, std::move(params)); + + unsigned id = RpcRequestPacket::LastId(); + // rpc_requests.insert_or_assign(id, (RequestContext){{args...}, method}); + rpc_requests.emplace(std::make_pair(id, RequestContext{{args...}, method})); + + // EM_ASM({ + // setTimeout(() => { + // Module.ccall("update_rpc_requests", null, ["number"], [$0]); + // }, 1000); + // }, id); + + return id; +} + #endif diff --git a/src/multiplayer/messages.h b/src/multiplayer/messages.h index b0e6b3d8c..4a3d44798 100644 --- a/src/multiplayer/messages.h +++ b/src/multiplayer/messages.h @@ -4,6 +4,7 @@ #include "connection.h" #include "packet.h" #include +#include #include #include "../game_pictures.h" @@ -119,7 +120,7 @@ namespace S2C { facing(Decode(v.at(1))) {} const int facing; }; - + class SpeedPacket : public PlayerPacket { public: SpeedPacket(const PL& v) @@ -149,7 +150,7 @@ namespace S2C { const int p; const int f; }; - + class RepeatingFlashPacket : public FlashPacket { public: RepeatingFlashPacket(const PL& v) @@ -357,6 +358,17 @@ namespace S2C { public: BadgeUpdatePacket(const PL& v) {} }; + + class RpcResponsePacket : public S2CPacket { + public: + RpcResponsePacket(const PL& v) + : id(stoi((std::string) v.at(0))), + code(stoi((std::string) v.at(1))), + payload(v.at(2)) {} + unsigned const id; + int code; + std::string payload; + }; } namespace C2S { using C2SPacket = Multiplayer::C2SPacket; @@ -589,6 +601,19 @@ namespace C2S { int action_bin; }; + class RpcRequestPacket : public C2SPacket { + public: + RpcRequestPacket(std::string _method, std::vector _params) : C2SPacket("rpc"), + id(++idseq), method(std::move(_method)), params(std::move(_params)) {} + std::string ToBytes() const override { return Build((int)id, method, params); } + unsigned const id; // to properly identify server responses + inline static unsigned LastId() noexcept { return idseq; } + protected: + inline static unsigned idseq = 0; + std::string method; + std::vector params; + }; + } } diff --git a/src/multiplayer/packet.h b/src/multiplayer/packet.h index 70b794ff7..1d0d951a4 100644 --- a/src/multiplayer/packet.h +++ b/src/multiplayer/packet.h @@ -1,9 +1,11 @@ #ifndef EP_MULTIPLAYER_PACKET_H #define EP_MULTIPLAYER_PACKET_H +#include #include #include #include +#include namespace Multiplayer { @@ -31,6 +33,16 @@ class C2SPacket : public Packet { static std::string ToString(int x) { return std::to_string(x); } static std::string ToString(bool x) { return x ? "1" : "0"; } static std::string ToString(std::string_view v) { return Sanitize(v); } + static std::string ToString(const std::vector& params) { + std::ostringstream out; + + for (auto it = params.cbegin(); it != params.cend(); ++it) { + if (it != params.cbegin()) out << PARAM_DELIM; + out << *it; + } + + return out.str(); + } template std::string Build(Args... args) const { diff --git a/src/player.h b/src/player.h index 13575c773..9f89924df 100644 --- a/src/player.h +++ b/src/player.h @@ -302,6 +302,8 @@ namespace Player { */ bool HasEasyRpgExtensions(); + bool HasYnoExtensions(); + /** * Update the game title displayed in the Player's UI */ @@ -509,4 +511,7 @@ inline bool Player::HasEasyRpgExtensions() { return game_config.patch_easyrpg.Get(); } +inline bool Player::HasYnoExtensions() { + return game_config.patch_yno.Get(); +} #endif