pecs

package module
v0.0.0-...-7a83356 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 11, 2026 License: MIT Imports: 29 Imported by: 0

README

PECS

Player Event Component System for Dragonfly

PECS is a game architecture framework designed for Dragonfly Minecraft servers. It provides structured game logic through handlers, components, systems, and dependency injection while respecting Dragonfly's transaction-based world model.

Table of Contents


Installation

go get github.com/oriumgames/pecs

Quick Start

package main

import (
    "time"
    "github.com/df-mc/dragonfly/server"
    "github.com/df-mc/dragonfly/server/player"
    "github.com/oriumgames/pecs"
)

// Components are plain structs
type Health struct {
    Current int
    Max     int
}

// Handlers respond to player events
type DamageHandler struct {
    Session *pecs.Session
    Health  *Health `pecs:"mut"`
}

func (h *DamageHandler) HandleHurt(ev *pecs.EventHurt) {
    h.Health.Current -= int(*ev.Damage)
}

// Loops run at fixed intervals
type RegenLoop struct {
    Session *pecs.Session
    Health  *Health `pecs:"mut"`
}

func (l *RegenLoop) Run(tx *world.Tx) {
    if l.Health.Current < l.Health.Max {
        l.Health.Current++
    }
}

func main() {
    // Create a bundle for your game logic
    bundle := pecs.NewBundle("gameplay").
        Handler(&DamageHandler{}).
        Loop(&RegenLoop{}, time.Second, pecs.Default).
        Build()

    // Initialize PECS
    mngr := pecs.NewBuilder().
        Bundle(bundle).
        Init()

    // Start your Dragonfly server
    srv := server.New()
    srv.Listen()

    for p := range srv.Accept() {
        sess, err := mngr.NewSession(p)
        if err != nil {
            p.Disconnect("failed to initialize session")
            continue
        }

        pecs.Add(sess, &Health{Current: 20, Max: 20})
        p.Handle(pecs.NewHandler(sess, p))
    }
}

Core Concepts

Sessions

Sessions wrap Dragonfly players with persistent identity and component storage. They survive world transfers and provide thread-safe component access.

// Create session when player joins
sess := mngr.NewSession(p)

// Retrieve session later
sess := mngr.GetSession(p)
sess := mngr.GetSessionByUUID(uuid)
sess := mngr.GetSessionByName("PlayerName")
sess := mngr.GetSessionByID(xuid)

// Get persistent identifier (XUID) - used for cross-server references
id := sess.ID()

// Access player within transaction context
if p, ok := sess.Player(tx); ok {
    p.Message("Hello!")
}

// Execute code in player's world transaction
sess.Exec(func(tx *world.Tx, p *player.Player) {
    p.Heal(10, healing.SourceFood{})
})

// Check session state
if sess.Closed() {
    return
}

// Get the current world
world := sess.World()

// Access manager from session
m := sess.Manager()

Session Type Helpers:

// Check if session is a fake player (testing bot)
if sess.IsFake() {
    // Has FakeMarker component
}

// Check if session is an NPC entity
if sess.IsEntity() {
    // Has EntityMarker component
}

// Check if session is any kind of actor (fake or entity)
if sess.IsActor() {
    // Not a real player (no network session)
}
Fake Players & NPCs

PECS supports spawning fake players (bots) and NPC entities that participate in the component system just like real players.

Types:

Type Marker Federated ID Use Case
Real Player None XUID Human players
Fake Player FakeMarker Custom ID Testing bots, lobby filling
NPC Entity EntityMarker None AI entities, shopkeepers

Spawning Bots:

// Configuration for spawning
cfg := pecs.ActorConfig{
    Name:     "TestBot",
    Skin:     someSkin,
    Position: mgl64.Vec3{0, 64, 0},
    Yaw:      0,
    Pitch:    0,
}

// Spawn a fake player (can participate in Peer[T] lookups with fakeID)
p, sess := mngr.SpawnFake(tx, cfg, "fake-player-123")

// Spawn an NPC entity (local only, no federation)
p, sess := mngr.SpawnEntity(tx, cfg)

// Add components as usual
pecs.Add(sess, &Health{Current: 100, Max: 100})

Federated ID Resolution:

// Real players: ID() returns XUID
// Fake players: ID() returns FakeMarker.ID
// Entities: ID() returns "" (no federated ID)
id := sess.ID()

// Unified lookup (works for real and fake players)
sess := mngr.GetSessionByID(id)

Marker Components:

// FakeMarker and EntityMarker are empty struct markers
type FakeMarker struct{}
type EntityMarker struct{}

// Check with helper methods
if sess.IsFake() {
    fmt.Println("Fake player ID:", sess.ID())
}
Components

Components are plain Go structs that hold data. No interfaces required.

type Health struct {
    Current int
    Max     int
}

type Frozen struct {
    Until    time.Time
    FrozenBy string
}

type Inventory struct {
    Items []item.Stack
    Gold  int
}

Component Operations:

// Add or replace component
pecs.Add(sess, &Health{Current: 20, Max: 20})

// Check existence
if pecs.Has[Health](sess) {
    // Has health component
}

// Get component (nil if missing)
if health := pecs.Get[Health](sess); health != nil {
    health.Current -= 5
}

// Get or add with default value
health := pecs.GetOrAdd(sess, &Health{Current: 100, Max: 100})

// Remove component
pecs.Remove[Frozen](sess)

Lifecycle Hooks:

Components can implement Attachable and/or Detachable for lifecycle callbacks:

type Tracker struct {
    StartTime time.Time
}

func (t *Tracker) Attach(s *pecs.Session) {
    t.StartTime = time.Now()
    fmt.Println("Tracker attached to", s.Name())
}

func (t *Tracker) Detach(s *pecs.Session) {
    duration := time.Since(t.StartTime)
    fmt.Printf("Player %s tracked for %v\n", s.Name(), duration)
}

Temporary Components:

Components can be added with an expiration time. They are automatically removed when the time passes.

// Add component that expires after duration
pecs.AddFor(sess, &SpeedBoost{Multiplier: 2.0}, 10*time.Second)

// Add component that expires at specific time
pecs.AddUntil(sess, &EventBuff{Bonus: 50}, eventEndTime)

// Check remaining time
remaining := pecs.ExpiresIn[SpeedBoost](sess)
if remaining > 0 {
    fmt.Printf("Speed boost expires in %v\n", remaining)
}

// Get exact expiration time
expireTime := pecs.ExpiresAt[SpeedBoost](sess)

// Check if expired (but not yet removed)
if pecs.Expired[SpeedBoost](sess) {
    // Will be removed next scheduler tick
}

Temporary components respect lifecycle hooks - Detach is called when the component expires.

Systems

Systems contain game logic and declare dependencies via struct tags. PECS automatically injects the required data before execution.

There are three types of systems:

  • Handlers - React to player events
  • Loops - Run at fixed intervals
  • Tasks - One-shot delayed execution

All systems share the same dependency injection model.

Handlers

Handlers respond to events emitted through PECS. They receive dependency injection just like loops and tasks.

type DamageHandler struct {
    Session *pecs.Session
    Health  *Health  `pecs:"mut"`
    GodMode *GodMode `pecs:"opt"`
}

func (h *DamageHandler) HandleHurt(ev *pecs.EventHurt) {
    if h.GodMode != nil {
        *ev.Damage = 0
        return
    }
    h.Health.Current -= int(*ev.Damage)
    if h.Health.Current <= 0 {
        ev.Ctx.Val().Kill(ev.Source)
    }
}

func (h *DamageHandler) HandleDeath(ev *pecs.EventDeath) {
    ev.Player.Message("You died!")
}

// Register with bundle
bundle.Handler(&DamageHandler{})

Built-in Events:

PECS wraps all Dragonfly player events as pooled event types. See event.go for the complete list including EventMove, EventHurt, EventDeath, EventChat, EventQuit, and more.

Custom Events:

Handlers also support custom event types. See Events for details on defining and emitting your own events.

Execution:

Handlers execute synchronously in registration order. All matching handlers complete before Emit() returns.

Global Handlers:

Handlers without a *pecs.Session field or session-scoped components are global handlers. They run once per event instead of once per session. This is useful for logging, analytics, anti-cheat, or any cross-cutting concern that doesn't need per-session state.

// Global handler - runs once per event, not per-session
type ChatLogger struct {
    Manager *pecs.Manager
    Config  *ServerConfig `pecs:"res"`
}

func (h *ChatLogger) HandleChat(ev *pecs.EventChat) {
    // Runs once when any player chats
    log.Printf("[CHAT] %s", *ev.Message)
}

// Session-scoped handler - runs for each matching session
type ChatFilter struct {
    Session *pecs.Session  // Having Session makes this session-scoped
    Muted   *MutedPlayer
}

func (h *ChatFilter) HandleChat(ev *pecs.EventChat) {
    // Runs for the player who sent the message
    ev.Cancel()
}

When using Manager.Emit(), global handlers are invoked first, then session-scoped handlers for each session. Use Manager.EmitGlobal() to invoke only global handlers.

Loops

Loops run at fixed intervals for all sessions that match their component requirements. Loops without a *pecs.Session field or session components are global and run once per interval instead of per-session.

type RegenLoop struct {
    Session *pecs.Session
    Health  *Health `pecs:"mut"`
    Config  *Config `pecs:"res"`
    
    _ pecs.Without[Combat]    // Don't regen while in combat
    _ pecs.Without[Spectator] // Spectators don't regen
}

func (l *RegenLoop) Run(tx *world.Tx) {
    if l.Health.Current < l.Health.Max {
        l.Health.Current += l.Config.RegenRate
        if l.Health.Current > l.Health.Max {
            l.Health.Current = l.Health.Max
        }
    }
}

// Register: runs every second in Default stage
bundle.Loop(&RegenLoop{}, time.Second, pecs.Default)

// Run every tick (interval of 0)
bundle.Loop(&TickLoop{}, 0, pecs.Before)

// Global loop (no session field or component) - runs once per interval
type WorldCleanupLoop struct {
    Manager *pecs.Manager
    Config  *ServerConfig `pecs:"res"`
}

func (l *WorldCleanupLoop) Run(tx *world.Tx) {
    for _, sess := range l.Manager.AllSessions() {
        // Clean up expired data...
    }
}

bundle.Loop(&WorldCleanupLoop{}, time.Minute, pecs.After)
Tasks

Tasks are one-shot systems scheduled for future execution. Tasks without a *pecs.Session field or session components are global and can be scheduled with ScheduleGlobal or DispatchGlobal.

type TeleportTask struct {
    Session *pecs.Session
    
    // Payload fields - set when scheduling
    Destination mgl64.Vec3
    Message     string
}

func (t *TeleportTask) Run(tx *world.Tx) {
    if p, ok := t.Session.Player(tx); ok {
        p.Teleport(t.Destination)
        p.Message(t.Message)
    }
}

// Register task type with bundle (enables pooling optimization)
bundle.Task(&TeleportTask{}, pecs.Default)

// Global task (no session field or component)
type ServerAnnouncementTask struct {
    Manager *pecs.Manager
    Message string
}

func (t *ServerAnnouncementTask) Run(tx *world.Tx) {
    for _, s := range t.Manager.AllSessions() {
        p, _ := s.Player(tx)
        p.Message(t.Message)
    }
}

Scheduling Tasks:

// Schedule for future execution
handle := pecs.Schedule(sess, &TeleportTask{
    Destination: mgl64.Vec3{0, 100, 0},
    Message:     "Welcome to spawn!",
}, 5*time.Second)

// Cancel if needed
handle.Cancel()

// Immediate dispatch (next tick)
pecs.Dispatch(sess, &SomeTask{})

// Schedule at specific time
pecs.ScheduleAt(sess, &DailyRewardTask{}, midnight)

// Repeating task (runs 5 times, every second)
repeatHandle := pecs.ScheduleRepeating(sess, &TickTask{}, time.Second, 5)

// Infinite repeating until cancelled
repeatHandle := pecs.ScheduleRepeating(sess, &HeartbeatTask{}, time.Second, -1)
repeatHandle.Cancel()

// Global tasks (not tied to any session)
pecs.ScheduleGlobal(mngr, &ServerAnnouncementTask{Message: "Restarting!"}, 5*time.Minute)
pecs.DispatchGlobal(mngr, &SomeGlobalTask{})

Multi-Session Tasks:

Tasks can involve two sessions (must be in the same world):

type TradeTask struct {
    Session  *pecs.Session  // First player (buyer)
    Buyer    *Inventory `pecs:"mut"`
    
    Session2 *pecs.Session  // Second player (seller)
    Seller   *Inventory `pecs:"mut"`
    
    // Payload
    Item  item.Stack
    Price int
}

func (t *TradeTask) Run(tx *world.Tx) {
    // Both sessions guaranteed to be valid
    t.Seller.Items = append(t.Seller.Items, t.Item)
    t.Seller.Gold += t.Price
    t.Buyer.Gold -= t.Price
}

// Schedule multi-session task
pecs.Schedule2(buyer, seller, &TradeTask{Item: sword, Price: 100}, time.Second)

Dependency Injection

Tag Reference
Tag Description Example
(none) Required read-only component Health *Health
pecs:"mut" Required mutable component Health *Health \pecs:"mut"``
pecs:"opt" Optional component (nil if missing) Shield *Shield \pecs:"opt"``
pecs:"opt,mut" Optional mutable component Buff *Buff \pecs:"opt,mut"``
pecs:"rel" Relation traversal Target *Health \pecs:"rel"``
pecs:"res" Resource Config *Config \pecs:"res"``
pecs:"res,mut" Mutable resource State *State \pecs:"res,mut"``
pecs:"peer" Peer data resolution Friend *Profile \pecs:"peer"``
pecs:"shared" Shared entity resolution Party *PartyInfo \pecs:"shared"``

