first commit

This commit is contained in:
2026-02-09 22:03:11 +03:00
commit 2e463a4923
5 changed files with 220 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
bin/
*.exe
*.test
*.out

22
README.md Normal file
View File

@@ -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`.

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/VexoraDevelopment/tx-teleport-example
go 1.25.0

142
txflow/adapter.go Normal file
View File

@@ -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)
})
}

49
txflow/interfaces.go Normal file
View File

@@ -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
}