PSF-BotServer/bot-docs/CODEBASE_MAP.md
2revoemag 2e5b5e0dbd feat: Add bot player system for PlanetSide population
Initial implementation of server-side bots that:
- Spawn as real Player entities with full equipment
- Move and broadcast position updates (10 tick/sec)
- Take damage and die with backpack drops
- Respawn after death
- Combat system with accuracy model (adjustment vs recoil)

Includes project documentation in bot-docs/ and Claude agent helpers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 00:22:30 -05:00

400 lines
13 KiB
Markdown

# PSF-LoginServer Codebase Map
> **Purpose**: Quick reference for understanding the PlanetSide server emulator codebase.
> This document maps key files, line numbers, and relationships for bot implementation.
## Quick Reference
| Concept | File | Key Lines | Notes |
|---------|------|-----------|-------|
| Player Entity | `objects/Player.scala` | 29-150 | Main player class |
| Player Behavior | `objects/avatar/PlayerControl.scala` | Full file | Akka actor for damage/death/equipment |
| Avatar Data | `objects/avatar/Avatar.scala` | 130-205 | Persistent character data (certs, loadouts) |
| Session Handling | `actors/session/SessionActor.scala` | 99-400 | Network packet processing (NOT needed for bots) |
| Zone Population | `objects/zones/ZonePopulationActor.scala` | 30-85 | Join/Spawn/Leave/Release flow |
| GUID Registration | `objects/guid/GUIDTask.scala` | 185-204 | `registerAvatar()` for full registration |
| Position Broadcast | `services/avatar/AvatarServiceMessage.scala` | 83-97 | `AvatarAction.PlayerState` |
| Turret AI Reference | `objects/serverobject/turret/auto/AutomatedTurretBehavior.scala` | Full file | Example of existing AI pattern |
---
## Core Entity System
### Player.scala
**Path**: `src/main/scala/net/psforever/objects/Player.scala`
```
Line 29-41: class Player extends PlanetSideServerObject with many traits
Line 54-70: Private state (armor, capacitor, exosuit, holsters, inventory)
Line 72-78: Movement state (facingYawUpper, crouching, jumping, cloaked, afk)
Line 123-133: Spawn() - resurrects a dead player
Line 135-139: Die() - kills the player
Line 141-147: Revive() - revives without full reset
```
**Key Traits Mixed In**:
- `Vitality` - health/damage system
- `FactionAffinity` - TR/NC/VS
- `Container` - inventory
- `ZoneAware` - knows which continent it's on
- `MountableEntity` - can sit in vehicles
### Avatar.scala
**Path**: `src/main/scala/net/psforever/objects/avatar/Avatar.scala`
```
Line 85-87: Factory method Avatar(charId, name, faction, sex, head, voice)
Line 130-151: case class Avatar with all fields:
- id: Int (DB row ID - could use negative for bots)
- basic: BasicCharacterData (name, faction, sex, head, voice)
- bep/cep: Experience points
- certifications: Set[Certification]
- implants, shortcuts, locker, deployables
- loadouts, cooldowns, scorecard
Line 155-156: br/cr: Battle Rank and Command Rank derived from BEP/CEP
```
**Important**: `Avatar.id` is described as "unique identifier corresponding to a database table row index". For bots, we can use negative IDs to avoid collision.
### PlayerControl.scala
**Path**: `src/main/scala/net/psforever/objects/avatar/PlayerControl.scala`
This is an **Akka Actor** that handles player behavior.
```
Key behaviors it handles:
- Damage intake and death
- Healing (from equipment, implants)
- Equipment changes
- Environment interaction (water, lava, radiation)
- Jamming effects
```
**Critical**: When `Zone.Population.Spawn()` is called, it creates a `PlayerControl` actor for the player:
```scala
// From ZonePopulationActor.scala line 60-63
player.Actor = context.actorOf(
Props(classOf[PlayerControl], player, avatarActor),
name = GetPlayerControlName(player, None)
)
```
---
## Zone & Population System
### Zone.scala
**Path**: `src/main/scala/net/psforever/objects/zones/Zone.scala`
Key properties:
- `LivePlayers` - list of alive players in zone
- `Corpses` - list of dead player backpacks
- `Population` - ActorRef to ZonePopulationActor
- `AvatarEvents` - ActorRef to AvatarService for broadcasting
- `GUID` - UniqueNumberOps for GUID allocation
### ZonePopulationActor.scala
**Path**: `src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala`
**This is critical for bot spawning.**
```
Line 31-34: Zone.Population.Join(avatar)
- Adds avatar.id to playerMap with None value
- Starts player management systems if first player
Line 36-50: Zone.Population.Leave(avatar)
- Removes from playerMap
- Cleans up player position, block map
Line 52-71: Zone.Population.Spawn(avatar, player, avatarActor)
- Associates player with avatar in playerMap
- Sets player.Zone = zone
- CREATES PlayerControl actor (line 60-63)
- Adds to block map
Line 73-84: Zone.Population.Release(avatar)
- Disassociates player from avatar
- Used when player dies/respawns
```
---
## GUID System
### GUIDTask.scala
**Path**: `src/main/scala/net/psforever/objects/guid/GUIDTask.scala`
```
Line 72-73: registerObject() - basic GUID registration
Line 102-107: registerTool() - register weapon + ammo boxes
Line 125-130: registerEquipment() - dispatches to appropriate registrar
Line 200-204: registerPlayer() - registers player + holsters + inventory
Usage:
TaskWorkflow.execute(GUIDTask.registerPlayer(zone.GUID, player))
```
---
## Broadcasting System
### AvatarServiceMessage.scala
**Path**: `src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala`
```
Line 20: AvatarServiceMessage(forChannel: String, actionMessage: AvatarAction.Action)
Line 83-97: AvatarAction.PlayerState - position/orientation broadcast
Fields: player_guid, pos, vel, facingYaw, facingPitch,
facingYawUpper, timestamp, is_crouching, is_jumping,
jump_thrust, is_cloaked, spectator, weaponInHand
```
**To broadcast bot position**:
```scala
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.PlayerState(
player.GUID,
player.Position,
Some(velocity),
facingYaw, facingPitch, facingYawUpper,
timestamp,
is_crouching, is_jumping, jump_thrust,
is_cloaked, spectator = false, weaponInHand
)
)
```
### Other Important AvatarActions
```
AvatarAction.LoadPlayer - Creates player on clients (spawn)
AvatarAction.ObjectDelete - Removes player from clients (despawn)
AvatarAction.ChangeFireState_Start/Stop - Weapon firing
AvatarAction.Killed - Death notification
AvatarAction.HitHint - Damage indicator
```
---
## Existing AI Pattern
### AutomatedTurretBehavior.scala
**Path**: `src/main/scala/net/psforever/objects/serverobject/turret/auto/AutomatedTurretBehavior.scala`
**NOT the behavior we want** (turrets are mechanical), but useful as a **technical reference** for:
- Akka actor message patterns
- Target tracking state management
- Periodic checks via scheduler
- Damage/retaliation responses
```
Line 26-27: trait AutomatedTurretBehavior { _: Actor with DamageableEntity =>
Shows how to mix behavior into an actor
Line 51-54: Timer pattern for periodic checks:
context.system.scheduler.scheduleWithFixedDelay(...)
Line 145-154: Target list management (AddTarget, RemoveTarget, Detected)
Line 245-258: Engagement flow (engageNewDetectedTarget)
```
---
## Session System (Reference Only)
Bots don't need SessionActor, but understanding it helps:
### SessionActor.scala
**Path**: `src/main/scala/net/psforever/actors/session/SessionActor.scala`
```
Line 99: class SessionActor - receives from middlewareActor (network)
Line 115: def receive = startup (initial state)
Line 168-171: parse() - handles PlanetSideGamePacket from client
Line 372+: handleGamePkt() - dispatches packets to handlers
```
**Key insight**: SessionActor's job is handling network I/O. Bots generate actions internally, so we don't need this.
### AvatarActor.scala
**Path**: `src/main/scala/net/psforever/actors/session/AvatarActor.scala`
Handles avatar persistence, certification management, loadouts, etc.
```
Line 70-88: Factory and commands
Line 81: apply() creates the actor with sessionActor reference
```
**For bots**: We need a stub `BotAvatarActor` that accepts the messages `PlayerControl` might send but doesn't persist anything.
---
## SpawnPoint System
### SpawnPoint.scala
**Path**: `src/main/scala/net/psforever/objects/SpawnPoint.scala`
```
Line 11-70: trait SpawnPoint
- GUID, Position, Orientation
- SpecificPoint(target) -> (Vector3, Vector3) for spawn pos/orient
Line 72-160: object SpawnPoint
- Default, Tube, AMS, Gate spawn point calculations
```
---
## File Locations Summary
```
src/main/scala/net/psforever/
├── actors/
│ ├── session/
│ │ ├── SessionActor.scala # Network session (NOT for bots)
│ │ ├── AvatarActor.scala # Avatar persistence
│ │ └── support/
│ │ ├── SessionData.scala # Session state
│ │ └── ZoningOperations.scala # Spawn flow reference
│ └── zone/
│ └── ZoneActor.scala # Zone management
├── objects/
│ ├── Player.scala # Player entity
│ ├── SpawnPoint.scala # Spawn locations
│ ├── avatar/
│ │ ├── Avatar.scala # Character data
│ │ └── PlayerControl.scala # Player behavior actor
│ ├── guid/
│ │ └── GUIDTask.scala # GUID registration
│ ├── serverobject/turret/auto/
│ │ └── AutomatedTurretBehavior.scala # AI reference
│ └── zones/
│ ├── Zone.scala # Zone class
│ └── ZonePopulationActor.scala # Population management
└── services/
└── avatar/
├── AvatarService.scala # Broadcasting service
└── AvatarServiceMessage.scala # Message definitions
```
---
## Quick Patterns
### Spawn a Player (Derived from Real Flow)
```scala
// 1. Create avatar
val avatar = Avatar(botId, name, faction, sex, head, voice)
// 2. Create player
val player = new Player(avatar)
player.Position = spawnPosition
player.Spawn()
// 3. Register GUIDs
TaskWorkflow.execute(GUIDTask.registerPlayer(zone.GUID, player))
// 4. Join zone
zone.Population ! Zone.Population.Join(avatar)
// 5. Spawn (creates PlayerControl)
zone.Population ! Zone.Population.Spawn(avatar, player, botAvatarActor)
// 6. Broadcast existence
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.LoadPlayer(...))
```
### Broadcast Position
```scala
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.PlayerState(player.GUID, pos, vel, yaw, pitch, yawUpper,
timestamp, crouch, jump, thrust, cloak, false, weaponOut)
)
```
### Despawn a Player
```scala
zone.Population ! Zone.Population.Leave(avatar)
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.ObjectDelete(guid, guid))
TaskWorkflow.execute(GUIDTask.unregisterAvatar(zone.GUID, player))
```
---
## Bot Implementation - Lessons Learned
### Critical: Use `registerAvatar` NOT `registerPlayer`
`PlayerControl` creates a `LockerContainerControl` actor in its constructor (line 74-80) which calls `PlanetSideServerObject.UniqueActorName(locker)`. This requires `locker.GUID` to be assigned.
- `registerPlayer()` skips locker registration
- `registerAvatar()` includes locker registration
- **Always use `registerAvatar()` for bots**
### Critical: Avatar IDs Must Be Positive
Packet encoding uses 32-bit unsigned integers. Negative IDs cause:
```
ERROR: basic_appearance/a/unk6: -1 is less than minimum value 0
```
**Solution**: Use high positive IDs (900000+) to avoid collision with real DB player IDs.
### Critical: GUID Registration is Async
`TaskWorkflow.execute()` returns a `Future[Any]`. You MUST wait for completion before:
- Accessing `player.GUID`
- Calling `Zone.Population.Spawn()`
- Broadcasting `LoadPlayer`
```scala
TaskWorkflow.execute(GUIDTask.registerAvatar(zone.GUID, player)).onComplete {
case Success(_) =>
// NOW safe to use player.GUID and spawn
self ! CompleteSpawn(...)
case Failure(ex) =>
log.error(s"GUID registration failed: ${ex.getMessage}")
}
```
### AvatarActor Must Be Typed
`Zone.Population.Spawn` expects `ActorRef[AvatarActor.Command]` (typed), not classic `ActorRef`.
Use Akka typed actor system:
```scala
val typedSystem = context.system.toTyped
val botAvatarActor: ActorRef[AvatarActor.Command] =
typedSystem.systemActorOf(BotAvatarActor(), s"bot-avatar-$botId")
```
### Bot Files Created
```
src/main/scala/net/psforever/actors/bot/
├── BotAvatarActor.scala # Stub typed actor - absorbs AvatarActor messages
└── BotManager.scala # Spawns/manages bots, handles async GUID flow
```
### Working Spawn Flow (Verified)
```scala
// 1. Create avatar with HIGH POSITIVE ID
val avatar = Avatar(900000, BasicCharacterData(name, faction, sex, head, voice))
// 2. Create player
val player = new Player(avatar)
player.Position = position
player.Spawn()
// 3. Create typed stub avatar actor
val botAvatarActor = typedSystem.systemActorOf(BotAvatarActor(), name)
// 4. Register GUIDs (ASYNC!) - includes locker
TaskWorkflow.execute(GUIDTask.registerAvatar(zone.GUID, player)).onComplete {
case Success(_) =>
// 5. Join zone
zone.Population ! Zone.Population.Join(avatar)
// 6. Spawn (creates PlayerControl)
zone.Population ! Zone.Population.Spawn(avatar, player, botAvatarActor)
// 7. Broadcast to clients
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.LoadPlayer(...))
}
```