Special Fields (auto-injected):

Field Description
Session *pecs.Session Current session
Manager *pecs.Manager Manager instance
Phantom Types

Use phantom types to filter which sessions a system runs on:

type CombatLoop struct {
    Session *pecs.Session
    Health  *Health `pecs:"mut"`
    
    _ pecs.With[InCombat]     // Only run if InCombat component exists
    _ pecs.Without[Spectator] // Skip if Spectator component exists
    _ pecs.Without[Dead]      // Skip if Dead component exists
}
Resources

Resources are global singletons available to all systems across all bundles. Register them with either the builder or a bundle:

type GameConfig struct {
    MaxPartySize  int
    RegenInterval time.Duration
    SpawnPoint    mgl64.Vec3
}

type Database struct {
    conn *sql.DB
}

type Logger struct {
    prefix string
}

// Register globally with builder
mngr := pecs.NewBuilder().
    Resource(&Database{conn: db}).
    Resource(&Logger{prefix: "[PECS]"}).
    Bundle(gameBundle).
    Init()

// Or register with bundle (still globally accessible)
bundle.Resource(&GameConfig{
    MaxPartySize:  5,
    RegenInterval: time.Second,
    SpawnPoint:    mgl64.Vec3{0, 64, 0},
})

// Access in any system
type SaveHandler struct {
    Session *pecs.Session
    DB      *Database   `pecs:"res"`
    Logger  *Logger     `pecs:"res"`
    Config  *GameConfig `pecs:"res"`
}

func (h *SaveHandler) HandleQuit(ev *pecs.EventQuit) {
    h.Logger.Log("Player", ev.Player.Name(), "disconnecting")
    h.DB.SavePlayer(h.Session)
}

func (h *SaveHandler) HandleRespawn(ev *pecs.EventRespawn) {
    *ev.Position = h.Config.SpawnPoint
}

Programmatic Access:

// From session
config := pecs.Resource[GameConfig](sess)

// From manager
db := pecs.ManagerResource[Database](mngr)

Relations

Relations create type-safe links between sessions. PECS automatically cleans up relations when sessions disconnect.

Local Relations

Use Relation[T] and RelationSet[T] for references between players on the same server:

// Single reference
type Following struct {
    Target pecs.Relation[Position]
}

// Multiple references
type PartyLeader struct {
    Name    string
    Members pecs.RelationSet[PartyMember]
}

type PartyMember struct {
    JoinedAt time.Time
    Leader   pecs.Relation[PartyLeader]
}

Using Relations:

// Set a relation
member := &PartyMember{JoinedAt: time.Now()}
member.Leader.Set(leaderSession)
pecs.Add(memberSession, member)

// Get target session
if targetSess := member.Leader.Get(); targetSess != nil {
    // Access target session
}

// Check validity
if member.Leader.Valid() {
    // Target exists and has the required component
}

// Clear relation
member.Leader.Clear()

Using RelationSets:

leader := pecs.Get[PartyLeader](leaderSession)

// Add member
leader.Members.Add(memberSession)

// Remove member
leader.Members.Remove(memberSession)

// Check membership
if leader.Members.Has(memberSession) {
    // Is a member
}

// Get count
count := leader.Members.Len()

// Get all non-closed sessions
for _, memberSess := range leader.Members.All() {
    // Process each member session
}

// Resolve all valid members with their components
for _, resolved := range leader.Members.Resolve() {
    sess := resolved.Session     // *Session
    member := resolved.Component // *PartyMember
    // Process member data
}

// Clear all
leader.Members.Clear()
Relation Resolution

Use the pecs:"rel" tag to automatically resolve relations in systems:

type PartyHealLoop struct {
    Session *pecs.Session
    Member  *PartyMember
    Leader  *PartyLeader `pecs:"rel"` // Resolved from Member.Leader
}

func (l *PartyHealLoop) Run(tx *world.Tx) {
    if l.Leader != nil {
        // Access leader's data
        fmt.Println("Leader:", l.Leader.Name)
    }
}

For relation sets, inject a slice:

type PartyBuffLoop struct {
    Session  *pecs.Session
    Leader   *PartyLeader
    Members  []*PartyMember `pecs:"rel"` // Resolved from Leader.Members
}

func (l *PartyBuffLoop) Run(tx *world.Tx) {
    for _, member := range l.Members {
        // Apply buff to each member
    }
}

Manual Resolution:

// Resolve relation to get session and component
if sess, comp, ok := member.Leader.Resolve(); ok {
    fmt.Println("Leader name:", comp.Name)
}

Federation

Federation enables cross-server data access. When players can be on different servers (in a network), you need a way to access their data regardless of which server they're on.

Federation Overview

PECS provides two reference types for cross-server data:

Type Purpose Target Example
Peer[T] Reference another player's data Player (by ID) Friend, party member
Shared[T] Reference shared entity data Entity (by ID) Party, guild, match

Data is fetched via Providers - interfaces you implement to connect PECS to your backend services.

Peer References

Peer[T] references another player's component data. Works whether the player is local or remote.

// Component with peer reference
type FriendsList struct {
    BestFriend Peer[FriendProfile]   // Single friend
    Friends    PeerSet[FriendProfile] // Multiple friends
}

type FriendProfile struct {
    Username string
    Online   bool
    Server   string
}

Using Peer:

// Set peer by player ID (e.g., XUID)
friends := &FriendsList{}
friends.BestFriend.Set("player-123-xuid")
pecs.Add(sess, friends)

// Get ID
id := friends.BestFriend.ID()

// Check if set
if friends.BestFriend.IsSet() {
    // Has a best friend set
}

// Clear
friends.BestFriend.Clear()

// Manual resolution (useful in commands/forms)
if profile, ok := friends.BestFriend.Resolve(sess.Manager()); ok {
    fmt.Println("Best friend:", profile.Username)
}

Using PeerSet:

// Set all IDs
friends.Friends.Set([]string{"player-1", "player-2", "player-3"})

// Add single
friends.Friends.Add("player-4")

// Remove
friends.Friends.Remove("player-2")

// Get all IDs
ids := friends.Friends.IDs()

// Get count
count := friends.Friends.Len()

// Clear all
friends.Friends.Clear()

// Manual resolution (useful in commands/forms)
profiles := friends.Friends.Resolve(sess.Manager())
for _, profile := range profiles {
    fmt.Println("Friend:", profile.Username)
}

Resolving Peer Data in Systems:

type FriendsDisplayLoop struct {
    Session     *pecs.Session
    FriendsList *FriendsList
    
    // PECS resolves these automatically via providers
    BestFriend  *FriendProfile   `pecs:"peer"` // From FriendsList.BestFriend
    AllFriends  []*FriendProfile `pecs:"peer"` // From FriendsList.Friends
}

func (l *FriendsDisplayLoop) Run(tx *world.Tx) {
    p, _ := l.Session.Player(tx)
    
    if l.BestFriend != nil {
        status := "offline"
        if l.BestFriend.Online {
            status = "online on " + l.BestFriend.Server
        }
        p.Message("Best friend: " + l.BestFriend.Username + " (" + status + ")")
    }
    
    for _, friend := range l.AllFriends {
        // Display friend info
    }
}
Shared References

Shared[T] references shared entities (parties, guilds, matches) that aren't tied to a specific player.

// Component with shared reference
type MatchmakingData struct {
    CurrentParty Shared[PartyInfo]
    ActiveMatch  Shared[MatchInfo]
}

type PartyInfo struct {
    ID       string
    LeaderID string
    Members  []PartyMemberInfo
    Open     bool
}

type PartyMemberInfo struct {
    ID       string
    Username string
}

Using Shared:

mmData := &MatchmakingData{}
mmData.CurrentParty.Set("party-456")
pecs.Add(sess, mmData)

// Same API as Peer
id := mmData.CurrentParty.ID()
mmData.CurrentParty.Clear()

// Manual resolution (useful in commands/forms)
if party, ok := mmData.CurrentParty.Resolve(sess.Manager()); ok {
    fmt.Println("Party:", party.ID, "Members:", len(party.Members))
}

Using SharedSet:

type GuildData struct {
    ActiveWars SharedSet[WarInfo]
}

// Same API as PeerSet
guildData.ActiveWars.Set([]string{"war-1", "war-2"})
guildData.ActiveWars.Add("war-3")

// Manual resolution
wars := guildData.ActiveWars.Resolve(sess.Manager())
for _, war := range wars {
    fmt.Println("War:", war.ID)
}

Resolving Shared Data in Systems:

type PartyDisplayHandler struct {
    Session *pecs.Session
    MMData  *MatchmakingData
    Party   *PartyInfo `pecs:"shared"` // Resolved from MMData.CurrentParty
}

func (h *PartyDisplayHandler) HandleJoin(ev *pecs.EventJoin) {
    if h.Party != nil {
        ev.Player.Message("You're in party: " + h.Party.ID)
        ev.Player.Message("Leader: " + h.Party.LeaderID)
        ev.Player.Message("Members: " + strconv.Itoa(len(h.Party.Members)))
    }
}
Providers

Providers fetch and sync data from your backend services. Implement PeerProvider for peer data and SharedProvider for shared data.

PeerProvider:

When a player joins, PECS automatically queries all registered `PeerProvider`s. If data is returned,
components are added to the session and kept in sync via subscriptions. This removes the need for
manual data fetching handlers.

You can mark a provider as required using `pecs.WithRequired(true)`. If a required provider fails to
fetch data during session creation, `NewSession` will return an error.

type PeerProvider interface {
    // Unique name for logging
    Name() string
    
    // Component types this provider handles
    PlayerComponents() []reflect.Type
    
    // Fetch single player's components
    FetchPlayer(ctx context.Context, playerID string) ([]any, error)
    
    // Batch fetch multiple players
    FetchPlayers(ctx context.Context, playerIDs []string) (map[string][]any, error)
    
    // Subscribe to real-time updates
    SubscribePlayer(ctx context.Context, playerID string, updates chan<- PlayerUpdate) (Subscription, error)
}

Example PeerProvider Implementation:

type StatusProvider struct {
    statusClient statusv1.StatusServiceClient
    nats         *nats.Conn
}

func (p *StatusProvider) Name() string {
    return "status"
}

func (p *StatusProvider) PlayerComponents() []reflect.Type {
    return []reflect.Type{reflect.TypeOf(FriendProfile{})}
}

func (p *StatusProvider) FetchPlayer(ctx context.Context, playerID string) ([]any, error) {
    resp, err := p.statusClient.GetStatus(ctx, &statusv1.GetStatusRequest{PlayerId: playerID})
    if err != nil {
        return nil, err
    }
    
    return []any{
        &FriendProfile{
            Username: resp.Username,
            Online:   resp.Online,
            Server:   resp.GetServerId(),
        },
    }, nil
}

func (p *StatusProvider) FetchPlayers(ctx context.Context, playerIDs []string) (map[string][]any, error) {
    resp, err := p.statusClient.GetStatuses(ctx, &statusv1.GetStatusesRequest{PlayerIds: playerIDs})
    if err != nil {
        return nil, err
    }
    
    result := make(map[string][]any)
    for id, status := range resp.Statuses {
        result[id] = []any{
            &FriendProfile{
                Username: status.Username,
                Online:   status.Online,
                Server:   status.GetServerId(),
            },
        }
    }
    return result, nil
}

func (p *StatusProvider) SubscribePlayer(ctx context.Context, playerID string, updates chan<- pecs.PlayerUpdate) (pecs.Subscription, error) {
    sub, err := p.nats.Subscribe("status.player."+playerID, func(msg *nats.Msg) {
        // Parse event and send update
        var event statusv1.StatusChanged
        proto.Unmarshal(msg.Data, &event)
        
        updates <- pecs.PlayerUpdate{
            ComponentType: reflect.TypeOf(FriendProfile{}),
            Data: &FriendProfile{
                Username: event.Username,
                Online:   event.Online,
                Server:   event.ServerId,
            },
        }
    })
    if err != nil {
        return nil, err
    }
    
    return &natsSubscription{sub}, nil
}

SharedProvider:

type SharedProvider interface {
    Name() string
    EntityComponents() []reflect.Type
    FetchEntity(ctx context.Context, entityID string) (any, error)
    FetchEntities(ctx context.Context, entityIDs []string) (map[string]any, error)
    SubscribeEntity(ctx context.Context, entityID string, updates chan<- any) (Subscription, error)
}

Registering Providers:

mngr := pecs.NewBuilder().
    Bundle(gameBundle).
    PeerProvider(&StatusProvider{...}).
    PeerProvider(&ProfileProvider{...}).
    SharedProvider(&PartyProvider{...}).
    SharedProvider(&MatchProvider{...}).
    Init()

// Or with options
mngr := pecs.NewBuilder().
    PeerProvider(&StatusProvider{...}, 
        pecs.WithFetchTimeout(2*time.Second),
        pecs.WithGracePeriod(time.Minute),
        pecs.WithStaleTimeout(5*time.Minute),
    ).
    Init()
When to Use Each Type
Type Use When Example
Relation[T] Target must be on same server Combat target, follow target
Peer[T] Target is a player (local or remote) Friend, party member, enemy
Shared[T] Target is a shared entity (not a player) Party, guild, match, server

Decision Flow:

Is the target a player?
├── YES: Could they be on a different server?
│   ├── YES → Peer[T]
│   └── NO (guaranteed same server) → Relation[T]
└── NO (party, guild, match, etc.) → Shared[T]

Events

Emit custom events to handler systems.

Define Events:

type DamageEvent struct {
    Amount int
    Source world.DamageSource
}

