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>
This commit is contained in:
2revoemag 2025-11-23 00:22:30 -05:00
parent 9646b3f99e
commit 2e5b5e0dbd
17 changed files with 3813 additions and 0 deletions

309
bot-docs/ARCHITECTURE.md Normal file
View file

@ -0,0 +1,309 @@
# PlanetSide Bots - Technical Architecture
## PSF-LoginServer Codebase Analysis
### Technology Stack
- **Language**: Scala (99.5%)
- **Actor Framework**: Akka (classic actors)
- **Database**: PostgreSQL
- **Build**: sbt
### Key Components Discovered
#### Entity Hierarchy
```
PlanetSideServerObject (base)
├── Player
│ ├── Vitality (health, armor)
│ ├── FactionAffinity (TR/NC/VS)
│ ├── Container (inventory)
│ ├── ZoneAware (continent awareness)
│ └── MountableEntity (vehicle seats)
├── Vehicle
├── Deployable
└── FacilityTurret
```
#### Actor System
```
SessionActor (per-connection)
├── Handles network packets from client
├── Manages player state
├── Routes to subsystem handlers
└── Mode-based behavior (Normal, Spectator, CSR)
PlayerControl (per-player entity)
├── Akka Actor controlling Player object
├── Handles damage, healing, death
├── Equipment management
├── Containable behavior
└── Environment interaction
```
#### Relevant Files
| File | Purpose |
|------|---------|
| `objects/Player.scala` | Player entity class |
| `objects/avatar/Avatar.scala` | Persistent player data (certs, loadouts) |
| `objects/avatar/PlayerControl.scala` | Player behavior Actor |
| `actors/session/SessionActor.scala` | Network session handler |
| `objects/SpawnPoint.scala` | Spawn location trait |
| `objects/serverobject/turret/auto/AutomatedTurretBehavior.scala` | **AI reference implementation** |
---
## Existing AI Pattern: AutomatedTurretBehavior
The codebase already has AI! `AutomatedTurretBehavior` is a trait that provides:
### Target Management
- `Targets` - list of known potential targets
- `Target` - current active target
- `AddTarget()` / `RemoveTarget()` - target list management
- `Detected()` - check if target is already known
### Detection & Engagement
- `Alert(target)` - new target spotted
- `Unalert(target)` - target lost
- `ConfirmShot(target)` - hit confirmation
- Range-based detection (`ranges.trigger`, `ranges.escape`, `ranges.detection`)
- Periodic validation sweeps (`detectionSweepTime`)
### Combat Logic
- `engageNewDetectedTarget()` - begin shooting
- `noLongerEngageDetectedTarget()` - stop shooting
- `trySelectNewTarget()` - target selection algorithm
- Target decay checks (destroyed, out of range, MIA)
- Retaliation behavior (respond to being attacked)
### Timing/Cooldowns
- `cooldowns.missedShot` - timeout for unconfirmed hits
- `cooldowns.targetSelect` - delay before selecting new target
- `cooldowns.targetElimination` - delay after killing target
- Self-reported refire timer for continuous fire
### Key Insight
> The turret AI works by being an Akka Actor that receives messages (`Alert`, `ConfirmShot`, `PeriodicCheck`) and maintains internal state. **Bots can follow the same pattern.**
---
## Proposed Bot Architecture
### Option A: Server-Side Native Bots (Preferred)
```
BotActor (extends Actor)
├── BotBehavior trait (similar to AutomatedTurretBehavior)
│ ├── Target detection (vision cone, partial/full spot)
│ ├── Combat engagement
│ ├── V-menu communication
│ └── Attitude/Vengeance system
├── BotMovement trait
│ ├── Pathfinding
│ ├── ADAD strafing
│ └── Retreat behavior
├── BotObjective trait
│ ├── Follow orders (attack/defend)
│ ├── Base capture
│ └── Help responses (VNG, VNH)
└── Controls a Player object (no SessionActor needed)
```
#### How It Would Work
1. **BotManager** actor manages bot lifecycle
- Spawns bots when population is low
- Removes bots when real players join
- Assigns bots to factions
2. **Bot entity** is a `Player` object
- Has `Avatar` with certs, loadouts (predefined by class)
- Has `PlayerControl` actor for damage/death handling
- Has **new** `BotActor` for AI decision-making
3. **Bot appears to clients** as normal player
- Spawns at SpawnPoints
- Sends PlayerStateMessage updates
- Fires weapons, takes damage, dies normally
#### Integration Points
- `Zone.LivePlayers` - bots appear here
- `Zone.AvatarEvents` - bots send/receive events
- `PlayerStateMessage` - bots broadcast position/orientation
- `ChatMsg` - bots send V-menu voice commands
### Option B: Bot-as-Client (Fallback)
External process connects to server as fake client, mimics player packets.
**Pros**: No server code changes needed initially
**Cons**: Network overhead, harder to scale, more fragile
---
## Bot Class Implementation
Each bot class needs:
### Data Definition
```scala
case class BotClass(
name: String,
certifications: Set[Certification],
loadout: Loadout,
experienceLevel: BotExperience, // Newbie, Vet, Ace
primaryRole: BotRole // Driver, Support, Hacker, AV, MAX, Vet, Ace
)
```
### Behavior Weights
```scala
trait BotPersonality {
def aggressionLevel: Float // 0.0 = passive, 1.0 = aggressive
def accuracyBase: Float // Base accuracy modifier
def movementStyle: MovementStyle // Newbie (straight), Vet (ADAD), etc.
def retreatThreshold: Float // HP percentage to retreat
}
```
---
## Communication System
### V-Menu Integration
```scala
// Bot sends voice command
def sendVoiceCommand(cmd: VoiceCommand): Unit = {
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.SendResponse(botGUID, ChatMsg(ChatMessageType.CMT_VOICE, cmd.text))
)
}
// Bot responds to nearby voice commands
def handleVoiceCommand(sender: Player, cmd: VoiceCommand): Unit = cmd match {
case VNG if canBeGunner => respondAndAssist(sender)
case VNH if canHack => respondAndAssist(sender)
case VVV => evaluateHelpRequest(sender)
// etc.
}
```
### Celebration Coordination
```scala
object CelebrationCoordinator {
def onBaseCapture(zone: Zone, faction: PlanetSideEmpire): Unit = {
val eligibleBots = zone.LivePlayers
.filter(_.isBot)
.filter(_.Faction == faction)
.filter(_.isAlive)
val responderCount = Random.nextInt(5) + 1 // 1-6
val responders = Random.shuffle(eligibleBots).take(responderCount)
responders.zipWithIndex.foreach { case (bot, i) =>
val delay = Random.nextFloat() * 1.5f // 0-1.5 seconds
scheduler.scheduleOnce(delay.seconds) {
bot.sendVoiceCommand(randomCelebration())
}
}
}
}
```
---
## Spawn/Despawn Logic
### Dynamic Population Management
```scala
class BotPopulationManager(zone: Zone) {
val targetBotsPerFaction = 100
val minRealPlayersBeforeScaling = 10
def tick(): Unit = {
PlanetSideEmpire.values.foreach { faction =>
val realPlayers = zone.LivePlayers.count(p => !p.isBot && p.Faction == faction)
val currentBots = zone.LivePlayers.count(p => p.isBot && p.Faction == faction)
val targetBots = math.max(0, targetBotsPerFaction - realPlayers)
if (currentBots < targetBots) spawnBots(faction, targetBots - currentBots)
if (currentBots > targetBots) despawnBots(faction, currentBots - targetBots)
}
}
def despawnBots(faction: PlanetSideEmpire, count: Int): Unit = {
// Remove non-Ace bots first, Ace is last to go
val bots = zone.LivePlayers
.filter(p => p.isBot && p.Faction == faction)
.sortBy(b => if (b.botClass == Ace) Int.MaxValue else 0)
.take(count)
bots.foreach(gracefulLogout)
}
}
```
---
## Proof of Concept Milestones
### Milestone 1: Static Bot
- [ ] Create `BotActor` skeleton
- [ ] Spawn a `Player` entity without `SessionActor`
- [ ] Bot appears in zone, visible to clients
- [ ] Bot stands still (no AI)
### Milestone 2: Moving Bot
- [ ] Implement basic movement
- [ ] Bot walks in a pattern
- [ ] `PlayerStateMessage` broadcasts correctly
### Milestone 3: Reactive Bot
- [ ] Detect nearby enemies (vision cone)
- [ ] Turn to face target
- [ ] Basic shooting (ChangeFireStateMessage)
### Milestone 4: Smart Bot
- [ ] Target selection logic
- [ ] Retreat on low HP
- [ ] V-menu help requests
### Milestone 5: Team Bot
- [ ] Follow orders from Ace
- [ ] Respond to V-menu requests
- [ ] Coordinated behavior
---
## Questions for PSForever Devs
1. **Player without SessionActor**: Is this currently possible? What breaks?
2. **Bot flag**: Should we add `isBot: Boolean` to `Player` class?
3. **GUID allocation**: How do we get GUIDs for bot entities?
4. **Zone registration**: What's the proper way to add a player to a zone without client connection?
5. **Existing NPC code**: Is there any other AI code beyond `AutomatedTurretBehavior`?
---
## File Structure (Proposed)
```
src/main/scala/net/psforever/
├── objects/
│ └── bot/
│ ├── Bot.scala # Bot entity (extends Player?)
│ ├── BotClass.scala # Class definitions (Driver, Support, etc.)
│ ├── BotLoadouts.scala # Predefined loadouts per class
│ └── BotPersonality.scala # Behavior weights
├── actors/
│ └── bot/
│ ├── BotActor.scala # Main bot AI actor
│ ├── BotBehavior.scala # Combat/detection trait
│ ├── BotMovement.scala # Movement trait
│ ├── BotObjective.scala # Objective handling trait
│ └── BotManager.scala # Population management
└── services/
└── bot/
├── BotService.scala # Global bot coordination
└── BotVoiceCoordinator.scala # V-menu coordination
```

