mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
netplay: input transforms stay per player online; rollback rows carry rx/ry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -219,6 +219,10 @@ struct ModPlugin {
|
||||
* run only inside the sandboxed own-view render
|
||||
* (psx_mod_render_local_view), never in the shared simulation. */
|
||||
bool netplay_local_view = false;
|
||||
/* netplay = "input": the plugin only transforms this player's own pad
|
||||
* before it is staged (psx_mod_set_pad_transform). Online it stays on per
|
||||
* player: the post-transform pad is what every peer simulates. */
|
||||
bool netplay_input = false;
|
||||
};
|
||||
|
||||
struct ModResource {
|
||||
@@ -354,6 +358,7 @@ struct ModResolution {
|
||||
std::string package_id;
|
||||
std::string feature_id;
|
||||
bool netplay_local_view = false;
|
||||
bool netplay_input = false;
|
||||
};
|
||||
std::vector<Plugin> plugins;
|
||||
struct Resource {
|
||||
|
||||
@@ -33,6 +33,9 @@ bool mod_runtime_commit_netplay_view(const std::filesystem::path& disc_path,
|
||||
std::string* error = nullptr);
|
||||
/* 1 while the session plan is such an own-view plan. */
|
||||
bool mod_runtime_netplay_view_active();
|
||||
/* 1 while that plan holds a [[plugin]] netplay = "input" (a pad transform
|
||||
* applied to this player's own pad at netplay staging). */
|
||||
bool mod_runtime_netplay_input_active();
|
||||
/* "package/feature" keys of `plan` that qualify as own-view features. */
|
||||
std::vector<std::string> mod_runtime_netplay_view_features(const ModResolution& plan);
|
||||
const std::string& mod_runtime_fingerprint();
|
||||
|
||||
@@ -32,7 +32,8 @@ typedef struct PsxNetplayRbBindings {
|
||||
int *input_delay;
|
||||
void (*publish_sio)(uint32_t tick); /* publish history or sealed rows to SIO */
|
||||
void (*apply_frame_slot)(int slot, uint32_t tick, uint16_t buttons,
|
||||
int8_t sx, int8_t sy, uint8_t analog);
|
||||
int8_t sx, int8_t sy, uint8_t analog,
|
||||
uint8_t rx, uint8_t ry);
|
||||
} PsxNetplayRbBindings;
|
||||
|
||||
void psx_netplay_rb_bind(const PsxNetplayRbBindings *b);
|
||||
|
||||
@@ -7826,6 +7826,8 @@ static void handle_unwatch(int id, const char *json)
|
||||
send_err(id, "watchpoint not found");
|
||||
}
|
||||
|
||||
static int s_trigger_override;
|
||||
static uint32_t s_trigger_lt, s_trigger_rt;
|
||||
static void handle_set_input(int id, const char *json)
|
||||
{
|
||||
char val_str[32];
|
||||
@@ -7848,9 +7850,25 @@ static void handle_set_input(int id, const char *json)
|
||||
int v = ax[i] < 0 ? 0x80 : (ax[i] > 255 ? 255 : ax[i]);
|
||||
s_axis_st[i] = (uint8_t)v;
|
||||
}
|
||||
/* Optional host triggers lt/rt (0..255) for a title pad transform, as a
|
||||
* gamepad's analog triggers would give it. Absent -> released. */
|
||||
{
|
||||
const int lt = json_get_int(json, "lt", -1), rt = json_get_int(json, "rt", -1);
|
||||
s_trigger_override = lt >= 0 || rt >= 0;
|
||||
s_trigger_lt = (uint32_t)(lt < 0 ? 0 : (lt > 255 ? 255 : lt));
|
||||
s_trigger_rt = (uint32_t)(rt < 0 ? 0 : (rt > 255 ? 255 : rt));
|
||||
}
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
int debug_server_get_trigger_override(uint32_t *lt, uint32_t *rt)
|
||||
{
|
||||
if (!s_trigger_override) return 0;
|
||||
*lt = s_trigger_lt;
|
||||
*rt = s_trigger_rt;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void handle_press(int id, const char *json)
|
||||
{
|
||||
int buttons = json_get_int(json, "buttons", -1);
|
||||
|
||||
@@ -6169,6 +6169,63 @@ static void netplay_present_gap_stats(uint32_t *p95_out, uint32_t *max_out) {
|
||||
/* Stage local pad + poll admit until published. Parks the guest fiber when
|
||||
* called from the vblank callback (or before scheduler entry). Latches one
|
||||
* sample per sim tick; stalls on INPUT_CONFIRM desync. */
|
||||
static void pad_ext_host_extras(void*, int s, uint32_t* flags, uint32_t* lt,
|
||||
uint32_t* rt);
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
extern "C" int debug_server_get_trigger_override(uint32_t* lt, uint32_t* rt);
|
||||
#endif
|
||||
|
||||
/* Netplay input transform ([[plugin]] netplay = "input"): this player's own
|
||||
* title pad transform runs on its local pad before staging, so the
|
||||
* transformed pad (buttons, sticks, presented type, NeGcon I/II) is what every
|
||||
* peer receives and simulates. Each peer transforms only its own seat. */
|
||||
static void netplay_transform_local_pad(PsxNetPad* pad, int override_active) {
|
||||
if (!PSXRecompV4::mod_runtime_netplay_input_active() ||
|
||||
psx_netplay_is_spectator())
|
||||
return;
|
||||
const int seat = psx_netplay_local_slot();
|
||||
if (seat < 0 || seat >= PSX_MAX_PLAYERS) return;
|
||||
PSXModPadFrame f{};
|
||||
f.struct_size = sizeof f;
|
||||
f.player = (uint32_t)seat;
|
||||
f.buttons = pad->buttons;
|
||||
f.lx = pad->lx; f.ly = pad->ly; f.rx = pad->rx; f.ry = pad->ry;
|
||||
f.type = pad->analog;
|
||||
if (override_active) {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
uint32_t lt = 0, rt = 0;
|
||||
if (debug_server_get_trigger_override(<, &rt)) {
|
||||
f.host_flags = PSX_MOD_PAD_HOST_GAMEPAD | PSX_MOD_PAD_HOST_LT |
|
||||
PSX_MOD_PAD_HOST_RT;
|
||||
f.host_lt = lt;
|
||||
f.host_rt = rt;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
const int card = psx_netplay_input_player();
|
||||
pad_ext_host_extras(nullptr, card >= 0 ? card : 0, &f.host_flags,
|
||||
&f.host_lt, &f.host_rt);
|
||||
}
|
||||
PSXModPadOutput stock{}, o{};
|
||||
stock.struct_size = sizeof stock;
|
||||
stock.buttons = f.buttons; stock.type = f.type;
|
||||
stock.lx = f.lx; stock.ly = f.ly; stock.rx = f.rx; stock.ry = f.ry;
|
||||
if (!mod_pad_transform_run((uint32_t)seat, &f, &stock, &o)) return;
|
||||
pad->buttons = (uint16_t)o.buttons;
|
||||
pad->analog = (uint8_t)o.type;
|
||||
if (o.type == PSX_MOD_PAD_NEGCON) {
|
||||
pad->lx = (uint8_t)o.lx;
|
||||
pad->ly = (uint8_t)o.negcon_l;
|
||||
pad->rx = (uint8_t)o.negcon_i;
|
||||
pad->ry = (uint8_t)o.negcon_ii;
|
||||
} else if (o.type == PSX_MOD_PAD_DIGITAL) {
|
||||
pad->lx = pad->ly = pad->rx = pad->ry = 0x80;
|
||||
} else {
|
||||
pad->lx = (uint8_t)o.lx; pad->ly = (uint8_t)o.ly;
|
||||
pad->rx = (uint8_t)o.rx; pad->ry = (uint8_t)o.ry;
|
||||
}
|
||||
}
|
||||
|
||||
static void netplay_barrier_admit(int override) {
|
||||
if (!psx_netplay_active()) return;
|
||||
/* Launcher/game window teardown can leave a queued SDL_QUIT; draining it
|
||||
@@ -6333,6 +6390,7 @@ static void netplay_barrier_admit(int override) {
|
||||
} else {
|
||||
capture_local_human_pad(&local);
|
||||
}
|
||||
netplay_transform_local_pad(&local, override >= 0);
|
||||
psx_netplay_stage_local(&local);
|
||||
} else if (psx_start_bisect_spin_log() && !g_headless) {
|
||||
/* Dense SDL-only samples while admit waits without capture. */
|
||||
|
||||
@@ -2540,10 +2540,12 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
|
||||
{
|
||||
const std::string netplay =
|
||||
toml::find_or<std::string>(v, "netplay", "");
|
||||
if (!netplay.empty() && netplay != "local_view")
|
||||
if (!netplay.empty() && netplay != "local_view" &&
|
||||
netplay != "input")
|
||||
throw std::runtime_error(
|
||||
"plugin netplay must be \"local_view\"");
|
||||
"plugin netplay must be \"local_view\" or \"input\"");
|
||||
plugin.netplay_local_view = netplay == "local_view";
|
||||
plugin.netplay_input = netplay == "input";
|
||||
}
|
||||
read_conditions(v, out.options, plugin.feature_id,
|
||||
plugin.when, "plugin");
|
||||
@@ -3803,6 +3805,7 @@ ModResolution ModPackageManager::resolve(const std::string& game_id,
|
||||
resolved.package_id = package->id;
|
||||
resolved.feature_id = plugin->feature_id;
|
||||
resolved.netplay_local_view = plugin->netplay_local_view;
|
||||
resolved.netplay_input = plugin->netplay_input;
|
||||
result.plugins.push_back(std::move(resolved));
|
||||
}
|
||||
for (const ModResource& resource : package->resources) {
|
||||
|
||||
@@ -1409,7 +1409,7 @@ std::vector<std::string> mod_runtime_netplay_view_features(const ModResolution&
|
||||
for (const ModResolution::Plugin& p : plan.plugins) {
|
||||
const std::string key = p.package_id + "/" + p.feature_id;
|
||||
auto it = ok.find(key);
|
||||
const bool good = p.netplay_local_view;
|
||||
const bool good = p.netplay_local_view || p.netplay_input;
|
||||
ok[key] = it == ok.end() ? good : (it->second && good);
|
||||
}
|
||||
for (const ModResolution::Write& w : plan.writes)
|
||||
@@ -1428,6 +1428,14 @@ std::vector<std::string> mod_runtime_netplay_view_features(const ModResolution&
|
||||
return out;
|
||||
}
|
||||
|
||||
bool mod_runtime_netplay_input_active() {
|
||||
const RuntimeMods& s = state();
|
||||
if (!s.netplay_view_plan || !s.plan.ok) return false;
|
||||
for (const ModResolution::Plugin& p : s.plan.plugins)
|
||||
if (p.netplay_input) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool mod_runtime_netplay_view_active() {
|
||||
const RuntimeMods& s = state();
|
||||
return s.netplay_view_plan && s.plan.ok && !s.plan.plugins.empty();
|
||||
|
||||
@@ -41,6 +41,8 @@ void netplay_ih_pad_to_frame(const PsxNetPad *pad, uint32_t tick, uint8_t predic
|
||||
out->stick_x = u8_to_i8_stick(n.lx);
|
||||
out->stick_y = u8_to_i8_stick(n.ly);
|
||||
out->analog = n.analog;
|
||||
out->rx = n.rx; /* the rest of the pad: right stick, NeGcon I/II */
|
||||
out->ry = n.ry;
|
||||
}
|
||||
|
||||
void netplay_ih_frame_to_pad(const RNetRbFrame *frame, PsxNetPad *pad)
|
||||
@@ -56,6 +58,8 @@ void netplay_ih_frame_to_pad(const RNetRbFrame *frame, PsxNetPad *pad)
|
||||
pad->lx = i8_to_u8_stick(frame->stick_x);
|
||||
pad->ly = i8_to_u8_stick(frame->stick_y);
|
||||
pad->analog = frame->analog <= PSX_NETPAD_TYPE_MAX ? frame->analog : 0u;
|
||||
pad->rx = frame->rx;
|
||||
pad->ry = frame->ry;
|
||||
psx_netplay_normalize_pad(pad);
|
||||
}
|
||||
|
||||
|
||||
@@ -2501,7 +2501,8 @@ static void np_publish_hist_sio(uint32_t tick)
|
||||
}
|
||||
|
||||
static void np_rb_apply_frame_slot(int slot, uint32_t tick, uint16_t buttons,
|
||||
int8_t sx, int8_t sy, uint8_t analog)
|
||||
int8_t sx, int8_t sy, uint8_t analog,
|
||||
uint8_t rx, uint8_t ry)
|
||||
{
|
||||
RNetRbFrame row;
|
||||
PsxNetPad pad;
|
||||
@@ -2511,6 +2512,8 @@ static void np_rb_apply_frame_slot(int slot, uint32_t tick, uint16_t buttons,
|
||||
row.stick_x = sx;
|
||||
row.stick_y = sy;
|
||||
row.analog = analog <= PSX_NETPAD_TYPE_MAX ? analog : 0u;
|
||||
row.rx = rx;
|
||||
row.ry = ry;
|
||||
row.is_valid = 1;
|
||||
netplay_ih_frame_to_pad(&row, &pad);
|
||||
force_session_pads_connected(g_np.slot_count);
|
||||
@@ -2613,7 +2616,8 @@ static void np_scrub_ahead_predicted(int slot, rnet_u32 release_tick,
|
||||
continue;
|
||||
if (row.buttons == released->buttons &&
|
||||
row.stick_x == released->stick_x &&
|
||||
row.stick_y == released->stick_y)
|
||||
row.stick_y == released->stick_y &&
|
||||
row.rx == released->rx && row.ry == released->ry)
|
||||
continue;
|
||||
scrub = *released;
|
||||
scrub.tick = t;
|
||||
@@ -2674,7 +2678,8 @@ static void np_tip_hold_coalesce_ahead(void)
|
||||
netplay_ih_pad_to_frame(&pad, t, 0, &wire_frame);
|
||||
if (wire_frame.buttons == tip_row.buttons &&
|
||||
wire_frame.stick_x == tip_row.stick_x &&
|
||||
wire_frame.stick_y == tip_row.stick_y)
|
||||
wire_frame.stick_y == tip_row.stick_y &&
|
||||
wire_frame.rx == tip_row.rx && wire_frame.ry == tip_row.ry)
|
||||
continue;
|
||||
edge = t;
|
||||
break;
|
||||
@@ -2862,6 +2867,7 @@ static void np_rollback_reconcile_wire(void)
|
||||
rnet_u32 wire;
|
||||
int pads_differ;
|
||||
int buttons_differ;
|
||||
int rest_differ;
|
||||
|
||||
if (!netplay_ih_get(&g_np.ih, slot, t, &published))
|
||||
continue;
|
||||
@@ -2877,7 +2883,11 @@ static void np_rollback_reconcile_wire(void)
|
||||
netplay_ih_frame_to_contract(&published, &pub_c);
|
||||
netplay_ih_frame_to_contract(&wire_frame, &wire_c);
|
||||
buttons_differ = (pub_c.buttons != wire_c.buttons);
|
||||
pads_differ = buttons_differ ||
|
||||
/* The rest of the pad (right stick, NeGcon I/II) is not in the
|
||||
* contract view; any difference there is a real mispredict. */
|
||||
rest_differ = published.rx != wire_frame.rx ||
|
||||
published.ry != wire_frame.ry;
|
||||
pads_differ = buttons_differ || rest_differ ||
|
||||
(pub_c.stick_x != wire_c.stick_x) ||
|
||||
(pub_c.stick_y != wire_c.stick_y);
|
||||
completed = (sim > t) ? 1u : 0u;
|
||||
@@ -2888,7 +2898,7 @@ static void np_rollback_reconcile_wire(void)
|
||||
* release) without opening an episode. Distinct from the old
|
||||
* ungated menu soft-promote that forked RAM: HC fail-closed
|
||||
* means a real state-affecting press still rewinds. */
|
||||
if (buttons_differ && completed &&
|
||||
if (buttons_differ && !rest_differ && completed &&
|
||||
np_hc_silent_promote_enabled() &&
|
||||
netplay_hc_confirm_through(&g_np.hc, t)) {
|
||||
(void)netplay_ih_promote(&g_np.ih, slot, &wire_frame);
|
||||
@@ -2937,6 +2947,8 @@ static void np_rollback_reconcile_wire(void)
|
||||
gate_ctx.tick = t;
|
||||
d = rnet_input_contract_stick_replace_decide(
|
||||
&pub_c, &wire_c, completed, ¶ms, &gates);
|
||||
if (rest_differ && completed)
|
||||
d = nRNetInputContractRewind;
|
||||
if (rnet_input_contract_decision_is_rewind(d)) {
|
||||
/* FMV/settle only: soft-promote releases (skip must rewind on
|
||||
* press). Menu soft-promote + hold-last invent forked RAM
|
||||
|
||||
@@ -1482,7 +1482,8 @@ static void dump_post_diverge_diag(const char *why)
|
||||
hist.is_predicted ? "P" : "");
|
||||
if (have_s && (seal.buttons != hist.buttons ||
|
||||
seal.stick_x != hist.stick_x ||
|
||||
seal.stick_y != hist.stick_y))
|
||||
seal.stick_y != hist.stick_y ||
|
||||
seal.rx != hist.rx || seal.ry != hist.ry))
|
||||
fprintf(stderr, "!");
|
||||
} else {
|
||||
fprintf(stderr, "/h=----");
|
||||
@@ -4170,7 +4171,7 @@ static void publish_sealed_sio(uint32_t tick)
|
||||
if (!rnet_rb_get_sealed_frame(g_rb, slot, tick, &row) || !row.is_valid)
|
||||
continue;
|
||||
g_b.apply_frame_slot(slot, tick, row.buttons, row.stick_x, row.stick_y,
|
||||
row.analog);
|
||||
row.analog, row.rx, row.ry);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user