type LevelUpEvent struct {
    NewLevel int
    OldLevel int
}

Handle Events:

type NotificationHandler struct {
    Session *pecs.Session
}

// Method names don't matter - matching is done by event type
func (h *NotificationHandler) HandleDamage(e *DamageEvent) {
    if p, ok := h.Session.Player(nil); ok {
        p.Message(fmt.Sprintf("Took %d damage!", e.Amount))
    }
}

func (h *NotificationHandler) HandleLevelUp(e *LevelUpEvent) {
    if p, ok := h.Session.Player(nil); ok {
        p.Message(fmt.Sprintf("Level up! %d -> %d", e.OldLevel, e.NewLevel))
    }
}

Emit Events:

// To single session
sess.Emit(&DamageEvent{Amount: 5, Source: src})

// To all sessions
mngr.Emit(&LevelUpEvent{NewLevel: 10, OldLevel: 9})

// To all except some
mngr.EmitExcept(&ChatEvent{Message: "Hello"}, sender)

Built-in Events:

// Emitted when a component is added
type ComponentAttachEvent struct {
    ComponentType reflect.Type
}

// Emitted when a component is removed
type ComponentDetachEvent struct {
    ComponentType reflect.Type
}

Commands & Forms

Helper functions for Dragonfly commands and forms:

type HealCommand struct {
    Amount int `cmd:"amount"`
}

func (c HealCommand) Run(src cmd.Source, out *cmd.Output, tx *world.Tx) {
    p, sess := pecs.Command(src)
    if sess == nil {
        out.Error("Player-only command")
        return
    }
    
    health := pecs.Get[Health](sess)
    if health == nil {
        out.Error("You don't have health!")
        return
    }
    
    health.Current = min(health.Current+c.Amount, health.Max)
    out.Printf("Healed %d HP!", c.Amount)
}

// Register command with bundle
bundle.Command(cmd.New("heal", "Heal yourself", nil, HealCommand{}))

Forms:

type SettingsForm struct {
    EnableNotifications bool
    Volume              int
}

func (f SettingsForm) Submit(sub form.Submitter, tx *world.Tx) {
    p, sess := pecs.Form(sub)
    if sess == nil {
        return
    }
    
    settings := pecs.GetOrAdd(sess, &Settings{})
    settings.Notifications = f.EnableNotifications
    settings.Volume = f.Volume
}

Bundle Organization

Structure your game with multiple bundles. Bundle names are used in panic/error messages for easier debugging.

func main() {
    core := pecs.NewBundle("core").
        Resource(&ServerConfig{}).
        Handler(&JoinHandler{}).
        Handler(&QuitHandler{}).
        Loop(&AutoSaveLoop{}, time.Minute, pecs.After).
        Build()

    combat := pecs.NewBundle("combat").
        Resource(&CombatConfig{}).
        Handler(&DamageHandler{}).
        Handler(&DeathHandler{}).
        Loop(&CombatTagLoop{}, time.Second, pecs.Default).
        Task(&RespawnTask{}, pecs.Default).
        Build()

    party := pecs.NewBundle("party").
        Resource(&PartyConfig{}).
        Handler(&PartyInviteHandler{}).
        Handler(&PartyChatHandler{}).
        Command(cmd.New("party", "Party commands", nil, PartyCommand{})).
        Build()

    economy := pecs.NewBundle("economy").
        Resource(&EconomyConfig{}).
        Handler(&ShopHandler{}).
        Command(cmd.New("balance", "Check balance", nil, BalanceCommand{})).
        Command(cmd.New("pay", "Pay another player", nil, PayCommand{})).
        Build()

    mngr := pecs.NewBuilder().
        Resource(&Database{}).
        Resource(&Logger{}).
        Bundle(core).
        Bundle(combat).
        Bundle(party).
        Bundle(economy).
        PeerProvider(&StatusProvider{}).
        SharedProvider(&PartyProvider{}).
        Init()
}

Execution Model

Stages

Control execution order with three stages:

const (
    pecs.Before  // Runs first - input handling, pre-processing
    pecs.Default // Runs second - main game logic
    pecs.After   // Runs last - cleanup, synchronization, rendering
)

bundle.Loop(&InputHandler{}, 0, pecs.Before)
bundle.Loop(&GameLogic{}, 0, pecs.Default)
bundle.Loop(&NetworkSync{}, 0, pecs.After)
Parallelism

PECS automatically parallelizes non-conflicting systems:

  • Systems in different stages run sequentially (Before → Default → After)
  • Systems in the same stage that access different components run in parallel
  • Systems that write to the same component type are serialized

The scheduler analyzes component access patterns via tags:

  • Read access (Health *Health) doesn't conflict with other reads
  • Write access (Health *Health \pecs:"mut"``) conflicts with any other access to that component
Transaction Context

All systems receive a *world.Tx parameter. Your session's player is guaranteed to be valid in this transaction.

func (l *MyLoop) Run(tx *world.Tx) {
    // Your session's player - always valid
    p, _ := l.Session.Player(tx)
    p.Message("Hello!")
    
    // Other players via relations - check these
    for _, memberSess := range l.Members {
        if member, ok := memberSess.Player(tx); ok {
            member.Message("Party message")
        }
    }
}

Concurrency

Context Safe Operations
Handlers Read/write components directly
Loops Read/write components directly
Tasks Read/write components directly
Commands Read/write components directly
Forms Read/write components directly
External goroutines Must use sess.Exec()

Critical Rule: Never call sess.Exec() from within a handler, loop, task, command, or form when targeting a session in the same world. This causes deadlock. Use the existing transaction instead.

// WRONG - potential deadlock
func (h *MyHandler) HandleChat(ev *pecs.EventChat) {
    otherSess.Exec(func(tx *world.Tx, p *player.Player) { // DEADLOCK!
        p.Message(*ev.Message)
    })
}

// CORRECT - use existing transaction context
func (h *MyHandler) HandleChat(ev *pecs.EventChat) {
    if other, ok := otherSess.Player(ev.Ctx.Val().Tx()); ok {
        other.Message(*ev.Message)
    }
}

API Reference

Manager
// Session management
mngr.NewSession(p *player.Player) (*Session, error)
mngr.GetSession(p *player.Player) *Session
mngr.GetSessionByUUID(id uuid.UUID) *Session
mngr.GetSessionByName(name string) *Session
mngr.GetSessionByID(id string) *Session
mngr.GetSessionByHandle(h *world.EntityHandle) *Session
mngr.AllSessions() []*Session
mngr.AllSessionsInWorld(w *world.World) []*Session
mngr.SessionCount() int

// Events
mngr.Emit(event any)
mngr.EmitExcept(event any, exclude ...*Session)
mngr.EmitGlobal(event any)

// Federation
mngr.RegisterPeerProvider(p PeerProvider, opts ...ProviderOption)
mngr.RegisterSharedProvider(p SharedProvider, opts ...ProviderOption)

// Lifecycle
mngr.Start()
mngr.Shutdown()
mngr.TickNumber() uint64

// Spawn
mngr.SpawnFake(tx *world.Tx, cfg ActorConfig, fakeID string) (*player.Player, *Session)
mngr.SpawnEntity(tx *world.Tx, cfg ActorConfig) (*player.Player, *Session)
Session
sess.Handle() *world.EntityHandle
sess.UUID() uuid.UUID
sess.Name() string
sess.XUID() string
sess.ID() string
sess.Player(tx *world.Tx) (*player.Player, bool)
sess.Exec(fn func(tx *world.Tx, p *player.Player)) bool
sess.World() *world.World
sess.Manager() *Manager
sess.Closed() bool
sess.Mask() Bitmask

// Type checks
sess.IsFake() bool
sess.IsEntity() bool
sess.IsActor() bool

// Events
sess.Emit(event any)
Components
pecs.Add[T any](s *Session, component *T)
pecs.AddFor[T any](s *Session, component *T, duration time.Duration)
pecs.AddUntil[T any](s *Session, component *T, expireAt time.Time)
pecs.Remove[T any](s *Session)
pecs.Get[T any](s *Session) *T
pecs.GetOrAdd[T any](s *Session, defaultVal *T) *T
pecs.Has[T any](s *Session) bool
pecs.ExpiresIn[T any](s *Session) time.Duration
pecs.ExpiresAt[T any](s *Session) time.Time
pecs.Expired[T any](s *Session) bool
Tasks
pecs.Schedule(s *Session, task Runnable, delay time.Duration) *TaskHandle
pecs.Schedule2(s1, s2 *Session, task Runnable, delay time.Duration) *TaskHandle
pecs.ScheduleAt(s *Session, task Runnable, at time.Time) *TaskHandle
pecs.ScheduleRepeating(s *Session, task Runnable, interval time.Duration, times int) *RepeatingTaskHandle
pecs.ScheduleGlobal(m *Manager, task Runnable, delay time.Duration) *TaskHandle
pecs.Dispatch(s *Session, task Runnable) *TaskHandle
pecs.Dispatch2(s1, s2 *Session, task Runnable) *TaskHandle
pecs.DispatchGlobal(m *Manager, task Runnable) *TaskHandle

handle.Cancel()
repeatHandle.Cancel()
Relations
// Relation[T]
relation.Set(target *Session)
relation.Get() *Session
relation.Clear()
relation.Valid() bool
relation.Resolve() (sess *Session, comp *T, ok bool)
relation.TargetType() reflect.Type

// RelationSet[T]
set.Add(target *Session)
set.Remove(target *Session)
set.Has(target *Session) bool
set.Clear()
set.Len() int
set.All() []*Session
set.Resolve() []Resolved[T]
set.TargetType() reflect.Type
Peer & Shared
// Peer[T]
peer.Set(playerID string)
peer.ID() string
peer.IsSet() bool
peer.Clear()
peer.Resolve(m *Manager) (*T, bool)
peer.TargetType() reflect.Type

// PeerSet[T]
peerSet.Set(playerIDs []string)
peerSet.Add(playerID string)
peerSet.Remove(playerID string)
peerSet.IDs() []string
peerSet.Len() int
peerSet.Clear()
peerSet.Resolve(m *Manager) []*T
peerSet.TargetType() reflect.Type

// Shared[T]
shared.Set(entityID string)
shared.ID() string
shared.IsSet() bool
shared.Clear()
shared.Resolve(m *Manager) (*T, bool)
shared.TargetType() reflect.Type

// SharedSet[T]
sharedSet.Set(entityIDs []string)
sharedSet.Add(entityID string)
sharedSet.Remove(entityID string)
sharedSet.IDs() []string
sharedSet.Len() int
sharedSet.Clear()
sharedSet.Resolve(m *Manager) []*T
sharedSet.TargetType() reflect.Type
Helpers
pecs.Command(src cmd.Source) (*player.Player, *Session)
pecs.Form(sub form.Submitter) (*player.Player, *Session)
pecs.Item(user item.User) (*player.Player, *Session)
pecs.NewHandler(sess *Session, p *player.Player) Handler
  • pecs.Resource[T](sess): Retrieve a global resource from a session.
  • pecs.ManagerResource[T](mngr): Retrieve a global resource from a manager.
Builder
pecs.NewBuilder() *Builder

builder.Bundle(callback func(*Manager) *Bundle) *Builder
builder.Resource(res any) *Builder
builder.Handler(h Handler) *Builder
builder.Loop(sys Runnable, interval time.Duration, stage Stage) *Builder
builder.Task(sys Runnable, stage Stage) *Builder
builder.Command(command cmd.Command) *Builder
builder.PeerProvider(p PeerProvider, opts ...ProviderOption) *Builder
builder.SharedProvider(p SharedProvider, opts ...ProviderOption) *Builder
builder.Init() *Manager
Bundle
pecs.NewBundle(name string) *Bundle

bundle.Name() string
bundle.Resource(res any) *Bundle
bundle.Handler(h Handler) *Bundle
bundle.Loop(sys Runnable, interval time.Duration, stage Stage) *Bundle
bundle.Task(sys Runnable, stage Stage) *Bundle
bundle.Command(command cmd.Command) *Bundle
bundle.Build() func(*Manager) *Bundle
Provider Options
pecs.WithFetchTimeout(d time.Duration) ProviderOption   // Default: 5s
pecs.WithGracePeriod(d time.Duration) ProviderOption    // Default: 30s
pecs.WithStaleTimeout(d time.Duration) ProviderOption   // Default: 5m

Acknowledgements

This work is inspired by andreashgk/peex.

Documentation

Overview

Package pecs provides a Player Entity Component System for Dragonfly servers.

PECS is an architectural layer built on top of Dragonfly that provides:

  • Session abstraction for persistent player identity
  • Component-based data storage per player
  • Declarative dependency injection via struct tags
  • Type-safe relations between sessions
  • Transaction-safe handlers, loops, and tasks
  • Multi-instance support for running multiple servers in one process

Quick Start

Initialize PECS in your server setup:

bundle := pecs.NewBundle("MyGame").
    Handler(&MyHandler{}).
    Loop(&MyLoop{}, time.Second, pecs.Default)

mngr := pecs.NewBuilder().
    Bundle(bundle).
    Init()

for p := range srv.Accept() {
    sess, err := mngr.NewSession(p)
    if err != nil {
        p.Disconnect("failed to initialize session")
        continue
    }
    pecs.Add(sess, &Health{Current: 100, Max: 100})
    p.Handle(pecs.NewHandler(sess, p))
}

Components

Components are plain Go structs attached to sessions:

type Health struct {
    Current int
    Max     int
}

pecs.Add(sess, &Health{100, 100})
health := pecs.Get[Health](sess)
pecs.Remove[Health](sess)

Systems

Systems declare dependencies via struct tags:

type MyHandler struct {
    Session *pecs.Session
    Manager *pecs.Manager        // Optional: for broadcasting, lookups
    Health  *Health              // Required
    Shield  *Shield `pecs:"opt"` // Optional
    Config  *Config `pecs:"res"` // Resource
    _ pecs.Without[Spectator]    // Skip if Spectator exists
}

func (h *MyHandler) HandleHurt(ev *pecs.EventHurt) {
    // Handle hurt event using ev.Damage, ev.Source, etc.
}

Tag Reference

(none)         Required read-only component
pecs:"mut"     Required mutable component
pecs:"opt"     Optional (nil if missing)
pecs:"opt,mut" Optional mutable
pecs:"rel"     Relation traversal
pecs:"res"     Resource
pecs:"res,mut" Mutable resource
pecs:"peer"    Peer[T] resolution (remote player data)
pecs:"shared"  Shared[T] resolution (shared entity data)

Index

Constants

View Source
const MaxComponents = 255

MaxComponents is the maximum number of component types supported per manager.

View Source
const Version = "1.0.0"

Version is the PECS version.

Variables

This section is empty.

Functions

func Add

func Add[T any](s *Session, component *T)

Add attaches a component to the session. If a component of this type already exists, it is replaced. If the component implements Attachable, its Attach method is called.

Concurrency: This function is thread-safe.

func AddFor

func AddFor[T any](s *Session, component *T, duration time.Duration)

AddFor attaches a component to the session that will be automatically removed after the specified duration. Useful for buffs, debuffs, and temporary effects.

If a component of this type already exists, it is replaced and the expiration is reset. If the component implements Attachable, its Attach method is called. When the component expires, if it implements Detachable, its Detach method is called.

Example:

pecs.AddFor(sess, &SpeedBoost{Multiplier: 2.0}, 10*time.Second)

func AddUntil

func AddUntil[T any](s *Session, component *T, expireAt time.Time)

AddUntil attaches a component to the session that will be automatically removed at the specified time. Useful for buffs, debuffs, and temporary effects.

If a component of this type already exists, it is replaced and the expiration is reset. If the component implements Attachable, its Attach method is called. When the component expires, if it implements Detachable, its Detach method is called.

Example:

pecs.AddUntil(sess, &EventBuff{}, eventEndTime)

func Expired

func Expired[T any](s *Session) bool

Expired returns true if the component has an expiration set AND that time has passed. Returns false if the component has no expiration or hasn't expired yet. Note: The component may still exist briefly after expiring until the next scheduler tick removes it.

Example:

if pecs.Expired[SpeedBoost](sess) {
    // Buff has expired (or will be removed very soon)
}

func ExpiresAt

func ExpiresAt[T any](s *Session) time.Time

ExpiresAt returns the time when a component will expire. Returns zero time if the component doesn't exist or has no expiration set.

Example:

expireTime := pecs.ExpiresAt[SpeedBoost](sess)
if !expireTime.IsZero() {
    fmt.Printf("Speed boost expires at %v\n", expireTime)
}

func ExpiresIn

func ExpiresIn[T any](s *Session) time.Duration

ExpiresIn returns the remaining duration until a component expires. Returns 0 if the component doesn't exist or has no expiration set. Returns negative duration if the component has already expired (but not yet removed).

Example:

remaining := pecs.ExpiresIn[SpeedBoost](sess)
if remaining > 0 {
    fmt.Printf("Speed boost expires in %v\n", remaining)
}

func Get

func Get[T any](s *Session) *T

Get retrieves a component from the session. Returns nil if the component is not present.

Concurrency: This function is thread-safe.

func GetOrAdd

func GetOrAdd[T any](s *Session, defaultVal *T) *T

GetOrAdd retrieves a component from the session, or adds the default if missing. Returns the existing component if present, otherwise adds defaultVal and returns it.

Concurrency: This function is thread-safe.

func Has

func Has[T any](s *Session) bool

Has checks if a component type is present on the session.

Concurrency: This function is fully thread-safe and can be called from any goroutine.

func ManagerResource

func ManagerResource[T any](m *Manager) *T

ManagerResource retrieves a global resource from the manager. Returns nil if the resource is not found.

func NewHandler

func NewHandler(s *Session, p *player.Player) player.Handler

NewHandler creates a new Dragonfly handler for the given session. This should be passed to player.Handle().

func Remove

func Remove[T any](s *Session)

Remove detaches a component from the session. If the component implements Detachable, its Detach method is called first.

Concurrency: This function is thread-safe.

func Resource

func Resource[T any](s *Session) *T

Resource retrieves a global resource via the session's manager. Returns nil if the session or resource is not found.

Types

type AccessMeta

type AccessMeta struct {
	Reads     []reflect.Type
	Writes    []reflect.Type
	ResReads  []reflect.Type
	ResWrites []reflect.Type
	// contains filtered or unexported fields
}

AccessMeta describes what components/resources a system reads or writes. Used for conflict detection and parallel scheduling.

func (*AccessMeta) Conflicts

func (a *AccessMeta) Conflicts(other *AccessMeta) bool

Conflicts returns true if this access pattern conflicts with another.

func (*AccessMeta) PrepareSets

func (a *AccessMeta) PrepareSets()

PrepareSets precomputes lookup sets from the slice fields for faster conflict checks.

type ActorConfig

type ActorConfig struct {
	// Identity & Core Settings
	Name     string
	Skin     skin.Skin
	GameMode world.GameMode

	// Position & Physics
	Position     mgl64.Vec3
	Velocity     mgl64.Vec3
	Rotation     cube.Rotation
	FallDistance float64

	// Vitals: Health
	Health    float64
	HealthMax float64

	// Vitals: Hunger
	Food       int
	FoodTick   int
	Saturation float64
	Exhaustion float64

	// Vitals: Breath
	AirSupply    int
	AirSupplyMax int

	// Inventory & Equipment
	Inventory  *inventory.Inventory
	EnderChest *inventory.Inventory
	OffHand    *inventory.Inventory
	Armour     *inventory.Armour
	HeldSlot   int

	// State, Effects & Progression
	Experience      int
	EnchantmentSeed int64
	FireTicks       int64
	Effects         []effect.Effect
}

ActorConfig configures the initial state of a fake player or NPC entity. It is used with Manager.SpawnFake and Manager.SpawnEntity.

type Attachable

type Attachable interface {
	Attach(s *Session)
}

Attachable is implemented by components that need initialization logic when attached to a session.

type Bitmask

type Bitmask [4]uint64

Bitmask is a 256-bit bitmask used for tracking component presence. It supports up to 256 unique component types.

func (Bitmask) And

func (m Bitmask) And(other Bitmask) Bitmask

And returns a new bitmask with only bits set in both m and other.

func (Bitmask) AndNot

func (m Bitmask) AndNot(other Bitmask) Bitmask

AndNot returns a new bitmask with bits set in m but not in other.

func (*Bitmask) Clear

func (m *Bitmask) Clear(id ComponentID)

Clear clears the bit at the given index.

func (Bitmask) Clone

func (m Bitmask) Clone() Bitmask

Clone returns a copy of the bitmask.

func (*Bitmask) ContainsAll

func (m *Bitmask) ContainsAll(other Bitmask) bool

ContainsAll returns true if all bits set in other are also set in m. This is used to check if all required components are present. Optimized for the common case where most systems require only a few components that fit in the first 64-bit segment.

func (*Bitmask) ContainsAny

func (m *Bitmask) ContainsAny(other Bitmask) bool

ContainsAny returns true if any bit set in other is also set in m. This is used to check if any excluded components (Without[T]) are present. Optimized similarly to ContainsAll for early exit.

func (*Bitmask) Count

func (m *Bitmask) Count() int

Count returns the number of bits set.

func (*Bitmask) Equals

func (m *Bitmask) Equals(other Bitmask) bool

Equals returns true if both bitmasks are identical.

func (*Bitmask) Has

func (m *Bitmask) Has(id ComponentID) bool

Has returns true if the bit at the given index is set.

func (*Bitmask) IsDisjoint

func (m *Bitmask) IsDisjoint(other Bitmask) bool

IsDisjoint returns true if no bits are set in both m and other.

func (*Bitmask) IsZero

func (m *Bitmask) IsZero() bool

IsZero returns true if no bits are set.

func (Bitmask) Or

func (m Bitmask) Or(other Bitmask) Bitmask

Or returns a new bitmask with bits set from both m and other.

func (*Bitmask) Set

func (m *Bitmask) Set(id ComponentID)

Set sets the bit at the given index.

type Builder

type Builder struct {
	// contains filtered or unexported fields
}

Builder configures PECS before initialization. Use NewBuilder() to create a builder and chain configuration methods.

func NewBuilder

func NewBuilder() *Builder

NewBuilder creates a new PECS builder.

func (*Builder) Bundle

func (b *Builder) Bundle(callback func(*Manager) *Bundle) *Builder

Bundle adds a bundle to the builder.

func (*Builder) Init

func (b *Builder) Init(ws ...*world.World) *Manager

Init initializes PECS with the configured settings. Returns the Manager instance which should be stored and used to create sessions. Multiple Manager instances can coexist for running multiple isolated servers.

func (*Builder) PeerProvider

func (b *Builder) PeerProvider(p PeerProvider, opts ...ProviderOption) *Builder

PeerProvider registers a provider for Peer[T] resolution. PeerProviders fetch and sync data for remote players.

Example:

builder.PeerProvider(&StatusProvider{...}, pecs.WithFetchTimeout(2000))

func (*Builder) Resource

func (b *Builder) Resource(res any) *Builder

Resource adds a global resource available to all bundles.

func (*Builder) SharedProvider

func (b *Builder) SharedProvider(p SharedProvider, opts ...ProviderOption) *Builder

SharedProvider registers a provider for Shared[T] resolution. SharedProviders fetch and sync data for shared entities (parties, matches, etc.).

Example:

builder.SharedProvider(&PartyProvider{...})

type Bundle

type Bundle struct {
	// contains filtered or unexported fields
}

Bundle groups related systems, handlers, and resources together. Bundles are registered with the PECS builder and provide isolation between different gameplay features.

func NewBundle

func NewBundle(name string) *Bundle

NewBundle creates a new bundle with the given name.

func (*Bundle) Build

func (b *Bundle) Build() func(*Manager) *Bundle

Build returns a callback function that returns this bundle. This allows for cleaner inline bundle initialization:

bund := pecs.NewBundle("gameplay").
    Handler(&ExampleHandler{}).
    Build()

mngr := pecs.NewBuilder().
    Bundle(bund).
    Init()

func (*Bundle) Command

func (b *Bundle) Command(command cmd.Command) *Bundle

Command registers a Dragonfly command for this bundle. Commands are automatically registered with Dragonfly's command system when the bundle is built.

func (*Bundle) Handler

func (b *Bundle) Handler(h any) *Bundle

Handler registers a handler for this bundle. Handlers are structs that implement event methods like HandleHurt(*EventHurt).

func (*Bundle) Loop

func (b *Bundle) Loop(sys Runnable, interval time.Duration, stage Stage) *Bundle

Loop registers a loop system that runs at fixed intervals. Interval of 0 means the loop runs every tick.

func (*Bundle) Name

func (b *Bundle) Name() string

Name returns the bundle name.

func (*Bundle) PeerProvider

func (b *Bundle) PeerProvider(p PeerProvider, opts ...ProviderOption) *Bundle

PeerProvider registers a provider for Peer[T] resolution.

func (*Bundle) PostInit

func (b *Bundle) PostInit(hook func(*Manager)) *Bundle

func (*Bundle) Resource

func (b *Bundle) Resource(res any) *Bundle

Resource registers a bundle-level resource. These are available to all systems via global manager.

func (*Bundle) SharedProvider

func (b *Bundle) SharedProvider(p SharedProvider, opts ...ProviderOption) *Bundle

SharedProvider registers a provider for Shared[T] resolution.

func (*Bundle) Task

func (b *Bundle) Task(sys Runnable, stage Stage) *Bundle

Task registers a task system type for pooling optimization. Tasks are one-shot systems scheduled dynamically.

type ComponentAttachEvent

type ComponentAttachEvent struct {
	ComponentType reflect.Type
}

ComponentAttachEvent is emitted when a component is added to a session.

type ComponentDetachEvent

type ComponentDetachEvent struct {
	ComponentType reflect.Type
}

ComponentDetachEvent is emitted when a component is removed from a session.

type ComponentID

type ComponentID uint8

ComponentID is a unique identifier for a component type. Valid IDs range from 0 to 255 per manager.

type Detachable

type Detachable interface {
	Detach(s *Session)
}

Detachable is implemented by components that need cleanup logic when detached from a session or when the session closes.

type EntityMarker

type EntityMarker struct{}

EntityMarker marks a session as an NPC entity. Entities do not participate in cross-server provider lookups.

type EventAttackEntity

type EventAttackEntity struct {
	Ctx      *player.Context
	Entity   world.Entity
	Force    *float64
	Height   *float64
	Critical *bool
}

EventAttackEntity is emitted when a player attacks an entity.

func (*EventAttackEntity) Cancel

func (e *EventAttackEntity) Cancel()

func (*EventAttackEntity) Tx

func (e *EventAttackEntity) Tx() *world.Tx

func (*EventAttackEntity) Val

func (e *EventAttackEntity) Val() *player.Player

type EventBlockBreak

type EventBlockBreak struct {
	Ctx        *player.Context
	Position   cube.Pos
	Drops      *[]item.Stack
	Experience *int
}

EventBlockBreak is emitted when a player breaks a block.

func (*EventBlockBreak) Cancel

func (e *EventBlockBreak) Cancel()

func (*EventBlockBreak) Tx

