Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request - Communication System + YNOP Data Get/Set system #55

Open
jetrotal opened this issue Feb 2, 2025 · 1 comment
Open

Comments

@jetrotal
Copy link
Contributor

jetrotal commented Feb 2, 2025

Heyo @Desdaemon, I put together some of what we spoke earlier about this feature:

Simple Communication System + YNOP Data receiver for EasyRPG

Allows the player to recieve or send data to the server

1. Basic Operations

GET Operation (0)

// Client sends:
{
    "request": "path/to/data",
    "type": 0,              // GET operation
}

// Server responds:
{
    "status": "ok",
    "data": "string_value"  // Converted based on target_type
}

SET Operation (1)

// Client sends:
{
    "request": "path/to/data",
    "type": 1,              // SET operation
    "data": "string_value", // Value to set
}

// Server responds:
{
    "status": "ok",
    "error": null          // Optional: contains error message if status is "error"
}

2. Other Features

Player Data Operations

// GET Player Name: "player/name"
Request:
{
    "request": "player/name",
    "type": 0
}

Response:
{
    "status": "ok",
    "data": "PlayerName"
}

3. Error Handling

// Error Response Format
{
    "status": "error",
    "error": "Detailed error message",
    "code": 404           // Optional: HTTP-style error code
}

// Common Error Types
{
    "status": "error",
    "error": "File not found",
    "code": 404
}

{
    "status": "error",
    "error": "Invalid JSON format",
    "code": 400
}

4. Command Structure (5000)

(Just for reference, code is almost pseudo-code)

//Inside GAME_INTERPRETER.CPP edit ExecuteCommand:
bool Game_Interpreter::ExecuteCommand(lcf::rpg::EventCommand const& com) { 
    switch (static_cast<Cmd>(com.code)) {
        case static_cast<Cmd>(5000):  // or "Cmd::YNOP_Bridge" If you want to mess with liblcf
            return CommandYNOPBridge(com);
    }
}

// Then add this method:
bool Game_Interpreter::CommandYNOPBridge(lcf::rpg::EventCommand const& com) { //5000
    
    std::string request_path = ToString(CommandStringOrVariable(com, 8, 9));
    int operation = ValueOrVariable(com.parameters[0], com.parameters[1]);
    int source_var_id = ValueOrVariable(com.parameters[2], com.parameters[3]);
    int target_type = ValueOrVariable(com.parameters[4], com.parameters[5]);
    int target_var_id = ValueOrVariable(com.parameters[6], com.parameters[7]);

    std::stringstream json;

    if (operation == 0) { //get
        json << "{"
             << "\"request\":\"" << request_path << "\","
             << "\"type\":" << operation 
             << "}";

        std::string response = ProcessBridgeRequest(json.str()); // Process GET request
        
        // Handle the response
        if (!response.empty()) {
            switch (target_type) {
                case 0: // Switch
                    Main_Data::game_switches->Set(target_var_id, atoi(response.c_str()) != 0);
                    break;
                case 1: // Variable
                    Main_Data::game_variables->Set(target_var_id, atoi(response.c_str()));
                    break;
                case 2: // String
                    Main_Data::game_strings->Set(target_var_id, response);
                    break;
                default:
                    Output::Warning("CommandYNOPBridge: Unsupported target_type {}", target_type);
                    return true;
            }
        }
    }

    if (operation == 1) { //set
        std::string value_data;
        
        switch (target_type) {
            case 0: // Switch
                value_data = std::to_string(Main_Data::game_switches->Get(target_var_id));
                break;
            case 1: // Variable
                value_data = std::to_string(Main_Data::game_variables->Get(target_var_id));
                break;
            case 2: // String
                value_data = ToString(Main_Data::game_strings->Get(target_var_id));
                break;
            default:
                Output::Warning("CommandYNOPBridge: Unsupported target_type {}", target_type);
                return true;
        }

        json << "{"
             << "\"request\":\"" << request_path << "\","
             << "\"type\":" << operation << ","
             << "\"data\":\"" << value_data << "\""
             << "}";
        
        std::string response = ProcessBridgeRequest(json.str()); // Process SET request
        
        // Handle response status if needed (stored in target_var_id)
        if (!response.empty() && target_var_id > 0) {
            Main_Data::game_strings->Set(target_var_id, response);
        }
    }

    return true;
}

5. Storage Guidelines

Server-Side Storage

  • Use flat file storage for JSON data
    • One file per map/player/configuration
    • Directory structure matches request paths
    • Example: cu/playermaps/PlayerName/mapname.json
  • File Operations
    • Read entire file for GET operations
    • Write atomic updates for SET operations
    • Use file locking for concurrent access
  • Basic Validation
    • Check for Logged Players
    • Check JSON syntax
    • Verify required fields
    • Validate data types

Client-Side Handling

  • Variable Types
    • Switches (0): Boolean values
    • Variables (1): Numeric values
    • Strings (2): Text and JSON data
@Desdaemon
Copy link
Contributor

Desdaemon commented Mar 7, 2025

@jetrotal Sorry for the delay since I have been away from contributing to ynoengine. I have a few suggestions:

  • We should keep the structure of a request uniform; I'm thinking of a RpcPacket that is roughly modelled after JSONRPC which will look something like this:
// in namespace C2S
class RpcPacket : public C2SPacket {
public:
  RpcPacket(std::string _method, std::vector<std::string> _params) : C2SPacket("rpc"),
    method(_method), params(_params), id(++idseq) {}
  std::string ToBytes() const override { return Build(id, method, params); }
  unsigned const id; // to properly identify server responses
protected:
  static unsigned idseq = 0;
  std::string method;
  std::vector<std::string> params;
};

The client would send this packet to the server and create a callback to asynchronously handle the server's response.

  • As a consequence, it is unlikely that ProcessBridgeRequest will be able to return a string, but instead will accept a callback:
// an example of how it may be defined

template <typename... Params> // each Arg must be compatible with std::to_string()
RpcRequestAsync* ProcessBridgeRequest(std::string method, Params... params);

// modelled after FileRequestAsync
class RpcRequestAsync {
  // unlike FileRequestAsync::Bind, we won't require caller to hold onto the request
  // since it is mainly called for side effects
  void Bind(std::function<void(std::string)>);
};

// then they are used like this:
auto* req = ProcessBridgeRequest("read", "some/data/path");
req->Bind([](std::string data) { ... });
  • We might also need to figure out when to drop the request, if the server doesn't eventually respond in time or fails to do so at all. File requests are dropped if the caller cannot hold onto its binding, so perhaps we could do the same and specify whether the intended effect is local or global, i.e. can it be observed outside of the current map.

If this distinction is important, we can split ProcessBridgeRequest into another variant that does return a binding and force the caller to hold onto it in some manner, perhaps on the Scene itself.

Would you be willing to submit a PR for this? I apologize for not being able to spend more time on this, but I should be able to work on the server part regardless. Most validation can be done on the server instead, and any protocol between client and server can be defined in an adhoc manner without recompiling the engine (except to change client's behavior).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants