Make spawn and movement acceptance server-authoritative
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||||
window.CHGRID_WEB_VERSION = "2026.02.25 R230";
|
window.CHGRID_WEB_VERSION = "2026.02.25 R231";
|
||||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ const renderer = new CanvasRenderer(dom.canvas);
|
|||||||
const audio = new AudioEngine();
|
const audio = new AudioEngine();
|
||||||
const settings = new SettingsStore();
|
const settings = new SettingsStore();
|
||||||
let worldGridSize = GRID_SIZE;
|
let worldGridSize = GRID_SIZE;
|
||||||
|
let movementTickMs = MOVE_COOLDOWN_MS;
|
||||||
let lastWallCollisionDirection: string | null = null;
|
let lastWallCollisionDirection: string | null = null;
|
||||||
let statusTimeout: number | null = null;
|
let statusTimeout: number | null = null;
|
||||||
let lastFocusedElement: Element | null = null;
|
let lastFocusedElement: Element | null = null;
|
||||||
@@ -1122,7 +1123,7 @@ function handleMovement(): void {
|
|||||||
if (state.mode !== 'normal') return;
|
if (state.mode !== 'normal') return;
|
||||||
if (activeTeleport) return;
|
if (activeTeleport) return;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - state.player.lastMoveTime < MOVE_COOLDOWN_MS) return;
|
if (now - state.player.lastMoveTime < movementTickMs) return;
|
||||||
|
|
||||||
let dx = 0;
|
let dx = 0;
|
||||||
let dy = 0;
|
let dy = 0;
|
||||||
@@ -1154,7 +1155,7 @@ function handleMovement(): void {
|
|||||||
persistPlayerPosition();
|
persistPlayerPosition();
|
||||||
state.player.lastMoveTime = now;
|
state.player.lastMoveTime = now;
|
||||||
void refreshAudioSubscriptions(true);
|
void refreshAudioSubscriptions(true);
|
||||||
void audio.playSample(randomFootstepUrl(), FOOTSTEP_GAIN, MOVE_COOLDOWN_MS);
|
void audio.playSample(randomFootstepUrl(), FOOTSTEP_GAIN, movementTickMs);
|
||||||
signaling.send({ type: 'update_position', x: nextX, y: nextY });
|
signaling.send({ type: 'update_position', x: nextX, y: nextY });
|
||||||
|
|
||||||
const namesOnTile = getPeerNamesAtPosition(nextX, nextY);
|
const namesOnTile = getPeerNamesAtPosition(nextX, nextY);
|
||||||
@@ -1307,7 +1308,6 @@ function getConnectionFlowDeps(): ConnectFlowDeps {
|
|||||||
signalingConnect: (handler) => signaling.connect(handler as (message: IncomingMessage) => Promise<void>),
|
signalingConnect: (handler) => signaling.connect(handler as (message: IncomingMessage) => Promise<void>),
|
||||||
signalingDisconnect: () => signaling.disconnect(),
|
signalingDisconnect: () => signaling.disconnect(),
|
||||||
onMessage: (message) => onSignalingMessage(message as IncomingMessage),
|
onMessage: (message) => onSignalingMessage(message as IncomingMessage),
|
||||||
worldGridSize,
|
|
||||||
persistPlayerPosition,
|
persistPlayerPosition,
|
||||||
peerManagerCleanupAll: () => peerManager.cleanupAll(),
|
peerManagerCleanupAll: () => peerManager.cleanupAll(),
|
||||||
radioCleanupAll: () => radioRuntime.cleanupAll(),
|
radioCleanupAll: () => radioRuntime.cleanupAll(),
|
||||||
@@ -1346,6 +1346,9 @@ const onAppMessage = createOnMessageHandler({
|
|||||||
setWorldGridSize: (size) => {
|
setWorldGridSize: (size) => {
|
||||||
worldGridSize = size;
|
worldGridSize = size;
|
||||||
},
|
},
|
||||||
|
setMovementTickMs: (value) => {
|
||||||
|
movementTickMs = Math.max(1, value);
|
||||||
|
},
|
||||||
setConnecting: (value) => {
|
setConnecting: (value) => {
|
||||||
mediaSession.setConnecting(value);
|
mediaSession.setConnecting(value);
|
||||||
updateConnectAvailability();
|
updateConnectAvailability();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { type WorldItem } from '../state/gameState';
|
|||||||
type MessageHandlerDeps = {
|
type MessageHandlerDeps = {
|
||||||
getWorldGridSize: () => number;
|
getWorldGridSize: () => number;
|
||||||
setWorldGridSize: (size: number) => void;
|
setWorldGridSize: (size: number) => void;
|
||||||
|
setMovementTickMs: (value: number) => void;
|
||||||
setConnecting: (value: boolean) => void;
|
setConnecting: (value: boolean) => void;
|
||||||
rendererSetGridSize: (size: number) => void;
|
rendererSetGridSize: (size: number) => void;
|
||||||
applyServerItemUiDefinitions: (defs: unknown) => boolean;
|
applyServerItemUiDefinitions: (defs: unknown) => boolean;
|
||||||
@@ -82,6 +83,9 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
|
|||||||
if (message.worldConfig?.gridSize && Number.isInteger(message.worldConfig.gridSize) && message.worldConfig.gridSize > 0) {
|
if (message.worldConfig?.gridSize && Number.isInteger(message.worldConfig.gridSize) && message.worldConfig.gridSize > 0) {
|
||||||
deps.setWorldGridSize(message.worldConfig.gridSize);
|
deps.setWorldGridSize(message.worldConfig.gridSize);
|
||||||
}
|
}
|
||||||
|
if (message.worldConfig?.movementTickMs && Number.isInteger(message.worldConfig.movementTickMs) && message.worldConfig.movementTickMs > 0) {
|
||||||
|
deps.setMovementTickMs(message.worldConfig.movementTickMs);
|
||||||
|
}
|
||||||
deps.rendererSetGridSize(deps.getWorldGridSize());
|
deps.rendererSetGridSize(deps.getWorldGridSize());
|
||||||
const schemaReady = deps.applyServerItemUiDefinitions(message.uiDefinitions);
|
const schemaReady = deps.applyServerItemUiDefinitions(message.uiDefinitions);
|
||||||
if (!schemaReady) {
|
if (!schemaReady) {
|
||||||
@@ -92,8 +96,8 @@ export function createOnMessageHandler(deps: MessageHandlerDeps): (message: Inco
|
|||||||
deps.state.player.id = message.id;
|
deps.state.player.id = message.id;
|
||||||
deps.state.running = true;
|
deps.state.running = true;
|
||||||
deps.setConnecting(false);
|
deps.setConnecting(false);
|
||||||
deps.state.player.x = Math.max(0, Math.min(deps.getWorldGridSize() - 1, deps.state.player.x));
|
deps.state.player.x = Math.max(0, Math.min(deps.getWorldGridSize() - 1, message.player.x));
|
||||||
deps.state.player.y = Math.max(0, Math.min(deps.getWorldGridSize() - 1, deps.state.player.y));
|
deps.state.player.y = Math.max(0, Math.min(deps.getWorldGridSize() - 1, message.player.y));
|
||||||
deps.dom.nicknameContainer.classList.add('hidden');
|
deps.dom.nicknameContainer.classList.add('hidden');
|
||||||
deps.dom.connectButton.classList.add('hidden');
|
deps.dom.connectButton.classList.add('hidden');
|
||||||
deps.dom.disconnectButton.classList.remove('hidden');
|
deps.dom.disconnectButton.classList.remove('hidden');
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ export const itemSchema = z.object({
|
|||||||
export const welcomeMessageSchema = z.object({
|
export const welcomeMessageSchema = z.object({
|
||||||
type: z.literal('welcome'),
|
type: z.literal('welcome'),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
|
player: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
nickname: z.string(),
|
||||||
|
x: z.number().int(),
|
||||||
|
y: z.number().int(),
|
||||||
|
}),
|
||||||
users: z.array(
|
users: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -32,6 +38,8 @@ export const welcomeMessageSchema = z.object({
|
|||||||
worldConfig: z
|
worldConfig: z
|
||||||
.object({
|
.object({
|
||||||
gridSize: z.number().int().positive(),
|
gridSize: z.number().int().positive(),
|
||||||
|
movementTickMs: z.number().int().positive().optional(),
|
||||||
|
movementMaxStepsPerTick: z.number().int().positive().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
serverInfo: z
|
serverInfo: z
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ export type ConnectFlowDeps = {
|
|||||||
signalingConnect: (onMessage: (message: unknown) => Promise<void>) => Promise<void>;
|
signalingConnect: (onMessage: (message: unknown) => Promise<void>) => Promise<void>;
|
||||||
signalingDisconnect: () => void;
|
signalingDisconnect: () => void;
|
||||||
onMessage: (message: unknown) => Promise<void>;
|
onMessage: (message: unknown) => Promise<void>;
|
||||||
worldGridSize: number;
|
|
||||||
persistPlayerPosition: () => void;
|
persistPlayerPosition: () => void;
|
||||||
peerManagerCleanupAll: () => void;
|
peerManagerCleanupAll: () => void;
|
||||||
radioCleanupAll: () => void;
|
radioCleanupAll: () => void;
|
||||||
@@ -66,25 +65,6 @@ export async function runConnectFlow(deps: ConnectFlowDeps): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
deps.state.player.x = Math.floor(Math.random() * deps.worldGridSize);
|
|
||||||
deps.state.player.y = Math.floor(Math.random() * deps.worldGridSize);
|
|
||||||
const storedPosition = localStorage.getItem('spatialChatPosition');
|
|
||||||
if (storedPosition) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(storedPosition) as { x?: number; y?: number };
|
|
||||||
if (Number.isFinite(parsed.x) && Number.isFinite(parsed.y)) {
|
|
||||||
const x = Math.floor(parsed.x as number);
|
|
||||||
const y = Math.floor(parsed.y as number);
|
|
||||||
if (x >= 0 && x < deps.worldGridSize && y >= 0 && y < deps.worldGridSize) {
|
|
||||||
deps.state.player.x = x;
|
|
||||||
deps.state.player.y = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore malformed saved positions.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deps.mediaPopulateAudioDevices();
|
await deps.mediaPopulateAudioDevices();
|
||||||
if (deps.dom.audioInputSelect.options.length === 0) {
|
if (deps.dom.audioInputSelect.options.length === 0) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
|||||||
|
|
||||||
## Client -> Server
|
## Client -> Server
|
||||||
|
|
||||||
- `update_position`: authoritative player position update.
|
- `update_position`: client movement intent; server enforces world bounds and movement rate policy.
|
||||||
- `update_nickname`: nickname change request (server enforces uniqueness).
|
- `update_nickname`: nickname change request (server enforces uniqueness).
|
||||||
- `chat_message`: player chat.
|
- `chat_message`: player chat.
|
||||||
- `ping`: latency measurement.
|
- `ping`: latency measurement.
|
||||||
@@ -45,6 +45,9 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
|||||||
## Welcome Metadata
|
## Welcome Metadata
|
||||||
|
|
||||||
- `welcome.worldConfig.gridSize`: server-authoritative grid size used by clients for bounds/drawing.
|
- `welcome.worldConfig.gridSize`: server-authoritative grid size used by clients for bounds/drawing.
|
||||||
|
- `welcome.worldConfig.movementTickMs`: server movement-rate window used for client movement pacing.
|
||||||
|
- `welcome.worldConfig.movementMaxStepsPerTick`: max allowed grid steps per movement window.
|
||||||
|
- `welcome.player`: server-assigned spawn/current self position at connect time.
|
||||||
- `welcome.serverInfo`: server process identity/version metadata:
|
- `welcome.serverInfo`: server process identity/version metadata:
|
||||||
- `instanceId`: unique id generated at server startup
|
- `instanceId`: unique id generated at server startup
|
||||||
- `version`: server package version (or `unknown` fallback)
|
- `version`: server package version (or `unknown` fallback)
|
||||||
@@ -60,6 +63,7 @@ This is a behavior guide for packet semantics beyond raw schemas.
|
|||||||
## Validation Boundaries
|
## Validation Boundaries
|
||||||
|
|
||||||
- Server is authoritative for all action validation and normalization.
|
- Server is authoritative for all action validation and normalization.
|
||||||
|
- Server is authoritative for movement acceptance (bounds + rate/delta checks).
|
||||||
- Client validates incoming packet shapes and applies runtime behavior.
|
- Client validates incoming packet shapes and applies runtime behavior.
|
||||||
- Client-side item edit validation is convenience only; server remains source of truth.
|
- Client-side item edit validation is convenience only; server remains source of truth.
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,13 @@
|
|||||||
4. Server sends `welcome` with users/items snapshot.
|
4. Server sends `welcome` with users/items snapshot.
|
||||||
5. Client:
|
5. Client:
|
||||||
- applies `welcome.worldConfig.gridSize` for authoritative grid bounds/rendering
|
- applies `welcome.worldConfig.gridSize` for authoritative grid bounds/rendering
|
||||||
|
- applies `welcome.worldConfig.movementTickMs` as movement pacing guidance
|
||||||
|
- applies `welcome.worldConfig.movementMaxStepsPerTick` for movement-rate parity
|
||||||
|
- uses `welcome.player` as authoritative starting position
|
||||||
- records `welcome.serverInfo` (`instanceId`, `version`) for restart detection
|
- records `welcome.serverInfo` (`instanceId`, `version`) for restart detection
|
||||||
- if `welcome.serverInfo.version` differs from running client version, auto-reloads the page
|
- if `welcome.serverInfo.version` differs from running client version, auto-reloads the page
|
||||||
- applies `welcome.uiDefinitions` for item menus/properties/options
|
- applies `welcome.uiDefinitions` for item menus/properties/options
|
||||||
- sends initial `update_position`
|
- sends initial `update_position` echo from server-assigned starting tile
|
||||||
- sends initial `update_nickname`
|
- sends initial `update_nickname`
|
||||||
- creates peer runtimes for known users
|
- creates peer runtimes for known users
|
||||||
- syncs item runtimes (`radio`, `emit`)
|
- syncs item runtimes (`radio`, `emit`)
|
||||||
@@ -24,6 +27,7 @@
|
|||||||
Each frame:
|
Each frame:
|
||||||
|
|
||||||
- Handle local movement input.
|
- Handle local movement input.
|
||||||
|
- Send movement intents; server remains authoritative on accepted movement updates.
|
||||||
- Update spatial voice audio.
|
- Update spatial voice audio.
|
||||||
- Update spatial radio audio.
|
- Update spatial radio audio.
|
||||||
- Update spatial item emit audio.
|
- Update spatial item emit audio.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class ClientConnection:
|
|||||||
nickname: str = "user..."
|
nickname: str = "user..."
|
||||||
x: int = 20
|
x: int = 20
|
||||||
y: int = 20
|
y: int = 20
|
||||||
|
last_position_update_ms: int = 0
|
||||||
|
|
||||||
def summary(self) -> dict[str, str | int]:
|
def summary(self) -> dict[str, str | int]:
|
||||||
"""Return a compact serializable snapshot for logs/diagnostics."""
|
"""Return a compact serializable snapshot for logs/diagnostics."""
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class WorldConfigSection(BaseModel):
|
|||||||
"""Authoritative world geometry options."""
|
"""Authoritative world geometry options."""
|
||||||
|
|
||||||
grid_size: int = Field(default=41, ge=1)
|
grid_size: int = Field(default=41, ge=1)
|
||||||
|
movement_tick_ms: int = Field(default=100, ge=1)
|
||||||
|
movement_max_steps_per_tick: int = Field(default=2, ge=1)
|
||||||
|
|
||||||
|
|
||||||
class AppConfig(BaseModel):
|
class AppConfig(BaseModel):
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ class RemoteUser(BaseModel):
|
|||||||
class WelcomePacket(BasePacket):
|
class WelcomePacket(BasePacket):
|
||||||
type: Literal["welcome"]
|
type: Literal["welcome"]
|
||||||
id: str
|
id: str
|
||||||
|
player: RemoteUser
|
||||||
users: list[RemoteUser]
|
users: list[RemoteUser]
|
||||||
items: list[dict] | None = None
|
items: list[dict] | None = None
|
||||||
worldConfig: dict | None = None
|
worldConfig: dict | None = None
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from importlib.metadata import PackageNotFoundError, version as package_version
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
import ssl
|
import ssl
|
||||||
import time
|
import time
|
||||||
@@ -88,6 +89,8 @@ class SignalingServer:
|
|||||||
max_message_size: int = 2_000_000,
|
max_message_size: int = 2_000_000,
|
||||||
state_file: Path | None = None,
|
state_file: Path | None = None,
|
||||||
grid_size: int = 41,
|
grid_size: int = 41,
|
||||||
|
movement_tick_ms: int = 100,
|
||||||
|
movement_max_steps_per_tick: int = 2,
|
||||||
state_save_debounce_ms: int = 200,
|
state_save_debounce_ms: int = 200,
|
||||||
state_save_max_delay_ms: int = 1000,
|
state_save_max_delay_ms: int = 1000,
|
||||||
):
|
):
|
||||||
@@ -104,6 +107,8 @@ class SignalingServer:
|
|||||||
self.piano_recording_state_by_item: dict[str, dict] = {}
|
self.piano_recording_state_by_item: dict[str, dict] = {}
|
||||||
self.piano_playback_tasks_by_item: dict[str, asyncio.Task[None]] = {}
|
self.piano_playback_tasks_by_item: dict[str, asyncio.Task[None]] = {}
|
||||||
self.grid_size = max(1, grid_size)
|
self.grid_size = max(1, grid_size)
|
||||||
|
self.movement_tick_ms = max(1, int(movement_tick_ms))
|
||||||
|
self.movement_max_steps_per_tick = max(1, int(movement_max_steps_per_tick))
|
||||||
self.instance_id = str(uuid.uuid4())
|
self.instance_id = str(uuid.uuid4())
|
||||||
self.server_version = self._resolve_server_version()
|
self.server_version = self._resolve_server_version()
|
||||||
self.state_save_debounce_ms = max(1, int(state_save_debounce_ms))
|
self.state_save_debounce_ms = max(1, int(state_save_debounce_ms))
|
||||||
@@ -564,6 +569,13 @@ class SignalingServer:
|
|||||||
|
|
||||||
return 0 <= x < self.grid_size and 0 <= y < self.grid_size
|
return 0 <= x < self.grid_size and 0 <= y < self.grid_size
|
||||||
|
|
||||||
|
def _max_allowed_position_delta(self, client: ClientConnection, now_ms: int) -> int:
|
||||||
|
"""Compute max allowed movement delta using server-authoritative rate policy."""
|
||||||
|
|
||||||
|
elapsed_ms = max(0, now_ms - max(0, client.last_position_update_ms))
|
||||||
|
windows = max(1, elapsed_ms // self.movement_tick_ms)
|
||||||
|
return max(1, windows * self.movement_max_steps_per_tick)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _normalize_clock_timezone(value: object) -> str:
|
def _normalize_clock_timezone(value: object) -> str:
|
||||||
"""Normalize timezone input to one of supported clock zones."""
|
"""Normalize timezone input to one of supported clock zones."""
|
||||||
@@ -650,6 +662,9 @@ class SignalingServer:
|
|||||||
"""Handle one websocket client's connect/message/disconnect lifecycle."""
|
"""Handle one websocket client's connect/message/disconnect lifecycle."""
|
||||||
|
|
||||||
client = ClientConnection(websocket=websocket, id=str(uuid.uuid4()))
|
client = ClientConnection(websocket=websocket, id=str(uuid.uuid4()))
|
||||||
|
client.x = random.randrange(self.grid_size)
|
||||||
|
client.y = random.randrange(self.grid_size)
|
||||||
|
client.last_position_update_ms = self.item_service.now_ms()
|
||||||
self.clients[websocket] = client
|
self.clients[websocket] = client
|
||||||
LOGGER.info("client connected id=%s total=%d", client.id, len(self.clients))
|
LOGGER.info("client connected id=%s total=%d", client.id, len(self.clients))
|
||||||
|
|
||||||
@@ -695,9 +710,14 @@ class SignalingServer:
|
|||||||
packet = WelcomePacket(
|
packet = WelcomePacket(
|
||||||
type="welcome",
|
type="welcome",
|
||||||
id=client.id,
|
id=client.id,
|
||||||
|
player=RemoteUser(id=client.id, nickname=client.nickname, x=client.x, y=client.y),
|
||||||
users=users,
|
users=users,
|
||||||
items=[item.model_dump(exclude_none=True) for item in self.items.values()],
|
items=[item.model_dump(exclude_none=True) for item in self.items.values()],
|
||||||
worldConfig={"gridSize": self.grid_size},
|
worldConfig={
|
||||||
|
"gridSize": self.grid_size,
|
||||||
|
"movementTickMs": self.movement_tick_ms,
|
||||||
|
"movementMaxStepsPerTick": self.movement_max_steps_per_tick,
|
||||||
|
},
|
||||||
uiDefinitions=self._build_ui_definitions(),
|
uiDefinitions=self._build_ui_definitions(),
|
||||||
serverInfo={"instanceId": self.instance_id, "version": self.server_version},
|
serverInfo={"instanceId": self.instance_id, "version": self.server_version},
|
||||||
)
|
)
|
||||||
@@ -770,8 +790,24 @@ class SignalingServer:
|
|||||||
self.grid_size,
|
self.grid_size,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
now_ms = self.item_service.now_ms()
|
||||||
|
requested_delta = max(abs(packet.x - client.x), abs(packet.y - client.y))
|
||||||
|
max_delta = self._max_allowed_position_delta(client, now_ms)
|
||||||
|
if requested_delta > max_delta:
|
||||||
|
PACKET_LOGGER.warning(
|
||||||
|
"position rate limit ignored id=%s from=%d,%d to=%d,%d requested_delta=%d max_delta=%d",
|
||||||
|
client.id,
|
||||||
|
client.x,
|
||||||
|
client.y,
|
||||||
|
packet.x,
|
||||||
|
packet.y,
|
||||||
|
requested_delta,
|
||||||
|
max_delta,
|
||||||
|
)
|
||||||
|
return
|
||||||
client.x = packet.x
|
client.x = packet.x
|
||||||
client.y = packet.y
|
client.y = packet.y
|
||||||
|
client.last_position_update_ms = now_ms
|
||||||
await self._broadcast(
|
await self._broadcast(
|
||||||
BroadcastPositionPacket(type="update_position", id=client.id, x=client.x, y=client.y),
|
BroadcastPositionPacket(type="update_position", id=client.id, x=client.x, y=client.y),
|
||||||
exclude=client.websocket,
|
exclude=client.websocket,
|
||||||
@@ -1345,6 +1381,8 @@ def run() -> None:
|
|||||||
max_message_size=config.network.max_message_bytes,
|
max_message_size=config.network.max_message_bytes,
|
||||||
state_file=state_file,
|
state_file=state_file,
|
||||||
grid_size=config.world.grid_size,
|
grid_size=config.world.grid_size,
|
||||||
|
movement_tick_ms=config.world.movement_tick_ms,
|
||||||
|
movement_max_steps_per_tick=config.world.movement_max_steps_per_tick,
|
||||||
state_save_debounce_ms=config.storage.state_save_debounce_ms,
|
state_save_debounce_ms=config.storage.state_save_debounce_ms,
|
||||||
state_save_max_delay_ms=config.storage.state_save_max_delay_ms,
|
state_save_max_delay_ms=config.storage.state_save_max_delay_ms,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,3 +29,7 @@ state_save_max_delay_ms = 1000
|
|||||||
[world]
|
[world]
|
||||||
# Grid width/height in cells. Valid coordinates are 0..grid_size-1.
|
# Grid width/height in cells. Valid coordinates are 0..grid_size-1.
|
||||||
grid_size = 41
|
grid_size = 41
|
||||||
|
# Server-authoritative movement rate window in milliseconds.
|
||||||
|
movement_tick_ms = 100
|
||||||
|
# Max grid steps allowed per movement tick window.
|
||||||
|
movement_max_steps_per_tick = 2
|
||||||
|
|||||||
Reference in New Issue
Block a user