func (e *EventBlockBreak) Tx() *world.Tx

func (*EventBlockBreak) Val

func (e *EventBlockBreak) Val() *player.Player

type EventBlockPick

type EventBlockPick struct {
	Ctx      *player.Context
	Position cube.Pos
	Block    world.Block
}

EventBlockPick is emitted when a player picks a block.

func (*EventBlockPick) Cancel

func (e *EventBlockPick) Cancel()

func (*EventBlockPick) Tx

func (e *EventBlockPick) Tx() *world.Tx

func (*EventBlockPick) Val

func (e *EventBlockPick) Val() *player.Player

type EventBlockPlace

type EventBlockPlace struct {
	Ctx      *player.Context
	Position cube.Pos
	Block    world.Block
}

EventBlockPlace is emitted when a player places a block.

func (*EventBlockPlace) Cancel

func (e *EventBlockPlace) Cancel()

func (*EventBlockPlace) Tx

func (e *EventBlockPlace) Tx() *world.Tx

func (*EventBlockPlace) Val

func (e *EventBlockPlace) Val() *player.Player

type EventChangeWorld

type EventChangeWorld struct {
	Player *player.Player
	Before *world.World
	After  *world.World
}

EventChangeWorld is emitted when a player changes worlds.

type EventChat

type EventChat struct {
	Ctx     *player.Context
	Message *string
}

EventChat is emitted when a player sends a chat message.

func (*EventChat) Cancel

func (e *EventChat) Cancel()

func (*EventChat) Tx

func (e *EventChat) Tx() *world.Tx

func (*EventChat) Val

func (e *EventChat) Val() *player.Player

type EventCommandExecution

type EventCommandExecution struct {
	Ctx     *player.Context
	Command cmd.Command
	Args    []string
}

EventCommandExecution is emitted when a player executes a command.

func (*EventCommandExecution) Cancel

func (e *EventCommandExecution) Cancel()

func (*EventCommandExecution) Tx

func (e *EventCommandExecution) Tx() *world.Tx

func (*EventCommandExecution) Val

type EventDeath

type EventDeath struct {
	Player        *player.Player
	Source        world.DamageSource
	KeepInventory *bool
}

EventDeath is emitted when a player dies.

type EventDiagnostics

type EventDiagnostics struct {
	Player      *player.Player
	Diagnostics session.Diagnostics
}

EventDiagnostics is emitted for diagnostics data.

type EventExperienceGain

type EventExperienceGain struct {
	Ctx    *player.Context
	Amount *int
}

EventExperienceGain is emitted when a player gains experience.

func (*EventExperienceGain) Cancel

func (e *EventExperienceGain) Cancel()

func (*EventExperienceGain) Tx

func (e *EventExperienceGain) Tx() *world.Tx

func (*EventExperienceGain) Val

func (e *EventExperienceGain) Val() *player.Player

type EventFireExtinguish

type EventFireExtinguish struct {
	Ctx      *player.Context
	Position cube.Pos
}

EventFireExtinguish is emitted when a player extinguishes fire.

func (*EventFireExtinguish) Cancel

func (e *EventFireExtinguish) Cancel()

func (*EventFireExtinguish) Tx

func (e *EventFireExtinguish) Tx() *world.Tx

func (*EventFireExtinguish) Val

func (e *EventFireExtinguish) Val() *player.Player

type EventFoodLoss

type EventFoodLoss struct {
	Ctx  *player.Context
	From int
	To   *int
}

EventFoodLoss is emitted when a player loses food.

func (*EventFoodLoss) Cancel

func (e *EventFoodLoss) Cancel()

func (*EventFoodLoss) Tx

func (e *EventFoodLoss) Tx() *world.Tx

func (*EventFoodLoss) Val

func (e *EventFoodLoss) Val() *player.Player

type EventHeal

type EventHeal struct {
	Ctx    *player.Context
	Health *float64
	Source world.HealingSource
}

EventHeal is emitted when a player is healed.

func (*EventHeal) Cancel

func (e *EventHeal) Cancel()

func (*EventHeal) Tx

func (e *EventHeal) Tx() *world.Tx

func (*EventHeal) Val

func (e *EventHeal) Val() *player.Player

type EventHeldSlotChange

type EventHeldSlotChange struct {
	Ctx  *player.Context
	From int
	To   int
}

EventHeldSlotChange is emitted when a player changes their held slot.

func (*EventHeldSlotChange) Cancel

func (e *EventHeldSlotChange) Cancel()

func (*EventHeldSlotChange) Tx

func (e *EventHeldSlotChange) Tx() *world.Tx

func (*EventHeldSlotChange) Val

func (e *EventHeldSlotChange) Val() *player.Player

type EventHurt

type EventHurt struct {
	Ctx      *player.Context
	Damage   *float64
	Immune   bool
	Immunity *time.Duration
	Source   world.DamageSource
}

EventHurt is emitted when a player is hurt.

func (*EventHurt) Cancel

func (e *EventHurt) Cancel()

func (*EventHurt) Tx

func (e *EventHurt) Tx() *world.Tx

func (*EventHurt) Val

func (e *EventHurt) Val() *player.Player

type EventItemConsume

type EventItemConsume struct {
	Ctx  *player.Context
	Item item.Stack
}

EventItemConsume is emitted when a player consumes an item.

func (*EventItemConsume) Cancel

func (e *EventItemConsume) Cancel()

func (*EventItemConsume) Tx

func (e *EventItemConsume) Tx() *world.Tx

func (*EventItemConsume) Val

func (e *EventItemConsume) Val() *player.Player

type EventItemDamage

type EventItemDamage struct {
	Ctx    *player.Context
	Item   item.Stack
	Damage *int
}

EventItemDamage is emitted when an item takes damage.

func (*EventItemDamage) Cancel

func (e *EventItemDamage) Cancel()

func (*EventItemDamage) Tx

func (e *EventItemDamage) Tx() *world.Tx

func (*EventItemDamage) Val

func (e *EventItemDamage) Val() *player.Player

type EventItemDrop

type EventItemDrop struct {
	Ctx  *player.Context
	Item item.Stack
}

EventItemDrop is emitted when a player drops an item.

func (*EventItemDrop) Cancel

func (e *EventItemDrop) Cancel()

func (*EventItemDrop) Tx

func (e *EventItemDrop) Tx() *world.Tx

func (*EventItemDrop) Val

func (e *EventItemDrop) Val() *player.Player

type EventItemPickup

type EventItemPickup struct {
	Ctx  *player.Context
	Item *item.Stack
}

EventItemPickup is emitted when a player picks up an item.

func (*EventItemPickup) Cancel

func (e *EventItemPickup) Cancel()

func (*EventItemPickup) Tx

func (e *EventItemPickup) Tx() *world.Tx

func (*EventItemPickup) Val

func (e *EventItemPickup) Val() *player.Player

type EventItemRelease

type EventItemRelease struct {
	Ctx      *player.Context
	Item     item.Stack
	Duration time.Duration
}

EventItemRelease is emitted when a player releases a charged item.

func (*EventItemRelease) Cancel

func (e *EventItemRelease) Cancel()

func (*EventItemRelease) Tx

func (e *EventItemRelease) Tx() *world.Tx

func (*EventItemRelease) Val

func (e *EventItemRelease) Val() *player.Player

type EventItemUse

type EventItemUse struct {
	Ctx *player.Context
}

EventItemUse is emitted when a player uses an item.

func (*EventItemUse) Cancel

func (e *EventItemUse) Cancel()

func (*EventItemUse) Tx

func (e *EventItemUse) Tx() *world.Tx

func (*EventItemUse) Val

func (e *EventItemUse) Val() *player.Player

type EventItemUseOnBlock

type EventItemUseOnBlock struct {
	Ctx      *player.Context
	Position cube.Pos
	Face     cube.Face
	ClickPos mgl64.Vec3
}

EventItemUseOnBlock is emitted when a player uses an item on a block.

func (*EventItemUseOnBlock) Cancel

func (e *EventItemUseOnBlock) Cancel()

func (*EventItemUseOnBlock) Tx

func (e *EventItemUseOnBlock) Tx() *world.Tx

func (*EventItemUseOnBlock) Val

func (e *EventItemUseOnBlock) Val() *player.Player

type EventItemUseOnEntity

type EventItemUseOnEntity struct {
	Ctx    *player.Context
	Entity world.Entity
}

EventItemUseOnEntity is emitted when a player uses an item on an entity.

func (*EventItemUseOnEntity) Cancel

func (e *EventItemUseOnEntity) Cancel()

func (*EventItemUseOnEntity) Tx

func (e *EventItemUseOnEntity) Tx() *world.Tx

func (*EventItemUseOnEntity) Val

type EventJoin

type EventJoin struct {
	Player *player.Player
}

EventJoin is emitted when a player joins the server.

type EventJump

type EventJump struct {
	Player *player.Player
}

EventJump is emitted when a player jumps.

type EventLecternPageTurn

type EventLecternPageTurn struct {
	Ctx      *player.Context
	Position cube.Pos
	OldPage  int
	NewPage  *int
}

EventLecternPageTurn is emitted when a player turns a lectern page.

func (*EventLecternPageTurn) Cancel

func (e *EventLecternPageTurn) Cancel()

func (*EventLecternPageTurn) Tx

func (e *EventLecternPageTurn) Tx() *world.Tx

func (*EventLecternPageTurn) Val

type EventMove

type EventMove struct {
	Ctx      *player.Context
	Position mgl64.Vec3
	Rotation cube.Rotation
}

EventMove is emitted when a player moves.

func (*EventMove) Cancel

func (e *EventMove) Cancel()

func (*EventMove) Tx

func (e *EventMove) Tx() *world.Tx

func (*EventMove) Val

func (e *EventMove) Val() *player.Player

type EventPunchAir

type EventPunchAir struct {
	Ctx *player.Context
}

EventPunchAir is emitted when a player punches air.

func (*EventPunchAir) Cancel

func (e *EventPunchAir) Cancel()

func (*EventPunchAir) Tx

func (e *EventPunchAir) Tx() *world.Tx

func (*EventPunchAir) Val

func (e *EventPunchAir) Val() *player.Player

type EventQuit

type EventQuit struct {
	Player *player.Player
}

EventQuit is emitted when a player quits the server.

type EventRespawn

type EventRespawn struct {
	Player   *player.Player
	Position *mgl64.Vec3
	World    **world.World
}

EventRespawn is emitted when a player respawns.

type EventSignEdit

type EventSignEdit struct {
	Ctx       *player.Context
	Position  cube.Pos
	FrontSide bool
	OldText   string
	NewText   string
}

EventSignEdit is emitted when a player edits a sign.

func (*EventSignEdit) Cancel

func (e *EventSignEdit) Cancel()

func (*EventSignEdit) Tx

func (e *EventSignEdit) Tx() *world.Tx

func (*EventSignEdit) Val

func (e *EventSignEdit) Val() *player.Player

type EventSkinChange

type EventSkinChange struct {
	Ctx  *player.Context
	Skin *skin.Skin
}

EventSkinChange is emitted when a player changes their skin.

func (*EventSkinChange) Cancel

func (e *EventSkinChange) Cancel()

func (*EventSkinChange) Tx

func (e *EventSkinChange) Tx() *world.Tx

func (*EventSkinChange) Val

func (e *EventSkinChange) Val() *player.Player

type EventSleep

type EventSleep struct {
	Ctx          *player.Context
	SendReminder *bool
}

EventSleep is emitted when a player sleeps.

func (*EventSleep) Cancel

func (e *EventSleep) Cancel()

func (*EventSleep) Tx

func (e *EventSleep) Tx() *world.Tx

func (*EventSleep) Val

func (e *EventSleep) Val() *player.Player

type EventStartBreak

type EventStartBreak struct {
	Ctx      *player.Context
	Position cube.Pos
}

EventStartBreak is emitted when a player starts breaking a block.

func (*EventStartBreak) Cancel

func (e *EventStartBreak) Cancel()

func (*EventStartBreak) Tx

func (e *EventStartBreak) Tx() *world.Tx

func (*EventStartBreak) Val

func (e *EventStartBreak) Val() *player.Player

type EventTeleport

type EventTeleport struct {
	Ctx      *player.Context
	Position mgl64.Vec3
}

EventTeleport is emitted when a player is teleported.

func (*EventTeleport) Cancel

func (e *EventTeleport) Cancel()

func (*EventTeleport) Tx

func (e *EventTeleport) Tx() *world.Tx

func (*EventTeleport) Val

func (e *EventTeleport) Val() *player.Player

type EventToggleSneak

type EventToggleSneak struct {
	Ctx   *player.Context
	After bool
}

EventToggleSneak is emitted when a player toggles sneaking.

func (*EventToggleSneak) Cancel

func (e *EventToggleSneak) Cancel()

func (*EventToggleSneak) Tx

func (e *EventToggleSneak) Tx() *world.Tx

func (*EventToggleSneak) Val

func (e *EventToggleSneak) Val() *player.Player

type EventToggleSprint

type EventToggleSprint struct {
	Ctx   *player.Context
	After bool
}

EventToggleSprint is emitted when a player toggles sprinting.

func (*EventToggleSprint) Cancel

func (e *EventToggleSprint) Cancel()

func (*EventToggleSprint) Tx

func (e *EventToggleSprint) Tx() *world.Tx

func (*EventToggleSprint) Val

func (e *EventToggleSprint) Val() *player.Player

type EventTransfer

type EventTransfer struct {
	Ctx     *player.Context
	Address *net.UDPAddr
}

EventTransfer is emitted when a player is transferred to another server.

func (*EventTransfer) Cancel

func (e *EventTransfer) Cancel()