399
bot-docs/CODEBASE_MAP.md Normal file
View file

@ -0,0 +1,399 @@
# 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(...))
}
```

123
bot-docs/DEV_SETUP.md Normal file
View file

@ -0,0 +1,123 @@
# Development Environment Setup
> Verified working on: Ubuntu 22.04 (Linux Mint 21.3 Virginia)
## Requirements
| Component | Version | Notes |
|-----------|---------|-------|
| Java | JDK 8 (1.8.0_xxx) | **Must be Java 8** - other versions fail |
| sbt | 1.8.2+ | Scala build tool |
| PostgreSQL | 14+ | Database |
## Installation (Ubuntu/Mint)
### 1. Java 8
```bash
sudo apt update
sudo apt install -y openjdk-8-jdk
# Set as default
sudo update-alternatives --set java /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java
sudo update-alternatives --set javac /usr/lib/jvm/java-8-openjdk-amd64/bin/javac
# Verify
java -version
# Should show: openjdk version "1.8.0_xxx"
```
### 2. sbt
```bash
echo "deb https://repo.scala-sbt.org/scalasbt/debian all main" | sudo tee /etc/apt/sources.list.d/sbt.list
curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x99E82A75642AC823" | sudo apt-key add -
sudo apt update
sudo apt install -y sbt
```
### 3. PostgreSQL
```bash
sudo apt install -y postgresql postgresql-contrib
sudo systemctl start postgresql
sudo systemctl enable postgresql
# Create database and user
sudo -u postgres psql -c "CREATE USER psforever WITH PASSWORD 'psforever';"
sudo -u postgres psql -c "CREATE DATABASE psforever OWNER psforever;"
```
## Building & Running
### First Compile
```bash
cd PSF-LoginServer
sbt compile
```
- Takes ~2-3 minutes on first run (downloads dependencies)
- Expect some warnings (unused imports, non-exhaustive matches) - these are fine
### Run Server
```bash
sbt "server/run"
```
**Expected behavior:**
- Terminal stays open with live debug output
- Shows player connections, actions, etc. in real-time
- Does NOT return to prompt - this is correct
**Expected startup output:**
```
PSForever Server - PSForever Project
Login server is running on 127.0.0.1:51000
```
### Known Startup Errors (IGNORE)
```
ERROR akka.actor.SupervisorStrategy - null
java.lang.NullPointerException: null
at net.psforever.objects.guid.GUIDTask$RegisterObjectTask.action(GUIDTask.scala:44)
...
at net.psforever.objects.serverobject.shuttle.OrbitalShuttlePadControl...
```
These are **HART shuttle (orbital pad) initialization errors** - known issue, doesn't affect gameplay or our bot work.
## Server Ports
| Port | Protocol | Purpose |
|------|----------|---------|
| 51000 | TCP | Login server |
| 51001 | UDP | World server |
## Connecting a Client
1. Use PlanetSide client version 3.15.84.0
2. Point client at server IP (127.0.0.1 for local)
3. Create account on first login (auto-created)
## Running in Background
```bash
# Start in background
nohup sbt "server/run" > server.log 2>&1 &
# View logs
tail -f server.log
# Find and kill
ps aux | grep sbt
kill <PID>
```
## Troubleshooting
### "authentication method 10 not supported"
PostgreSQL version too old or password encryption mismatch. Upgrade PostgreSQL or check `postgresql.conf`.
### Java version errors
Make sure `java -version` shows 1.8.x. Use `update-alternatives` to switch if needed.
### Port already in use
```bash
sudo lsof -i :51000
# Kill the process using the port
```

414
bot-docs/GAME_FEEL.md Normal file
View file

@ -0,0 +1,414 @@
# PlanetSide Bot Behavior & Game Feel
## Vision System
**Field of View**: 60 degrees horizontal and vertical
- Adjustable variable, expected final range: 60-90 degrees
- Bots cannot see beyond this cone - must turn to acquire targets
- Creates realistic "blind spots" and flanking opportunities for players
### Spotting Mechanics
**Full Spot**: Clear visual on target, can identify and engage
- Time to acquire scales with distance:
- Close range: Very fast (nearly instant)
- Long range: Takes longer to confirm target
**Partial Spot**: Know something is there but can't fully engage
- Target went behind cover (saw them go there)
- See tracers coming from a direction (investigate)
- Heard gunfire/explosions from location
- **Behavior**: Move towards OR around to get better angle
---
## Core Decision Logic
```
SPOTTED TARGET?
├── Partial spot?
│ └── Move towards target OR move around target (reposition for better view)
├── Full spot?
│ └── ATTACK or COMPLETE OBJECTIVE
│ (enter vehicle, unlock door, hack terminal, etc.)
└── Cannot proceed?
└── REQUEST HELP (V-menu)
└── State what you need (VNG, VNH, etc.)
```
---
## V-Menu Communication System
Bots use the in-game quick voice system to communicate, creating authentic battlefield chatter.
### Voice Commands Used
**Requests & Responses**
| Command | Meaning | Use Case |
|---------|---------|----------|
| VVV | "HELP!" | General distress, need assistance |
| VNG | "Need Gunner" | Vehicle waiting for gunner before proceeding |
| VNH | "Need Hacker" | Door locked, can't proceed, need hacker |
| VVY | "Yes" | Acknowledging help request, on my way |
| VVB | Taunt (variant B) | Vengeance kill, celebration |
| VVZ | Taunt (variant Z) | Vengeance kill, celebration |
**Warnings (Intel Sharing)**
| Command | Meaning | Use Case |
|---------|---------|----------|
| VWA | "Warning: Aircraft!" | Enemy air incoming, triggers AA MAX spawns |
| VWV | "Warning: Vehicles!" | Enemy armor incoming, triggers AV MAX spawns |
| VWT | "Warning: Troops!" | Enemy infantry push, triggers AI MAX spawns |
### Help Request Flow
1. Bot encounters obstacle it cannot handle alone
2. Bot broadcasts appropriate V-menu request
3. Capable bots in range evaluate:
- Can I help with this? (Do I have the cert/ability?)
- Can I path to them? (Pull `/loc` of requester, check route)
4. If yes to both:
- Respond with `VVY`
- Begin movement to assist
5. **Result**: Organic teamwork feel, tactical advantage, immersion
---
## Bot Classes
PlanetSide 1 has no rigid classes, but bots need defined roles for sanity. Each class is a **pre-built loadout** with associated behavior logic.
### Class Definitions
| Class | Role | Key Abilities | Notes |
|-------|------|---------------|-------|
| **Driver** | Vehicle operation | Drive/pilot vehicles | Low priority on VNG (see below) |
| **Support** | Heal & Repair | Medical applicator, BANK/Nano repair | LOVES gunning (can heal vehicle) |
| **Hacker** | Infiltration & Capture | REK, door unlocking, base/tower/vehicle hacking | Critical for objective play |
| **AV** | Anti-Vehicle | Decimator, Lancer, Phoenix, Striker? | Tank hunters |
| **MAX** | Heavy Exosuit | Faction MAX suits (AI/AV/AA variants) | Walking tanks |
| **Vet** | Versatile Veteran | HA + AV + Basic Hacking + Basic Support | Jack of all trades |
| **Ace** | Empire Leader | Best at everything + Command authority | ONE per empire, special role |
### Vet Class Details
- Can do *most* things but slower/weaker than specialists
- Won't get stuck at locked doors (can hack, just slower)
- Won't panic at incoming tank (has AV, can fight back)
- Field-flexible, self-sufficient
- Good baseline "competent soldier" behavior
### Ace Class Details
- One per empire (TR Ace, NC Ace, VS Ace)
- **Last bot to logout** as real players join
- Uses **Command Chat** (CR3+ channel) for tactical orders
- Makes strategic decisions for bot team:
- Where to attack
- Where to defend
- Force distribution
- **CRITICAL**: Defers to real players
- Won't overstep human commanders
- Offers advice if no player is commanding
- Lets players make the calls when they want to
### MAX Class Details
**Situational Loadout Switching** based on intel:
- **Indoors** (last death location or teammate intel): AI MAX
- **VWV received** (Warning: Vehicles): AV MAX
- **VWA received** (Warning: Aircraft): AA MAX
- **VWT received** (Warning: Troops) + outdoors: Context-dependent
This creates reactive, intelligent heavy support that adapts to battlefield needs.
### Driver & Gunner Dynamics
- **Anyone** can be a gunner - it's not role-restricted
- **Drivers** respond to VNG but low priority (they want to drive)
- **Support** actively seeks gunner seats - can heal vehicle while gunning
- **Drivers** can also heal their own vehicle but teamwork = faster repairs
- Vehicles wait for gunner before proceeding (VNG call) for effectiveness
---
## Attitude & Vengeance System
Gives Vet+ bots personality and memorable behavior.
### Attitude Stat
- Internal emotional state of the bot
- Affects decision-making and aggression
### Vengeance Mechanic
1. **Death Memory**: Vet remembers where they died and who killed them
2. **Revenge Decision**: Based on attitude, may decide to hunt killer
3. **Constraints**:
- Must be reasonable range (same base spawn area)
- No cross-continent adventures
4. **On Successful Revenge**:
- Taunt victim (VVB or VVZ)
- Attitude decreases (calms down)
5. **On Repeated Death (no revenge)**:
- Attitude increases ("rage" building)
- **Rage Effects**:
- Increased accuracy
- Increased aggression
6. **Rage Reset Conditions**:
- Achieve vengeance
- Get a 3-kill streak
- Participate in base capture
---
## Movement Patterns
Movement varies by experience level and empire. Critical for not looking like a bot.
### By Experience Level
**New Players / Basic Bots**
- Run mostly straight at target
- Held strafe to maintain firing angle (not ADAD, just constant drift)
- Minimal evasion, focused on keeping crosshairs on target
**Vets & Aces**
- Heavy ADAD strafing (left-right-left-right)
- Combined with crouch spam during firefights
- Much harder to hit, more "tryhard" movement
### Empire-Specific Movement
- **NC**: Benefits from closing distance (shotguns) - more aggressive advance
- **TR/VS**: TBD - circle back when adding faction identity
### What NOT To Do
- **Almost no jumping** - PS players don't bunny hop
- Exception: MAXes might jump to dodge AV rockets (ADVANCED - maybe skip for v1)
- Robotic pathing, perfect angles, inhuman reaction times
### Tuning Note
> "I don't know what [bot movement] would feel like yet, we've never had bots before. I will help dial this in with many many play tests."
Movement feel will require extensive playtesting iteration.
---
## Retreat & Self-Preservation
### When Bots Retreat
| Class | Retreat Trigger |
|-------|-----------------|
| **Vets** | Low HP |
| **Drivers** | Low vehicle HP (save the vehicle!) |
| **Everyone** | Out of ammo (not just reloading - actually empty) |
### Order Context Matters
**If orders = DEFEND:**
- Stay at position no matter what (unless need supplies)
- Quick runs to spawn room / nearest terminal for ammo/resupply
- Return immediately to defensive position
**If orders = ATTACK:**
- More flexible retreat for self-preservation
- Regroup and push again
### What Retreat Looks Like
- Not a panicked sprint
- Backing away while facing threat when possible
- Finding cover, then breaking line of sight
---
## Combat & Accuracy System
Bot shooting should feel natural, not robotic. Two opposing forces create a "sweet spot" in sustained fire.
### Target Recognition Time
Before engaging, bots must **recognize** their target. Time to recognize scales with distance:
| Distance | Recognition Time | Notes |
|----------|-----------------|-------|
| **Close** (< 15m) | Near instant | Light them up immediately |
| **Medium** (15-50m) | 0.3-0.8 sec | Brief pause, then engage |
| **Far** (50m+) | 1-2 sec | Takes time to confirm target |
| **Obscured** | Investigate | Move to get better angle (defer to navmesh) |
### Accuracy System (Two Forces)
**Force 1: Adjustment (Accuracy improves)**
- First shots are inaccurate (bot is "dialing in" aim)
- Accuracy increases with each shot as bot finds the target
- Close targets = higher base accuracy, faster adjustment
- Far targets = lower base accuracy, slower adjustment
**Force 2: Recoil (Spread worsens)**
- Each shot adds recoil
- Recoil accumulates, making spread worse over time
- Creates "diminishing returns" on sustained fire
### The Sweet Spot
```
Accuracy over time during sustained fire:
↑ Accuracy
│ ╭──────╮
╲ ← recoil takes over
│╱ ← adjusting aim
└────────────────────────→ Time/Shots
First Middle Late
shots shots shots
(miss) (HIT!) (spray)
```
**Result by distance:**
- **Close range**: High base accuracy + fast adjustment = nearly all shots hit
- **Medium range**: Sweet spot matters - middle of burst is most dangerous
- **Long range**: May only land a few hits early (lucky) or middle (adjusted)
### Class Modifiers (Future)
| Class | Accuracy Modifier | Notes |
|-------|------------------|-------|
| Basic/Newbie | 1.0x (baseline) | Average accuracy |
| Vet | 1.3x accuracy, 0.8x recoil | Much better aim |
| Ace | 1.5x accuracy, 0.7x recoil | Best of the best |
| Support | 0.9x accuracy | Focused on healing |
| MAX | 0.8x accuracy, 1.2x recoil | Big guns, more spray |
### Practical Implications
1. **Players should win fair fights** - bots aren't aimbots
2. **Getting close is dangerous** - bots don't miss at point blank
3. **Distance = safety** - but not immunity (lucky shots happen)
4. **Burst fire beats spray** - short controlled bursts reset recoil
5. **Flanking works** - recognition time gives advantage
---
## The Chaos Factor
> "Spam? Spam. Be it bullets, grenades, the voice menus it's all about the chaotic spam. It's a war unlike the world has ever recreated."
### What Makes PS Fights Feel Like PS
- **Bullet spam**: Walls of tracers, suppressive fire is real
- **Grenade spam**: Explosions everywhere
- **Voice spam**: V-menu callouts constantly firing
- **Scale**: Hundreds of participants, not 32v32
### Bot Contribution to Chaos
- Bots should ADD to the chaos, not feel sterile
- Coordinated V-menu chatter (see calibration below)
- Miss shots (don't be laser accurate)
- React to nearby explosions/deaths
- Fire at enemies even when hit chance is low (suppression)
---
## Damage Reaction
How bots respond to taking damage depends on context.
### In Combat (Already Engaged)
- **Direct bullet hit from new direction**: May turn toward new threat OR smart-switch targets
- **Explosions/grenades**: Ignore direction (damage comes from everywhere) - stay on current target
- **Decision**: Stick with current target vs switch based on threat assessment
### Out of Combat (Doing Task)
Example: Repairing a vehicle, then gets shot
1. **PANIC** - immediate state change
2. **Run to cover** - most likely the vehicle they were repairing
3. **Swap to weapon** while running
- Drivers: Have sidearm ready (fast)
- Support: Must swap from repair tool (slower, panicked weapon swap animation)
4. **Engage threat** once in cover with weapon out
### Key Feel
- The "panic run while swapping weapons" is authentic PS behavior
- Not a clean tactical response - messy, human reaction
- Repair slot = weapon slot means class differences in response time
---
## V-Menu Calibration
Balancing authentic chatter vs annoying spam.
### Always Fire (Functional)
| Type | When | Purpose |
|------|------|---------|
| **Needs** (VNG, VNH, VVV) | When actually needed | Gameplay function |
| **Warnings** (VWA, VWV, VWT) | When threat spotted | Intel sharing |
| **Acknowledgment** (VVY) | When responding to request | Coordination |
### Rare/Conditional
| Type | When | Frequency |
|------|------|-----------|
| **Taunts** (VVB, VVZ) | Vengeance kills, domination | Low |
| **Celebrations** (VVF, VVE) | Exceptional events only | Very controlled |
### Celebration Events
Triggers for VVF ("Fantastic!") and VVE ("Excellent!"):
- Pesky vehicle destroyed
- Killed someone with 10+ kill streak
- Base capture
### Coordinated Celebration System
**Problem**: 50 bots all saying "VVE!" at once = annoying
**Solution**: Backend coordination
```
EVENT: Base captured
1. RNG determines responder count (1-6 bots)
2. Eligible bots (alive, in range) "claim" slots via backend
3. Each responder gets RNG delay timer
4. Staggered, natural response
EXAMPLE TIMELINE:
0.01s - Bot 1: "VVE" (Excellent!)
0.30s - Bot 2: "VVF" (Fantastic!)
1.20s - Bot 3: "VVF"
1.30s - Bot 4 (Vet): "VVB VVF" ("You can't beat me." + "Outstanding!")
```
### Vet/Ace Flavor
- Vets and Aces can do **combo callouts** (taunt + celebration)
- Adds personality, shows experience
- E.g., "You can't beat me." followed by "Outstanding!"
---
## Open Questions / Needs Expansion
### Resolved
- [x] "Partial spot" vs full spot - distance + time based, also tracers/intel
- [x] Driver class VNG response - low priority, anyone can gun
- [x] MAX variants - switches based on intel (VWA/VWV/VWT, indoor/outdoor)
- [x] Movement patterns - ADAD for vets, straight-run for newbies
- [x] Retreat behavior - HP, ammo, vehicle HP triggers
### Deferred (Future Phases)
- [ ] **FACTION TACTICS**: TR vs NC vs VS combat style differences (post-v1)
- [ ] **MAX jumping to dodge AV**: Advanced behavior, maybe skip
### Resolved (Round 2)
- [x] **REACTION TO DAMAGE**: Context-dependent (see Damage Reaction section)
- [x] **V-menu calibration**: Coordinated celebration system (see V-menu section)
---
## Implementation Notes
### Server Integration Questions
- Can bots trigger V-menu voice commands through server?
- Is Command Chat accessible for bot messages?
- Is CR (Command Rank) system implemented in emulator?
### Technical Considerations
- `/loc` command - server-side coordinate access for pathfinding
- Death tracking - need to log killer + location per bot
- Attitude persistence - per-session or reset on logout?

313
bot-docs/HANDOFF.md Normal file
View file

@ -0,0 +1,313 @@
# PlanetSide Bots - Handoff Document
> **Read this first** if you're a new Claude instance or agent picking up this project.
## What Is This Project?
We're adding **bot players** to PlanetSide 1 via the PSForever server emulator. The goal is to make the game world feel alive even with low real player population.
### The Vision
- Hundreds of bots per server (scaling down as real players join)
- Bots that feel like real players, not mechanical turrets
- V-menu chatter, teamwork, attitude/personality systems
- Infantry first, vehicles later
### Key Stakeholder
The user is a **veteran PlanetSide player** (decade+ experience) and serves as the expert on "game feel". They have detailed notes on how bots should behave. Always defer to them on gameplay questions.
---
## Project Structure
```
PSF-LoginServer/
├── bot-docs/ ← Bot project documentation
│ ├── HANDOFF.md ← YOU ARE HERE - Start here
│ ├── PROJECT.md ← Overview, goals, status
│ ├── GAME_FEEL.md ← Behavioral spec (vision, movement, V-menu, classes, etc.)
│ ├── ARCHITECTURE.md ← Technical design decisions
│ ├── CODEBASE_MAP.md ← Key files with line numbers (CRITICAL REFERENCE)
│ ├── POC_PLAN.md ← Phased implementation milestones
│ ├── DEV_SETUP.md ← Development environment setup
│ └── SKETCHES/ ← Conceptual code (not production)
│ ├── BotActor_v1.scala
│ └── BotSpawner_v1.scala
├── src/main/scala/net/psforever/
│ └── actors/bot/ ← Bot implementation code
│ ├── BotManager.scala
│ └── BotAvatarActor.scala
└── ... ← Rest of PSForever server codebase
```
### Read Order for New Instance
1. **HANDOFF.md** (this file) - Context and decisions
2. **CODEBASE_MAP.md** - Where things are in the code
3. **GAME_FEEL.md** - How bots should behave
4. **SKETCHES/** - Conceptual code to understand approach
---
## Key Technical Decisions Made
### 1. BotActor Approach (NOT SessionActor)
We decided to create a new `BotActor` rather than emulating `SessionActor` because:
- SessionActor's main job is handling network packets from clients
- Bots have no client, so no incoming packets
- BotActor can be optimized specifically for AI needs
- Less overhead, better scalability
### 2. Server-Side Native Bots
Bots are first-class server entities, not fake clients connecting from outside.
- Bots use existing `Player` and `PlayerControl` code
- No network overhead
- Actions broadcast through same services as real players
### 3. Bot Identity
Bot characters use:
- **High positive IDs** (900000+) to avoid collision with real DB players
- **No special characters in names** - use `xxBOTxxName` format (not `[BOT]Name`)
- Predefined loadouts per class
### 4. Tick Rate
- 10-20 FPS for bot AI decisions (not 60)
- Can reduce further for bots far from real players
- Server load monitoring will inform final values
---
## Bot Architecture Summary
```
BotManager (per zone)
├── Monitors real player population
├── Spawns/despawns bots to maintain target count
└── Manages one Ace per faction (last to logout)
BotActor (per bot)
├── 10 FPS tick loop
├── Vision cone detection (60-90 degrees)
├── Decision making (attack, retreat, objective, help)
├── Movement execution
├── State broadcasting
└── Controls a Player entity
BotAvatarActor (stub)
└── Minimal - just accepts messages PlayerControl sends
Player + PlayerControl (existing code)
├── Player entity with Vitality, inventory, etc.
└── PlayerControl handles damage/death (already exists)
```
---
## Spawn Flow (Critical Path)
```scala
// 1. Create avatar (HIGH POSITIVE ID - 900000+)
val avatar = Avatar(900000 + botNum, BasicCharacterData(name, faction, sex, head, voice))
// 2. Create player entity
val player = new Player(avatar)
player.Position = spawnPosition
player.Spawn()
// 3. Create typed stub avatar actor (MUST be typed ActorRef)
val typedSystem = context.system.toTyped
val botAvatarActor: ActorRef[AvatarActor.Command] =
typedSystem.systemActorOf(BotAvatarActor(), s"bot-avatar-$botId")
// 4. Register GUIDs (ASYNC - use registerAvatar, NOT registerPlayer!)
TaskWorkflow.execute(GUIDTask.registerAvatar(zone.GUID, player)).onComplete {
case Success(_) =>
// 5. Join zone population
zone.Population ! Zone.Population.Join(avatar)
// 6. Spawn player (creates PlayerControl actor)
zone.Population ! Zone.Population.Spawn(avatar, player, botAvatarActor)
// 7. Broadcast to clients
zone.AvatarEvents ! AvatarServiceMessage(zone.id, AvatarAction.LoadPlayer(...))
case Failure(ex) =>
log.error(s"Failed: ${ex.getMessage}")
}
```
---
## Broadcasting (How Bots Appear to Real Players)
Bots broadcast their state via the same channels real players use:
```scala
// Position updates (10-20 times per second)
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.PlayerState(guid, pos, vel, facing, crouch, jump, ...)
)
// Weapon fire
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.ChangeFireState_Start(playerGuid, weaponGuid)
)
// V-menu voice commands
// (Need to investigate ChatMsg format)
```
---
## Bot Classes (From User's Notes)
| Class | Role | Behavior |
|-------|------|----------|
| Driver | Vehicle operation | Low priority for gunning, wants to drive |
| Support | Heal & Repair | LOVES gunning (can heal vehicle), prioritizes healing |
| Hacker | Infiltration | Door unlocking, base capture |
| AV | Anti-Vehicle | Tank hunters |
| MAX | Heavy Exosuit | AI/AV/AA variants, switches based on intel |
| Vet | Versatile | Jack of all trades, ADAD movement, vengeance system |
| Ace | Empire Leader | ONE per empire, last to logout, uses Command Chat |
---
## Implementation Status
### COMPLETED (POC Working!)
- [x] BotAvatarActor stub (typed actor, absorbs messages)
- [x] BotManager (spawns bots, handles async GUID flow)
- [x] `!bot` chat command for testing
- [x] Bot spawns visible to players
- [x] Bot takes damage from players (PlayerControl works!)
- [x] Bot movement (10 tick/sec, random wandering, PlayerState broadcasts)
- [x] Multiple bots work independently
### KNOWN ISSUES (Expected for POC)
- No backpack on death (likely because bot has no items/loadout)
- Walks through walls (no collision detection)
- No Z-height adjustment (melts into stairs, terrain)
- Walk speed slightly fast for animation (4 units/sec, try 3)
### NOT YET IMPLEMENTED
- [ ] Bot loadout/equipment
- [ ] Bot shooting (weapon fire broadcasts)
- [ ] Bot death/respawn cycle
- [ ] Terrain following (Z height from map data)
- [ ] Collision avoidance
- [ ] Pathfinding
- [ ] V-menu voice command sending
- [ ] Celebration coordination system
- [ ] Vengeance/attitude system
- [ ] Population scaling (spawn/despawn based on real players)
---
## Resolved Questions (Through Trial & Error)
1. **Avatar IDs**: Must be POSITIVE. Negative IDs break packet encoding (32-bit unsigned). Use 900000+.
2. **PlayerControl + stub AvatarActor**: WORKS! BotAvatarActor just absorbs messages, PlayerControl functions normally.
3. **GUID Registration**: Must use `registerAvatar()` not `registerPlayer()` - locker needs GUID for PlayerControl init.
## Open Questions for PSForever Devs
1. **Bot identification**: Should we add `isBot: Boolean` to Player?
2. **Spawn point access**: Best way to find valid faction spawn locations?
---
## Spawn Location Logic (For Future Implementation)
When a player dies, they choose from up to 3 spawn options (all must be friendly-owned):
1. **Nearest Tower** - Guard towers scattered across continents
2. **Nearest AMS** - Advanced Mobile Station (deployable spawn vehicle)
3. **Nearest Base/Facility** - The main base spawn tubes
**Special Cases:**
- **Sanctuary** (home2, home1, home3): Only one spawn option (the sanctuary itself)
- **Warpgates**: Can spawn at warpgates your faction owns
**For Bots:**
- Currently: Respawn at original spawn position (simple)
- Future: Query zone for valid faction spawn points, pick nearest or random
- Need to find: `SpawnPoint` instances, check faction ownership, calculate distance
**Code References to Investigate:**
- `SpawnPoint.scala` - Spawn point trait and calculations
- `Zone.spawnGroups` - Map of building -> spawn points
- `Building.Faction` - Check ownership for spawn eligibility
---
## Important Behavioral Notes
### From GAME_FEEL.md (Key Points)
- **Vision**: 60-90 degree FOV, distance-based spotting time
- **Movement**: Newbies run straight, Vets do ADAD + crouch spam
- **Retreat**: Vets retreat at low HP, Drivers save vehicles, everyone retreats when out of ammo
- **V-menu**: "Needs" always fire, celebrations are coordinated (1-6 responders, staggered timing)
- **Vengeance**: Vets remember who killed them, may seek revenge, taunt on success
- **Panic**: Non-combat bots (repairing) panic when shot, run to cover while swapping weapons
- **Chaos**: The goal is authentic battlefield chaos - spam bullets, grenades, voice commands
### What NOT to Do
- No bunny hopping (almost no one jumps except MAX dodging AV)
- No laser accuracy (bots should miss)
- No instant reactions (bots need reaction time)
- No perfect coordination (spread out, don't stand on same pixel)
- No mechanical behavior (turrets are terrible, we want human-like chaos)
---
## Development Environment
```bash
# Server codebase
cd PSF-LoginServer
sbt compile # Compile
sbt server/run # Run server
# Requirements
- Java 8 JDK
- sbt (Scala Build Tool)
- PostgreSQL 10+
- PlanetSide client (version 3.15.84.0)
```
---
## Context for Future Agents
If you're a **coding agent** tasked with implementation:
1. Read `CODEBASE_MAP.md` for file locations
2. Read `SKETCHES/` for conceptual starting points
3. Start with Milestone 1: spawn a static bot that appears in-game
4. Test empirically - compile, run, connect with client, observe
If you're a **research agent** investigating something:
1. The PSF-LoginServer codebase is in `./PSF-LoginServer/`
2. All server code is under `src/main/scala/net/psforever/`
3. Tests are under `src/test/scala/`
4. Use grep/glob to find patterns
If the user says **"update the docs"**:
1. Update the relevant .md files
2. Keep `CODEBASE_MAP.md` current with any new discoveries
3. Add line numbers when referencing code
---
## Last Session Summary
**Date**: 2024 (context creation date)
**Accomplished**:
- Analyzed PSF-LoginServer codebase architecture
- Mapped spawn/broadcast/GUID flows
- Created behavioral spec from user's notes
- Sketched BotActor and BotSpawner concepts
- Documented key files with line numbers
**Next Steps**:
- Set up dev environment
- Start Phase 1: spawn a static bot
- OR wait for dev team answers on open questions

291
bot-docs/POC_PLAN.md Normal file
View file

@ -0,0 +1,291 @@
# PlanetSide Bots - Proof of Concept Plan
## Goal
Get a single bot to spawn, be visible to clients, and perform basic actions.
---
## Phase 0: Research & Setup (DONE)
- [x] Clone PSF-LoginServer
- [x] Analyze codebase architecture
- [x] Identify Player entity system
- [x] Find existing AI patterns (AutomatedTurretBehavior)
- [x] Document integration points
---
## Phase 1: "Hello World" Bot
### Objective
Spawn a bot entity that appears in the game world as a player character.
### Tasks
#### 1.1 Understand Player Creation Flow
- [ ] Trace how a real player is created when they log in
- [ ] Find `Player` instantiation code
- [ ] Find zone registration code (`Zone.AddPlayer` or similar)
- [ ] Find GUID allocation for new entities
#### 1.2 Create Minimal Bot Infrastructure
- [ ] Create `src/main/scala/net/psforever/objects/bot/` directory
- [ ] Create `Bot.scala` - wrapper or extension of Player
- [ ] Create `BotActor.scala` - minimal actor (just keeps bot alive)
#### 1.3 Bot Spawning
- [ ] Find how to spawn a player at a SpawnPoint without client
- [ ] Allocate GUID for bot
- [ ] Register bot in zone
- [ ] Broadcast bot's existence to other players
#### 1.4 Validation
- [ ] Connect real client to server
- [ ] Verify bot appears as a player character
- [ ] Bot should be standing still at spawn location
### Questions to Answer
- Can we create a Player without Avatar from database?
- Do we need to fake an Account/Character entry?
- What minimum packets must be sent to make bot visible?
---
## Phase 2: Moving Bot
### Objective
Bot moves through the world and other players can see it moving.
### Tasks
#### 2.1 PlayerStateMessage Broadcasting
- [ ] Understand how player position updates are broadcast
- [ ] Find the packet: `PlayerStateMessage`
- [ ] Implement position update loop in BotActor
#### 2.2 Simple Movement
- [ ] Bot walks forward
- [ ] Bot turns (change orientation)
- [ ] Movement speed matches infantry walking
#### 2.3 Patrol Behavior
- [ ] Bot walks between two points
- [ ] Bot stops, turns, walks back
### Success Criteria
- Real player sees bot walking around
- Bot movement is smooth (not teleporting)
---
## Phase 3: Combat Bot
### Objective
Bot can detect enemies and shoot at them.
### Tasks
#### 3.1 Vision System
- [ ] Implement FOV cone check (60-90 degrees)
- [ ] Detect players in range
- [ ] Filter by faction (don't shoot friendlies)
#### 3.2 Target Tracking
- [ ] Store current target reference
- [ ] Turn to face target (orientation updates)
- [ ] Lose target when out of range/sight
#### 3.3 Shooting
- [ ] Find weapon fire packets (`ChangeFireStateMessage_Start`, `WeaponFireMessage`)
- [ ] Implement firing at target
- [ ] Basic accuracy (not perfect, not terrible)
#### 3.4 Taking Damage
- [ ] Bot receives damage normally (via PlayerControl)
- [ ] Bot dies when HP reaches 0
- [ ] Bot respawns after death
### Success Criteria
- Bot shoots at enemy players
- Bot can be killed
- Bot respawns
---
## Phase 4: Smart Bot
### Objective
Bot makes tactical decisions.
### Tasks
#### 4.1 Health-Based Retreat
- [ ] Track bot's HP
- [ ] Retreat when HP below threshold
- [ ] Find cover or run toward spawn
#### 4.2 Ammunition Management
- [ ] Track ammo count
- [ ] Retreat to resupply when empty
#### 4.3 V-Menu Communication
- [ ] Bot sends VVV when needs help
- [ ] Bot sends VNG when in vehicle without gunner
- [ ] Proper chat message format
### Success Criteria
- Bot retreats when hurt
- Bot calls for help appropriately
---
## Phase 5: Bot Classes
### Objective
Different bot types with different behaviors.
### Tasks
#### 5.1 Class Definitions
- [ ] Define loadouts for each class (Driver, Support, Hacker, AV, MAX)
- [ ] Create certification sets per class
- [ ] Assign behavior weights per class
#### 5.2 Class-Specific Behavior
- [ ] Support: Prioritize healing/repairing
- [ ] Hacker: Prioritize hacking doors/objectives
- [ ] AV: Prioritize anti-vehicle combat
- [ ] MAX: Heavy combat behavior
#### 5.3 Vet & Ace Classes
- [ ] Vet: Multi-role capability
- [ ] Ace: Command behavior, last to logout
### Success Criteria
- Multiple bot types visible
- Each type behaves differently
---
## Phase 6: Population Management
### Objective
Bots scale with player population.
### Tasks
#### 6.1 BotManager
- [ ] Track real player count per faction
- [ ] Calculate target bot count
- [ ] Spawn/despawn bots to maintain balance
#### 6.2 Graceful Despawn
- [ ] Non-Ace bots leave first
- [ ] Ace leaves last
- [ ] Bots "log out" naturally (not poof)
#### 6.3 Faction Balance
- [ ] Equal bots per faction
- [ ] Adjust for real player imbalance?
### Success Criteria
- Server maintains ~100 bots per faction when empty
- Bots reduce as real players join
- Ace is last bot standing per faction
---
## Phase 7: Team Coordination
### Objective
Bots work together and follow orders.
### Tasks
#### 7.1 Order System
- [ ] Ace issues attack/defend orders
- [ ] Bots receive and follow orders
- [ ] Order priority system
#### 7.2 Help Response
- [ ] Bots respond to VNG/VNH/VVV
- [ ] Check if can help
- [ ] Navigate to requester
#### 7.3 Celebration Coordination
- [ ] Coordinate V-menu celebrations
- [ ] Staggered timing
- [ ] Limited responders (1-6)
### Success Criteria
- Bots attack/defend as ordered
- Bots help each other
- Celebrations feel natural
---
## Development Environment Setup
### Prerequisites
- Java 8 JDK
- sbt (Scala Build Tool)
- PostgreSQL 10+
- PlanetSide client (version 3.15.84.0)
### Build & Run
```bash
cd PSF-LoginServer
sbt compile
sbt server/run
```
### Testing Approach
1. Run server locally
2. Connect with PS client
3. Observe bot behavior
4. Iterate
---
## Risk Assessment
| Risk | Likelihood | Impact | Mitigation |
|------|------------|--------|------------|
| Player without SessionActor breaks things | Medium | High | May need dummy SessionActor |
| GUID allocation conflicts | Medium | High | Coordinate with existing GUID system |
| Performance with 300+ bots | Medium | Medium | Profile early, optimize as needed |
| Network overhead for bot updates | Low | Medium | Batch updates, reduce frequency |
| Client can't render many players | Low | High | Test with max players early |
---
## Success Metrics
### Phase 1 Success
- 1 bot visible in game world
### Phase 3 Success
- Bot engages in combat
### Phase 5 Success
- Multiple bot classes working
### Phase 7 Success (MVP)
- 100+ bots per faction
- Dynamic population scaling
- Team coordination
- "Feels like PlanetSide"
---
## Estimated Complexity
| Phase | Complexity | Notes |
|-------|------------|-------|
| 1 | Medium | Unknown integration points |
| 2 | Low | Straightforward once Phase 1 works |
| 3 | Medium | Combat systems are complex |
| 4 | Low | Building on Phase 3 |
| 5 | Medium | Many classes to implement |
| 6 | Low | Straightforward management |
| 7 | High | Coordination is complex |

145
bot-docs/PROJECT.md Normal file
View file

@ -0,0 +1,145 @@
# PlanetSide Bots Project
## Overview
Bringing bot support to PlanetSide through the PSForever server emulator project.
## Resources
- **Server Emulator**: [PSF-LoginServer](https://github.com/psforever/PSF-LoginServer) (Scala)
- **Wiki Reference**: https://www.psforever.net/PlanetSide/
- **Community**: PSForever dev team and community
---
## Documentation Structure
> **Important**: Keep documentation updated for context handoff between sessions.
| Document | Purpose |
|----------|---------|
| `HANDOFF.md` | **Start here** - Context for new Claude instances |
| `PROJECT.md` | This file - Overview and status |
| `GAME_FEEL.md` | Behavioral spec from user's notes |
| `ARCHITECTURE.md` | Technical design decisions |
| `CODEBASE_MAP.md` | Key files with line numbers |
| `POC_PLAN.md` | Implementation milestones |
| `DEV_SETUP.md` | Dev environment setup (verified working) |
| `SKETCHES/` | Conceptual code (not production) |
### Documentation Practices
- When discovering new code patterns, update `CODEBASE_MAP.md` with file paths and line numbers
- Link related concepts (e.g., "PlayerControl handles damage, see line 50-80")
- Write for a fresh Claude instance that has no prior context
- Keep `HANDOFF.md` current with latest decisions and next steps
---
## Game Context (PlanetSide 1)
- **Genre**: MMOFPS (Massively Multiplayer Online First Person Shooter)
- **Factions**: Terran Republic (TR), New Conglomerate (NC), Vanu Sovereignty (VS)
- **Scale**: Designed for hundreds to thousands of concurrent players
- **Core Loop**: Territory control via base capture through lattice-linked continents
- **Emulator Status**: ~98% complete - virtually indistinguishable from original to veteran players
### Key Gameplay Elements
- **Combined Arms**: Infantry, ground vehicles, aircraft all operating together
- **Armor Types**: Standard, Reinforced, Agile, Infiltration suits
- **Certification System**: Players unlock equipment/vehicles via certs
- **Base Capture**: Lattice-based progression, facility benefits cascade through links
- **Continents**: 10 landmass continents + underground caverns
### Vehicle Categories
- **Air**: Reaver, Mosquito, Liberator, Galaxy
- **Ground**: Lightning, Magrider, Vanguard, Harasser, ATV, ANT, Sunderer
- **Specialized**: BattleFrame Robotics (BFRs)
---
## Project Goals
### Primary Goal
**Population bots** - Make the world feel alive even with low real player counts
- Target: Hundreds of bots capable of running simultaneously
- **Dynamic scaling**: Bots log out as real players join each faction
- Bots should participate in the war naturally (capture bases, fight, move with purpose)
### Scope Phases
1. **Phase 1**: Infantry bots only
2. **Phase 2+**: Ground vehicles, aircraft, advanced behaviors (TBD)
### Intelligence Level
- Start realistic, iterate based on what's achievable
- Goal: Bots that don't break immersion, feel like "bad but trying" players
---
## Technical Approach
### Decided: BotActor (Server-Side Native)
After analyzing the codebase, we chose to create a new `BotActor` rather than emulating `SessionActor`:
- SessionActor handles network I/O from clients - bots have no client
- BotActor makes AI decisions internally, broadcasts via zone services
- Less overhead, better scalability, full optimization control
### Architecture Overview
```
BotManager (per zone)
└── BotActor (per bot)
└── Controls Player entity
└── PlayerControl (existing) handles damage/death
```
### Key Integration Points
- `Zone.Population` - Join/Spawn/Leave for bots
- `zone.AvatarEvents` - Broadcast position/actions
- `GUIDTask.registerPlayer()` - GUID allocation
- `PlayerControl` - Handles damage/death (reused)
See `CODEBASE_MAP.md` for file locations and line numbers.
### Dev Team Support
- Team wants this feature but bandwidth-limited
- Will assist with major blockers
- Full support expected as project nears completion
- **Project Lead**: Community member (us!) taking point
---
## Status
**Phase**: POC Complete - Bots Spawn and Move!
### Completed
- [x] Clone PSF-LoginServer codebase
- [x] Analyze codebase architecture
- [x] Map spawn/broadcast/GUID flows
- [x] Document behavioral spec from user's notes
- [x] Design bot architecture
- [x] Create conceptual code sketches
- [x] Build handoff documentation
- [x] Set up dev environment (Java 8, sbt, PostgreSQL)
- [x] Verify baseline compile and server run
- [x] **Phase 1: Spawn a static bot** - WORKING!
- [x] **Phase 2: Bot moves and broadcasts** - WORKING!
### Resolved Questions (Through Trial & Error)
- Avatar IDs must be POSITIVE (900000+), not negative
- PlayerControl works perfectly with stub BotAvatarActor
- Must use `registerAvatar()` not `registerPlayer()` (locker needs GUID)
### Next Steps
- [ ] Bot loadout/equipment (no backpack on death without items)
- [ ] Phase 3: Bot detects enemies and shoots
- [ ] Death/respawn cycle
- [ ] Terrain following (Z height)
---
## Faction Behavior & Game Feel
*See `GAME_FEEL.md` for detailed behavioral spec including:*
- Vision system (60-90 degree FOV, partial/full spotting)
- V-menu communication system
- Bot classes (Driver, Support, Hacker, AV, MAX, Vet, Ace)
- Movement patterns (newbie vs veteran)
- Retreat behaviors
- Attitude and vengeance system
- Chaos factor

View file

@ -0,0 +1,367 @@
// SKETCH - NOT PRODUCTION CODE
// This is a conceptual exploration of what BotActor might look like
package net.psforever.actors.bot
import akka.actor.{Actor, ActorRef, Cancellable, Props}
import net.psforever.objects.Player
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.zones.Zone
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
import net.psforever.types.{PlanetSideGUID, Vector3}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
/**
* BotActor - Controls a single bot entity
*
* Unlike SessionActor (which handles network packets from a client),
* BotActor makes decisions internally and broadcasts state to other players.
*
* Key differences from SessionActor:
* - No middlewareActor (no network connection)
* - No incoming packets to process
* - Generates actions based on AI logic
* - Still broadcasts via zone.AvatarEvents (same as real players)
*/
class BotActor(
player: Player,
avatar: Avatar,
zone: Zone,
botClass: BotClass,
botPersonality: BotPersonality
) extends Actor {
// Tick timer - how often bot makes decisions and broadcasts state
private var tickTimer: Cancellable = _
// Timestamp counter for PlayerStateMessage
private var timestamp: Int = 0
// Current AI state
private var currentTarget: Option[Player] = None
private var currentObjective: Option[BotObjective] = None
private var attitude: Float = 0.5f // 0 = calm, 1 = raging
// Death memory for vengeance system
private var lastDeathLocation: Option[Vector3] = None
private var lastKiller: Option[PlanetSideGUID] = None
override def preStart(): Unit = {
// Start the tick loop
// 10 FPS = 100ms, could go lower for distant bots
tickTimer = context.system.scheduler.scheduleWithFixedDelay(
initialDelay = 100.millis,
delay = 100.millis, // 10 ticks per second
receiver = self,
message = BotActor.Tick
)
}
override def postStop(): Unit = {
tickTimer.cancel()
}
def receive: Receive = {
case BotActor.Tick =>
tick()
case BotActor.TakeDamage(amount, source, sourcePosition) =>
handleDamage(amount, source, sourcePosition)
case BotActor.Die(killer) =>
handleDeath(killer)
case BotActor.Respawn(spawnPoint) =>
handleRespawn(spawnPoint)
case BotActor.ReceiveOrder(order) =>
handleOrder(order)
case BotActor.HelpRequest(requester, helpType, location) =>
handleHelpRequest(requester, helpType, location)
}
/**
* Main AI tick - called every 100ms (10 FPS)
*/
private def tick(): Unit = {
if (!player.isAlive) return
timestamp = (timestamp + 1) % 65536
// 1. Perception - what can we see?
val visibleTargets = detectTargets()
// 2. Decision - what should we do?
val action = decideAction(visibleTargets)
// 3. Execute - do the thing
executeAction(action)
// 4. Broadcast - tell everyone where we are
broadcastState()
}
/**
* Detect targets within vision cone
*/
private def detectTargets(): Seq[Player] = {
val myPos = player.Position
val myFacing = player.Orientation.z
val fovHalf = botPersonality.fovDegrees / 2
val maxRange = botPersonality.detectionRange
zone.LivePlayers
.filter(p => p != player)
.filter(p => p.Faction != player.Faction) // enemies only
.filter(p => p.isAlive)
.filter { target =>
val targetPos = target.Position
val distance = Vector3.Distance(myPos, targetPos)
val angle = calculateAngle(myPos, targetPos, myFacing)
distance <= maxRange && math.abs(angle) <= fovHalf
}
.toSeq
.sortBy(t => Vector3.DistanceSquared(myPos, t.Position))
}
/**
* Decide what action to take based on current state and perception
*/
private def decideAction(visibleTargets: Seq[Player]): BotAction = {
// Check health - should we retreat?
if (player.Health < player.MaxHealth * botPersonality.retreatThreshold) {
return BotAction.Retreat
}
// Check ammo - need resupply?
if (isOutOfAmmo()) {
return BotAction.Resupply
}
// Have a target?
visibleTargets.headOption match {
case Some(target) =>
currentTarget = Some(target)
BotAction.Attack(target)
case None =>
currentTarget = None
// Follow objective or patrol
currentObjective match {
case Some(obj) => BotAction.FollowObjective(obj)
case None => BotAction.Patrol
}
}
}
/**
* Execute the decided action
*/
private def executeAction(action: BotAction): Unit = action match {
case BotAction.Attack(target) =>
// Turn toward target
val newFacing = calculateFacingToward(player.Position, target.Position)
player.Orientation = Vector3(0, 0, newFacing)
// Move based on bot type
botPersonality.movementStyle match {
case MovementStyle.Newbie =>
// Run toward target with held strafe
moveToward(target.Position, strafeOffset = 2f)
case MovementStyle.Veteran =>
// ADAD strafe
moveWithADAD(target.Position)
case MovementStyle.NCClose =>
// Close distance aggressively (shotgun range)
moveToward(target.Position, speed = 1.5f)
}
// Fire if we have line of sight
if (hasLineOfSight(target)) {
fireWeapon(target)
}
case BotAction.Retreat =>
val retreatPosition = findRetreatPosition()
moveToward(retreatPosition)
// Call for help
if (shouldCallForHelp()) {
sendVoiceCommand("VVV") // HELP!
}
case BotAction.Resupply =>
val terminal = findNearestTerminal()
terminal.foreach(moveToward)
case BotAction.FollowObjective(obj) =>
moveToward(obj.targetPosition)
case BotAction.Patrol =>
patrol()
}
/**
* Broadcast current state to other players via AvatarService
*/
private def broadcastState(): Unit = {
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.PlayerState(
player.GUID,
player.Position,
Some(player.Velocity),
player.Orientation.z, // facingYaw
player.Orientation.x, // facingPitch
player.facingYawUpper, // upper body yaw
timestamp,
player.Crouching,
player.Jumping,
jumpThrust = false,
player.Cloaked,
spectator = false,
weaponInHand = player.DrawnSlot != Player.HandsDownSlot
)
)
}
/**
* Handle taking damage
*/
private def handleDamage(amount: Int, source: PlanetSideGUID, sourcePosition: Vector3): Unit = {
// If not in combat, react to damage
if (currentTarget.isEmpty) {
// Turn toward damage source
val newFacing = calculateFacingToward(player.Position, sourcePosition)
player.Orientation = Vector3(0, 0, newFacing)
// Panic behavior based on class
if (botClass.role == BotRole.Support || botClass.role == BotRole.Hacker) {
// Panic! Run to cover, swap to weapon
// (Support was probably repairing/healing)
}
}
// Increase attitude if repeatedly dying to same source
if (botPersonality.experienceLevel >= ExperienceLevel.Veteran) {
lastKiller.foreach { killer =>
if (killer == source) {
attitude = math.min(1.0f, attitude + 0.1f)
}
}
}
}
/**
* Handle death
*/
private def handleDeath(killer: PlanetSideGUID): Unit = {
lastDeathLocation = Some(player.Position)
lastKiller = Some(killer)
// Veteran+ bots remember for vengeance
if (botPersonality.experienceLevel >= ExperienceLevel.Veteran) {
// Store vengeance target
}
}
/**
* Send a V-menu voice command
*/
private def sendVoiceCommand(command: String): Unit = {
// TODO: Send ChatMsg with voice command
// zone.AvatarEvents ! ... ChatMsg ...
}
// Helper methods (stubs)
private def calculateAngle(from: Vector3, to: Vector3, facing: Float): Float = ???
private def calculateFacingToward(from: Vector3, to: Vector3): Float = ???
private def moveToward(target: Vector3, strafeOffset: Float = 0f, speed: Float = 1f): Unit = ???
private def moveWithADAD(target: Vector3): Unit = ???
private def hasLineOfSight(target: Player): Boolean = ???
private def fireWeapon(target: Player): Unit = ???
private def isOutOfAmmo(): Boolean = ???
private def findRetreatPosition(): Vector3 = ???
private def findNearestTerminal(): Option[Vector3] = ???
private def shouldCallForHelp(): Boolean = ???
private def patrol(): Unit = ???
}
object BotActor {
def props(player: Player, avatar: Avatar, zone: Zone, botClass: BotClass, personality: BotPersonality): Props =
Props(classOf[BotActor], player, avatar, zone, botClass, personality)
// Messages
case object Tick
case class TakeDamage(amount: Int, source: PlanetSideGUID, sourcePosition: Vector3)
case class Die(killer: PlanetSideGUID)
case class Respawn(spawnPoint: Vector3)
case class ReceiveOrder(order: BotOrder)
case class HelpRequest(requester: PlanetSideGUID, helpType: String, location: Vector3)
}
// Supporting types (would be in separate files)
sealed trait BotAction
object BotAction {
case class Attack(target: Player) extends BotAction
case object Retreat extends BotAction
case object Resupply extends BotAction
case class FollowObjective(objective: BotObjective) extends BotAction
case object Patrol extends BotAction
}
sealed trait BotRole
object BotRole {
case object Driver extends BotRole
case object Support extends BotRole
case object Hacker extends BotRole
case object AV extends BotRole
case object MAX extends BotRole
case object Veteran extends BotRole
case object Ace extends BotRole
}
sealed trait ExperienceLevel
object ExperienceLevel {
case object Newbie extends ExperienceLevel
case object Regular extends ExperienceLevel
case object Veteran extends ExperienceLevel
case object Ace extends ExperienceLevel
}
sealed trait MovementStyle
object MovementStyle {
case object Newbie extends MovementStyle // straight run with held strafe
case object Veteran extends MovementStyle // ADAD + crouch spam
case object NCClose extends MovementStyle // aggressive closing for shotguns
}
case class BotClass(
name: String,
role: BotRole,
// certifications, loadout, etc.
)
case class BotPersonality(
experienceLevel: ExperienceLevel,
movementStyle: MovementStyle,
fovDegrees: Float = 60f,
detectionRange: Float = 100f,
retreatThreshold: Float = 0.25f, // retreat at 25% HP
accuracyModifier: Float = 1.0f
)
case class BotObjective(
targetPosition: Vector3,
objectiveType: String // "attack", "defend", "capture", etc.
)
case class BotOrder(
orderType: String,
targetPosition: Option[Vector3],
priority: Int
)

View file

@ -0,0 +1,394 @@
// SKETCH - NOT PRODUCTION CODE
// Bot spawning flow - what would it take to spawn a bot?
package net.psforever.actors.bot
import akka.actor.{Actor, ActorContext, ActorRef, Props}
import net.psforever.objects.{GlobalDefinitions, Player}
import net.psforever.objects.avatar.Avatar
import net.psforever.objects.definition.ExoSuitDefinition
import net.psforever.objects.guid.GUIDTask
import net.psforever.objects.loadouts.InfantryLoadout
import net.psforever.objects.zones.Zone
import net.psforever.packet.game.objectcreate.BasicCharacterData
import net.psforever.types._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.{Success, Failure}
import java.util.concurrent.atomic.AtomicInteger
/**
* BotSpawner - Responsible for spawning bots into a zone
*
* Key insight: Looking at ZonePopulationActor.scala:
* - Zone.Population.Join(avatar) -> registers avatar in playerMap
* - Zone.Population.Spawn(avatar, player, avatarActor) -> creates PlayerControl
* - GUIDTask.registerPlayer(zone.GUID, player) -> assigns GUIDs
*
* For bots, we need:
* 1. Create an Avatar (normally from DB, but we can construct directly)
* 2. Create a Player with that Avatar
* 3. Equip the player with loadout
* 4. Register GUIDs for player and equipment
* 5. Join zone population
* 6. Spawn player
* 7. Create BotActor to control AI
*/
object BotSpawner {
// Counter for generating unique bot IDs
// Using negative numbers to avoid collision with real player charIds
private val botIdCounter = new AtomicInteger(-1)
/**
* Spawn a single bot into a zone
*
* @param zone The zone to spawn into
* @param faction Which empire (TR, NC, VS)
* @param botClass The class/role of the bot
* @param spawnPosition Where to spawn
* @param context ActorContext for creating BotActor
* @return The spawned player entity
*/
def spawnBot(
zone: Zone,
faction: PlanetSideEmpire.Value,
botClass: BotClass,
spawnPosition: Vector3,
context: ActorContext
): Player = {
// 1. Generate unique bot ID (negative to avoid DB collision)
val botId = botIdCounter.getAndDecrement()
// 2. Create Avatar
val avatar = createBotAvatar(botId, faction, botClass)
// 3. Create Player entity
val player = new Player(avatar)
// 4. Configure player
configurePlayer(player, botClass, spawnPosition)
// 5. Register GUIDs for player and all equipment
// This is async - we need to wait for it to complete
val registerTask = GUIDTask.registerPlayer(zone.GUID, player)
TaskWorkflow.execute(registerTask)
// 6. Join zone population (avatar-level)
zone.Population ! Zone.Population.Join(avatar)
// 7. Create a placeholder BotAvatarActor
// Real AvatarActor handles DB persistence - we don't need that
val botAvatarActor = context.actorOf(
BotAvatarActor.props(avatar),
name = s"bot-avatar-$botId"
)
// 8. Spawn player in zone (creates PlayerControl actor)
zone.Population ! Zone.Population.Spawn(avatar, player, botAvatarActor)
// 9. Add to block map for spatial queries
zone.actor ! ZoneActor.AddToBlockMap(player, spawnPosition)
// 10. Create BotActor for AI control
val personality = createPersonality(botClass)
val botActor = context.actorOf(
BotActor.props(player, avatar, zone, botClass, personality),
name = s"bot-ai-$botId"
)
// 11. Broadcast player existence to all connected clients
broadcastPlayerSpawn(zone, player)
player
}
/**
* Create an Avatar for a bot
*/
private def createBotAvatar(
botId: Int,
faction: PlanetSideEmpire.Value,
botClass: BotClass
): Avatar = {
val name = generateBotName(faction, botClass, botId)
val sex = if (scala.util.Random.nextBoolean()) CharacterSex.Male else CharacterSex.Female
val head = scala.util.Random.nextInt(5) + 1
val voice = CharacterVoice.values.toSeq(scala.util.Random.nextInt(CharacterVoice.values.size))
// Create avatar with predefined certifications for the class
Avatar(
id = botId,
basic = BasicCharacterData(name, faction, sex, head, voice),
bep = botClass.battleRank * 1000L, // Fake BEP for appearance
cep = if (botClass.role == BotRole.Ace) 10000L else 0L, // CR for Ace only
certifications = botClass.certifications
)
}
/**
* Generate a bot name like "[BOT]Grunt_TR_042"
*/
private def generateBotName(
faction: PlanetSideEmpire.Value,
botClass: BotClass,
botId: Int
): String = {
val factionPrefix = faction match {
case PlanetSideEmpire.TR => "TR"
case PlanetSideEmpire.NC => "NC"
case PlanetSideEmpire.VS => "VS"
case _ => "XX"
}
val classPrefix = botClass.role match {
case BotRole.Driver => "Driver"
case BotRole.Support => "Medic"
case BotRole.Hacker => "Hacker"
case BotRole.AV => "Heavy"
case BotRole.MAX => "MAX"
case BotRole.Veteran => "Vet"
case BotRole.Ace => "Ace"
}
f"[BOT]${classPrefix}_${factionPrefix}_${math.abs(botId)}%03d"
}
/**
* Configure player entity with position, equipment, etc.
*/
private def configurePlayer(
player: Player,
botClass: BotClass,
spawnPosition: Vector3
): Unit = {
// Set position and orientation
player.Position = spawnPosition
player.Orientation = Vector3(0, 0, scala.util.Random.nextFloat() * 360f)
// Set exosuit based on class
val exosuit = botClass.role match {
case BotRole.MAX => ExoSuitType.MAX
case BotRole.Hacker => ExoSuitType.Agile // Infiltrators use Agile
case _ => ExoSuitType.Reinforced
}
player.ExoSuit = exosuit
// Equip loadout
equipLoadout(player, botClass)
// Spawn the player (set health, armor)
player.Spawn()
}
/**
* Equip player with class-appropriate loadout
*/
private def equipLoadout(player: Player, botClass: BotClass): Unit = {
// TODO: Load from predefined loadouts
// For now, just give basic equipment based on faction + class
// Example: Standard infantry loadout
// player.Slot(0).Equipment = ... // Rifle
// player.Slot(1).Equipment = ... // Sidearm
// etc.
}
/**
* Create personality/behavior weights for a bot class
*/
private def createPersonality(botClass: BotClass): BotPersonality = {
botClass.role match {
case BotRole.Veteran | BotRole.Ace =>
BotPersonality(
experienceLevel = if (botClass.role == BotRole.Ace) ExperienceLevel.Ace else ExperienceLevel.Veteran,
movementStyle = MovementStyle.Veteran,
fovDegrees = 75f, // Better awareness
accuracyModifier = 1.2f,
retreatThreshold = 0.3f
)
case BotRole.MAX =>
BotPersonality(
experienceLevel = ExperienceLevel.Regular,
movementStyle = MovementStyle.Newbie, // MAXes are slower
fovDegrees = 60f,
accuracyModifier = 1.0f,
retreatThreshold = 0.2f // MAXes don't retreat easily
)
case _ =>
BotPersonality(
experienceLevel = ExperienceLevel.Newbie,
movementStyle = MovementStyle.Newbie,
fovDegrees = 60f,
accuracyModifier = 0.8f, // Worse accuracy
retreatThreshold = 0.25f
)
}
}
/**
* Broadcast player spawn to all connected clients
*/
private def broadcastPlayerSpawn(zone: Zone, player: Player): Unit = {
// Use ObjectCreateMessage to create the player on all clients
// This is what makes the bot visible to everyone
import net.psforever.packet.game.ObjectCreateMessage
import net.psforever.packet.game.objectcreate._
// Build the player data for ObjectCreateMessage
val playerData = PlayerData.create(player)
// Broadcast via AvatarService
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.LoadPlayer(
player.GUID,
player.Definition.ObjectId,
player.GUID, // target_guid - same as player for self
playerData,
None // no parent (not in vehicle)
)
)
}
/**
* Despawn a bot from a zone (graceful logout)
*/
def despawnBot(zone: Zone, player: Player): Unit = {
// 1. Stop BotActor
// 2. Notify zone population
zone.Population ! Zone.Population.Leave(player.avatar)
// 3. Broadcast player deletion
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
AvatarAction.ObjectDelete(player.GUID, player.GUID)
)
// 4. Unregister GUIDs
TaskWorkflow.execute(GUIDTask.unregisterPlayer(zone.GUID, player))
// 5. Remove from block map
zone.actor ! ZoneActor.RemoveFromBlockMap(player)
}
}
/**
* Simplified AvatarActor for bots
*
* The real AvatarActor handles:
* - DB persistence (saving stats, certs, etc.)
* - Character selection
* - Login/logout flow
*
* For bots, we don't need most of that. Just a stub actor
* that can handle the messages PlayerControl expects.
*/
class BotAvatarActor(avatar: Avatar) extends Actor {
// Minimal implementation - just accept messages and do nothing
def receive: Receive = {
case _ => // Ignore most messages - bots don't persist
}
}
object BotAvatarActor {
def props(avatar: Avatar): Props = Props(classOf[BotAvatarActor], avatar)
}
/**
* BotManager - Manages bot population across a zone
*/
class BotManager(zone: Zone) extends Actor {
import scala.concurrent.duration._
private val targetBotsPerFaction = 100
private var bots: Map[PlanetSideGUID, Player] = Map.empty
// Population check timer
context.system.scheduler.scheduleWithFixedDelay(
initialDelay = 5.seconds,
delay = 10.seconds,
receiver = self,
message = BotManager.CheckPopulation
)(context.dispatcher)
def receive: Receive = {
case BotManager.CheckPopulation =>
balancePopulation()
case BotManager.SpawnBot(faction, botClass, position) =>
val player = BotSpawner.spawnBot(zone, faction, botClass, position, context)
bots += (player.GUID -> player)
case BotManager.DespawnBot(guid) =>
bots.get(guid).foreach { player =>
BotSpawner.despawnBot(zone, player)
bots -= guid
}
case BotManager.DespawnAll =>
bots.values.foreach(player => BotSpawner.despawnBot(zone, player))
bots = Map.empty
}
private def balancePopulation(): Unit = {
PlanetSideEmpire.values.foreach { faction =>
if (faction != PlanetSideEmpire.NEUTRAL) {
val realPlayers = zone.LivePlayers.count(p =>
!isBotPlayer(p) && p.Faction == faction
)
val currentBots = bots.values.count(_.Faction == faction)
val targetBots = math.max(0, targetBotsPerFaction - realPlayers)
if (currentBots < targetBots) {
// Spawn more bots
val toSpawn = targetBots - currentBots
(0 until toSpawn).foreach { _ =>
val position = findSpawnPosition(faction)
val botClass = randomBotClass()
self ! BotManager.SpawnBot(faction, botClass, position)
}
} else if (currentBots > targetBots) {
// Despawn excess bots (non-Ace first)
val toRemove = currentBots - targetBots
val botsToRemove = bots.values
.filter(_.Faction == faction)
.toSeq
.sortBy(p => if (isAce(p)) 1 else 0) // Ace last
.take(toRemove)
botsToRemove.foreach(p => self ! BotManager.DespawnBot(p.GUID))
}
}
}
}
private def isBotPlayer(player: Player): Boolean = {
// Check if name starts with [BOT]
player.Name.startsWith("[BOT]")
}
private def isAce(player: Player): Boolean = {
player.Name.contains("Ace_")
}
private def findSpawnPosition(faction: PlanetSideEmpire.Value): Vector3 = {
// TODO: Find appropriate spawn point for faction
// Could use owned bases, warpgates, etc.
Vector3(100f, 100f, 10f) // Placeholder
}
private def randomBotClass(): BotClass = {
// Weighted random class selection
// More grunts than specialists
BotClass("Grunt", BotRole.Veteran, Set(), 10) // Placeholder
}
}
object BotManager {
case object CheckPopulation
case class SpawnBot(faction: PlanetSideEmpire.Value, botClass: BotClass, position: Vector3)
case class DespawnBot(guid: PlanetSideGUID)
case object DespawnAll
}