PSF-BotServer/bot-docs/ARCHITECTURE.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

9.3 KiB

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

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

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

// 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

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

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