func (*EventTransfer) Tx

func (e *EventTransfer) Tx() *world.Tx

func (*EventTransfer) Val

func (e *EventTransfer) Val() *player.Player

type FakeMarker

type FakeMarker struct{}

FakeMarker marks a session as a fake player (testing bot). The federated ID is stored on Session.fakeID, not in the marker.

type FieldKind

type FieldKind int

FieldKind represents the type of field for injection.

const (
	// KindSession indicates a *Session field
	KindSession FieldKind = iota
	// KindManager indicates a *Manager field
	KindManager
	// KindComponent indicates a component field
	KindComponent
	// KindRelation indicates a relation traversal field
	KindRelation
	// KindRelationSlice indicates a relation set traversal field (slice)
	KindRelationSlice
	// KindResource indicates a resource field
	KindResource
	// KindPhantomWith indicates a With[T] phantom type
	KindPhantomWith
	// KindPhantomWithout indicates a Without[T] phantom type
	KindPhantomWithout
	// KindPayload indicates a non-injected payload field
	KindPayload
	// KindPeer indicates a Peer[T] resolution field (single remote player)
	KindPeer
	// KindPeerSlice indicates a PeerSet[T] resolution field (multiple remote players)
	KindPeerSlice
	// KindShared indicates a Shared[T] resolution field (single shared entity)
	KindShared
	// KindSharedSlice indicates a SharedSet[T] resolution field (multiple shared entities)
	KindSharedSlice
	// KindPeerSource indicates a Peer[T] field in a component (source for resolution)
	KindPeerSource
	// KindPeerSetSource indicates a PeerSet[T] field in a component (source for resolution)
	KindPeerSetSource
	// KindSharedSource indicates a Shared[T] field in a component (source for resolution)
	KindSharedSource
	// KindSharedSetSource indicates a SharedSet[T] field in a component (source for resolution)
	KindSharedSetSource
)

func (FieldKind) String

func (k FieldKind) String() string

String returns the string representation of FieldKind.

type FieldMeta

type FieldMeta struct {
	// Offset is the field offset in the struct for unsafe injection
	Offset uintptr

	// Size is the field size in bytes (for payload field zeroing)
	Size uintptr

	// Name is the field name for debugging
	Name string

	// Kind is the type of field (component, resource, etc.)
	Kind FieldKind

	// ComponentID is the ID of the component type (for component fields)
	ComponentID ComponentID

	// ComponentType is the reflect.Type of the component.
	// For payload fields, this stores the type of the field itself.
	ComponentType reflect.Type

	// Optional indicates the field can be nil
	Optional bool

	// Mutable indicates the field has write access
	Mutable bool

	// WindowIndex indicates which session window this field belongs to
	WindowIndex int

	// RelationSourceField is the name of the field containing the relation
	// (for relation traversal fields)
	RelationSourceField string

	// RelationSourceIndex is the index of the source field in Fields
	RelationSourceIndex int

	// RelationDataOffset is the offset of the Relation/RelationSet field in the source component
	RelationDataOffset uintptr

	// IsSlice indicates this is a slice field (for RelationSet resolution)
	IsSlice bool

	// PeerSourceIndex is the index of the source component field for Peer/PeerSet resolution
	PeerSourceIndex int

	// PeerSourceOffset is the offset of the Peer[T]/PeerSet[T] field in the source component
	PeerSourceOffset uintptr

	// SharedSourceIndex is the index of the source component field for Shared/SharedSet resolution
	SharedSourceIndex int

	// SharedSourceOffset is the offset of the Shared[T]/SharedSet[T] field in the source component
	SharedSourceOffset uintptr
}

FieldMeta holds metadata about a single injectable field.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager is the central PECS coordinator. It manages sessions, bundles, and the scheduler. Multiple Manager instances can coexist in the same process for running multiple isolated servers.

func (*Manager) AllSessions

func (m *Manager) AllSessions() []*Session

AllSessions returns a slice of all active sessions.

func (*Manager) AllSessionsInWorld

func (m *Manager) AllSessionsInWorld(w *world.World) []*Session

AllSessionsInWorld returns all active sessions in the specified world.

func (*Manager) Emit

func (m *Manager) Emit(event any)

Emit sends an event to all active sessions. Global handlers are invoked first, then session-scoped handlers for each session.

func (*Manager) EmitExcept

func (m *Manager) EmitExcept(event any, exclude ...*Session)

EmitExcept sends an event to all active sessions except the specified ones. Global handlers are invoked first, then session-scoped handlers for each non-excluded session.

func (*Manager) EmitGlobal

func (m *Manager) EmitGlobal(event any)

EmitGlobal sends an event to global handlers only.

func (*Manager) GetSession

func (m *Manager) GetSession(p *player.Player) *Session

GetSession retrieves the session for a player.

func (*Manager) GetSessionByHandle

func (m *Manager) GetSessionByHandle(h *world.EntityHandle) *Session

GetSessionByHandle retrieves a session by entity handle.

func (*Manager) GetSessionByID

func (m *Manager) GetSessionByID(id string) *Session

GetSessionByID retrieves a session by its federated ID. This works for both real players (using XUID) and fake players (using FakeMarker.ID). Returns nil for entities (which have no federated ID).

func (*Manager) GetSessionByName

func (m *Manager) GetSessionByName(name string) *Session

GetSessionByName retrieves a session by player Name.

func (*Manager) GetSessionByUUID

func (m *Manager) GetSessionByUUID(id uuid.UUID) *Session

GetSessionByUUID retrieves a session by UUID.

func (*Manager) MoveSession

func (m *Manager) MoveSession(s *Session, from, to *world.World)

MoveSession updates the session's world in the index.

func (*Manager) NewSession

func (m *Manager) NewSession(p *player.Player) (*Session, error)

NewSession creates a new session for a player. This should be called when a player joins and the returned session should be passed to player.Handle() wrapped with NewHandler(). Automatically fetches data from registered PeerProviders and subscribes to updates.

func (*Manager) RegisterPeerProvider

func (m *Manager) RegisterPeerProvider(p PeerProvider, opts ...ProviderOption)

RegisterPeerProvider registers a provider for Peer[T] resolution.

func (*Manager) RegisterSharedProvider

func (m *Manager) RegisterSharedProvider(p SharedProvider, opts ...ProviderOption)

RegisterSharedProvider registers a provider for Shared[T] resolution.

func (*Manager) ResolvePeer

func (m *Manager) ResolvePeer(playerID string, componentType reflect.Type) unsafe.Pointer

ResolvePeer resolves a Peer[T] reference to the target's component. If the player is local, returns their component directly. If remote, fetches and caches via the registered PeerProvider.

func (*Manager) ResolvePeers

func (m *Manager) ResolvePeers(playerIDs []string, componentType reflect.Type) []unsafe.Pointer

ResolvePeers resolves multiple Peer[T] references. Uses batch fetching for efficiency when multiple players are remote.

func (*Manager) ResolveShared

func (m *Manager) ResolveShared(entityID string, dataType reflect.Type) unsafe.Pointer

ResolveShared resolves a Shared[T] reference to the entity's data.

func (*Manager) ResolveSharedMany

func (m *Manager) ResolveSharedMany(entityIDs []string, dataType reflect.Type) []unsafe.Pointer

ResolveSharedMany resolves multiple Shared[T] references.

func (*Manager) SessionCount

func (m *Manager) SessionCount() int

SessionCount returns the number of active sessions.

func (*Manager) Shutdown

func (m *Manager) Shutdown()

Shutdown gracefully shuts down the manager.

func (*Manager) SpawnEntity

func (m *Manager) SpawnEntity(tx *world.Tx, cfg ActorConfig) (*player.Player, *Session)

SpawnEntity creates an NPC entity and registers it as a PECS session. Entities do not participate in cross-server Peer[T] lookups. Returns the session for adding components.

func (*Manager) SpawnFake

func (m *Manager) SpawnFake(tx *world.Tx, cfg ActorConfig, fakeID string) (*player.Player, *Session)

SpawnFake creates a fake player (testing bot) and registers it as a PECS session. The fakeID is used as the federated identifier for Peer[T] resolution. Returns the session for adding components.

func (*Manager) Start

func (m *Manager) Start()

Start starts the manager and scheduler.

func (*Manager) TickNumber

func (m *Manager) TickNumber() uint64

TickNumber returns the current scheduler tick number.

type Peer

type Peer[T any] struct {
	// contains filtered or unexported fields
}

Peer[T] references another player's component data by their persistent ID.

When resolved:

  • If the player is on the same server, their local Session component is used directly.
  • If the player is remote, PECS fetches and syncs their data via the registered PeerProvider.

Usage:

type SocialData struct {
    BestFriend Peer[FriendProfile]
}

// In a system - PECS injects the resolved data
type ShowFriendSystem struct {
    Session    *Session
    Social     *SocialData
    FriendInfo *FriendProfile `pecs:"peer"` // Resolved from Social.BestFriend
}

// In a command - manual resolution
func (c ShowFriendCommand) Run(src cmd.Source, out *cmd.Output, tx *world.Tx) {
    p, sess := pecs.Command(src)
    if sess == nil {
        return
    }
    social := pecs.Get[SocialData](sess)
    if friend, ok := social.BestFriend.Resolve(sess.Manager()); ok {
        out.Printf("Best friend: %s", friend.Username)
    }
}

func (*Peer[T]) Clear

func (p *Peer[T]) Clear()

Clear removes the target reference.

func (*Peer[T]) ID

func (p *Peer[T]) ID() string

ID returns the target player's persistent ID.

func (*Peer[T]) IsSet

func (p *Peer[T]) IsSet() bool

IsSet returns true if a target is set.

func (*Peer[T]) Resolve

func (p *Peer[T]) Resolve(m *Manager) (*T, bool)

Resolve fetches the target player's component. If the player is local, returns their component directly. If remote, fetches via the registered PeerProvider. Returns (nil, false) if the peer is not set, player doesn't exist, or the component is not available.

func (*Peer[T]) Set

func (p *Peer[T]) Set(playerID string)

Set sets the target player by their persistent ID (e.g., XUID).

func (*Peer[T]) TargetType

func (p *Peer[T]) TargetType() reflect.Type

TargetType returns the reflect.Type of the component T.

type PeerProvider

type PeerProvider interface {
	Provider

	// PlayerComponents returns the component types this provider handles.
	// PECS will only call this provider for Peer[T] where T is in this list.
	PlayerComponents() []reflect.Type

	// FetchPlayer retrieves components for a single player.
	// Returns nil (not error) if the player doesn't exist.
	FetchPlayer(ctx context.Context, playerID string) ([]any, error)

	// FetchPlayers batch-fetches components for multiple players.
	// Returns a map of playerID -> components.
	// Missing players should be omitted from the map (not nil values).
	FetchPlayers(ctx context.Context, playerIDs []string) (map[string][]any, error)

	// SubscribePlayer starts receiving real-time updates for a player.
	// Updates should be sent to the channel until the subscription is closed.
	// Return an error if the player doesn't exist or subscription fails.
	SubscribePlayer(ctx context.Context, playerID string, updates chan<- PlayerUpdate) (Subscription, error)
}

PeerProvider fetches and syncs per-player data for Peer[T] resolution. Implement this interface to enable cross-server player data access.

type PeerSet

type PeerSet[T any] struct {
	// contains filtered or unexported fields
}

PeerSet[T] references multiple players' component data.

Usage:

type PartyData struct {
    Members PeerSet[MemberInfo]
}

type PartyDisplaySystem struct {
    Session *Session
    Party   *PartyData
    Members []*MemberInfo `pecs:"peer"` // Resolved from Party.Members
}

func (*PeerSet[T]) Add

func (ps *PeerSet[T]) Add(playerID string)

Add adds a player ID to the set.

func (*PeerSet[T]) Clear

func (ps *PeerSet[T]) Clear()

Clear removes all target references.

func (*PeerSet[T]) IDs

func (ps *PeerSet[T]) IDs() []string

IDs returns a copy of all target player IDs.

func (*PeerSet[T]) Len

func (ps *PeerSet[T]) Len() int

Len returns the number of targets.

func (*PeerSet[T]) Remove

func (ps *PeerSet[T]) Remove(playerID string)

Remove removes a player ID from the set.

func (*PeerSet[T]) Resolve

func (ps *PeerSet[T]) Resolve(m *Manager) []*T

Resolve fetches all target players' components. Returns only successfully resolved components (nil entries are filtered out).

func (*PeerSet[T]) Set

func (ps *PeerSet[T]) Set(playerIDs []string)

Set replaces all target player IDs.

func (*PeerSet[T]) TargetType

func (ps *PeerSet[T]) TargetType() reflect.Type

TargetType returns the reflect.Type of the component T.

type PhantomTypeInfo

type PhantomTypeInfo interface {
	ComponentType() reflect.Type
	IsWithout() bool
}

PhantomTypeInfo provides component type information for phantom types.

type PlayerUpdate

type PlayerUpdate struct {
	// ComponentType is the type of component being updated.
	ComponentType reflect.Type

	// Data is a pointer to the new component data.
	// If nil, the component should be removed.
	Data any
}

PlayerUpdate represents an update to a player's component from a provider.

type Provider

type Provider interface {
	// Name returns a unique identifier for this provider (for logging/debugging).
	Name() string
}

Provider is the base interface for all data providers. Providers bridge PECS with external data sources (gRPC services, databases, etc.).

type ProviderOption

type ProviderOption func(*ProviderOptions)

ProviderOption configures a provider.

func WithFetchTimeout

func WithFetchTimeout(d time.Duration) ProviderOption

WithFetchTimeout sets the fetch timeout.

func WithGracePeriod

