# 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`
