UNPKG

reldens

Version:
373 lines (286 loc) 11.3 kB
# Player State Flow - Complete Technical Guide ## Overview This document explains the complete player state management system in Reldens, including the database entity refactor that introduced the "related_" naming convention, and how player state flows from database to runtime. --- ## Architecture Layers ### 1. Database Layer (Persistent Storage) After the entity refactor, all database relations use the **"related_" prefix** (this is the NEW/CURRENT convention, NOT legacy): ```javascript UsersModel { id: number, email: string, username: string, password: string, role_id: number, // NEW: Database relations with "related_" prefix related_users_login: UsersLoginModel[], related_players: PlayersModel[] // Array of all players for this user } PlayersModel { id: number, user_id: number, name: string, created_at: Date, updated_at: Date, // NEW: Player state from database (persistent) related_players_state: PlayersStateModel { id: number, player_id: number, room_id: number, // Last SAVED room x: number, // Last SAVED position y: number, dir: string // NOTE: NO scene property in database model! } } ``` **Key Points:** - `related_players` is an **array** (users can have multiple characters) - `related_players_state` is the **database snapshot** of player position - Database model does NOT include `scene` property (only `room_id`) --- ### 2. Runtime Layer (In-Memory During Gameplay) During login and gameplay, additional properties are added for runtime state management: ```javascript // After login processing: userModel { ...database fields, related_players: PlayersModel[], // From database // ADDED AT RUNTIME: Selected player reference player: PlayersModel { // Selected from related_players[] ...database fields, related_players_state: { ... }, // Database snapshot // ADDED AT RUNTIME: Enhanced runtime state state: { room_id: number, // CURRENT room (updated during gameplay) x: number, // CURRENT position y: number, dir: string, scene: string // ADDED: Room name (not in database!) } } } ``` **Key Points:** - `userModel.player` is **assigned at runtime** from `related_players[]` - `player.state` is **created during login** and updated during gameplay - `player.state.scene` is **added by server**, not from database - `related_players_state` remains **unchanged** after initial load (becomes stale) --- ## Complete Login Flow ### Step 1: User Authentication **File:** `lib/rooms/server/login.js:70-107` (onAuth) ```javascript async onAuth(client, options, request) { // Load user from database let loginResult = await this.loginManager.processUserRequest(options); // Select player if specified if(sc.hasOwn(options, 'selectedPlayer')){ loginResult.user.player = this.getPlayerByIdFromArray( loginResult.user.related_players, // From database array options.selectedPlayer ); } return loginResult.user; // Becomes userModel in onJoin } ``` ### Step 2: Load User From Database **File:** `lib/users/server/manager.js:67-83` ```javascript async loadUserByUsername(username) { let loadedUser = await this.usersRepository.loadOneByWithRelations( 'username', username, ['related_users_login', 'related_players.related_players_state'] // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // Loads players WITH their state from DB ); return loadedUser; } ``` **Result:** User loaded with `related_players[]` array, each player has `related_players_state` from database. ### Step 3: Map Player State Relation **File:** `lib/game/server/login-manager.js:351-361` ```javascript mapPlayerStateRelation(user) { if(!sc.isArray(user.related_players)){ return; } for(let player of user.related_players){ if(player.related_players_state && !player.state){ // Create runtime state from database state player.state = player.related_players_state; } } } ``` **CRITICAL:** This creates `player.state` by assigning `player.related_players_state`. **Question:** Is this assignment by reference or copy? - In JavaScript, object assignment is **by reference** - BUT: Database ORM models might be immutable/frozen - **Result:** They can diverge during gameplay ### Step 4: Set Scene On Players **File:** `lib/game/server/login-manager.js:423-441` ```javascript async setSceneOnPlayers(user, userData) { for(let player of user.related_players){ if(!player.state){ continue; } // Check if user selected a different scene on login let config = this.config.get('client/rooms/selection'); if(config.allowOnLogin && userData['selectedScene'] && userData['selectedScene'] !== RoomsConst.ROOM_LAST_LOCATION_KEY){ await this.applySelectedLocation(player, userData['selectedScene']); } // CRITICAL: Add scene property to state player.state.scene = await this.getRoomNameById(player.state.room_id); // ^^^^^ ADDED HERE - not in database! } } ``` **Result:** Each player now has `player.state.scene` with the room name string. ### Step 5: Select Player (Runtime Assignment) **File:** `lib/rooms/server/login.js:89-91` ```javascript if(sc.hasOwn(options, 'selectedPlayer')){ loginResult.user.player = this.getPlayerByIdFromArray( loginResult.user.related_players, options.selectedPlayer ); } ``` **Result:** `userModel.player` now references ONE player from the array with both: - `player.related_players_state` (database snapshot) - `player.state` (runtime state with scene) --- ## Gameplay Flow ### Joining Scene Room **File:** `lib/rooms/server/scene.js:126-156` ```javascript async onJoin(client, options, userModel) { // userModel already has player selected from onAuth // Validate using RUNTIME state (not database state!) if(this.validateRoomData){ if(!userModel.player.state){ // Check runtime state exists Logger.warning('Missing user player state.', userModel); return false; } if(!this.validateRoom(userModel.player.state.scene, isGuest)){ // ^^^^^ Use runtime state with scene! return false; } } // Create player schema in room... } ``` **FIX APPLIED:** Changed from `related_players_state.scene` (doesn't exist) to `state.scene` (exists). ### Saving Player State During Gameplay **File:** `lib/rooms/server/scene.js:708-737` ```javascript async savePlayerState(sessionId) { let playerSchema = this.playerBySessionIdFromState(sessionId); // Extract CURRENT position from runtime state let {room_id, x, y, dir} = playerSchema.state; // From state, NOT related_players_state let playerId = playerSchema.player_id; let updatePatch = {room_id, x: parseInt(x), y: parseInt(y), dir}; // Update database with CURRENT position updateResult = await this.loginManager.usersManager.updateUserStateByPlayerId( playerId, updatePatch ); return playerSchema; } ``` **Key Points:** - Database updated FROM `playerSchema.state` (runtime) - Database updated TO `players_state` table (will become `related_players_state` on next login) - `related_players_state` in current session is NEVER updated (remains stale) --- ## Data Flow Diagram **Step 1: DATABASE (players_state table)** - room_id: 4, x: 400, y: 345, dir: 'down' - (NO scene property) **Step 2: LOAD - UsersManager.loadUserByUsername()** - related_players[].related_players_state = database snapshot **Step 3: MAP - LoginManager.mapPlayerStateRelation()** - player.state = player.related_players_state - (Assignment creates runtime state) **Step 4: ENHANCE - LoginManager.setSceneOnPlayers()** - player.state.scene = getRoomNameById(player.state.room_id) - (Adds scene property to runtime state) **Step 5: SELECT - RoomLogin.onAuth()** - userModel.player = getPlayerByIdFromArray(...) - (Assigns selected player to userModel.player) **Step 6: VALIDATE - RoomScene.onJoin()** - Check: userModel.player.state exists - Validate: userModel.player.state.scene matches room **Step 7: GAMEPLAY - Player moves, changes scenes** - Updates: playerSchema.state (runtime) - Unchanged: player.related_players_state (stale) **Step 8: SAVE - RoomScene.savePlayerState()** - Read FROM: playerSchema.state (current position) - Write TO: database players_state table - (Becomes related_players_state on next login) --- ## State Divergence After login, you have **TWO sources of state** that can diverge: ### Example Session: **Initial Login:** ```javascript userModel.player.related_players_state = { room_id: 4, // Town (from database) x: 400, y: 345, dir: 'down' } userModel.player.state = { room_id: 4, // Same as database x: 400, y: 345, dir: 'down', scene: 'reldens-town' // Added by server } ``` **After Scene Change (player moves to house):** ```javascript userModel.player.related_players_state = { room_id: 4, // UNCHANGED (stale) x: 400, y: 345, dir: 'down' } userModel.player.state = { room_id: 2, // UPDATED to house x: 548, y: 615, dir: 'up', scene: 'reldens-house-1' // UPDATED } ``` **On Logout:** `state` is saved to database, becomes `related_players_state` on next login. --- ## Key Takeaways 1. **"related_" prefix is the NEW database relation naming** (not legacy) 2. **`related_players_state`** = Database snapshot (stale after load, no scene property) 3. **`state`** = Runtime state (active, has scene property, source of truth for gameplay) 4. **`scene` property** = Only exists in runtime `state`, NOT in database model 5. **Validation must use** `player.state.scene`, NOT `player.related_players_state.scene` 6. **Database updates** read from `state` and write to `players_state` table 7. **`related_players_state` is never updated** during a session (snapshot only) --- ## Code References **Key Files:** - `lib/users/server/manager.js:67-83` - Load user with relations - `lib/game/server/login-manager.js:351-361` - Map player state relation - `lib/game/server/login-manager.js:423-441` - Set scene on players - `lib/rooms/server/login.js:70-107` - Authentication and player selection - `lib/rooms/server/scene.js:126-156` - Scene validation - `lib/rooms/server/scene.js:708-737` - Save player state **Database Tables:** - `users` - User accounts - `players` - Player characters - `players_state` - Player positions (becomes `related_players_state` when loaded) **Entity Relations:** - `UsersModel.related_players` relates to `PlayersModel[]` - `PlayersModel.related_players_state` relates to `PlayersStateModel`