func WithGracePeriod(d time.Duration) ProviderOption

WithGracePeriod sets the grace period.

func WithRequired

func WithRequired(required bool) ProviderOption

WithRequired marks the provider as required for session creation. If a required provider fails during NewSession, the session creation fails.

func WithStaleTimeout

func WithStaleTimeout(d time.Duration) ProviderOption

WithStaleTimeout sets the stale timeout.

type ProviderOptions

type ProviderOptions struct {
	// FetchTimeout is the maximum time to wait for Fetch calls.
	// Default: 5 seconds.
	FetchTimeout time.Duration

	// GracePeriod is how long to keep cached data after the last reference is released.
	// This prevents thrashing when players rapidly reference/dereference the same target.
	// Default: 30 seconds.
	GracePeriod time.Duration

	// StaleTimeout defines when cached data is considered too old to use.
	// If a subscription fails and data is older than this, resolution fails.
	// Default: 5 minutes.
	StaleTimeout time.Duration

	// Required indicates this provider must succeed for session creation.
	// If true, NewSession returns an error if this provider fails.
	// If false, provider failures are logged but session creation continues.
	// Default: false.
	Required bool
}

ProviderOptions configures provider behavior.

type Relation

type Relation[T any] struct {
	// contains filtered or unexported fields
}

Relation represents a reference from one session to another. The type parameter T indicates what component the target session must have. This provides type-safe references between players.

Usage:

type Following struct {
    Target pecs.Relation[Health] // Target must have Health component
}

func (*Relation[T]) Clear

func (r *Relation[T]) Clear()

Clear removes the target reference.

func (*Relation[T]) Get

func (r *Relation[T]) Get() *Session

Get returns the target session, or nil if not set or target is closed.

func (*Relation[T]) Resolve

func (r *Relation[T]) Resolve() (*Session, *T, bool)

Resolve retrieves the target session and its component of type T. Returns (nil, nil, false) if the relation is unset, the target is closed, or the component is missing.

func (*Relation[T]) Set

func (r *Relation[T]) Set(target *Session)

Set sets the target session for this relation. The target should have a component of type T, though this is validated at system execution time, not at set time.

func (*Relation[T]) TargetType

func (r *Relation[T]) TargetType() reflect.Type

TargetType returns the reflect.Type of the component the target must have.

func (*Relation[T]) Valid

func (r *Relation[T]) Valid() bool

Valid returns true if the target exists and has the required component.

type RelationSet

type RelationSet[T any] struct {
	// contains filtered or unexported fields
}

RelationSet represents a set of references to other sessions. The type parameter T indicates what component target sessions should have.

Usage:

type PartyLeader struct {
    Members pecs.RelationSet[PartyMember]
}

func (*RelationSet[T]) Add

func (rs *RelationSet[T]) Add(target *Session)

Add adds a session to the relation set.

func (*RelationSet[T]) All

func (rs *RelationSet[T]) All() []*Session

All returns all non-closed target sessions. Closed sessions are lazily removed on subsequent calls.

func (*RelationSet[T]) Clear

func (rs *RelationSet[T]) Clear()

Clear removes all sessions from the relation set.

func (*RelationSet[T]) Has

func (rs *RelationSet[T]) Has(target *Session) bool

Has checks if a session is in the relation set.

func (*RelationSet[T]) Len

func (rs *RelationSet[T]) Len() int

Len returns the number of sessions in the relation set.

func (*RelationSet[T]) Remove

func (rs *RelationSet[T]) Remove(target *Session)

Remove removes a session from the relation set.

func (*RelationSet[T]) Resolve

func (rs *RelationSet[T]) Resolve() []Resolved[T]

Resolve returns all valid sessions with their components. A session is valid if it's not closed and has the required component T. Closed sessions are lazily removed on subsequent calls.

func (*RelationSet[T]) TargetType

func (rs *RelationSet[T]) TargetType() reflect.Type

TargetType returns the reflect.Type of the component targets must have.

type RepeatingTaskHandle

type RepeatingTaskHandle struct {
	// contains filtered or unexported fields
}

RepeatingTaskHandle allows cancelling a repeating scheduled task.

func ScheduleRepeating

func ScheduleRepeating(s *Session, task Runnable, interval time.Duration, times int) *RepeatingTaskHandle

ScheduleRepeating schedules a task to run repeatedly at the given interval. If times is -1, the task repeats indefinitely until cancelled. If times is > 0, the task runs exactly that many times. Returns a RepeatingTaskHandle that can be used to cancel future executions.

func (*RepeatingTaskHandle) Cancel

func (h *RepeatingTaskHandle) Cancel()

Cancel cancels the repeating task, preventing future executions.

type Resolved

type Resolved[T any] struct {
	Session   *Session
	Component *T
}

Resolved holds a resolved relation with both session and component.

type Runnable

type Runnable interface {
	Run(tx *world.Tx)
}

Runnable is the interface implemented by loops and tasks. The Run method contains the system's logic and is called when the system executes. The tx parameter is the active world transaction - use it instead of opening new transactions to avoid deadlocks. All sessions in a system are guaranteed to be in this transaction's world.

type Scheduler

type Scheduler struct {
	// contains filtered or unexported fields
}

Scheduler manages the execution of loops and tasks. It supports parallel execution of non-conflicting systems.

func (*Scheduler) Start

func (s *Scheduler) Start()

Start begins the scheduler's tick loop.

func (*Scheduler) Stop

func (s *Scheduler) Stop()

Stop gracefully shuts down the scheduler.

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session represents a player's session in PECS. It wraps the player's EntityHandle (which is persistent across transactions) and stores all components attached to the player.

Sessions are created when players join and destroyed when they leave. They implement pecs.Handler to intercept all player events.

func Command

func Command(src cmd.Source) (*player.Player, *Session)

Command extracts the player and session from a command source. Returns (nil, nil) if the source is not a player or has no session.

Usage:

func (c MyCommand) Run(src cmd.Source, out *cmd.Output, tx *world.Tx) {
    p, sess := pecs.Command(src)
    if sess == nil {
        out.Error("Player-only command")
        return
    }

    // Use p and sess...
}

Concurrency: Commands are executed synchronously with the player, just like handlers. It is safe to access and modify components directly.

func Form

func Form(sub form.Submitter) (*player.Player, *Session)

Form extracts the player and session from a form submitter. Returns (nil, nil) if the submitter is not a player or has no session.

Usage:

func (f MyForm) Submit(sub form.Submitter, tx *world.Tx) {
    p, sess := pecs.Form(sub)
    if sess == nil {
        return
    }

    // Use p and sess...
}

Concurrency: Form submissions are executed synchronously with the player, just like handlers. It is safe to access and modify components directly.

func Item

func Item(user item.User) (*player.Player, *Session)

Item extracts the player and session from an item user. Returns (nil, nil) if the user is not a player or has no session.

Usage:

func (i MyItem) Use(tx *world.Tx, user item.User, ctx *item.UseContext) bool {
    p, sess := pecs.Item(user)
    if sess == nil {
        return
    }

    // Use p and sess...
}

Concurrency: Item uses are executed synchronously with the player, just like handlers. It is safe to access and modify components directly.

func (*Session) Closed

func (s *Session) Closed() bool

Closed returns true if the session has been closed.

func (*Session) Emit

func (s *Session) Emit(event any)

Emit sends an event to all registered handlers that listen for it. Handlers listen for events by implementing a method with the signature:

func (h *MyHandler) HandleMyEvent(ev *MyEventType)

The method name does not matter, only the signature (one argument).

func (*Session) Exec

func (s *Session) Exec(fn func(tx *world.Tx, p *player.Player)) bool

Exec runs a function within the session's world transaction. Returns false if the player is offline or the session is closed.

func (*Session) Handle

func (s *Session) Handle() *world.EntityHandle

Handle returns the underlying EntityHandle.

func (*Session) ID

func (s *Session) ID() string

ID returns the federated identifier for this session. For real players, this is their XUID. For fake players, this is their FakeMarker.ID. For entities, this returns an empty string (no federated ID). This is used for Peer[T] resolution to identify players across servers.

func (*Session) IsActor

func (s *Session) IsActor() bool

IsActor returns true if this session is an actor (fake player or entity). Bots have session.Nop as their network session.

func (*Session) IsEntity

func (s *Session) IsEntity() bool

IsEntity returns true if this session is an NPC entity.

func (*Session) IsFake

func (s *Session) IsFake() bool

IsFake returns true if this session is a fake player (testing bot).

func (*Session) Manager

func (s *Session) Manager() *Manager

Manager returns the PECS manager for this session.

func (*Session) Mask

func (s *Session) Mask() Bitmask

Mask returns a copy of the session's component bitmask. This is primarily for debugging and testing.

func (*Session) Name

func (s *Session) Name() string

Name returns the player's name.

func (*Session) Player

func (s *Session) Player(tx *world.Tx) (*player.Player, bool)

Player retrieves the *player.Player instance associated with this session within the given transaction. It returns (nil, false) if the player entity is not present in the transaction (e.g. offline or in another world).

Usage:

if p, ok := s.Player(tx); ok {
    p.Message("Hello!")
}

func (*Session) String

func (s *Session) String() string

String returns a string representation of the session for debugging.

func (*Session) UUID

func (s *Session) UUID() uuid.UUID

UUID returns the player's UUID.

func (*Session) World

func (s *Session) World() *world.World

World returns the world the player is currently in. Returns the cached world (may be slightly stale).

func (*Session) XUID

func (s *Session) XUID() string

XUID returns the player's XUID.

type SessionHandler

type SessionHandler struct {
	// contains filtered or unexported fields
}

SessionHandler wraps a PECS session to implement Dragonfly's player.Handler. It receives Dragonfly events and emits them as pooled PECS events.

