first commit
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
bin/
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
22
README.md
Normal file
22
README.md
Normal 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
3
go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module github.com/VexoraDevelopment/tx-teleport-example
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
142
txflow/adapter.go
Normal file
142
txflow/adapter.go
Normal 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
49
txflow/interfaces.go
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user