Fix transition capture and enforce title-disabled mod packages

This commit is contained in:
Matthew Stanley
2026-10-17 13:15:40 -07:00
parent bc0c521575
commit 40892ebb2c
11 changed files with 142 additions and 10 deletions
+5
View File
@@ -464,6 +464,10 @@ public:
developer_channel_ = visible;
}
bool developer_channel_visible() const { return developer_channel_; }
/* Trusted title policy: hide and deactivate these ids even when an old
* bundle or an installed copy survives an update. Saved choices stay
* dormant and files remain untouched. Set before scan(). */
void set_disabled_packages(std::vector<std::string> ids) { disabled_packages_ = std::move(ids); }
/* Build-owned catalog: wiped and re-staged by every build. */
std::filesystem::path bundled_root() const { return root_ / "bundled"; }
@@ -588,6 +592,7 @@ private:
bool developer_channel_ = kDeveloperChannelDefault;
std::vector<std::string> scan_errors_;
std::map<std::string, std::map<std::string, ModPackage>> packages_;
std::vector<std::string> disabled_packages_;
std::map<std::string, ModSelection> selections_;
std::map<std::string, std::filesystem::path> sources_;
};
+11 -2
View File
@@ -1023,7 +1023,8 @@ int psx_mod_set_controller_source_connected(uint32_t player, int connected);
* host_flags bit 0 = a
* gamepad is assigned, bit 1 / bit 2 = it has a left / right trigger axis;
* host_lt / host_rt are those triggers, 0 released .. 255 fully pressed
* (0 when absent or while the savestate input guard is armed).
* (0 when absent or while the savestate input guard is armed). Bit 3 marks
* axes/type supplied by an offline input source (PSX_MOD_PAD_HOST_SOURCE).
*
* The output arrives pre-filled with the stock pad (what the player gets
* without a transform, so a digital pad keeps its fold and centred sticks;
@@ -1050,7 +1051,10 @@ enum {
enum {
PSX_MOD_PAD_HOST_GAMEPAD = 1u << 0,
PSX_MOD_PAD_HOST_LT = 1u << 1,
PSX_MOD_PAD_HOST_RT = 1u << 2
PSX_MOD_PAD_HOST_RT = 1u << 2,
/* Axes/type came from an offline input source, rather than the host.
* Transforms can preserve already transformed diagnostic recordings. */
PSX_MOD_PAD_HOST_SOURCE = 1u << 3
};
typedef struct PSXModPadFrame {
uint32_t struct_size, player, buttons, lx, ly, rx, ry, type;
@@ -1147,6 +1151,11 @@ int psx_mod_set_local_mouse_policy(const PSXModMousePolicy* policy);
/* Optional action-game capture: click once to capture, Escape to release.
* Later clicks reach the normal bindings. Existing hold-to-look is default. */
int psx_mod_set_local_mouse_persistent_capture(int enabled);
/* Optional persistent-capture companion: keep existing capture through title
* eligibility changes (menus/cutscenes/loading), Start and host stalls, while
* clearing mouse motion and suspending output. Focus loss, host UI, disconnect,
* native right-stick takeover and Escape still release. Never auto-acquires. */
int psx_mod_set_local_mouse_capture_across_transitions(int enabled);
/*
* Register a C plugin before main() on the compilers supported by the runtime.
+46 -7
View File
@@ -23,7 +23,7 @@ void LocalMousePolicy::clear() {
clock_valid_ = false;
clock_ = event_clock_ = release_clock_ = 0;
conflict_noticed_ = capture_failure_noticed_ = false;
persistent_ = false;
persistent_ = transitions_ = suspended_ = false;
}
bool LocalMousePolicy::persistent_capture(bool enabled) {
if (!installed()) return false;
@@ -31,6 +31,12 @@ bool LocalMousePolicy::persistent_capture(bool enabled) {
persistent_ = enabled;
return true;
}
bool LocalMousePolicy::capture_across_transitions(bool enabled) {
if (!installed()) return false;
reset();
transitions_ = enabled;
return true;
}
void LocalMousePolicy::dispatch(uint32_t type, uint64_t t, double dx, double dy) {
if (!installed()) return;
const PSXModMouseEvent event{sizeof(PSXModMouseEvent), type, t, dx, dy};
@@ -39,6 +45,7 @@ void LocalMousePolicy::dispatch(uint32_t type, uint64_t t, double dx, double dy)
void LocalMousePolicy::reset() {
if (captured_) backend_.capture(backend_.context, false);
captured_ = false;
suspended_ = false;
release_clock_ = clock_;
event_valid_ = false;
hold_blocked_ = hold_down_;
@@ -54,12 +61,33 @@ bool LocalMousePolicy::allowed() const {
return installed() && host_.live && host_.focused && host_.connected &&
host_.analog && !host_.native_right && !host_.start && policy_.eligible(host_.buttons);
}
bool LocalMousePolicy::capture_allowed() const {
return installed() && host_.live && host_.focused && host_.connected &&
host_.analog && !host_.native_right;
}
void LocalMousePolicy::suspend() {
release_clock_ = clock_;
event_valid_ = false;
hold_blocked_ = hold_down_;
hold_active_ = false;
if (!suspended_) dispatch(PSX_MOD_MOUSE_RESET);
suspended_ = true;
}
void LocalMousePolicy::update(uint64_t now, LocalMouseHost host) {
const bool stalled = clock_valid_ && (now < clock_ || now - clock_ > StallNs);
clock_ = now;
clock_valid_ = true;
host_ = host;
if (stalled || !allowed()) reset();
if (transitions_ && persistent_ && captured_ && capture_allowed()) {
if (stalled || !allowed()) suspend();
else if (suspended_) {
// Reject motion queued during the transition; resuming never
// replays an old camera target or requires another click.
release_clock_ = now;
event_valid_ = false;
suspended_ = false;
}
} else if (stalled || !allowed()) reset();
if (host.hold_conflict && !conflict_noticed_) {
backend_.notice(backend_.context, "Mouse hold disabled: control is bound to P1");
conflict_noticed_ = true;
@@ -91,12 +119,13 @@ bool LocalMousePolicy::control(uint64_t t, uint64_t now, MouseControl control,
const bool fresh = down && !activation_down_ && !repeat;
activation_down_ = down;
if (!down) {
if (persistent_ && captured_ && allowed()) {
if (persistent_ && captured_ && (allowed() ||
(transitions_ && capture_allowed()))) {
const bool owned = activation_owned_;
activation_owned_ = false;
// t=0 is the backend's release reconciliation, not a queued
// event. Malformed real event times still release capture.
if (t) (void)valid_time(t, now);
if (t && !(transitions_ && t < release_clock_)) (void)valid_time(t, now);
return owned;
}
// Release is unconditional: a future/backwards event time cannot
@@ -150,7 +179,7 @@ bool LocalMousePolicy::control(uint64_t t, uint64_t now, MouseControl control,
event_clock_ = std::max(event_clock_, release_time);
event_valid_ = true;
}
} else if (captured_ && fresh && !host_.hold_conflict && !hold_blocked_) {
} else if (captured_ && allowed() && fresh && !host_.hold_conflict && !hold_blocked_) {
if (valid_time(t, now)) {
hold_active_ = true;
dispatch(PSX_MOD_MOUSE_HOLD_PRESS, t);
@@ -170,14 +199,24 @@ void LocalMousePolicy::sync_left(uint64_t now, bool physically_down) {
}
void LocalMousePolicy::motion(uint64_t t, uint64_t now, double dx, double dy) {
if (!captured_) return;
if (!allowed() || !std::isfinite(dx) || !std::isfinite(dy) ||
if (!allowed()) {
if (transitions_ && persistent_ && capture_allowed()) suspend();
else reset();
return;
}
if (transitions_ && t < release_clock_) return;
if (!std::isfinite(dx) || !std::isfinite(dy) ||
std::abs(dx) > 1e9 || std::abs(dy) > 1e9) { reset(); return; }
if (!valid_time(t, now) || (dx == 0.0 && dy == 0.0)) return;
dispatch(PSX_MOD_MOUSE_MOTION, t, dx, dy);
}
void LocalMousePolicy::sample(uint64_t now, uint8_t& rx, uint8_t& ry) {
if (!captured_) return;
if (!allowed()) { reset(); return; }
if (!allowed()) {
if (transitions_ && persistent_ && capture_allowed()) suspend();
else reset();
return;
}
PSXModMouseOutput out{sizeof(PSXModMouseOutput), 0, 128, 128};
policy_.sample(now, &out);
if (out.struct_size != sizeof(out) || out.override_right > 1 || out.rx > 255 || out.ry > 255) {
+4
View File
@@ -22,6 +22,7 @@ public:
explicit LocalMousePolicy(LocalMouseBackend backend) : backend_(backend) {}
bool install(const PSXModMousePolicy* policy);
bool persistent_capture(bool enabled);
bool capture_across_transitions(bool enabled);
void clear();
void reset();
void update(uint64_t now, LocalMouseHost host);
@@ -35,6 +36,8 @@ public:
MouseControl hold_control() const;
private:
bool allowed() const;
bool capture_allowed() const;
void suspend();
bool valid_time(uint64_t t, uint64_t now);
void dispatch(uint32_t type, uint64_t t = 0, double dx = 0, double dy = 0);
LocalMouseBackend backend_;
@@ -47,5 +50,6 @@ private:
bool hold_active_ = false; // accepted interpretation, separate from physical state
bool conflict_noticed_ = false, capture_failure_noticed_ = false;
bool persistent_ = false;
bool transitions_ = false, suspended_ = false;
};
} // namespace psx
+3
View File
@@ -97,6 +97,9 @@ extern "C" int psx_mod_set_local_mouse_policy(const PSXModMousePolicy* policy) {
extern "C" int psx_mod_set_local_mouse_persistent_capture(int enabled) {
return (enabled == 0 || enabled == 1) && state().persistent_capture(enabled != 0);
}
extern "C" int psx_mod_set_local_mouse_capture_across_transitions(int enabled) {
return (enabled == 0 || enabled == 1) && state().capture_across_transitions(enabled != 0);
}
void psx_local_mouse_clear() { state().clear(); host_ = {}; window_ = nullptr; }
bool psx_local_mouse_installed() { return state().installed(); }
void psx_local_mouse_reset() {
+2
View File
@@ -2765,6 +2765,8 @@ bool ModPackageManager::scan_root(const fs::path& packages_root,
": package path does not match manifest id/version");
continue;
}
if (std::find(disabled_packages_.begin(), disabled_packages_.end(), package.id) != disabled_packages_.end())
continue;
if (!developer_channel_ && !strip_developer_features(package)) {
/* Every feature was developer-channel, so on this build the
* package has nothing to offer. Not an error: it is the
+11
View File
@@ -1524,6 +1524,17 @@ bool mod_runtime_initialize(const std::filesystem::path& root,
s.disc_enabled = false;
s.disc_guard_failed = false;
s.manager.set_root(root);
// Optional comma-separated trusted title policy, stronger than omitting
// bundled manifests: stale/installed copies cannot reactivate these ids.
#ifdef PSX_MOD_DISABLED_PACKAGES
std::vector<std::string> disabled_packages;
std::istringstream disabled_stream(PSX_MOD_DISABLED_PACKAGES);
for (std::string id; std::getline(disabled_stream, id, ','); )
if (!id.empty()) disabled_packages.push_back(id);
s.manager.set_disabled_packages(std::move(disabled_packages));
#else
s.manager.set_disabled_packages({});
#endif
s.game_id = game_id;
s.entry_phys = game_entry_pc & 0x1FFFFFFFu;
#if defined(PSX_LAUNCHER_MOD_COMMIT_WORKER_SAFE)
+1
View File
@@ -39,6 +39,7 @@ static void pad_ext_transform(const PadExtHooks *h, int s, int guarded,
}
}
if (h->host_extras) h->host_extras(h->ctx, s, &f.host_flags, &f.host_lt, &f.host_rt);
if (from_source) f.host_flags |= PSX_MOD_PAD_HOST_SOURCE;
if (guarded) f.host_lt = f.host_rt = 0;
if (!h->pad_transform(h->ctx, s, &f, &stock, &o)) return;
if (guarded) {
+43
View File
@@ -278,6 +278,49 @@ int main() {
lost.clear();
}
// Action games retain relative capture across native menus, cutscenes,
// Start and loading stalls, but discard all motion from those intervals.
{
psx::LocalMousePolicy game({nullptr,capture,suppress,notice});
CHECK(game.install(&callbacks));
CHECK(game.persistent_capture(true));
CHECK(game.capture_across_transitions(true));
uint64_t now=20000*Ms;
game.update(now,host);
CHECK(game.control(now,now,psx::MouseControl::Left,true,false));
CHECK(game.control(now+Ms,now+Ms,psx::MouseControl::Left,false,false));
for(int gate=0;gate<3;++gate) {
now+=10*Ms;game.update(now,host);game.motion(now,now,30,0);CHECK(active);
auto off=host;
if(gate==0)guest=false;
if(gate==1)off.start=true;
if(gate==2)now+=300*Ms;
game.update(now+Ms,off);CHECK(game.captured() && !active);
game.control(now-Ms,now+Ms,psx::MouseControl::Left,false,false);
CHECK(game.captured()); // queued release cannot expose the cursor
if(gate!=2)game.motion(now+2*Ms,now+2*Ms,30,0);
CHECK(!active && game.captured());
game.control(now+2*Ms,now+2*Ms,psx::MouseControl::Left,false,false);
CHECK(game.captured());
guest=true;game.update(now+3*Ms,host);
game.motion(now+2*Ms,now+3*Ms,30,0);CHECK(!active && game.captured());
game.sample(now+3*Ms,x,y);CHECK(x==128 && y==128);
game.motion(now+4*Ms,now+4*Ms,30,0);CHECK(active);
}
for(int gate=0;gate<5;++gate) {
now+=10*Ms;auto off=host;
if(gate==0)off.focused=false;if(gate==1)off.live=false;
if(gate==2)off.connected=false;if(gate==3)off.analog=false;
if(gate==4)off.native_right=true;
game.update(now,off);CHECK(!game.captured() && !active);
game.update(now+Ms,host);CHECK(!game.captured());
CHECK(game.control(now+2*Ms,now+2*Ms,psx::MouseControl::Left,true,false));
game.control(now+3*Ms,now+3*Ms,psx::MouseControl::Left,false,false);
}
CHECK(game.control(now+4*Ms,now+4*Ms,psx::MouseControl::Escape,true,false));
game.update(now+5*Ms,host);CHECK(!game.captured());
game.clear();
}
policy.reset();host.hold_conflict=false;capture_ok=false;CHECK(acquire());CHECK(!policy.captured());
const unsigned failure_notices=notices;CHECK(acquire());CHECK(notices==failure_notices);
policy.clear(); CHECK(!policy.installed() && !policy.captured());
+15
View File
@@ -1798,6 +1798,21 @@ int main() {
"both absent packages must be reported as dormant selections");
check(std::find(dormant.begin(), dormant.end(), "kept.mod") == dormant.end(),
"a present package is never dormant");
// Strong title policy also suppresses stale bundles and installed
// copies. Preserve the user's choice and files for a later re-enable.
write_text(stale_root / "installed/kept.mod/1.0.0/manifest.toml", kept);
stale.set_disabled_packages({"kept.mod"});
check(stale.scan(&error), error.c_str());
const auto disabled = stale.resolve("SLUS-TEST");
check(disabled.ok && disabled.ordered.empty() && disabled.writes.empty(),
"a title-disabled package must not resolve from either catalog");
check(stale.packages().count("kept.mod") == 0 && stale.dormant_selections().size() == 3,
"title-disabled packages must be hidden and their saved choices dormant");
check(fs::exists(stale_root / "bundled/kept.mod/1.0.0/manifest.toml") &&
fs::exists(stale_root / "installed/kept.mod/1.0.0/manifest.toml"),
"disabling never deletes catalog files");
stale.set_disabled_packages({});
check(stale.scan(&error), error.c_str());
/* Saving (every launch commit does) must keep the dormant choices. */
check(stale.save_state(&error), error.c_str());
+1 -1
View File
@@ -186,7 +186,7 @@ int main(void) {
CHECK(pad_ext_resolve(&hx, 0, &o) == 1);
CHECK(T.xf_seen.struct_size == sizeof T.xf_seen && T.xf_seen.player == 0);
CHECK(T.xf_seen.buttons == (0xfeff & 0xfffe) && T.xf_seen.lx == 200 &&
T.xf_seen.type == 1 && T.xf_seen.host_flags == 7 &&
T.xf_seen.type == 1 && T.xf_seen.host_flags == (7 | PSX_MOD_PAD_HOST_SOURCE) &&
T.xf_seen.host_lt == 60 && T.xf_seen.host_rt == 250);
CHECK(o.analog == PSX_MOD_PAD_NEGCON && o.buttons == 0xbfff &&
o.lx == 0x30 && o.rx == 250 && o.ry == 60 && o.ly == 5);