Add fake-client dev scripts and GUI actions
Add developer-only fake-client scripts and UI actions for testing proxy behavior. Files updated: server/fake_player.py, server/dev_server_app.py, server/README.md, docs/dev-log.md. New scripts/actions: Walk Circle, Jump Once, Toggle Sneak, Leave Cell (Left Cell), Return To Cell, and Teleport Test. UI: added buttons and handlers in the DevServerWindow to trigger these actions. fake_player: implemented script/state handling, one-shot jump thread, teleport toggle, sneak flag, walk-circle movement, cell change transforms, a socket send lock, and protection for one-shot/action state. README and dev log updated with usage, testing notes, and known issues. Basic headless smoke tests and py_compile checks were performed; full in-game Fallout 4/F4SE validation remains manual.
This commit is contained in:
@@ -1095,6 +1095,72 @@ through the Python server.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-02 - Fake Client Test Scripts
|
||||
|
||||
### Summary
|
||||
|
||||
Added developer-only fake-client test scripts and actions to the PySide6 Fake
|
||||
Clients tab. GUI-managed fake clients can now test jump state, sneak state,
|
||||
circle movement, cell mismatch holding, return-to-cell behavior, and teleport
|
||||
snapping without launching multiple Fallout 4 instances.
|
||||
|
||||
### Files Changed
|
||||
|
||||
- `server/fake_player.py`
|
||||
- `server/dev_server_app.py`
|
||||
- `server/README.md`
|
||||
- `docs/dev-log.md`
|
||||
|
||||
### Details
|
||||
|
||||
- Added `Walk Circle` and `Left Cell` continuous fake-client modes.
|
||||
- Added one-shot `Jump Once`, `Toggle Sneak`, `Return To Cell`, and
|
||||
`Teleport Test` actions.
|
||||
- Kept fake clients as external TCP clients that send normal newline-separated
|
||||
JSON transform packets through the existing server socket.
|
||||
- Preserved the existing `Idle` and `Walk To Player` controls.
|
||||
- Added Fake Clients tab buttons for `Jump Once`, `Toggle Sneak`, `Walk Circle`,
|
||||
`Leave Cell`, `Return To Cell`, and `Teleport Test`.
|
||||
- Kept Fake Clients table columns unchanged while continuing to refresh script,
|
||||
cell, and position snapshots from the manager.
|
||||
- Added a socket send lock and protected script, pose, movement flags, and
|
||||
one-shot action state with the existing client lock.
|
||||
- Left the Fallout 4 plugin, protocol, `server/server.py`, `server/fake_client.py`,
|
||||
and server networking behavior unchanged.
|
||||
|
||||
### Testing
|
||||
|
||||
- Ran `python -m py_compile server.py server_core.py client_session.py
|
||||
fake_client.py dev_server_app.py fake_player.py` from `server`; it succeeded.
|
||||
- Checked editor diagnostics for `server/fake_player.py` and
|
||||
`server/dev_server_app.py`; no linter errors were reported.
|
||||
- Ran a headless smoke test on alternate local port `7788`; a GUI-managed fake
|
||||
client connected through the real server socket and exercised Walk Circle,
|
||||
Toggle Sneak, Jump Once, Leave Cell, Return To Cell, and Teleport Test without
|
||||
disconnecting.
|
||||
- Not yet run: `python server.py` terminal launcher smoke test.
|
||||
- Not yet run: `python dev_server_app.py`, start the server, add/remove fake
|
||||
clients, and click each Fake Clients script/action button.
|
||||
- Not yet run: Fallout 4/F4SE validation inside `F4TTestCell01` that Walk
|
||||
Circle, Jump Once, Toggle Sneak, Leave Cell, Return To Cell, and Teleport Test
|
||||
produce the expected proxy behavior.
|
||||
|
||||
### Known Issues
|
||||
|
||||
- Full Fallout 4/F4SE validation still needs to be run manually.
|
||||
- Walk Circle is a simple mathematical circle only; it does not use pathfinding,
|
||||
navmesh, or obstacle avoidance.
|
||||
|
||||
### Next Steps
|
||||
|
||||
- Manually validate the GUI scripts with a real Fallout 4 client in
|
||||
`F4TTestCell01`.
|
||||
- Consider future fake-client scripts such as Walk Square, Follow Player, Sprint
|
||||
Toggle, Weapon Drawn Toggle, Disconnect After Delay, and multi-fake-player
|
||||
choreography.
|
||||
|
||||
---
|
||||
|
||||
## Entry Template
|
||||
|
||||
Use this format for future updates:
|
||||
|
||||
+33
-10
@@ -55,6 +55,19 @@ Select a fake client row to use the movement controls:
|
||||
inside the real player. While moving, it sends normal transform packets at
|
||||
about 10 Hz with `isMoving=true`; when it reaches the stop distance, it
|
||||
returns to low-rate idle keep-alive transforms.
|
||||
- `Walk Circle`: makes the selected fake client move continuously around a
|
||||
simple circle in `F4TTestCell01`, sending normal moving transforms at about
|
||||
10 Hz.
|
||||
- `Jump Once`: sends a short normal-movement jump arc with `isJumping=true`,
|
||||
then restores the fake client's ground Z and resumes the current script.
|
||||
- `Toggle Sneak`: toggles the fake client's persistent `isSneaking` flag. It
|
||||
does not move the fake client by itself.
|
||||
- `Leave Cell`: sends a `cell_change` transform to fake cell `DEADBEEF` and
|
||||
keeps the fake client connected in a low-rate `Left Cell` mode.
|
||||
- `Return To Cell`: sends a `cell_change` transform back to `0B000F99`, restores
|
||||
`worldspaceId=""`, and returns the fake client to `Idle`.
|
||||
- `Teleport Test`: sends one `movementType=teleport` transform to a
|
||||
deterministic test-cell offset, then returns the fake client to `Idle`.
|
||||
|
||||
These controls are developer test tooling only. They do not change the network
|
||||
protocol, server relay behavior, or Fallout 4 plugin behavior.
|
||||
@@ -123,8 +136,9 @@ python dev_server_app.py
|
||||
Click `Start Server`, then run `python fake_client.py` or launch Fallout 4
|
||||
through F4SE. You can also click `+ Add Fake Client` to create a GUI-managed
|
||||
synthetic client that sends an idle transform once per second. Select a fake
|
||||
client row and click `Set Idle` or `Walk To Player` to control its script. Click
|
||||
`- Remove Selected` to disconnect it cleanly.
|
||||
client row and click `Set Idle`, `Walk To Player`, `Walk Circle`, `Jump Once`,
|
||||
`Toggle Sneak`, `Leave Cell`, `Return To Cell`, or `Teleport Test` to control
|
||||
its dev script/action. Click `- Remove Selected` to disconnect it cleanly.
|
||||
|
||||
Expected behavior:
|
||||
|
||||
@@ -143,6 +157,9 @@ Expected behavior:
|
||||
for cell `0B000F99`.
|
||||
- GUI fake clients can be switched between `Idle` and `Walk To Player` scripts
|
||||
from the Fake Clients tab.
|
||||
- GUI fake clients can also run `Walk Circle`, `Left Cell`, and one-shot jump,
|
||||
sneak toggle, return-to-cell, and teleport test actions from the Fake Clients
|
||||
tab.
|
||||
- Walk To Player uses `FalloutTogetherServer.get_clients()` snapshots to find
|
||||
the lowest non-fake client with a valid last transform, then walks gradually
|
||||
toward that player with a simple straight-line movement test.
|
||||
@@ -185,17 +202,21 @@ python dev_server_app.py
|
||||
Click `Start Server`, then connect `fake_client.py` or the Fallout 4 plugin.
|
||||
To validate GUI fake clients, click `+ Add Fake Client` and confirm the fake
|
||||
client appears in the Fake Clients table and Connected Clients table with an
|
||||
assigned player ID. Select the fake client, click `Set Idle`, and confirm the
|
||||
Script column stays or changes to `Idle`. Click `Walk To Player` with no real
|
||||
Fallout 4 client connected and confirm the GUI logs that no real player target
|
||||
is available. Click `- Remove Selected` and confirm it disconnects.
|
||||
assigned player ID. Select the fake client, click each Fake Clients action, and
|
||||
confirm the Script, Cell ID, Position, and console log output update as expected.
|
||||
Click `Walk To Player` with no real Fallout 4 client connected and confirm the
|
||||
GUI logs that no real player target is available. Click `- Remove Selected` and
|
||||
confirm it disconnects.
|
||||
|
||||
For in-game validation, launch Fallout 4 through F4SE and confirm the plugin
|
||||
connects, receives a `welcome` packet, sends transforms, and receives relayed
|
||||
remote transforms or disconnect packets when another client is connected. In
|
||||
`F4TTestCell01`, select a GUI fake client and click `Walk To Player`; the fake
|
||||
client should walk smoothly toward the real player's latest transform offset and
|
||||
then stop near that offset.
|
||||
then stop near that offset. Test `Walk Circle`, `Jump Once`, `Toggle Sneak`,
|
||||
`Leave Cell`, `Return To Cell`, and `Teleport Test` while watching the proxy to
|
||||
confirm movement, jump/sneak state, cell mismatch holding, return, and teleport
|
||||
snap behavior.
|
||||
|
||||
## Implemented
|
||||
|
||||
@@ -217,6 +238,8 @@ then stop near that offset.
|
||||
- Fake Clients tab movement controls for `Set Idle` and `Walk To Player`
|
||||
- GUI-managed fake clients that can walk gradually toward a real Fallout 4
|
||||
client using normal transform packets
|
||||
- Fake Clients tab dev scripts/actions for `Jump Once`, `Toggle Sneak`,
|
||||
`Walk Circle`, `Leave Cell`, `Return To Cell`, and `Teleport Test`
|
||||
|
||||
## Working In Fake Client Only
|
||||
|
||||
@@ -227,9 +250,9 @@ then stop near that offset.
|
||||
|
||||
## Planned Later
|
||||
|
||||
- Additional fake client scripts such as Walk Circle, Walk Square, Follow
|
||||
Player, Jump Loop, Sneak Toggle, Teleport Test, Leave Cell / Return, and
|
||||
Disconnect After Delay
|
||||
- Additional fake client scripts such as Walk Square, Follow Player, Sprint
|
||||
Toggle, Weapon Drawn Toggle, Disconnect After Delay, and multi-fake-player
|
||||
choreography
|
||||
- Gameplay synchronization
|
||||
- Authentication
|
||||
- Public matchmaking
|
||||
|
||||
@@ -184,9 +184,38 @@ class DevServerWindow(QMainWindow):
|
||||
self.walk_to_player_button.clicked.connect(self._set_selected_fake_client_walk_to_player)
|
||||
action_layout.addWidget(self.walk_to_player_button)
|
||||
|
||||
self.walk_circle_button = QPushButton("Walk Circle")
|
||||
self.walk_circle_button.clicked.connect(self._set_selected_fake_client_walk_circle)
|
||||
action_layout.addWidget(self.walk_circle_button)
|
||||
|
||||
action_layout.addStretch()
|
||||
layout.addLayout(action_layout)
|
||||
|
||||
script_action_layout = QHBoxLayout()
|
||||
|
||||
self.jump_once_button = QPushButton("Jump Once")
|
||||
self.jump_once_button.clicked.connect(self._trigger_selected_fake_client_jump_once)
|
||||
script_action_layout.addWidget(self.jump_once_button)
|
||||
|
||||
self.toggle_sneak_button = QPushButton("Toggle Sneak")
|
||||
self.toggle_sneak_button.clicked.connect(self._toggle_selected_fake_client_sneak)
|
||||
script_action_layout.addWidget(self.toggle_sneak_button)
|
||||
|
||||
self.leave_cell_button = QPushButton("Leave Cell")
|
||||
self.leave_cell_button.clicked.connect(self._trigger_selected_fake_client_leave_cell)
|
||||
script_action_layout.addWidget(self.leave_cell_button)
|
||||
|
||||
self.return_to_cell_button = QPushButton("Return To Cell")
|
||||
self.return_to_cell_button.clicked.connect(self._trigger_selected_fake_client_return_to_cell)
|
||||
script_action_layout.addWidget(self.return_to_cell_button)
|
||||
|
||||
self.teleport_test_button = QPushButton("Teleport Test")
|
||||
self.teleport_test_button.clicked.connect(self._trigger_selected_fake_client_teleport_test)
|
||||
script_action_layout.addWidget(self.teleport_test_button)
|
||||
|
||||
script_action_layout.addStretch()
|
||||
layout.addLayout(script_action_layout)
|
||||
|
||||
self.fake_clients_table = QTableWidget()
|
||||
self.fake_clients_table.setColumnCount(6)
|
||||
self.fake_clients_table.setHorizontalHeaderLabels(
|
||||
@@ -198,9 +227,9 @@ class DevServerWindow(QMainWindow):
|
||||
self.fake_clients_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
layout.addWidget(self.fake_clients_table)
|
||||
|
||||
# TODO: Add future fake-client scripts: Walk Circle, Walk Square,
|
||||
# Follow Player, Jump Loop, Sneak Toggle, Teleport Test,
|
||||
# Leave Cell / Return, and Disconnect After Delay.
|
||||
# TODO: Add future fake-client scripts: Walk Square, Follow Player,
|
||||
# Sprint Toggle, Weapon Drawn Toggle, Disconnect After Delay, and
|
||||
# Multi-fake-player choreography.
|
||||
|
||||
return tab
|
||||
|
||||
@@ -264,6 +293,72 @@ class DevServerWindow(QMainWindow):
|
||||
|
||||
self._refresh_fake_clients_table()
|
||||
|
||||
def _set_selected_fake_client_walk_circle(self) -> None:
|
||||
client_key = self._get_selected_fake_client_key_for_control()
|
||||
if client_key is None:
|
||||
return
|
||||
|
||||
if not self.fake_player_manager.set_client_walk_circle(client_key):
|
||||
self._append_log("Select a fake client first.")
|
||||
return
|
||||
|
||||
self._refresh_fake_clients_table()
|
||||
|
||||
def _trigger_selected_fake_client_jump_once(self) -> None:
|
||||
client_key = self._get_selected_fake_client_key_for_control()
|
||||
if client_key is None:
|
||||
return
|
||||
|
||||
if not self.fake_player_manager.trigger_client_jump_once(client_key):
|
||||
self._append_log("Select a fake client first.")
|
||||
return
|
||||
|
||||
self._refresh_fake_clients_table()
|
||||
|
||||
def _toggle_selected_fake_client_sneak(self) -> None:
|
||||
client_key = self._get_selected_fake_client_key_for_control()
|
||||
if client_key is None:
|
||||
return
|
||||
|
||||
if not self.fake_player_manager.toggle_client_sneak(client_key):
|
||||
self._append_log("Select a fake client first.")
|
||||
return
|
||||
|
||||
self._refresh_fake_clients_table()
|
||||
|
||||
def _trigger_selected_fake_client_leave_cell(self) -> None:
|
||||
client_key = self._get_selected_fake_client_key_for_control()
|
||||
if client_key is None:
|
||||
return
|
||||
|
||||
if not self.fake_player_manager.trigger_client_leave_cell(client_key):
|
||||
self._append_log("Select a fake client first.")
|
||||
return
|
||||
|
||||
self._refresh_fake_clients_table()
|
||||
|
||||
def _trigger_selected_fake_client_return_to_cell(self) -> None:
|
||||
client_key = self._get_selected_fake_client_key_for_control()
|
||||
if client_key is None:
|
||||
return
|
||||
|
||||
if not self.fake_player_manager.trigger_client_return_to_cell(client_key):
|
||||
self._append_log("Select a fake client first.")
|
||||
return
|
||||
|
||||
self._refresh_fake_clients_table()
|
||||
|
||||
def _trigger_selected_fake_client_teleport_test(self) -> None:
|
||||
client_key = self._get_selected_fake_client_key_for_control()
|
||||
if client_key is None:
|
||||
return
|
||||
|
||||
if not self.fake_player_manager.trigger_client_teleport_test(client_key):
|
||||
self._append_log("Select a fake client first.")
|
||||
return
|
||||
|
||||
self._refresh_fake_clients_table()
|
||||
|
||||
def _get_selected_fake_client_key_for_control(self) -> int | None:
|
||||
client_key = self._get_selected_fake_client_key()
|
||||
if client_key is None:
|
||||
|
||||
+288
-5
@@ -13,6 +13,8 @@ HOST = "127.0.0.1"
|
||||
PORT = 7777
|
||||
SCRIPT_IDLE = "Idle"
|
||||
SCRIPT_WALK_TO_PLAYER = "Walk To Player"
|
||||
SCRIPT_WALK_CIRCLE = "Walk Circle"
|
||||
SCRIPT_LEFT_CELL = "Left Cell"
|
||||
STATUS_CONNECTING = "Connecting"
|
||||
STATUS_CONNECTED = "Connected"
|
||||
STATUS_DISCONNECTED = "Disconnected"
|
||||
@@ -22,16 +24,25 @@ WALK_TO_PLAYER_SEND_RATE_HZ = 10.0
|
||||
WALK_TO_PLAYER_SPEED = 180.0
|
||||
WALK_TO_PLAYER_STOP_DISTANCE = 100.0
|
||||
WALK_TO_PLAYER_OFFSET_X = 150.0
|
||||
WALK_CIRCLE_RADIUS = 300.0
|
||||
WALK_CIRCLE_SPEED = 180.0
|
||||
WALK_CIRCLE_SEND_RATE_HZ = 10.0
|
||||
JUMP_DURATION_SECONDS = 0.8
|
||||
JUMP_HEIGHT = 120.0
|
||||
JUMP_SEND_RATE_HZ = 10.0
|
||||
NO_TARGET_LOG_INTERVAL_SECONDS = 5.0
|
||||
SOCKET_TIMEOUT_SECONDS = 0.05
|
||||
STOP_JOIN_TIMEOUT_SECONDS = 2.0
|
||||
|
||||
IDLE_CELL_ID = "0B000F99"
|
||||
IDLE_WORLDSPACE_ID = ""
|
||||
LEFT_CELL_ID = "DEADBEEF"
|
||||
IDLE_X = -1792.0
|
||||
IDLE_Y = -1344.0
|
||||
IDLE_Z = 0.0
|
||||
IDLE_ANGLE_Z = 3.93
|
||||
TELEPORT_TEST_OFFSET_X = 900.0
|
||||
TELEPORT_TEST_OFFSET_Y = 450.0
|
||||
|
||||
ClientSnapshotProvider = Callable[[], list[dict[str, Any]]]
|
||||
WalkTargetProvider = Callable[[int], dict[str, Any] | None]
|
||||
@@ -57,10 +68,13 @@ class FakePlayerClient:
|
||||
self.worldspace_id = IDLE_WORLDSPACE_ID
|
||||
self.position = (IDLE_X, IDLE_Y, IDLE_Z)
|
||||
self.angle_z = IDLE_ANGLE_Z
|
||||
self.is_sneaking = False
|
||||
self.is_jumping = False
|
||||
|
||||
self._log_callback = log_callback
|
||||
self._walk_target_provider = walk_target_provider
|
||||
self._lock = threading.RLock()
|
||||
self._send_lock = threading.Lock()
|
||||
self._stop_event = threading.Event()
|
||||
self._script_changed_event = threading.Event()
|
||||
self._thread: threading.Thread | None = None
|
||||
@@ -71,6 +85,11 @@ class FakePlayerClient:
|
||||
self._last_no_target_log_at = 0.0
|
||||
self._last_target_player_id: int | None = None
|
||||
self._reached_target_player_id: int | None = None
|
||||
self._walk_circle_center = self.position
|
||||
self._walk_circle_phase = 0.0
|
||||
self._one_shot_active = False
|
||||
self._jump_thread: threading.Thread | None = None
|
||||
self._teleport_count = 0
|
||||
|
||||
def start(self) -> None:
|
||||
with self._lock:
|
||||
@@ -91,6 +110,10 @@ class FakePlayerClient:
|
||||
if thread is not None and thread is not threading.current_thread():
|
||||
thread.join(timeout=STOP_JOIN_TIMEOUT_SECONDS)
|
||||
|
||||
jump_thread = self._jump_thread
|
||||
if jump_thread is not None and jump_thread is not threading.current_thread():
|
||||
jump_thread.join(timeout=STOP_JOIN_TIMEOUT_SECONDS)
|
||||
|
||||
with self._lock:
|
||||
if self._status != STATUS_ERROR:
|
||||
self._status = STATUS_DISCONNECTED
|
||||
@@ -101,6 +124,103 @@ class FakePlayerClient:
|
||||
def set_walk_to_player(self) -> None:
|
||||
self._set_script(SCRIPT_WALK_TO_PLAYER)
|
||||
|
||||
def set_walk_circle(self) -> None:
|
||||
with self._lock:
|
||||
self.cell_id = IDLE_CELL_ID
|
||||
self.worldspace_id = IDLE_WORLDSPACE_ID
|
||||
self._walk_circle_center = self.position
|
||||
self._walk_circle_phase = 0.0
|
||||
|
||||
self._set_script(SCRIPT_WALK_CIRCLE)
|
||||
|
||||
def trigger_jump_once(self) -> None:
|
||||
with self._lock:
|
||||
if self._one_shot_active:
|
||||
return
|
||||
|
||||
self._one_shot_active = True
|
||||
previous_script = self.script
|
||||
thread = threading.Thread(
|
||||
target=self._run_jump_once,
|
||||
args=(previous_script,),
|
||||
name=f"{self.name} Jump Once",
|
||||
daemon=True,
|
||||
)
|
||||
self._jump_thread = thread
|
||||
|
||||
self._log(f"{self.name} triggered Jump Once.")
|
||||
thread.start()
|
||||
|
||||
def toggle_sneak(self) -> bool:
|
||||
with self._lock:
|
||||
self.is_sneaking = not self.is_sneaking
|
||||
is_sneaking = self.is_sneaking
|
||||
|
||||
self._script_changed_event.set()
|
||||
self._log(f"{self.name} toggled sneak: {str(is_sneaking).lower()}.")
|
||||
return is_sneaking
|
||||
|
||||
def trigger_leave_cell(self) -> None:
|
||||
with self._lock:
|
||||
self.script = SCRIPT_LEFT_CELL
|
||||
self.cell_id = LEFT_CELL_ID
|
||||
self.worldspace_id = IDLE_WORLDSPACE_ID
|
||||
self._last_no_target_log_at = 0.0
|
||||
self._last_target_player_id = None
|
||||
self._reached_target_player_id = None
|
||||
|
||||
self._script_changed_event.set()
|
||||
self._send_immediate_transform(
|
||||
movement_type="cell_change",
|
||||
is_moving=False,
|
||||
movement_speed=0.0,
|
||||
)
|
||||
self._log(f"{self.name} left test cell.")
|
||||
|
||||
def trigger_return_to_cell(self) -> None:
|
||||
with self._lock:
|
||||
self.script = SCRIPT_IDLE
|
||||
self.cell_id = IDLE_CELL_ID
|
||||
self.worldspace_id = IDLE_WORLDSPACE_ID
|
||||
self.position = (IDLE_X, IDLE_Y, IDLE_Z)
|
||||
self.angle_z = IDLE_ANGLE_Z
|
||||
self._last_no_target_log_at = 0.0
|
||||
self._last_target_player_id = None
|
||||
self._reached_target_player_id = None
|
||||
|
||||
self._script_changed_event.set()
|
||||
self._send_immediate_transform(
|
||||
movement_type="cell_change",
|
||||
is_moving=False,
|
||||
movement_speed=0.0,
|
||||
)
|
||||
self._log(f"{self.name} returned to test cell.")
|
||||
|
||||
def trigger_teleport_test(self) -> None:
|
||||
with self._lock:
|
||||
self.script = SCRIPT_IDLE
|
||||
self.cell_id = IDLE_CELL_ID
|
||||
self.worldspace_id = IDLE_WORLDSPACE_ID
|
||||
current_x, current_y, current_z = self.position
|
||||
direction = -1.0 if self._teleport_count % 2 else 1.0
|
||||
self._teleport_count += 1
|
||||
self.position = (
|
||||
current_x + (TELEPORT_TEST_OFFSET_X * direction),
|
||||
current_y + (TELEPORT_TEST_OFFSET_Y * direction),
|
||||
current_z,
|
||||
)
|
||||
self._last_no_target_log_at = 0.0
|
||||
self._last_target_player_id = None
|
||||
self._reached_target_player_id = None
|
||||
|
||||
self._script_changed_event.set()
|
||||
self._send_immediate_transform(
|
||||
movement_type="teleport",
|
||||
is_moving=False,
|
||||
movement_speed=0.0,
|
||||
)
|
||||
self._log(f"{self.name} triggered Teleport Test.")
|
||||
|
||||
def get_snapshot(self) -> dict[str, Any]:
|
||||
with self._lock:
|
||||
return {
|
||||
@@ -218,8 +338,13 @@ class FakePlayerClient:
|
||||
|
||||
def _send_script_transform(self, connection: socket.socket, delta_seconds: float) -> float:
|
||||
script = self._get_script()
|
||||
if self._is_one_shot_active():
|
||||
return SOCKET_TIMEOUT_SECONDS
|
||||
|
||||
if script == SCRIPT_WALK_TO_PLAYER:
|
||||
return self._send_walk_to_player_transform(connection, delta_seconds)
|
||||
if script == SCRIPT_WALK_CIRCLE:
|
||||
return self._send_walk_circle_transform(connection, delta_seconds)
|
||||
|
||||
self._send_transform(connection, is_moving=False, movement_speed=0.0)
|
||||
return IDLE_SEND_INTERVAL_SECONDS
|
||||
@@ -283,12 +408,117 @@ class FakePlayerClient:
|
||||
self._send_transform(connection, is_moving=is_moving, movement_speed=movement_speed)
|
||||
return walk_interval if is_moving else IDLE_SEND_INTERVAL_SECONDS
|
||||
|
||||
def _send_transform(self, connection: socket.socket, *, is_moving: bool, movement_speed: float) -> None:
|
||||
def _send_walk_circle_transform(self, connection: socket.socket, delta_seconds: float) -> float:
|
||||
walk_interval = 1.0 / WALK_CIRCLE_SEND_RATE_HZ
|
||||
step_delta = max(delta_seconds, walk_interval)
|
||||
angular_speed = WALK_CIRCLE_SPEED / WALK_CIRCLE_RADIUS
|
||||
|
||||
with self._lock:
|
||||
self.cell_id = IDLE_CELL_ID
|
||||
self.worldspace_id = IDLE_WORLDSPACE_ID
|
||||
center_x, center_y, center_z = self._walk_circle_center
|
||||
self._walk_circle_phase += angular_speed * step_delta
|
||||
phase = self._walk_circle_phase
|
||||
new_x = center_x + (WALK_CIRCLE_RADIUS * math.cos(phase))
|
||||
new_y = center_y + (WALK_CIRCLE_RADIUS * math.sin(phase))
|
||||
new_z = center_z
|
||||
tangent_x = -math.sin(phase)
|
||||
tangent_y = math.cos(phase)
|
||||
self.position = (new_x, new_y, new_z)
|
||||
self.angle_z = math.atan2(tangent_x, tangent_y)
|
||||
|
||||
self._send_transform(connection, is_moving=True, movement_speed=WALK_CIRCLE_SPEED)
|
||||
return walk_interval
|
||||
|
||||
def _run_jump_once(self, previous_script: str) -> None:
|
||||
interval_seconds = 1.0 / JUMP_SEND_RATE_HZ
|
||||
steps = max(2, int(JUMP_DURATION_SECONDS * JUMP_SEND_RATE_HZ))
|
||||
|
||||
with self._lock:
|
||||
ground_x, ground_y, ground_z = self.position
|
||||
|
||||
try:
|
||||
for step_index in range(steps + 1):
|
||||
if self._stop_event.is_set():
|
||||
return
|
||||
|
||||
progress = step_index / steps
|
||||
height = JUMP_HEIGHT * (1.0 - abs((2.0 * progress) - 1.0))
|
||||
with self._lock:
|
||||
self.position = (ground_x, ground_y, ground_z + height)
|
||||
self.is_jumping = True
|
||||
|
||||
self._send_immediate_transform(
|
||||
movement_type="normal",
|
||||
is_moving=False,
|
||||
movement_speed=0.0,
|
||||
is_jumping=True,
|
||||
)
|
||||
time.sleep(interval_seconds)
|
||||
finally:
|
||||
with self._lock:
|
||||
self.position = (ground_x, ground_y, ground_z)
|
||||
self.is_jumping = False
|
||||
self._one_shot_active = False
|
||||
if self.script == previous_script and previous_script not in (
|
||||
SCRIPT_WALK_TO_PLAYER,
|
||||
SCRIPT_WALK_CIRCLE,
|
||||
SCRIPT_LEFT_CELL,
|
||||
):
|
||||
self.script = SCRIPT_IDLE
|
||||
|
||||
self._script_changed_event.set()
|
||||
self._send_immediate_transform(
|
||||
movement_type="normal",
|
||||
is_moving=False,
|
||||
movement_speed=0.0,
|
||||
is_jumping=False,
|
||||
)
|
||||
|
||||
def _send_immediate_transform(
|
||||
self,
|
||||
*,
|
||||
movement_type: str,
|
||||
is_moving: bool,
|
||||
movement_speed: float,
|
||||
is_jumping: bool | None = None,
|
||||
) -> None:
|
||||
with self._lock:
|
||||
connection = self._socket
|
||||
is_connected = self._status == STATUS_CONNECTED
|
||||
|
||||
if connection is None or not is_connected:
|
||||
return
|
||||
|
||||
try:
|
||||
self._send_transform(
|
||||
connection,
|
||||
movement_type=movement_type,
|
||||
is_moving=is_moving,
|
||||
movement_speed=movement_speed,
|
||||
is_jumping=is_jumping,
|
||||
)
|
||||
except OSError as error:
|
||||
if not self._stop_event.is_set():
|
||||
self._set_error(str(error))
|
||||
self._log(f"{self.name} error: {error}")
|
||||
|
||||
def _send_transform(
|
||||
self,
|
||||
connection: socket.socket,
|
||||
*,
|
||||
is_moving: bool,
|
||||
movement_speed: float,
|
||||
movement_type: str = "normal",
|
||||
is_jumping: bool | None = None,
|
||||
) -> None:
|
||||
with self._lock:
|
||||
x, y, z = self.position
|
||||
angle_z = self.angle_z
|
||||
cell_id = self.cell_id
|
||||
worldspace_id = self.worldspace_id
|
||||
is_sneaking = self.is_sneaking
|
||||
jumping = self.is_jumping if is_jumping is None else is_jumping
|
||||
|
||||
packet = {
|
||||
"type": "transform",
|
||||
@@ -296,19 +526,20 @@ class FakePlayerClient:
|
||||
"y": y,
|
||||
"z": z,
|
||||
"angleZ": angle_z,
|
||||
"movementType": "normal",
|
||||
"movementType": movement_type,
|
||||
"cellId": cell_id,
|
||||
"worldspaceId": worldspace_id,
|
||||
"clientTime": time.time(),
|
||||
"isMoving": is_moving,
|
||||
"isSprinting": False,
|
||||
"isSneaking": False,
|
||||
"isJumping": False,
|
||||
"isSneaking": is_sneaking,
|
||||
"isJumping": jumping,
|
||||
"weaponDrawn": False,
|
||||
"movementSpeed": movement_speed,
|
||||
}
|
||||
encoded = json.dumps(packet, separators=(",", ":")).encode("utf-8") + b"\n"
|
||||
connection.sendall(encoded)
|
||||
with self._send_lock:
|
||||
connection.sendall(encoded)
|
||||
|
||||
def _get_script(self) -> str:
|
||||
with self._lock:
|
||||
@@ -335,6 +566,10 @@ class FakePlayerClient:
|
||||
with self._lock:
|
||||
return self._status == STATUS_CONNECTED
|
||||
|
||||
def _is_one_shot_active(self) -> bool:
|
||||
with self._lock:
|
||||
return self._one_shot_active
|
||||
|
||||
def _set_error(self, message: str) -> None:
|
||||
with self._lock:
|
||||
self._status = STATUS_ERROR
|
||||
@@ -422,6 +657,54 @@ class FakePlayerManager:
|
||||
client.set_walk_to_player()
|
||||
return True
|
||||
|
||||
def set_client_walk_circle(self, client_key: int) -> bool:
|
||||
client = self._get_client(client_key)
|
||||
if client is None:
|
||||
return False
|
||||
|
||||
client.set_walk_circle()
|
||||
return True
|
||||
|
||||
def trigger_client_jump_once(self, client_key: int) -> bool:
|
||||
client = self._get_client(client_key)
|
||||
if client is None:
|
||||
return False
|
||||
|
||||
client.trigger_jump_once()
|
||||
return True
|
||||
|
||||
def toggle_client_sneak(self, client_key: int) -> bool:
|
||||
client = self._get_client(client_key)
|
||||
if client is None:
|
||||
return False
|
||||
|
||||
client.toggle_sneak()
|
||||
return True
|
||||
|
||||
def trigger_client_leave_cell(self, client_key: int) -> bool:
|
||||
client = self._get_client(client_key)
|
||||
if client is None:
|
||||
return False
|
||||
|
||||
client.trigger_leave_cell()
|
||||
return True
|
||||
|
||||
def trigger_client_return_to_cell(self, client_key: int) -> bool:
|
||||
client = self._get_client(client_key)
|
||||
if client is None:
|
||||
return False
|
||||
|
||||
client.trigger_return_to_cell()
|
||||
return True
|
||||
|
||||
def trigger_client_teleport_test(self, client_key: int) -> bool:
|
||||
client = self._get_client(client_key)
|
||||
if client is None:
|
||||
return False
|
||||
|
||||
client.trigger_teleport_test()
|
||||
return True
|
||||
|
||||
def stop_all(self) -> None:
|
||||
with self._lock:
|
||||
clients = list(self._clients.values())
|
||||
|
||||
Reference in New Issue
Block a user