commit 2e463a4923874790bdf7e0c456e99b98f0c7bfa8 Author: Andrewkydev Date: Mon Feb 9 22:03:11 2026 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad6af75 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +bin/ +*.exe +*.test +*.out diff --git a/README.md b/README.md new file mode 100644 index 0000000..5672daf --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# tx-teleport-example + +Пример вынесенного кода по безопасной работе с транзакциями мира и телепортами. + +## Что внутри + +- `txflow/interfaces.go`: минимальные интерфейсы (`World`, `Tx`, `Entity`, `Handle`), чтобы код не зависел от конкретного движка. +- `txflow/adapter.go`: адаптер игрока с: + - `WithPlayerTx(...)` (предпочитает текущий `Tx`, fallback через `ExecWorld`), + - очередью телепортов (`queue + process`), + - безопасным разделением same-world и cross-world телепорта. + +## Зачем + +Ключевая цель: не блокировать world transaction loop повторным `ExecWorld` изнутри уже выполняющейся транзакции. + +## Интеграция + +1. Подключить свои реализации интерфейсов из проекта. +2. Создавать `txflow.Adapter` на игрока. +3. Все операции игрока (сообщения, инвентарь, телепорт) запускать через `WithPlayerTx` или `Teleport`. + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..968ac85 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/VexoraDevelopment/tx-teleport-example + +go 1.25.0 diff --git a/txflow/adapter.go b/txflow/adapter.go new file mode 100644 index 0000000..6d63643 --- /dev/null +++ b/txflow/adapter.go @@ -0,0 +1,142 @@ +package txflow + +import ( + "errors" + "sync" + "sync/atomic" +) + +type Adapter struct { + handle Handle + worlds WorldResolver + + currentTx SafeBox[Tx] + + mu sync.Mutex + pendingTeleport *Position + queuedTeleport atomic.Bool + processing atomic.Bool +} + +func NewAdapter(handle Handle, worlds WorldResolver) *Adapter { + return &Adapter{handle: handle, worlds: worlds} +} + +func (a *Adapter) WithTx(tx Tx, fn func()) { + if fn == nil { + return + } + a.currentTx.Set(tx) + defer a.currentTx.Set(nil) + fn() +} + +func (a *Adapter) WithPlayerTx(fn func(Tx, Entity)) { + if a == nil || a.handle == nil || fn == nil { + return + } + if tx := a.currentTx.Get(); tx != nil { + if e, ok := a.handle.Entity(tx); ok { + fn(tx, e) + return + } + } + a.handle.ExecWorld(func(tx Tx, e Entity) { + fn(tx, e) + }) +} + +func (a *Adapter) Teleport(pos Position) error { + if a == nil || a.handle == nil { + return errors.New("adapter is nil") + } + if pos.World == "" { + a.WithPlayerTx(func(_ Tx, e Entity) { e.Teleport(pos) }) + return nil + } + target, err := a.worlds.Resolve(pos.World) + if err != nil { + return err + } + if target == nil { + return errors.New("target world not found") + } + if tx := a.currentTx.Get(); tx != nil && tx.World() != nil { + if tx.World() == target { + a.WithPlayerTx(func(_ Tx, e Entity) { e.Teleport(pos) }) + return nil + } + a.queueTeleport(pos) + go a.ProcessQueuedTeleport(nil) + return nil + } + a.queueTeleport(pos) + a.ProcessQueuedTeleport(nil) + return nil +} + +func (a *Adapter) queueTeleport(pos Position) { + a.mu.Lock() + cp := pos + a.pendingTeleport = &cp + a.mu.Unlock() + a.queuedTeleport.Store(true) +} + +func (a *Adapter) peekQueuedTeleport() *Position { + a.mu.Lock() + defer a.mu.Unlock() + if a.pendingTeleport == nil { + return nil + } + cp := *a.pendingTeleport + return &cp +} + +func (a *Adapter) clearQueuedTeleport() { + a.mu.Lock() + a.pendingTeleport = nil + a.mu.Unlock() + a.queuedTeleport.Store(false) +} + +func (a *Adapter) HasQueuedTeleport() bool { + return a.queuedTeleport.Load() +} + +func (a *Adapter) ProcessQueuedTeleport(tx Tx) { + if a == nil || a.handle == nil { + return + } + if !a.processing.CompareAndSwap(false, true) { + return + } + defer a.processing.Store(false) + + pos := a.peekQueuedTeleport() + if pos == nil { + return + } + target, err := a.worlds.Resolve(pos.World) + if err != nil || target == nil { + return + } + exec := func(currentTx Tx) { + if currentTx == nil || currentTx.World() == nil { + return + } + if e, ok := a.handle.Entity(currentTx); ok { + // В примере просто телепортируем. + // В реальном движке здесь обычно remove/add entity между мирами. + e.Teleport(*pos) + a.clearQueuedTeleport() + } + } + if tx != nil { + exec(tx) + return + } + target.Exec(func(t Tx) { + exec(t) + }) +} diff --git a/txflow/interfaces.go b/txflow/interfaces.go new file mode 100644 index 0000000..bb85b68 --- /dev/null +++ b/txflow/interfaces.go @@ -0,0 +1,49 @@ +package txflow + +import "sync" + +type Position struct { + X, Y, Z float64 + World string +} + +type World interface { + Name() string + Exec(func(Tx)) +} + +type Tx interface { + World() World +} + +type Entity interface { + Teleport(Position) +} + +type Handle interface { + Entity(tx Tx) (Entity, bool) + ExecWorld(func(tx Tx, e Entity)) +} + +type WorldResolver interface { + Resolve(name string) (World, error) +} + +type SafeBox[T any] struct { + mu sync.RWMutex + v T +} + +func (s *SafeBox[T]) Set(v T) { + s.mu.Lock() + s.v = v + s.mu.Unlock() +} + +func (s *SafeBox[T]) Get() T { + s.mu.RLock() + v := s.v + s.mu.RUnlock() + return v +} +