Concurrency: Handlers are executed synchronously by Dragonfly (typically within the world's tick loop or packet processing). This means they are serialized with respect to the world state and generally do not race with PECS Loops or Tasks, as those are also executed within the world's transaction context via the Scheduler.

It is safe for Handlers to read/write Components, as they effectively have exclusive access during execution relative to the specific world.

func (*SessionHandler) HandleAttackEntity

func (h *SessionHandler) HandleAttackEntity(ctx *player.Context, e world.Entity, force, height *float64, critical *bool)

HandleAttackEntity handles attacking an entity.

func (*SessionHandler) HandleBlockBreak

func (h *SessionHandler) HandleBlockBreak(ctx *player.Context, pos cube.Pos, drops *[]item.Stack, xp *int)

HandleBlockBreak handles block breaking.

func (*SessionHandler) HandleBlockPick

func (h *SessionHandler) HandleBlockPick(ctx *player.Context, pos cube.Pos, b world.Block)

HandleBlockPick handles picking a block.

func (*SessionHandler) HandleBlockPlace

func (h *SessionHandler) HandleBlockPlace(ctx *player.Context, pos cube.Pos, b world.Block)

HandleBlockPlace handles block placement.

func (*SessionHandler) HandleChangeWorld

func (h *SessionHandler) HandleChangeWorld(p *player.Player, before, after *world.World)

HandleChangeWorld handles the player changing worlds.

func (*SessionHandler) HandleChat

func (h *SessionHandler) HandleChat(ctx *player.Context, message *string)

HandleChat handles the player sending a chat message.

func (*SessionHandler) HandleCommandExecution

func (h *SessionHandler) HandleCommandExecution(ctx *player.Context, command cmd.Command, args []string)

HandleCommandExecution handles executing a command.

func (*SessionHandler) HandleDeath

func (h *SessionHandler) HandleDeath(p *player.Player, src world.DamageSource, keepInv *bool)

HandleDeath handles the player dying.

func (*SessionHandler) HandleDiagnostics

func (h *SessionHandler) HandleDiagnostics(p *player.Player, d session.Diagnostics)

HandleDiagnostics handles a diagnostics request.

func (*SessionHandler) HandleExperienceGain

func (h *SessionHandler) HandleExperienceGain(ctx *player.Context, amount *int)

HandleExperienceGain handles XP gain.

func (*SessionHandler) HandleFireExtinguish

func (h *SessionHandler) HandleFireExtinguish(ctx *player.Context, pos cube.Pos)

HandleFireExtinguish handles the player extinguishing fire.

func (*SessionHandler) HandleFoodLoss

func (h *SessionHandler) HandleFoodLoss(ctx *player.Context, from int, to *int)

HandleFoodLoss handles the player losing food.

func (*SessionHandler) HandleHeal

func (h *SessionHandler) HandleHeal(ctx *player.Context, health *float64, src world.HealingSource)

HandleHeal handles the player being healed.

func (*SessionHandler) HandleHeldSlotChange

func (h *SessionHandler) HandleHeldSlotChange(ctx *player.Context, from, to int)

HandleHeldSlotChange handles held hotbar slot change.

func (*SessionHandler) HandleHurt

func (h *SessionHandler) HandleHurt(ctx *player.Context, damage *float64, immune bool, attackImmunity *time.Duration, src world.DamageSource)

HandleHurt handles the player being hurt.

func (*SessionHandler) HandleItemConsume

func (h *SessionHandler) HandleItemConsume(ctx *player.Context, it item.Stack)

HandleItemConsume handles consuming an item.

func (*SessionHandler) HandleItemDamage

func (h *SessionHandler) HandleItemDamage(ctx *player.Context, it item.Stack, damage *int)

HandleItemDamage handles damaging an item.

func (*SessionHandler) HandleItemDrop

func (h *SessionHandler) HandleItemDrop(ctx *player.Context, it item.Stack)

HandleItemDrop handles dropping an item.

func (*SessionHandler) HandleItemPickup

func (h *SessionHandler) HandleItemPickup(ctx *player.Context, it *item.Stack)

HandleItemPickup handles picking up an item.

func (*SessionHandler) HandleItemRelease

func (h *SessionHandler) HandleItemRelease(ctx *player.Context, it item.Stack, dur time.Duration)

HandleItemRelease handles releasing a charged-use item.

func (*SessionHandler) HandleItemUse

func (h *SessionHandler) HandleItemUse(ctx *player.Context)

HandleItemUse handles general item use.

func (*SessionHandler) HandleItemUseOnBlock

func (h *SessionHandler) HandleItemUseOnBlock(ctx *player.Context, pos cube.Pos, face cube.Face, clickPos mgl64.Vec3)

HandleItemUseOnBlock handles using an item on a block.

func (*SessionHandler) HandleItemUseOnEntity

func (h *SessionHandler) HandleItemUseOnEntity(ctx *player.Context, e world.Entity)

HandleItemUseOnEntity handles using an item on an entity.

func (*SessionHandler) HandleJump

func (h *SessionHandler) HandleJump(p *player.Player)

HandleJump handles the player jumping.

func (*SessionHandler) HandleLecternPageTurn

func (h *SessionHandler) HandleLecternPageTurn(ctx *player.Context, pos cube.Pos, oldPage int, newPage *int)

HandleLecternPageTurn handles page turning on lecterns.

func (*SessionHandler) HandleMove

func (h *SessionHandler) HandleMove(ctx *player.Context, newPos mgl64.Vec3, newRot cube.Rotation)

HandleMove handles the player moving.

func (*SessionHandler) HandlePunchAir

func (h *SessionHandler) HandlePunchAir(ctx *player.Context)

HandlePunchAir handles punching air.

func (*SessionHandler) HandleQuit

func (h *SessionHandler) HandleQuit(p *player.Player)

HandleQuit handles a player quitting the server.

func (*SessionHandler) HandleRespawn

func (h *SessionHandler) HandleRespawn(p *player.Player, pos *mgl64.Vec3, w **world.World)

HandleRespawn handles the player respawning.

func (*SessionHandler) HandleSignEdit

func (h *SessionHandler) HandleSignEdit(ctx *player.Context, pos cube.Pos, frontSide bool, oldText, newText string)

HandleSignEdit handles sign text editing.

func (*SessionHandler) HandleSkinChange

func (h *SessionHandler) HandleSkinChange(ctx *player.Context, sk *skin.Skin)

HandleSkinChange handles the player changing their skin.

func (*SessionHandler) HandleSleep

func (h *SessionHandler) HandleSleep(ctx *player.Context, sendReminder *bool)

HandleSleep handles sleeping in a bed.

func (*SessionHandler) HandleStartBreak

func (h *SessionHandler) HandleStartBreak(ctx *player.Context, pos cube.Pos)

HandleStartBreak handles the player starting to break a block.

func (*SessionHandler) HandleTeleport

func (h *SessionHandler) HandleTeleport(ctx *player.Context, pos mgl64.Vec3)

HandleTeleport handles the player being teleported.

func (*SessionHandler) HandleToggleSneak

func (h *SessionHandler) HandleToggleSneak(ctx *player.Context, after bool)

HandleToggleSneak handles the player toggling sneak.

func (*SessionHandler) HandleToggleSprint

func (h *SessionHandler) HandleToggleSprint(ctx *player.Context, after bool)

HandleToggleSprint handles the player toggling sprint.

func (*SessionHandler) HandleTransfer

func (h *SessionHandler) HandleTransfer(ctx *player.Context, addr *net.UDPAddr)

HandleTransfer handles server transfer.

func (*SessionHandler) Session

func (h *SessionHandler) Session() *Session

Session returns the session associated with this handler.

type Shared

type Shared[T any] struct {
	// contains filtered or unexported fields
}

Shared[T] references a shared entity's data by its ID.

Unlike Peer[T] which references player data, Shared[T] references entities that are shared across multiple players (parties, matches, guilds, etc.). The data is cached globally and all references point to the same instance.

Usage:

type MatchmakingData struct {
    CurrentParty Shared[PartyInfo]
    ActiveMatch  Shared[MatchInfo]
}

// In a system - PECS injects the resolved data
type PartyDisplaySystem struct {
    Session *Session
    MMData  *MatchmakingData
    Party   *PartyInfo `pecs:"shared"` // Resolved from MMData.CurrentParty
}

// In a command - manual resolution
func (c PartyInfoCommand) Run(src cmd.Source, out *cmd.Output, tx *world.Tx) {
    p, sess := pecs.Command(src)
    if sess == nil {
        return
    }
    mmData := pecs.Get[MatchmakingData](sess)
    if party, ok := mmData.CurrentParty.Resolve(sess.Manager()); ok {
        out.Printf("Party: %s (%d members)", party.Name, len(party.Members))
    }
}

func (*Shared[T]) Clear

func (s *Shared[T]) Clear()

Clear removes the target reference.

func (*Shared[T]) ID

func (s *Shared[T]) ID() string

ID returns the target entity ID.

func (*Shared[T]) IsSet

func (s *Shared[T]) IsSet() bool

IsSet returns true if a target is set.

func (*Shared[T]) Resolve

func (s *Shared[T]) Resolve(m *Manager) (*T, bool)

Resolve fetches the shared entity's data. Returns (nil, false) if the entity is not set, doesn't exist, or the data is not available.

func (*Shared[T]) Set

func (s *Shared[T]) Set(entityID string)

Set sets the target entity ID.

func (*Shared[T]) TargetType

func (s *Shared[T]) TargetType() reflect.Type

TargetType returns the reflect.Type of the data type T.

type SharedProvider

type SharedProvider interface {
	Provider

	// EntityComponents returns the component types this provider handles.
	// PECS will only call this provider for Shared[T] where T is in this list.
	EntityComponents() []reflect.Type

	// FetchEntity retrieves a shared entity by ID.
	// Returns nil (not error) if the entity doesn't exist.
	FetchEntity(ctx context.Context, entityID string) (any, error)

	// FetchEntities batch-fetches components for multiple entities.
	// Returns a map of entityID -> component.
	// Missing entities should be omitted from the map.
	FetchEntities(ctx context.Context, entityIDs []string) (map[string]any, error)

	// SubscribeEntity starts receiving real-time updates for an entity.
	// Updates should be sent to the channel until the subscription is closed.
	// Return an error if the entity doesn't exist or subscription fails.
	SubscribeEntity(ctx context.Context, entityID string, updates chan<- any) (Subscription, error)
}

SharedProvider fetches and syncs shared entity data for Shared[T] resolution. Implement this interface for data shared across multiple players (parties, matches, etc.).

type SharedSet

type SharedSet[T any] struct {
	// contains filtered or unexported fields
}

SharedSet[T] references multiple shared entities.

Usage:

type GuildData struct {
    ActiveWars SharedSet[WarInfo]
}

type WarDisplaySystem struct {
    Session *Session
    Guild   *GuildData
    Wars    []*WarInfo `pecs:"shared"` // Resolved from Guild.ActiveWars
}

func (*SharedSet[T]) Add

func (ss *SharedSet[T]) Add(entityID string)

Add adds an entity ID to the set.

func (*SharedSet[T]) Clear

func (ss *SharedSet[T]) Clear()

Clear removes all target references.

func (*SharedSet[T]) IDs

func (ss *SharedSet[T]) IDs() []string

IDs returns a copy of all target entity IDs.

func (*SharedSet[T]) Len

func (ss *SharedSet[T]) Len() int

Len returns the number of targets.

func (*SharedSet[T]) Remove

func (ss *SharedSet[T]) Remove(entityID string)

Remove removes an entity ID from the set.

func (*SharedSet[T]) Resolve

func (ss *SharedSet[T]) Resolve(m *Manager) []*T

Resolve fetches all shared entities' data. Returns only successfully resolved data (nil entries are filtered out).

func (*SharedSet[T]) Set

func (ss *SharedSet[T]) Set(entityIDs []string)

Set replaces all target entity IDs.

func (*SharedSet[T]) TargetType

func (ss *SharedSet[T]) TargetType() reflect.Type

TargetType returns the reflect.Type of the data type T.

type Stage

type Stage int

Stage represents a scheduling stage for system execution. Systems are executed in stage order: Before → Default → After.

const (
	// Before stage runs first. Use for pre-processing, input handling,
	// and setup logic that other systems depend on.
	Before Stage = iota

	// Default stage runs second. Use for main game logic including
	// combat, movement, abilities, and most gameplay systems.
	Default

	// After stage runs last. Use for cleanup, synchronization,
	// statistics logging, and network state updates.
	After
)

func (Stage) String

func (s Stage) String() string

String returns the string representation of the stage.

type Subscription

type Subscription interface {
	Close() error
}

Subscription represents an active subscription to updates. Call Close() to stop receiving updates and release resources.

type SystemMeta

type SystemMeta struct {
	// Type is the reflect.Type of the system struct
	Type reflect.Type

	// Name is the type name for debugging
	Name string

	// RequireMask is the bitmask of required components
	RequireMask Bitmask

	// ExcludeMask is the bitmask of excluded components (Without[T])
	ExcludeMask Bitmask

	// Fields holds injection metadata for each field
	Fields []FieldMeta

	// Windows defines session windows for multi-session systems
	Windows []WindowMeta

	// Stage is the execution stage
	Stage Stage

	// IsMultiSession indicates this is a multi-session system
	IsMultiSession bool

	// IsGlobal indicates this is a global system that runs once, not per-session.
	IsGlobal bool

	// Pool is the sync.Pool for this system type
	Pool *sync.Pool

	// Bundle is the bundle this system belongs to
	Bundle *Bundle

	// AccessMeta for conflict detection
	Access AccessMeta
}

SystemMeta holds pre-computed metadata about a system type. This is computed once at registration time and reused for all executions.

type TagInfo

type TagInfo struct {
	Mutable  bool // pecs:"mut"
	Optional bool // pecs:"opt"
	Relation bool // pecs:"rel"
	Resource bool // pecs:"res"
	Peer     bool // pecs:"peer"
	Shared   bool // pecs:"shared"
}

TagInfo holds parsed tag information.

type TaskHandle

type TaskHandle struct {
	// contains filtered or unexported fields
}

TaskHandle allows cancelling a scheduled task.

func Dispatch

func Dispatch(s *Session, task Runnable) *TaskHandle

Dispatch immediately executes a task in the next tick. Returns a TaskHandle that can be used to cancel the task.

func Dispatch2

func Dispatch2(s1, s2 *Session, task Runnable) *TaskHandle

Dispatch2 immediately executes a multi-session task in the next tick. Returns a TaskHandle that can be used to cancel the task.

func DispatchGlobal

func DispatchGlobal(m *Manager, task Runnable) *TaskHandle

DispatchGlobal schedules a global task for execution immediately. The task runs once in the manager's default world and is not tied to any session. Returns a TaskHandle that can be used to cancel the task.

func Schedule

func Schedule(s *Session, task Runnable, delay time.Duration) *TaskHandle

Schedule schedules a task for execution after the given delay. The task will only run if the session passes the bitmask check at execution time. Returns a TaskHandle that can be used to cancel the task.

func Schedule2

func Schedule2(s1, s2 *Session, task Runnable, delay time.Duration) *TaskHandle

Schedule2 schedules a multi-session task for execution after the given delay. Both sessions must be in the same world at execution time and belong to the same manager. Returns a TaskHandle that can be used to cancel the task.

func ScheduleAt

func ScheduleAt(s *Session, task Runnable, at time.Time) *TaskHandle

ScheduleAt schedules a task for execution at a specific time. If the time is in the past, the task will execute on the next tick. Returns a TaskHandle that can be used to cancel the task.

func ScheduleGlobal

func ScheduleGlobal(m *Manager, task Runnable, delay time.Duration) *TaskHandle

ScheduleGlobal schedules a global task for execution after a given delay. The task runs once in the manager's default world and is not tied to any session. Returns a TaskHandle that can be used to cancel the task.

func (*TaskHandle) Cancel

func (h *TaskHandle) Cancel()

Cancel cancels the scheduled task.

type WindowMeta

type WindowMeta struct {
	// SessionFieldIndex is the index of the *Session field in Fields
	SessionFieldIndex int

	// StartFieldIndex is the first field index for this window
	StartFieldIndex int

	// EndFieldIndex is one past the last field index for this window
	EndFieldIndex int

	// RequireMask is the bitmask of required components for this window
	RequireMask Bitmask

	// ExcludeMask is the bitmask of excluded components for this window
	ExcludeMask Bitmask
}

WindowMeta defines a session window in a multi-session system.

type With

type With[T any] struct{}

With is a phantom type that indicates a component must exist for the system to run. The component is not injected into the field - it's only used for filtering.

Usage:

type MySystem struct {
    Session *pecs.Session
    _ pecs.With[VampireTag] // Only run if VampireTag exists
}

func (With[T]) ComponentType

func (With[T]) ComponentType() reflect.Type

ComponentType implements PhantomTypeInfo for With[T].

func (With[T]) IsWithout

func (With[T]) IsWithout() bool

IsWithout implements PhantomTypeInfo for With[T].

type Without

type Without[T any] struct{}

Without is a phantom type that indicates a component must NOT exist for the system to run. The system will be skipped if the component is present on the session.

Usage:

type MySystem struct {
    Session *pecs.Session
    _ pecs.Without[Spectator] // Skip if Spectator exists
}

func (Without[T]) ComponentType

func (Without[T]) ComponentType() reflect.Type

ComponentType implements PhantomTypeInfo for Without[T].

func (Without[T]) IsWithout

func (Without[T]) IsWithout() bool

IsWithout implements PhantomTypeInfo for Without[T].

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL