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