ps5dbg-scanner 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ """ps5dbg-scanner — cross-platform 'Cheat Engine for PS5' GUI built on ps5dbg.
2
+
3
+ Run with ``ps5dbg-scanner`` (entry point) or ``python -m ps5dbg_scanner``.
4
+
5
+ from ps5dbg_scanner.app import main
6
+ main()
7
+ """
8
+ from __future__ import annotations
9
+
10
+ __version__ = "0.2.0"
11
+ __all__ = ["__version__"]
@@ -0,0 +1,9 @@
1
+ """Allow ``python -m ps5dbg_scanner``."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+
6
+ from .app import main
7
+
8
+ if __name__ == "__main__":
9
+ sys.exit(main())
ps5dbg_scanner/app.py ADDED
@@ -0,0 +1,362 @@
1
+ """ScannerApp — the tkinter root + threading-safe worker dispatch.
2
+
3
+ Owns:
4
+ - the Tk root window and all panels
5
+ - the live ps5dbg.PS5Debug client (None until connected)
6
+ - a worker-thread pool: blocking ps5dbg calls run off the UI thread and post
7
+ results back via ``root.after(0, callback)`` (the only tkinter-safe path)
8
+ - the current scan session state (turbo resident handle, survivor count,
9
+ value type/length for the active session)
10
+
11
+ UI thread never blocks on network I/O.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ import threading
17
+ import time
18
+ import tkinter as tk
19
+ from tkinter import messagebox, ttk
20
+ from typing import Callable, Optional
21
+
22
+ from ps5dbg import PS5Debug
23
+ from ps5dbg import turboscan as ts
24
+ from ps5dbg.constants import ScanCompareType, ScanValueType
25
+
26
+ from .freeze_manager import FreezeManager
27
+ from .results_table import ResultsTable
28
+ from .scan_panel import ScanPanel
29
+ from .connection_panel import ConnectionPanel
30
+
31
+ # value type -> (ps5dbg ScanValueType, fixed byte width). Bytes/string excluded
32
+ # from the core v0.2 value scan (they use the AOB pane in v0.3).
33
+ _VALUE_TYPES: dict[str, tuple[ScanValueType, int]] = {
34
+ "u8": (ScanValueType.UINT8, 1),
35
+ "u16": (ScanValueType.UINT16, 2),
36
+ "u32": (ScanValueType.UINT32, 4),
37
+ "u64": (ScanValueType.UINT64, 8),
38
+ "i8": (ScanValueType.INT8, 1),
39
+ "i16": (ScanValueType.INT16, 2),
40
+ "i32": (ScanValueType.INT32, 4),
41
+ "i64": (ScanValueType.INT64, 8),
42
+ "f32": (ScanValueType.FLOAT, 4),
43
+ "f64": (ScanValueType.DOUBLE, 8),
44
+ }
45
+
46
+ # scan type -> (ps5dbg ScanCompareType, needs_value)
47
+ _SCAN_TYPES: dict[str, tuple[ScanCompareType, bool]] = {
48
+ "Exact value": (ScanCompareType.EXACT_VALUE, True),
49
+ "Bigger than": (ScanCompareType.BIGGER_THAN, True),
50
+ "Smaller than": (ScanCompareType.SMALLER_THAN, True),
51
+ "Increased value": (ScanCompareType.INCREASED_VALUE, False),
52
+ "Decreased value": (ScanCompareType.DECREASED_VALUE, False),
53
+ "Changed value": (ScanCompareType.CHANGED_VALUE, False),
54
+ "Unchanged value": (ScanCompareType.UNCHANGED_VALUE, False),
55
+ "Unknown initial value": (ScanCompareType.UNKNOWN_INITIAL_VALUE, False),
56
+ }
57
+
58
+
59
+ class ScannerApp:
60
+ """The scanner application. Construct, then call run()."""
61
+
62
+ def __init__(self) -> None:
63
+ self.root = tk.Tk()
64
+ self.root.title("ps5dbg-scanner — PS5 memory scanner")
65
+ self.root.geometry("900x600")
66
+ self._client: Optional[PS5Debug] = None
67
+ self._pid: Optional[int] = None
68
+
69
+ # scan session state
70
+ self._session_active = False
71
+ self._survivor_count = 0
72
+ self._value_type_name = "u32"
73
+ self._value_length = 4
74
+
75
+ # freeze manager (started after connect)
76
+ self._freeze: Optional[FreezeManager] = None
77
+
78
+ # status bar
79
+ self._status = tk.StringVar(value="disconnected")
80
+
81
+ # build UI
82
+ self._conn = ConnectionPanel(self.root, self)
83
+ self._scan = ScanPanel(self.root, self,
84
+ value_types=list(_VALUE_TYPES),
85
+ scan_types=list(_SCAN_TYPES))
86
+ self._table = ResultsTable(self.root, self)
87
+
88
+ self._layout()
89
+ self.root.protocol("WM_DELETE_WINDOW", self._on_close)
90
+
91
+ # -- layout --------------------------------------------------------------
92
+
93
+ def _layout(self) -> None:
94
+ self.root.grid_rowconfigure(2, weight=1)
95
+ self.root.grid_columnconfigure(0, weight=1)
96
+ self._conn.frame.grid(row=0, column=0, sticky="ew", padx=8, pady=(8, 4))
97
+ self._scan.frame.grid(row=1, column=0, sticky="ew", padx=8, pady=4)
98
+ self._table.frame.grid(row=2, column=0, sticky="nsew", padx=8, pady=4)
99
+ ttk.Label(self.root, textvariable=self._status,
100
+ relief="sunken", anchor="w").grid(
101
+ row=3, column=0, sticky="ew", padx=8, pady=(0, 4))
102
+
103
+ # -- threading-safe worker dispatch -------------------------------------
104
+
105
+ def run_async(
106
+ self,
107
+ work: Callable[[], object],
108
+ on_done: Optional[Callable[[object], None]] = None,
109
+ on_error: Optional[Callable[[BaseException], None]] = None,
110
+ ) -> threading.Thread:
111
+ """Run ``work`` on a worker thread; marshal result back to the UI thread.
112
+
113
+ on_done/on_error run on the UI thread via root.after(0, ...).
114
+ """
115
+ def runner() -> None:
116
+ try:
117
+ result = work()
118
+ except BaseException as e: # noqa: BLE001
119
+ if on_error:
120
+ self.root.after(0, lambda: on_error(e))
121
+ else:
122
+ self.root.after(0, lambda: self._fail(e))
123
+ return
124
+ if on_done:
125
+ self.root.after(0, lambda: on_done(result))
126
+
127
+ t = threading.Thread(target=runner, daemon=True)
128
+ t.start()
129
+ return t
130
+
131
+ # -- status / errors (UI thread) ----------------------------------------
132
+
133
+ def set_status(self, text: str) -> None:
134
+ self._status.set(text)
135
+
136
+ def _fail(self, e: BaseException) -> None:
137
+ self.set_status(f"error: {e}")
138
+ messagebox.showerror("ps5dbg-scanner", str(e))
139
+
140
+ # -- connection (called by ConnectionPanel) -----------------------------
141
+
142
+ def connect(self, host: str, port: int) -> None:
143
+ def work() -> str:
144
+ c = PS5Debug(host=host, port=port, timeout=10.0)
145
+ c.connect()
146
+ self._client = c
147
+ ver = c.version()
148
+ fw = c.fw_version()
149
+ # start the freeze supervisor against this client
150
+ self._freeze = FreezeManager(
151
+ writer=lambda a, d: c.write(self._pid or 0, a, d),
152
+ rate_hz=10.0,
153
+ on_error=lambda slot, exc: self.root.after(
154
+ 0, lambda: self.set_status(f"freeze failed @ {slot.address:#x}: {exc}")),
155
+ )
156
+ self._freeze.start()
157
+ return f"{ver} (FW {fw // 100}.{fw % 100:02d})"
158
+
159
+ self.run_async(
160
+ work,
161
+ on_done=lambda label: (
162
+ self.set_status(f"connected: ps5debug-NG {label}"),
163
+ self._conn.on_connected(),
164
+ self._refresh_targets(),
165
+ ),
166
+ on_error=lambda e: self._fail(e),
167
+ )
168
+
169
+ @property
170
+ def client(self) -> Optional[PS5Debug]:
171
+ return self._client
172
+
173
+ @property
174
+ def pid(self) -> Optional[int]:
175
+ return self._pid
176
+
177
+ @pid.setter
178
+ def pid(self, value: Optional[int]) -> None:
179
+ self._pid = value
180
+ if self._freeze and self._client:
181
+ # update the freeze writer's target pid
182
+ self._freeze._writer = lambda a, d: self._client.write(value or 0, a, d) # type: ignore[union-attr]
183
+ self.set_status(f"target pid = {value}")
184
+
185
+ def _refresh_targets(self) -> None:
186
+ def work():
187
+ assert self._client is not None
188
+ fg = self._client.foreground_app()
189
+ procs = self._client.procs()
190
+ return fg, procs
191
+ self.run_async(work, on_done=lambda r: self._conn.populate_targets(*r))
192
+
193
+ # -- scanning (called by ScanPanel) -------------------------------------
194
+
195
+ @property
196
+ def value_type_name(self) -> str:
197
+ return self._value_type_name
198
+
199
+ def first_scan(self, value_type_name: str, scan_type_name: str, value_text: str) -> None:
200
+ if self._client is None or self._pid is None:
201
+ self._fail(RuntimeError("not connected / no target"))
202
+ return
203
+ vt, width = _VALUE_TYPES[value_type_name]
204
+ ct, needs_value = _SCAN_TYPES[scan_type_name]
205
+ # validate/encode in the UI thread so a bad value surfaces cleanly
206
+ # instead of crashing the Tk callback
207
+ try:
208
+ value = self._encode_value(value_type_name, value_text) if needs_value else 0
209
+ except ValueError as e:
210
+ self._fail(e)
211
+ return
212
+ self._value_type_name = value_type_name
213
+ self._value_length = width
214
+ self.set_status("first scan…")
215
+
216
+ def work():
217
+ assert self._client is not None
218
+ self._client.authenticate(flags=2)
219
+ # default to scanning the whole address space: pass pid's low page
220
+ # up; the resident scan wants a contiguous range. For v0.2 we scan
221
+ # each writable region and let the server keep one resident set.
222
+ # Simpler: scan the first RW region (where game state usually lives).
223
+ # (A multi-region variant is a v0.3 refinement.)
224
+ maps = self._client.maps(self._pid)
225
+ rw = [m for m in maps if "w" in m.perms and "x" not in m.perms]
226
+ if not rw:
227
+ raise RuntimeError("no writable data region found in target")
228
+ # pick the largest RW region — usually the main heap
229
+ region = max(rw, key=lambda m: m.end - m.start)
230
+ stored, count = ts.turboscan_start_resident(
231
+ self._client.connection, self._pid,
232
+ region.start, region.end - region.start,
233
+ vt, ct, width, value, ts.TS_SERVER_RESIDENT,
234
+ )
235
+ if not stored:
236
+ raise RuntimeError("server declined resident scan (too many hits?)")
237
+ self._session_active = True
238
+ self._survivor_count = count
239
+ self._table.set_session(vt, width, count)
240
+ return count
241
+
242
+ def on_done(count: int) -> None:
243
+ self.set_status(f"first scan: {count} survivors")
244
+ self._scan.on_scan_done(count)
245
+ self._table.refresh_window()
246
+
247
+ self.run_async(work, on_done=on_done, on_error=lambda e: self._fail(e))
248
+
249
+ def next_scan(self, scan_type_name: str, value_text: str) -> None:
250
+ if not self._session_active or self._client is None:
251
+ self._fail(RuntimeError("no active scan session — run First Scan first"))
252
+ return
253
+ vt, _ = _VALUE_TYPES[self._value_type_name]
254
+ ct, needs_value = _SCAN_TYPES[scan_type_name]
255
+ try:
256
+ value = self._encode_value(self._value_type_name, value_text) if needs_value else 0
257
+ except ValueError as e:
258
+ self._fail(e)
259
+ return
260
+ self.set_status("next scan…")
261
+
262
+ def work():
263
+ assert self._client is not None
264
+ count = ts.turboscan_count_resident(
265
+ self._client.connection, vt, ct, value,
266
+ )
267
+ self._survivor_count = count
268
+ return count
269
+
270
+ def on_done(count: int) -> None:
271
+ self.set_status(f"narrowed to {count} survivors")
272
+ self._scan.on_scan_done(count)
273
+ self._table.set_count(count)
274
+ self._table.refresh_window()
275
+
276
+ self.run_async(work, on_done=on_done, on_error=lambda e: self._fail(e))
277
+
278
+ def new_scan(self) -> None:
279
+ if self._session_active and self._client is not None:
280
+ def work():
281
+ ts.turboscan_end(self._client.connection) # type: ignore[union-attr]
282
+ self.run_async(work, on_done=lambda _: self._reset_session())
283
+ else:
284
+ self._reset_session()
285
+
286
+ def _reset_session(self) -> None:
287
+ self._session_active = False
288
+ self._survivor_count = 0
289
+ self._table.clear()
290
+ self.set_status("new scan")
291
+ self._scan.on_new_scan()
292
+
293
+ def fetch_survivors(self, start: int, count: int):
294
+ """Called by ResultsTable on the UI thread; returns synchronously.
295
+
296
+ Used inside a worker thread spawned by the table itself.
297
+ """
298
+ assert self._client is not None
299
+ return ts.turboscan_get_resident(
300
+ self._client.connection, start, count, self._value_length,
301
+ )
302
+
303
+ # -- freeze (called by ResultsTable) ------------------------------------
304
+
305
+ def freeze_address(self, address: int, value_bytes: bytes) -> None:
306
+ if self._freeze:
307
+ self._freeze.freeze(address, value_bytes)
308
+ self.set_status(f"froze {address:#x}")
309
+
310
+ def unfreeze_address(self, address: int) -> None:
311
+ if self._freeze:
312
+ self._freeze.unfreeze(address)
313
+ self.set_status(f"unfroze {address:#x}")
314
+
315
+ def is_frozen(self, address: int) -> bool:
316
+ return bool(self._freeze and self._freeze.is_frozen(address))
317
+
318
+ # -- value encoding ------------------------------------------------------
319
+
320
+ @staticmethod
321
+ def _encode_value(value_type_name: str, text: str):
322
+ """Parse a user-entered value string into the right Python type.
323
+
324
+ Float types accept decimal notation; integer types accept decimal or
325
+ 0x hex. Raises ValueError with a usable message on bad input (the
326
+ caller surfaces it via _fail rather than crashing the callback).
327
+ """
328
+ s = text.strip()
329
+ if not s:
330
+ raise ValueError("enter a value first (the scan type needs one)")
331
+ if value_type_name.startswith("f"):
332
+ return float(s) # raises ValueError on bad input, which we want
333
+ # integer types: allow 0x hex or decimal; int(s, 0) handles both
334
+ return int(s, 0)
335
+
336
+ # -- lifecycle -----------------------------------------------------------
337
+
338
+ def _on_close(self) -> None:
339
+ try:
340
+ if self._freeze:
341
+ self._freeze.stop()
342
+ if self._session_active and self._client is not None:
343
+ try:
344
+ ts.turboscan_end(self._client.connection)
345
+ except Exception:
346
+ pass
347
+ if self._client is not None:
348
+ self._client.close()
349
+ finally:
350
+ self.root.destroy()
351
+
352
+ def run(self) -> None:
353
+ self.root.mainloop()
354
+
355
+
356
+ def main() -> int:
357
+ ScannerApp().run()
358
+ return 0
359
+
360
+
361
+ if __name__ == "__main__":
362
+ raise SystemExit(main())
@@ -0,0 +1,96 @@
1
+ """Connection panel — host/port, connect, target picker."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import tkinter as tk
6
+ from tkinter import ttk
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from .app import ScannerApp
11
+
12
+
13
+ class ConnectionPanel:
14
+ """Top bar: host/port entries, Connect button, target dropdown."""
15
+
16
+ def __init__(self, parent: tk.Widget, app: "ScannerApp") -> None:
17
+ self.app = app
18
+ self.frame = ttk.LabelFrame(parent, text="Connection")
19
+ self.frame.columnconfigure(4, weight=1)
20
+
21
+ ttk.Label(self.frame, text="Host:").grid(row=0, column=0, padx=(8, 2), pady=6)
22
+ default_host = os.environ.get("PS5_HOST", "")
23
+ self.host_var = tk.StringVar(value=default_host)
24
+ self.host_entry = ttk.Entry(self.frame, textvariable=self.host_var, width=18)
25
+ self.host_entry.grid(row=0, column=1, padx=2, pady=6)
26
+
27
+ ttk.Label(self.frame, text="Port:").grid(row=0, column=2, padx=(6, 2), pady=6)
28
+ self.port_var = tk.StringVar(value=os.environ.get("PS5_PORT", "744"))
29
+ self.port_entry = ttk.Entry(self.frame, textvariable=self.port_var, width=6)
30
+ self.port_entry.grid(row=0, column=3, padx=2, pady=6)
31
+
32
+ self.connect_btn = ttk.Button(self.frame, text="Connect", command=self._on_connect)
33
+ self.connect_btn.grid(row=0, column=4, padx=8, pady=6, sticky="w")
34
+
35
+ ttk.Label(self.frame, text="Target:").grid(row=0, column=5, padx=(12, 2), pady=6)
36
+ self.target_var = tk.StringVar()
37
+ self.target_combo = ttk.Combobox(
38
+ self.frame, textvariable=self.target_var, state="readonly", width=44
39
+ )
40
+ self.target_combo.grid(row=0, column=6, padx=2, pady=6)
41
+ self.target_combo.bind("<<ComboboxSelected>>", self._on_target_selected)
42
+ self._targets: list[tuple[int, str, str]] = [] # (pid, name, titleid)
43
+
44
+ # -- callbacks -----------------------------------------------------------
45
+
46
+ def _on_connect(self) -> None:
47
+ host = self.host_var.get().strip()
48
+ if not host:
49
+ self.app.set_status("enter a host first")
50
+ return
51
+ try:
52
+ port = int(self.port_var.get().strip())
53
+ except ValueError:
54
+ self.app.set_status("invalid port")
55
+ return
56
+ self.connect_btn.config(state="disabled")
57
+ self.app.set_status(f"connecting to {host}:{port}…")
58
+ self.app.connect(host, port)
59
+
60
+ def on_connected(self) -> None:
61
+ """Called by app (UI thread) after a successful connect."""
62
+ self.connect_btn.config(text="Disconnect", state="normal",
63
+ command=self._on_disconnect)
64
+
65
+ def _on_disconnect(self) -> None:
66
+ # simple: tell user to restart for now (full disconnect/reconnect is v0.3 polish)
67
+ self.app.set_status("disconnect: close & reopen the app (v0.3 will add live reconnect)")
68
+
69
+ # -- targets -------------------------------------------------------------
70
+
71
+ def populate_targets(self, fg, procs) -> None:
72
+ """Populate the target dropdown. Runs on UI thread."""
73
+ self._targets = []
74
+ labels = []
75
+ # foreground app first (if any)
76
+ if fg.pid != 0:
77
+ self._targets.append((fg.pid, fg.name, fg.titleid))
78
+ labels.append(f"▶ {fg.name} (pid {fg.pid}, {fg.titleid}) [foreground]")
79
+ # then all procs, sorted, excluding kernel pid 0
80
+ for p in sorted(procs, key=lambda x: x.name.lower()):
81
+ if p.pid == 0:
82
+ continue
83
+ self._targets.append((p.pid, p.name, ""))
84
+ labels.append(f" {p.name} (pid {p.pid})")
85
+ self.target_combo["values"] = labels
86
+ if labels:
87
+ self.target_combo.current(0 if fg.pid != 0 else 0)
88
+ self._select_index(0)
89
+
90
+ def _on_target_selected(self, _evt) -> None:
91
+ self._select_index(self.target_combo.current())
92
+
93
+ def _select_index(self, idx: int) -> None:
94
+ if 0 <= idx < len(self._targets):
95
+ pid, name, _titleid = self._targets[idx]
96
+ self.app.pid = pid
@@ -0,0 +1,125 @@
1
+ """Freeze manager — keeps a set of addresses pinned to fixed values.
2
+
3
+ A single supervisor thread wakes at a configurable interval (default 10 Hz) and
4
+ re-writes every frozen address via the ps5dbg client. This is the classic
5
+ "freeze health/ammo at 999" feature.
6
+
7
+ The manager is pure logic + one background thread; it takes a write callable so
8
+ it's unit-testable with a fake. All ps5dbg calls happen on the supervisor
9
+ thread, never on the tkinter UI thread.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import threading
14
+ import time
15
+ from dataclasses import dataclass
16
+ from typing import Callable, Optional
17
+
18
+
19
+ @dataclass
20
+ class FrozenSlot:
21
+ """One frozen address: where, what value, and the encoded bytes to write."""
22
+
23
+ address: int
24
+ value_bytes: bytes
25
+ label: str = ""
26
+
27
+
28
+ class FreezeManager:
29
+ """Supervisor that periodically re-writes frozen addresses.
30
+
31
+ Args:
32
+ writer: callable(address:int, data:bytes) -> None. Typically
33
+ ``lambda a, d: client.write(pid, a, d)``. Raised exceptions are
34
+ swallowed and reported via ``on_error`` (a freeze failing should
35
+ not crash the supervisor or the UI).
36
+ rate_hz: how often per second to re-write. Default 10.
37
+ on_error: optional callback(slot, exc) invoked when a write raises.
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ writer: Callable[[int, bytes], None],
43
+ rate_hz: float = 10.0,
44
+ on_error: Optional[Callable[[FrozenSlot, BaseException], None]] = None,
45
+ ) -> None:
46
+ self._writer = writer
47
+ self._period = 1.0 / rate_hz if rate_hz > 0 else 0.1
48
+ self._on_error = on_error
49
+ self._slots: dict[int, FrozenSlot] = {}
50
+ self._lock = threading.Lock()
51
+ self._stop = threading.Event()
52
+ self._thread: Optional[threading.Thread] = None
53
+
54
+ # -- slot management (UI thread calls these) ----------------------------
55
+
56
+ def freeze(self, address: int, value_bytes: bytes, label: str = "") -> None:
57
+ """Add or replace a frozen slot. Idempotent on address."""
58
+ with self._lock:
59
+ self._slots[address] = FrozenSlot(
60
+ address=address, value_bytes=value_bytes, label=label
61
+ )
62
+
63
+ def unfreeze(self, address: int) -> None:
64
+ with self._lock:
65
+ self._slots.pop(address, None)
66
+
67
+ def is_frozen(self, address: int) -> bool:
68
+ with self._lock:
69
+ return address in self._slots
70
+
71
+ def frozen_addresses(self) -> list[int]:
72
+ with self._lock:
73
+ return list(self._slots.keys())
74
+
75
+ def set_value(self, address: int, value_bytes: bytes) -> None:
76
+ """Update the frozen value without toggling freeze state."""
77
+ with self._lock:
78
+ slot = self._slots.get(address)
79
+ if slot is not None:
80
+ self._slots[address] = FrozenSlot(
81
+ address=address, value_bytes=value_bytes, label=slot.label
82
+ )
83
+
84
+ # -- supervisor thread ---------------------------------------------------
85
+
86
+ def start(self) -> None:
87
+ if self._thread and self._thread.is_alive():
88
+ return
89
+ self._stop.clear()
90
+ self._thread = threading.Thread(
91
+ target=self._run, name="ps5dbg-scanner-freeze", daemon=True
92
+ )
93
+ self._thread.start()
94
+
95
+ def stop(self) -> None:
96
+ self._stop.set()
97
+ t = self._thread
98
+ self._thread = None
99
+ if t and t.is_alive():
100
+ t.join(timeout=1.0)
101
+
102
+ def _run(self) -> None:
103
+ while not self._stop.is_set():
104
+ with self._lock:
105
+ slots = list(self._slots.values())
106
+ for slot in slots:
107
+ if self._stop.is_set():
108
+ break
109
+ try:
110
+ self._writer(slot.address, slot.value_bytes)
111
+ except BaseException as e: # noqa: BLE001 — supervisor must survive
112
+ if self._on_error:
113
+ try:
114
+ self._on_error(slot, e)
115
+ except Exception:
116
+ pass
117
+ # precise-ish sleep, wake early on stop
118
+ self._stop.wait(self._period)
119
+
120
+ @property
121
+ def rate_hz(self) -> float:
122
+ return 1.0 / self._period if self._period > 0 else 0.0
123
+
124
+
125
+ __all__ = ["FreezeManager", "FrozenSlot"]
@@ -0,0 +1,260 @@
1
+ """Results table — virtualized ttk.Treeview over the server-resident survivor set.
2
+
3
+ The naive "insert every survivor as a row" approach freezes tkinter past ~50k
4
+ rows. This widget instead keeps a sliding window of rows (PAGE_SIZE) and
5
+ re-fills them as the user scrolls, fetching survivors from the PS5 on demand
6
+ via ``app.fetch_survivors(start, count)`` in a worker thread.
7
+
8
+ Columns: Address | Current | Previous | Frozen.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import threading
13
+ import tkinter as tk
14
+ from tkinter import ttk
15
+ from typing import TYPE_CHECKING, Optional
16
+
17
+ from ps5dbg.constants import ScanValueType
18
+
19
+ if TYPE_CHECKING:
20
+ from .app import ScannerApp
21
+
22
+ PAGE_SIZE = 500 # rows materialized in the Treeview at once
23
+ MAX_ROWS = 5000 # cap to keep the scrollbar honest; full count shown in status
24
+
25
+
26
+ class ResultsTable:
27
+ """Virtualized survivor table. Fetches windows from the PS5."""
28
+
29
+ def __init__(self, parent: tk.Widget, app: "ScannerApp") -> None:
30
+ self.app = app
31
+ self.frame = ttk.LabelFrame(parent, text="Results")
32
+ self.frame.columnconfigure(0, weight=1)
33
+ self.frame.rowconfigure(0, weight=1)
34
+
35
+ columns = ("address", "current", "previous", "frozen")
36
+ self.tree = ttk.Treeview(self.frame, columns=columns, show="headings",
37
+ selectmode="browse")
38
+ for col, text, width in (
39
+ ("address", "Address", 160),
40
+ ("current", "Current", 120),
41
+ ("previous", "Previous", 120),
42
+ ("frozen", "Frozen", 60),
43
+ ):
44
+ self.tree.heading(col, text=text)
45
+ self.tree.column(col, width=width, anchor="w")
46
+ self.tree.grid(row=0, column=0, sticky="nsew")
47
+
48
+ vsb = ttk.Scrollbar(self.frame, orient="vertical", command=self.tree.yview)
49
+ vsb.grid(row=0, column=1, sticky="ns")
50
+ self.tree.configure(yscrollcommand=self._on_scroll)
51
+ self._vsb = vsb
52
+
53
+ # right-click context menu
54
+ self.menu = tk.Menu(self.tree, tearoff=0)
55
+ self.menu.add_command(label="Freeze / Unfreeze", command=self._toggle_freeze)
56
+ self.menu.add_command(label="Edit value…", command=self._edit_value)
57
+ self.menu.add_command(label="Copy address", command=self._copy_address)
58
+ self.tree.bind("<Button-3>", self._on_right_click)
59
+
60
+ # session state
61
+ self._value_type: ScanValueType = ScanValueType.UINT32
62
+ self._value_length: int = 4
63
+ self._count: int = 0
64
+ self._fetch_lock = threading.Lock()
65
+ self._last_fetch_start: int = -1
66
+
67
+ # -- session wiring (called by app on UI thread) ------------------------
68
+
69
+ def set_session(self, value_type: ScanValueType, value_length: int, count: int) -> None:
70
+ self._value_type = value_type
71
+ self._value_length = value_length
72
+ self._count = count
73
+ self.clear()
74
+
75
+ def set_count(self, count: int) -> None:
76
+ self._count = count
77
+ self.clear()
78
+
79
+ def clear(self) -> None:
80
+ self.tree.delete(*self.tree.get_children())
81
+ self._last_fetch_start = -1
82
+
83
+ # -- virtualized window fetch -------------------------------------------
84
+
85
+ def refresh_window(self, start: int = 0) -> None:
86
+ """Fetch the [start, start+PAGE_SIZE) survivor window and re-fill rows.
87
+
88
+ Runs the fetch on a worker thread; re-fills the Treeview on the UI thread.
89
+ """
90
+ if self._count == 0 or self.app.client is None:
91
+ return
92
+ start = max(0, min(start, max(0, self._count - 1)))
93
+ fetch_count = min(PAGE_SIZE, self._count - start)
94
+ if start == self._last_fetch_start:
95
+ return # already showing this window
96
+
97
+ def work():
98
+ return self.app.fetch_survivors(start, fetch_count)
99
+
100
+ def on_done(rows):
101
+ self._fill_rows(start, rows)
102
+ self._last_fetch_start = start
103
+
104
+ self.app.run_async(work, on_done=on_done)
105
+
106
+ def _fill_rows(self, start: int, rows) -> None:
107
+ """Replace the Treeview contents with ``rows`` starting at ``start``."""
108
+ self.tree.delete(*self.tree.get_children())
109
+ for i, (addr, cur, prev, _first) in enumerate(rows):
110
+ idx = start + i
111
+ frozen = "🔒" if self.app.is_frozen(addr) else ""
112
+ self.tree.insert(
113
+ "", "end", iid=str(idx),
114
+ values=(f"{addr:#018x}", self._fmt(cur), self._fmt(prev), frozen),
115
+ )
116
+
117
+ def _fmt(self, raw: bytes) -> str:
118
+ """Format a raw value bytes according to the session's value type."""
119
+ try:
120
+ if self._value_type in (ScanValueType.UINT8, ScanValueType.UINT16,
121
+ ScanValueType.UINT32, ScanValueType.UINT64):
122
+ return str(int.from_bytes(raw, "little", signed=False))
123
+ if self._value_type in (ScanValueType.INT8, ScanValueType.INT16,
124
+ ScanValueType.INT32, ScanValueType.INT64):
125
+ return str(int.from_bytes(raw, "little", signed=True))
126
+ if self._value_type is ScanValueType.FLOAT:
127
+ import struct
128
+ return f"{struct.unpack('<f', raw)[0]:.4g}"
129
+ if self._value_type is ScanValueType.DOUBLE:
130
+ import struct
131
+ return f"{struct.unpack('<d', raw)[0]:.4g}"
132
+ except Exception:
133
+ pass
134
+ return raw.hex()
135
+
136
+ # -- scroll handling -----------------------------------------------------
137
+
138
+ def _on_scroll(self, *args) -> None:
139
+ """Treeview yview callback — detect when the user nears a window edge."""
140
+ self._vsb.set(*args)
141
+ if not args:
142
+ return
143
+ # args = (first, last); if last close to 1.0, page forward; first close to 0, back
144
+ first = float(args[0])
145
+ # figure out which survivor index is at the top of the visible area
146
+ children = self.tree.get_children()
147
+ if not children:
148
+ return
149
+ top_idx = int(children[0])
150
+ visible = len(children)
151
+ # if the user has scrolled to near the bottom of the current window, fetch next
152
+ if first > 0.7 and top_idx + visible < self._count:
153
+ self.refresh_window(top_idx + visible - 20)
154
+ elif first < 0.2 and top_idx > 0:
155
+ self.refresh_window(max(0, top_idx - PAGE_SIZE + 20))
156
+
157
+ # -- context menu actions -----------------------------------------------
158
+
159
+ def _on_right_click(self, event) -> None:
160
+ row = self.tree.identify_row(event.y)
161
+ if row:
162
+ self.tree.selection_set(row)
163
+ self.menu.tk_popup(event.x_root, event.y_root)
164
+
165
+ def _selected_address(self) -> Optional[int]:
166
+ sel = self.tree.selection()
167
+ if not sel:
168
+ return None
169
+ idx = int(sel[0])
170
+ # we need the actual address; re-fetch this single survivor
171
+ if self.app.client is None:
172
+ return None
173
+ rows = self.app.fetch_survivors(idx, 1)
174
+ if not rows:
175
+ return None
176
+ return rows[0][0]
177
+
178
+ def _toggle_freeze(self) -> None:
179
+ addr = self._selected_address()
180
+ if addr is None:
181
+ return
182
+ if self.app.is_frozen(addr):
183
+ self.app.unfreeze_address(addr)
184
+ else:
185
+ # freeze at the current value
186
+ rows = self.app.fetch_survivors(int(self.tree.selection()[0]), 1)
187
+ if rows:
188
+ _, cur, _prev, _first = rows[0]
189
+ self.app.freeze_address(addr, cur)
190
+ # refresh the visible window to update the lock icon
191
+ self.refresh_window(self._last_fetch_start)
192
+
193
+ def _edit_value(self) -> None:
194
+ sel = self.tree.selection()
195
+ if not sel or self.app.client is None or self.app.pid is None:
196
+ return
197
+ idx = int(sel[0])
198
+ rows = self.app.fetch_survivors(idx, 1)
199
+ if not rows:
200
+ return
201
+ addr, cur, _prev, _first = rows[0]
202
+ new = self._prompt("New value", "Edit value", self._fmt(cur))
203
+ if new is None:
204
+ return
205
+ try:
206
+ data = self._encode(new)
207
+ except ValueError:
208
+ from tkinter import messagebox
209
+ messagebox.showerror("ps5dbg-scanner", f"invalid value for {self._value_type.name}")
210
+ return
211
+ # write synchronously off the UI thread
212
+ def work():
213
+ self.app.client.write(self.app.pid, addr, data) # type: ignore[union-attr]
214
+ self.app.run_async(work, on_done=lambda _: (
215
+ self.app.set_status(f"wrote {data.hex()} -> {addr:#x}"),
216
+ self.refresh_window(self._last_fetch_start),
217
+ ))
218
+
219
+ def _copy_address(self) -> None:
220
+ addr = self._selected_address()
221
+ if addr is None:
222
+ return
223
+ self.frame.clipboard_clear()
224
+ self.frame.clipboard_append(f"{addr:#x}")
225
+ self.app.set_status(f"copied {addr:#x}")
226
+
227
+ # -- helpers -------------------------------------------------------------
228
+
229
+ def _prompt(self, title, prompt, initial="") -> Optional[str]:
230
+ from tkinter import simpledialog
231
+ return simpledialog.askstring(title, prompt, initialvalue=initial,
232
+ parent=self.frame)
233
+
234
+ def _encode(self, text: str) -> bytes:
235
+ import struct
236
+ vt = self._value_type
237
+ n = int(text, 0) if not text.lstrip("-").replace(".", "", 1).isdigit() else (
238
+ float(text) if "." in text else int(text, 0)
239
+ )
240
+ if vt in (ScanValueType.UINT8,):
241
+ return struct.pack("<B", int(n) & 0xFF)
242
+ if vt in (ScanValueType.INT8,):
243
+ return struct.pack("<b", int(n))
244
+ if vt in (ScanValueType.UINT16,):
245
+ return struct.pack("<H", int(n) & 0xFFFF)
246
+ if vt in (ScanValueType.INT16,):
247
+ return struct.pack("<h", int(n))
248
+ if vt in (ScanValueType.UINT32,):
249
+ return struct.pack("<I", int(n) & 0xFFFFFFFF)
250
+ if vt in (ScanValueType.INT32,):
251
+ return struct.pack("<i", int(n))
252
+ if vt in (ScanValueType.UINT64,):
253
+ return struct.pack("<Q", int(n) & 0xFFFFFFFFFFFFFFFF)
254
+ if vt in (ScanValueType.INT64,):
255
+ return struct.pack("<q", int(n))
256
+ if vt is ScanValueType.FLOAT:
257
+ return struct.pack("<f", float(n))
258
+ if vt is ScanValueType.DOUBLE:
259
+ return struct.pack("<d", float(n))
260
+ raise ValueError(f"unsupported value type {vt}")
@@ -0,0 +1,92 @@
1
+ """Scan panel — value type, scan type, value entry, scan buttons."""
2
+ from __future__ import annotations
3
+
4
+ import tkinter as tk
5
+ from tkinter import ttk
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from .app import ScannerApp
10
+
11
+
12
+ class ScanPanel:
13
+ """Middle bar: value-type / scan-type dropdowns, value entry, scan buttons."""
14
+
15
+ def __init__(
16
+ self,
17
+ parent: tk.Widget,
18
+ app: "ScannerApp",
19
+ value_types: list[str],
20
+ scan_types: list[str],
21
+ ) -> None:
22
+ self.app = app
23
+ self.frame = ttk.LabelFrame(parent, text="Scan")
24
+ self.frame.columnconfigure(3, weight=1)
25
+
26
+ ttk.Label(self.frame, text="Value type:").grid(row=0, column=0, padx=(8, 2), pady=8)
27
+ self.value_type_var = tk.StringVar(value="u32")
28
+ self.value_type_combo = ttk.Combobox(
29
+ self.frame, textvariable=self.value_type_var, state="readonly",
30
+ width=6, values=value_types,
31
+ )
32
+ self.value_type_combo.grid(row=0, column=1, padx=2, pady=8)
33
+
34
+ ttk.Label(self.frame, text="Scan type:").grid(row=0, column=2, padx=(10, 2), pady=8)
35
+ self.scan_type_var = tk.StringVar(value="Exact value")
36
+ self.scan_type_combo = ttk.Combobox(
37
+ self.frame, textvariable=self.scan_type_var, state="readonly",
38
+ width=22, values=scan_types,
39
+ )
40
+ self.scan_type_combo.grid(row=0, column=3, sticky="w", padx=2, pady=8)
41
+ self.scan_type_combo.bind("<<ComboboxSelected>>", self._on_scan_type_changed)
42
+
43
+ ttk.Label(self.frame, text="Value:").grid(row=0, column=4, padx=(10, 2), pady=8)
44
+ self.value_var = tk.StringVar()
45
+ self.value_entry = ttk.Entry(self.frame, textvariable=self.value_var, width=14)
46
+ self.value_entry.grid(row=0, column=5, padx=2, pady=8)
47
+
48
+ self.first_btn = ttk.Button(self.frame, text="First Scan", command=self._on_first)
49
+ self.first_btn.grid(row=0, column=6, padx=(10, 2), pady=8)
50
+ self.next_btn = ttk.Button(self.frame, text="Next Scan", command=self._on_next, state="disabled")
51
+ self.next_btn.grid(row=0, column=7, padx=2, pady=8)
52
+ self.new_btn = ttk.Button(self.frame, text="New Scan", command=self._on_new, state="disabled")
53
+ self.new_btn.grid(row=0, column=8, padx=(2, 8), pady=8)
54
+
55
+ self._set_buttons(post_first=False)
56
+
57
+ # -- callbacks -----------------------------------------------------------
58
+
59
+ def _on_scan_type_changed(self, _evt) -> None:
60
+ # valueless scan types (Increased/Decreased/Changed/Unknown) disable the entry
61
+ valueless = {"Increased value", "Decreased value", "Changed value",
62
+ "Unchanged value", "Unknown initial value"}
63
+ state = "disabled" if self.scan_type_var.get() in valueless else "normal"
64
+ self.value_entry.config(state=state)
65
+
66
+ def _on_first(self) -> None:
67
+ self.app.first_scan(self.value_type_var.get(), self.scan_type_var.get(),
68
+ self.value_var.get())
69
+
70
+ def _on_next(self) -> None:
71
+ self.app.next_scan(self.scan_type_var.get(), self.value_var.get())
72
+
73
+ def _on_new(self) -> None:
74
+ self.app.new_scan()
75
+
76
+ # -- state transitions (called by app on UI thread) ---------------------
77
+
78
+ def on_scan_done(self, survivor_count: int) -> None:
79
+ self._set_buttons(post_first=True)
80
+
81
+ def on_new_scan(self) -> None:
82
+ self._set_buttons(post_first=False)
83
+
84
+ def _set_buttons(self, post_first: bool) -> None:
85
+ if post_first:
86
+ self.first_btn.config(state="disabled")
87
+ self.next_btn.config(state="normal")
88
+ self.new_btn.config(state="normal")
89
+ else:
90
+ self.first_btn.config(state="normal")
91
+ self.next_btn.config(state="disabled")
92
+ self.new_btn.config(state="disabled")
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: ps5dbg-scanner
3
+ Version: 0.2.0
4
+ Summary: Cross-platform 'Cheat Engine for PS5' GUI built on the ps5dbg library.
5
+ Author: Darkatek7
6
+ License: GPL-3.0-only
7
+ Project-URL: Homepage, https://github.com/Darkatek7/ps5dbg-scanner
8
+ Project-URL: Repository, https://github.com/Darkatek7/ps5dbg-scanner
9
+ Project-URL: Issues, https://github.com/Darkatek7/ps5dbg-scanner/issues
10
+ Keywords: ps5,playstation5,ps5debug,memory scanner,cheat engine,homebrew,jailbreak
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: End Users/Desktop
13
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Games/Entertainment
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: ps5dbg>=0.1.1
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=8; extra == "dev"
29
+ Requires-Dist: ps5dbg[dev]>=0.1.1; extra == "dev"
30
+ Provides-Extra: build
31
+ Requires-Dist: build; extra == "build"
32
+ Requires-Dist: twine; extra == "build"
33
+ Dynamic: license-file
34
+
35
+ # ps5dbg-scanner
36
+
37
+ > A cross-platform **"Cheat Engine for PS5"** built on the [`ps5dbg`](https://github.com/Darkatek7/ps5dbg) library. Scan a game's memory, narrow down to the values you care about, then freeze or edit them — from any OS, no Windows required.
38
+
39
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
40
+ [![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-green.svg)](./LICENSE)
41
+ [![Status: Alpha](https://img.shields.io/badge/status-alpha-orange.svg)](#roadmap)
42
+
43
+ `ps5dbg-scanner` is a desktop GUI for the classic memory-editor loop:
44
+
45
+ 1. **Pick the foreground app** (or any process) on your jailbroken PS5.
46
+ 2. **First scan** — find all addresses holding a value (e.g. your current gold = 1000).
47
+ 3. **Next scan** — change the value in-game, then narrow the survivor list (now it's 1050 → only addresses that went from 1000 → 1050).
48
+ 4. Repeat until you have a handful of candidates, then **freeze** or **edit** the value.
49
+
50
+ It uses ps5debug-NG's **server-resident turbo scan**, so survivors live on the PS5 and narrow scans are fast even when the first scan returns hundreds of thousands of hits.
51
+
52
+ Runs anywhere Python 3.10+ runs (Linux, macOS, Windows) with **no extra runtime dependencies** beyond `ps5dbg` (tkinter ships with Python).
53
+
54
+ ---
55
+
56
+ ## Why it exists
57
+
58
+ Every existing PS5 memory scanner is **Windows-only** (`PS_Trainer_Viewer`, the PS4-era `PS4CheaterNeo`). Mac and Linux users have nothing. `ps5dbg-scanner` is the cross-platform answer — built on the [`ps5dbg`](https://pypi.org/project/ps5dbg/) library, which implements the full ps5debug-NG wire protocol.
59
+
60
+ ---
61
+
62
+ ## Install
63
+
64
+ ```bash
65
+ pip install ps5dbg-scanner
66
+ # or
67
+ pipx install ps5dbg-scanner
68
+
69
+ # then run
70
+ export PS5_HOST=192.168.1.10 # your PS5's LAN IP
71
+ ps5dbg-scanner
72
+ ```
73
+
74
+ Requires a PS5 running [ps5debug-NG](https://github.com/OpenSourcereR-dev/ps5debug-NG) reachable on your LAN.
75
+
76
+ ---
77
+
78
+ ## Quick start
79
+
80
+ 1. Launch the app: `ps5dbg-scanner`
81
+ 2. Enter your PS5's IP, click **Connect**.
82
+ 3. Pick the target from the dropdown (the foreground app is marked `▶`).
83
+ 4. Choose a **value type** (u32 is the common case for game state) and **scan type**.
84
+ 5. Enter the current value, click **First Scan**.
85
+ 6. Change the value in-game, enter the new value, click **Next Scan**.
86
+ 7. Repeat until the survivor count is small.
87
+ 8. Right-click a row → **Freeze** (lock it) or **Edit value**.
88
+
89
+ ---
90
+
91
+ ## Features
92
+
93
+ - **Value scan** across u8/u16/u32/u64, i8/i16/i32/i64, float, double.
94
+ - **Scan types**: exact, bigger/smaller, increased/decreased, changed/unchanged, unknown-initial-value.
95
+ - **Server-resident narrow loop** — survivors stay on the PS5; no re-upload per scan.
96
+ - **Virtualized results table** — scrolls smoothly through millions of hits.
97
+ - **Freeze** values at a fixed number (the classic "infinite health" — re-written 10×/sec).
98
+ - **Edit** any survivor's value directly.
99
+ - **Foreground-app detection** — pick the running game with one click.
100
+
101
+ ---
102
+
103
+ ## How it works
104
+
105
+ ```
106
+ ┌─────────────────────────────────────────────┐
107
+ │ tkinter GUI (UI thread only) │
108
+ │ Connection panel Scan panel Results │
109
+ └──────────────────┬──────────────────────────┘
110
+ │ worker threads (blocking I/O off the UI thread)
111
+
112
+ ┌─────────────────┐
113
+ │ ps5dbg library │ ← PyPI: pip install ps5dbg
114
+ └────────┬────────┘
115
+ │ TCP wire protocol (port 744)
116
+
117
+ ┌─────────────────┐
118
+ │ PS5 (jailed) │
119
+ │ ps5debug-NG │
120
+ └─────────────────┘
121
+ ```
122
+
123
+ All blocking network calls run off the UI thread; results are marshaled back via `root.after`. The scan engine uses the turbo-resident path so narrow scans stay fast at scale.
124
+
125
+ ---
126
+
127
+ ## Requirements
128
+
129
+ - **Python** 3.10+ (tkinter ships with standard CPython).
130
+ - **ps5dbg** ≥ 0.1.0 (auto-installed as a dependency).
131
+ - **Console** any PS5 running ps5debug-NG with the turbo-scan family advertised.
132
+
133
+ ---
134
+
135
+ ## Roadmap
136
+
137
+ - **v0.2** — core scanner: value scan → narrow → freeze/edit (this release, alpha)
138
+ - **v0.3** — hex viewer pane, AOB pattern scanner, bookmark/address-list manager
139
+ - **v0.4** — debugger UI (breakpoints/registers) on top of ps5dbg's `DebugSession`
140
+
141
+ Contributions welcome — see [`AGENTS.md`](./AGENTS.md) for architecture and conventions.
142
+
143
+ ---
144
+
145
+ ## Acknowledgments
146
+
147
+ - **[ps5dbg](https://github.com/Darkatek7/ps5dbg)** — the library this is built on.
148
+ - **[OpenSourcereR-dev/ps5debug-NG](https://github.com/OpenSourcereR-dev/ps5debug-NG)** — the debugger payload and wire protocol.
149
+ - **[etaHEN](https://github.com/etaHEN/etaHEN)** — the HEN chain that loads it.
150
+
151
+ ## License
152
+
153
+ [GPL-3.0-only](./LICENSE) — matching ps5dbg and ps5debug-NG.
@@ -0,0 +1,13 @@
1
+ ps5dbg_scanner/__init__.py,sha256=Wy7pBDj2AhqyAVWXGNwdGDfgNjQfQEjSkXgAQiDqYig,308
2
+ ps5dbg_scanner/__main__.py,sha256=M7YujKbHQTdwcSIdRUsKTd5p-71mCUlqOaEneHVVbHg,161
3
+ ps5dbg_scanner/app.py,sha256=RjICsrjPQS4CeE1Usezssueu0XxjZNCRUG3UiU3Zfek,13760
4
+ ps5dbg_scanner/connection_panel.py,sha256=-u4S3FxIFGC3_UdziKTJsLPCG_J8PKZpcKaZMAZlavo,4053
5
+ ps5dbg_scanner/freeze_manager.py,sha256=2d8wf4z_VvRsu38jnvjysfzDpNAnANMxK4sx8LXPZsE,4307
6
+ ps5dbg_scanner/results_table.py,sha256=zbsVVCTkEaWHrKJAdYP0WkgQQySgcbODFHwyI8ERc6g,10384
7
+ ps5dbg_scanner/scan_panel.py,sha256=wVktXI9U88YMkzc1gjCFbOte-TXznOsxail0MvIIZ8w,3805
8
+ ps5dbg_scanner-0.2.0.dist-info/licenses/LICENSE,sha256=94ywdt7ckRTD9PK0pT2rDm65LFNFo7q-1G1KZGlW_wc,958
9
+ ps5dbg_scanner-0.2.0.dist-info/METADATA,sha256=9AG1SSY_c_8al3x-buXjXPdJDX05j5bCTRr741PcO8Q,6738
10
+ ps5dbg_scanner-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
11
+ ps5dbg_scanner-0.2.0.dist-info/entry_points.txt,sha256=YDbYUMdtl84N-izWmctBa1xNYmytXnFyavE53jtjgu0,59
12
+ ps5dbg_scanner-0.2.0.dist-info/top_level.txt,sha256=cbgQHV0pCsDiruadhmauLKi8-IPhMAyRqZboX-SjQPA,15
13
+ ps5dbg_scanner-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ps5dbg-scanner = ps5dbg_scanner.app:main
@@ -0,0 +1,22 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ ps5dbg-scanner is licensed under GPL-3.0-only, matching the ps5dbg library
9
+ and ps5debug-NG it builds upon.
10
+
11
+ The full text of the GPL-3.0 is available at:
12
+ https://www.gnu.org/licenses/gpl-3.0.txt
13
+
14
+ This program is free software: you can redistribute it and/or modify
15
+ it under the terms of the GNU General Public License as published by
16
+ the Free Software Foundation, either version 3 of the License, or
17
+ (at your option) any later version.
18
+
19
+ This program is distributed in the hope that it will be useful,
20
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
21
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22
+ GNU General Public License for more details.
@@ -0,0 +1 @@
1
+ ps5dbg_scanner