diff --git a/client/danmaku!/Network/Game Modes/br_server_tick_manager.gd b/client/danmaku!/Network/Game Modes/br_server_tick_manager.gd index d37354e..bf1c3a9 100644 --- a/client/danmaku!/Network/Game Modes/br_server_tick_manager.gd +++ b/client/danmaku!/Network/Game Modes/br_server_tick_manager.gd @@ -11,9 +11,9 @@ func _broadcast() -> void: if !controlled_world: return - var pos = controlled_world.player.position - var json_string = JSON.stringify({"utctime": Time.get_unix_time_from_system(), "tick": LocalTimer.predicted_tick, "x": pos.x, "y": pos.y}) - print("SEND: ", json_string) + var delta_vel = controlled_world.player.get_and_reset_delta_vel() + var json_string = JSON.stringify({"utctime": Time.get_unix_time_from_system(), "tick": LocalTimer.predicted_tick, "vel_x": delta_vel.x, "vel_y": delta_vel.y}) + #print("SEND: ", json_string) network.nakama_socket.send_match_state_async(network.current_match_id, 0, json_string) func _on_match_state(p_state : NakamaRTAPI.MatchData): @@ -23,10 +23,9 @@ func _on_match_state(p_state : NakamaRTAPI.MatchData): match p_state.op_code: 2: var data = JSON.parse_string(p_state.data) - print("RECV: ", data) # Set player position if server demands a forced position - if data["forcePlayerPos"]: + if true:#data["forcePlayerPos"]: controlled_world.player.set_position_data( Vector2( float(data["playerHitPos"]["x"]), @@ -38,31 +37,31 @@ func _on_match_state(p_state : NakamaRTAPI.MatchData): controlled_world.ui.text = "Graze: " + str(data["graze"]) # Handle player death if there is an ongoing death timer - if int(data["deathTimer"]) > 0: - controlled_world.player.kill() - elif int(data["deathTimer"]) == 0: - controlled_world.player.resurrect() + #if int(data["deathTimer"]) > 0: + # controlled_world.player.kill() + #elif int(data["deathTimer"]) == 0: + # controlled_world.player.resurrect() # Spawn new bullets - for b in data["newBullets"]: - var bullet = DanmakuBullet.new() - bullet.setup_bullet( - int(b["class"]), - int(b["tick"]), - b["x"], - b["y"], - b["radius"], - b["vel_x"], - b["vel_y"]) - bullet.texture = load("res://test-bullet.png") - bullet.position = bullet.get_current_pos(int(b["tick"])) - - # Reimplemented from ScalableSprite2D here atm - var scale_ratio = (b["radius"] * 2) / bullet.texture.get_width() - bullet.scale = Vector2(scale_ratio, scale_ratio) - bullet.z_index = 4 - - controlled_world.bullet_manager.add_child(bullet) - controlled_world.bullet_manager.bullets.append(bullet) - LocalTimer.predicted_tick = int(b["tick"]) - LocalTimer.delta_counter = 0 + #for b in data["serverEvents"]: + #var bullet = DanmakuBullet.new() + #bullet.setup_bullet( + #int(b["class"]), + #int(b["tick"]), + #b["x"], + #b["y"], + #b["radius"], + #b["vel_x"], + #b["vel_y"]) + #bullet.texture = load("res://test-bullet.png") + #bullet.position = bullet.get_current_pos(int(b["tick"])) + # + ## Reimplemented from ScalableSprite2D here atm + #var scale_ratio = (b["radius"] * 2) / bullet.texture.get_width() + #bullet.scale = Vector2(scale_ratio, scale_ratio) + #bullet.z_index = 4 + # + #controlled_world.bullet_manager.add_child(bullet) + #controlled_world.bullet_manager.bullets.append(bullet) + #LocalTimer.predicted_tick = int(b["tick"]) + #LocalTimer.delta_counter = 0 diff --git a/client/danmaku!/Player/player.gd b/client/danmaku!/Player/player.gd index 88b534f..28867ea 100644 --- a/client/danmaku!/Player/player.gd +++ b/client/danmaku!/Player/player.gd @@ -1,9 +1,11 @@ class_name Player extends Node2D -@export var speed = 80 @export var controlled = false -var velocity := Vector2.ZERO +const SPEED = 80 +var slow = false +var tick_vel := Vector2.ZERO +var delta_vel_since_last_broadcast := Vector2.ZERO var hurt_collision: DanmakuCircle = DanmakuCircle.new() var graze_collision: DanmakuCircle = DanmakuCircle.new() const PLAYER_BODY_WIDTH_MULTIPLIER = 0.18 @@ -12,25 +14,30 @@ var alive: bool = true func _ready() -> void: $BodySprite.scale_sprite(PLAYER_BODY_WIDTH_MULTIPLIER * Globals.SERVER_SIZE.x) -func get_input(): +func get_input(delta): + slow = false if Input.is_action_pressed("Slow Mode"): - speed = 30 - else: - speed = 80 + slow = true - velocity = Input.get_vector("Left", "Right", "Up", "Down") * speed + tick_vel = Input.get_vector("Left", "Right", "Up", "Down") * ((SPEED / 3) if slow else SPEED) * delta + delta_vel_since_last_broadcast += tick_vel func _physics_process(delta: float): - get_input() + get_input(delta) - if !alive: - return + #if !alive: + #return # Bounds checking - var attempted_position := position + (velocity * delta) - attempted_position = attempted_position.clamp(Vector2(0, 0), Globals.SERVER_SIZE) + #var attempted_position := position + (velocity * delta) + #attempted_position = attempted_position.clamp(Vector2(0, 0), Globals.SERVER_SIZE) - set_position_data(attempted_position, null, null) + #set_position_data(attempted_position, null, null) + +func get_and_reset_delta_vel(): + var ret = delta_vel_since_last_broadcast + delta_vel_since_last_broadcast = Vector2.ZERO + return ret func set_position_data(pos: Vector2, hurtcircle_radius, grazecircle_radius): position = pos @@ -45,7 +52,6 @@ func set_position_data(pos: Vector2, hurtcircle_radius, grazecircle_radius): graze_collision.set_radius(grazecircle_radius) $GrazecircleSprite.scale_sprite(grazecircle_radius*2) - func kill(): if alive == false: return diff --git a/server/Dockerfile b/server/Dockerfile index e1a2122..4273421 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -6,10 +6,17 @@ ENV CGO_ENABLED 1 WORKDIR /backend COPY . . -RUN go build --trimpath --buildmode=plugin -o ./backend.so +RUN apt-get update && \ + apt-get -y upgrade && \ + apt-get install -y --no-install-recommends gcc libc6-dev -FROM heroiclabs/nakama:3.26.0 +RUN go build --trimpath --gcflags "all=-N -l" --mod=vendor --buildmode=plugin -o ./backend.so +RUN go install github.com/go-delve/delve/cmd/dlv@latest + +FROM heroiclabs/nakama-dsym:3.26.0 COPY --from=builder /backend/backend.so /nakama/data/modules #COPY --from=builder /backend/local.yml /nakama/data/ -COPY --from=builder /backend/*.json /nakama/data/modules \ No newline at end of file +COPY --from=builder /backend/*.json /nakama/data/modules + +ENTRYPOINT [ "/bin/bash" ] \ No newline at end of file diff --git a/server/docker-compose.yaml b/server/docker-compose.yaml index 84525d8..ab5f5b1 100644 --- a/server/docker-compose.yaml +++ b/server/docker-compose.yaml @@ -24,7 +24,7 @@ services: - "-ecx" - > /nakama/nakama migrate up --database.address root@cockroachdb:26257 && - exec /nakama/nakama --name nakama1 --database.address root@cockroachdb:26257 --logger.level DEBUG --session.token_expiry_sec 7200 --metrics.prometheus_port 9100 + exec /nakama/nakama --name nakama1 --database.address root@cockroachdb:26257 --logger.level ERROR --session.token_expiry_sec 7200 --metrics.prometheus_port 9100 restart: "no" links: - "cockroachdb:db" diff --git a/server/ffi/ffi.go b/server/ffi/ffi.go index 75c0a94..f3b1d33 100644 --- a/server/ffi/ffi.go +++ b/server/ffi/ffi.go @@ -7,6 +7,7 @@ package ffi #include */ import "C" +import "strconv" // Circles type Circle struct { @@ -19,6 +20,14 @@ func NewCircle(x float64, y float64, rad float64) *Circle { } } +func (c *Circle) Clone() *Circle { + if c == nil { + return nil + } + x, y := c.GetPos() + return NewCircle(x, y, float64(c.cptr.radius)) +} + func (c *Circle) UpdatePos(x float64, y float64) { c.cptr.x = C.double(x) c.cptr.y = C.double(y) @@ -30,6 +39,7 @@ func DestroyCircle(circle *Circle) { } C.destroy_circle(circle.cptr) + circle.cptr = nil } func (c *Circle) GetPos() (float64, float64) { @@ -38,20 +48,18 @@ func (c *Circle) GetPos() (float64, float64) { // Bullets type Bullet struct { - cptr *C.Bullet - DeletionTick int64 + cptr *C.Bullet } // Values for selecting bullet paths const ( BULLET_LINEAR = 0 - ACTIVE_BULLET = -1 ) func NewLinearBullet(tick int64, spawnX float64, spawnY float64, radius float64, velX float64, velY float64) *Bullet { return &Bullet{ cptr: C.new_bullet( - C.uint8_t(0), + C.uint8_t(BULLET_LINEAR), C.int64_t(tick), C.double(spawnX), C.double(spawnY), @@ -59,7 +67,29 @@ func NewLinearBullet(tick int64, spawnX float64, spawnY float64, radius float64, C.double(velX), C.double(velY), ), - DeletionTick: ACTIVE_BULLET, + } +} + +func (b *Bullet) Clone() *Bullet { + if b == nil { + return nil + } + + switch int(b.cptr.class_) { + case BULLET_LINEAR: + nb := NewLinearBullet( + int64(b.cptr.spawn_time), + float64(b.cptr.spawn_x), + float64(b.cptr.spawn_y), + float64(b.cptr.radius), + float64(b.cptr.parameters[0]), // vel_x + float64(b.cptr.parameters[1]), // vel_y + ) + return nb + + default: + // Mirror other bullet classes here with their exact constructors. + panic("ffi.Bullet.Clone: unsupported bullet class: " + strconv.Itoa(int(b.cptr.class_))) } } @@ -69,6 +99,7 @@ func DestroyBullet(bullet *Bullet) { } C.destroy_bullet(bullet.cptr) + bullet.cptr = nil } func (b *Bullet) BeyondKillBoundary(tick int64) bool { @@ -94,13 +125,17 @@ func (b *Bullet) GetRadius() float64 { return float64(b.cptr.radius) } -func (b *Bullet) Serialize(tick int64) map[string]any { - x, y := b.GetPos(tick) +func (b *Bullet) GetSpawnTick() int64 { + return int64(b.cptr.spawn_time) +} + +func (b *Bullet) Serialize( /*tick int64*/ ) map[string]any { + //x, y := b.GetPos(tick) return map[string]any{ - "class": b.GetType(), - "tick": tick, - "x": x, - "y": y, + "class": b.GetType(), + "tick": b.GetSpawnTick(), + //"x": x, + //"y": y, "radius": b.GetRadius(), "vel_x": float64(b.cptr.parameters[0]), "vel_y": float64(b.cptr.parameters[1]), diff --git a/server/game-modes/battle-royale/consts.go b/server/game-modes/battle-royale/consts.go index 646e24f..6b60c52 100644 --- a/server/game-modes/battle-royale/consts.go +++ b/server/game-modes/battle-royale/consts.go @@ -7,6 +7,18 @@ const ( FINAL_PHASE MATCH_END ) +const ( + LEFT = iota + RIGHT + UP + DOWN +) + +const ( + ALIVE = iota + DEATH_QUEUED + DEAD +) const ( TICK_RATE = 60 diff --git a/server/game-modes/battle-royale/game-loop.go b/server/game-modes/battle-royale/game-loop.go index 2ebf6f9..678eba8 100644 --- a/server/game-modes/battle-royale/game-loop.go +++ b/server/game-modes/battle-royale/game-loop.go @@ -31,20 +31,22 @@ func StorePlayerInputs(lobbyState *MatchState, messages []runtime.MatchData, log continue } - var update ClientUpdate - if err := json.Unmarshal(msg.GetData(), &update); err != nil { + var newInput ClientUpdate + if err := json.Unmarshal(msg.GetData(), &newInput); err != nil { (*logger).Warn("Failed to parse input: %v", err) continue } - // Store the input in the player's stage state - lobbyState.presences[msg.GetSessionId()].stageState.playerInputs = append(lobbyState.presences[msg.GetSessionId()].stageState.playerInputs, &update) + // Store the input in the player's stage state if it arrived in time + if tick-newInput.Tick < GRACE_WINDOW_TICKS { + lobbyState.presences[msg.GetSessionId()].stageState.playerInputs = append(lobbyState.presences[msg.GetSessionId()].stageState.playerInputs, &newInput) + } } } func BroadcastToPresences(tick int64, lobbyState *MatchState, logger *runtime.Logger, dispatcher *runtime.MatchDispatcher) { for _, v := range lobbyState.presences { - var tickData = v.stageState.MakeServerTick(tick) + var tickData = v.stageState.SimulateAndMakeServerTick(tick) data, err := json.Marshal(tickData) if err != nil { @@ -71,10 +73,7 @@ func (m *Match) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB StorePlayerInputs(lobbyState, messages, &logger, tick) for _, playerState := range lobbyState.presences { - playerState.stageState.MarkBulletsBeyondKillBoundary(tick) - playerState.stageState.ProcessPlayerInputs(tick) - playerState.stageState.HandleDeath(tick) - playerState.stageState.CleanupOldBullets(tick) + playerState.stageState = playerState.stageState.ResolveLockedInState(tick) } BroadcastToPresences(tick, lobbyState, &logger, &dispatcher) diff --git a/server/game-modes/battle-royale/player-stage.go b/server/game-modes/battle-royale/player-stage.go index 9d13963..eba1592 100644 --- a/server/game-modes/battle-royale/player-stage.go +++ b/server/game-modes/battle-royale/player-stage.go @@ -5,189 +5,186 @@ import ( "danmaku/ffi" "math" "slices" - "time" ) type PlayerStageState struct { - hitCol *ffi.Circle - grazeCol *ffi.Circle - bullets []*ffi.Bullet - updatePlayerPos bool - health int - graze int - grazePerTick map[int64]int - deathTick int64 - dead bool - cancelDeath bool - survivedGraceWindow bool - playerInputs []*ClientUpdate - lastInput *ClientUpdate -} - -func (s *PlayerStageState) ProcessPlayerInputs(tick int64) { - // Lock-in the earned graze - cutoffTick := tick - GRACE_WINDOW_TICKS - for t, v := range s.grazePerTick { - if t < cutoffTick { - s.graze += v - delete(s.grazePerTick, t) - } - } - - // Clean up inputs outside the grace window - s.playerInputs = slices.DeleteFunc(s.playerInputs, func(input *ClientUpdate) bool { - return tick-input.Tick > GRACE_WINDOW_TICKS - }) - - // Sort inputs by tick - slices.SortFunc(s.playerInputs, func(a, b *ClientUpdate) int { - return cmp.Compare(a.Tick, b.Tick) - }) - - // Replay each tick within the grace window - s.survivedGraceWindow = true - for t := tick - GRACE_WINDOW_TICKS; t <= tick; t++ { - // Find the input for the current tick - var currentInput *ClientUpdate - for _, input := range s.playerInputs { - if input.Tick == t { - currentInput = input - break - } - } - - // Apply the correct movement or no movement - if currentInput != nil { - s.clampedMove(currentInput.X, currentInput.Y) - s.lastInput = currentInput - } else if s.lastInput != nil { - s.clampedMove(s.lastInput.X, s.lastInput.Y) - } - - // If the player dies in the grace window, don't cancel their death - if s.CheckPlayerDeadOnTick(t) { - s.survivedGraceWindow = false - - for grazeT := range s.grazePerTick { - if grazeT > t { - delete(s.grazePerTick, grazeT) - } - } - break - } else { - s.grazePerTick[t] = s.CalculateGrazeDelta(t) - } - } + lastLockedInTick int64 + hitCol *ffi.Circle + grazeCol *ffi.Circle + bullets []*ffi.Bullet + playerInputs []*ClientUpdate + deathState int + health int + graze int } func NewPlayerStage() *PlayerStageState { return &PlayerStageState{ - hitCol: ffi.NewCircle(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1, STAGE_WIDTH*PLAYER_HIT_COL_RADIUS_MULTIPLIER), - grazeCol: ffi.NewCircle(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1, STAGE_WIDTH*PLAYER_GRAZE_COL_RADIUS_MULTIPLIER), - bullets: []*ffi.Bullet{}, - updatePlayerPos: true, - grazePerTick: make(map[int64]int), - health: 3, - deathTick: PLAYER_ALIVE, + hitCol: ffi.NewCircle(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1, STAGE_WIDTH*PLAYER_HIT_COL_RADIUS_MULTIPLIER), + grazeCol: ffi.NewCircle(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1, STAGE_WIDTH*PLAYER_GRAZE_COL_RADIUS_MULTIPLIER), + bullets: []*ffi.Bullet{}, + lastLockedInTick: 0, + health: 3, + graze: 0, } } +func (s *PlayerStageState) DeepClone() *PlayerStageState { + clone := &PlayerStageState{ + lastLockedInTick: s.lastLockedInTick, + health: s.health, + graze: s.graze, + } + + // Circles + if s.hitCol != nil { + clone.hitCol = s.hitCol.Clone() + } + if s.grazeCol != nil { + clone.grazeCol = s.grazeCol.Clone() + } + + // Bullets + if s.bullets != nil { + clone.bullets = make([]*ffi.Bullet, len(s.bullets)) + for i, b := range s.bullets { + clone.bullets[i] = b.Clone() + } + } + + // Player inputs + if s.playerInputs != nil { + clone.playerInputs = make([]*ClientUpdate, len(s.playerInputs)) + for i, u := range s.playerInputs { + if u == nil { + continue + } + clone.playerInputs[i] = &ClientUpdate{ + VelX: u.VelX, + VelY: u.VelY, + Tick: u.Tick, + } + } + } + + return clone +} + func (s *PlayerStageState) Delete() { for _, bullet := range s.bullets { ffi.DestroyBullet(bullet) } + s.bullets = nil + ffi.DestroyCircle(s.hitCol) ffi.DestroyCircle(s.grazeCol) + s.hitCol = nil + s.grazeCol = nil } -func (s *PlayerStageState) clampedMove(x, y float64) bool { - clampedX := x < 0 || x > STAGE_WIDTH - clampedY := y < 0 || y > STAGE_HEIGHT +func (s *PlayerStageState) ResolveLockedInState(tick int64) *PlayerStageState { + // Rotate the player's input queue and lock in the results of the oldest inputs if they're outside the grace window + // Sort inputs by tick + slices.SortFunc(s.playerInputs, func(a, b *ClientUpdate) int { + return cmp.Compare(a.Tick, b.Tick) + }) + + // Clean up inputs outside the grace window and store them in a separate list + var lockedInInputs, inputQueue []*ClientUpdate + for _, input := range s.playerInputs { + if tick-input.Tick < GRACE_WINDOW_TICKS { + inputQueue = append(inputQueue, input) + } else { + lockedInInputs = append(lockedInInputs, input) + } + } + + newLockedInState := s.NewStateFromInputs(lockedInInputs, tick-GRACE_WINDOW_TICKS) + newLockedInState.playerInputs = inputQueue + s.Delete() + + return newLockedInState +} + +func (s *PlayerStageState) NewStateFromInputs(inputs []*ClientUpdate, tickToSimulateTo int64) *PlayerStageState { + stageStateCopy := s.DeepClone() + + // Replay each tick between the last locked in tick and the tick to simulate to + for t := stageStateCopy.lastLockedInTick; t < tickToSimulateTo; t++ { + // Handle inputs for the current tick on the client + for _, input := range inputs { + if input.Tick == t { + stageStateCopy.ApplyUpdate(input) + } + } + + // Filter bullets in place + stageStateCopy.bullets = slices.DeleteFunc(stageStateCopy.bullets, func(b *ffi.Bullet) bool { + if b.BeyondKillBoundary(t) { + ffi.DestroyBullet(b) + return true // remove from slice + } + + // Otherwise, handle collisions + if b.CollidesWith(stageStateCopy.hitCol, t) && stageStateCopy.deathState == ALIVE { + // TODO fix whatever this weird sliding issue is on the client + // TODO handle the issue of the transient state being used to compute bullets + // i.e. where do those bullets go? in the past? in the future? + // TODO bullets are impacting the past + // TODO Then replay, since bullets should be working again + // TODO: Finally, handle death and revival using deathState + + ffi.DestroyBullet(b) + return true + } + + if b.CollidesWith(stageStateCopy.grazeCol, t) && stageStateCopy.deathState == ALIVE { + stageStateCopy.graze += GRAZE_ADDITION_MULTIPLIER + } + return false // keep bullet + }) + + // Handle server events + stageStateCopy.MakeServerEvents(t) + } + + stageStateCopy.lastLockedInTick = tickToSimulateTo + return stageStateCopy +} + +func (s *PlayerStageState) ApplyUpdate(update *ClientUpdate) { + dx := update.VelX + dy := update.VelY + + if dx == 0 && dy == 0 { + return + } + + // clamp max movement distance to 100 units + if dist := math.Hypot(dx, dy); dist > 100 { + scale := 100 / dist + dx *= scale + dy *= scale + } + + s.clampedMoveDelta(dx, dy) +} + +// Move by a delta +func (s *PlayerStageState) clampedMoveDelta(dx, dy float64) bool { + posX, posY := s.hitCol.GetPos() + x := posX + dx + y := posY + dy newX := math.Max(0, math.Min(x, STAGE_WIDTH)) newY := math.Max(0, math.Min(y, STAGE_HEIGHT)) + clamped := (newX != x) || (newY != y) + s.hitCol.UpdatePos(newX, newY) s.grazeCol.UpdatePos(newX, newY) - return clampedX || clampedY -} - -func (s *PlayerStageState) MarkBulletsBeyondKillBoundary(tick int64) { - for _, b := range s.bullets { - if b.BeyondKillBoundary(tick) && b.DeletionTick == ACTIVE_BULLET { - b.DeletionTick = tick - } - } -} - -func (s *PlayerStageState) CleanupOldBullets(tick int64) { - s.bullets = slices.DeleteFunc(s.bullets, func(b *ffi.Bullet) bool { - if b.DeletionTick > ACTIVE_BULLET && tick-b.DeletionTick > GRACE_WINDOW_TICKS { - ffi.DestroyBullet(b) - return true - } - return false - }) -} - -func (s *PlayerStageState) Kill(tick int64) { - s.deathTick = tick - s.dead = true -} - -func (s *PlayerStageState) Revive() { - s.hitCol.UpdatePos(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1) - s.grazeCol.UpdatePos(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1) - s.updatePlayerPos = true - s.playerInputs = nil - s.dead = false - s.deathTick = PLAYER_ALIVE -} - -func (s *PlayerStageState) CancelDeath() { - s.dead = false - s.deathTick = PLAYER_ALIVE - s.cancelDeath = true -} - -func (s *PlayerStageState) HandleDeath(tick int64) { - // If the player didn't survive the grace window and they're currently alive, kill them - if !(s.survivedGraceWindow) && (s.dead == false) { - s.Kill(tick) - return - } - - // If the player is currently dead and they survived the grace window, cancel their death - if (s.dead == true) && s.survivedGraceWindow { - s.CancelDeath() - return - } - - // If the player is currently dead and they have been dead for greater than PLAYER_DEATH_TIMER_MAX ticks, revive them - if s.dead == true && ((tick - s.deathTick) > PLAYER_DEATH_TIMER_MAX) { - s.Revive() - return - } -} - -func (s *PlayerStageState) CheckPlayerDeadOnTick(tick int64) bool { - if slices.ContainsFunc(s.bullets, func(b *ffi.Bullet) bool { - return b.CollidesWith(s.hitCol, tick) - }) { - return true - } - return false -} - -func (s *PlayerStageState) CalculateGrazeDelta(tick int64) int { - count := 0 - for _, b := range s.bullets { - if b.CollidesWith(s.grazeCol, tick) { - count++ - } - } - return count * GRAZE_ADDITION_MULTIPLIER + return clamped } func (s *PlayerStageState) AddBullet(b *ffi.Bullet) { @@ -228,27 +225,32 @@ func MakeTestFireBullets(tick int64) []*ffi.Bullet { return bullets } -func (s *PlayerStageState) GetBoardStateDiff(tick int64) []map[string]any { - var newBulletsToBroadcast = []map[string]any{} - if !s.dead { +// Contains most timed server-side game logic +func (s *PlayerStageState) MakeServerEvents(tick int64) { + if s.deathState == ALIVE { newBullets := MakeTestFireBullets(tick) for _, bullet := range newBullets { s.AddBullet(bullet) - newBulletsToBroadcast = append(newBulletsToBroadcast, bullet.Serialize(tick)) } } - return newBulletsToBroadcast } -func (s *PlayerStageState) MakeServerTick(tick int64) *ServerTickUpdate { - hitPosX, hitPosY := s.hitCol.GetPos() - grazePosX, grazePosY := s.grazeCol.GetPos() +func (s *PlayerStageState) SerializeServerEventsWithinGraceWindow(tick int64) []map[string]any { + var bulletsWithinGraceWindow = []map[string]any{} + for _, bullet := range s.bullets { + if bullet.GetSpawnTick() > tick-GRACE_WINDOW_TICKS { + bulletsWithinGraceWindow = append(bulletsWithinGraceWindow, bullet.Serialize()) + } - prospectiveGraze := 0 - for _, v := range s.grazePerTick { - prospectiveGraze += v } + return bulletsWithinGraceWindow +} + +func (s *PlayerStageState) SimulateAndMakeServerTick(tick int64) *ServerTickUpdate { + transientCurrentState := s.NewStateFromInputs(s.playerInputs, tick) + hitPosX, hitPosY := transientCurrentState.hitCol.GetPos() + grazePosX, grazePosY := transientCurrentState.grazeCol.GetPos() var tickData = ServerTickUpdate{ Tick: tick, @@ -262,17 +264,11 @@ func (s *PlayerStageState) MakeServerTick(tick int64) *ServerTickUpdate { "y": grazePosY, "radius": STAGE_WIDTH * PLAYER_GRAZE_COL_RADIUS_MULTIPLIER, }, - StageStateDiff: s.GetBoardStateDiff(tick), - ForcePlayerPos: s.updatePlayerPos, - Graze: s.graze + prospectiveGraze, - Dead: s.dead, - CancelDeath: s.cancelDeath, - DeathTick: s.deathTick, - UTCTime: float64(time.Now().UnixMilli()) / 1000.0, + ServerEvents: s.SerializeServerEventsWithinGraceWindow(tick), + Graze: transientCurrentState.graze, + DeathState: transientCurrentState.deathState, } - s.cancelDeath = false - s.updatePlayerPos = false - + transientCurrentState.Delete() return &tickData } diff --git a/server/game-modes/battle-royale/types.go b/server/game-modes/battle-royale/types.go index 9fe299c..1e4e29f 100644 --- a/server/game-modes/battle-royale/types.go +++ b/server/game-modes/battle-royale/types.go @@ -23,8 +23,8 @@ type PresenceState struct { // present time! hahahahahahahah! // Struct to serialize client->server updates type ClientUpdate struct { - X float64 `json:"x"` - Y float64 `json:"y"` + VelX float64 `json:"vel_x"` + VelY float64 `json:"vel_y"` Tick int64 `json:"tick"` } @@ -33,11 +33,7 @@ type ServerTickUpdate struct { Tick int64 `json:"tick"` PlayerHitPos map[string]any `json:"playerHitPos"` PlayerGrazePos map[string]any `json:"playerGrazePos"` - StageStateDiff []map[string]any `json:"stageStateDiff"` - ForcePlayerPos bool `json:"forcePlayerPos"` - Dead bool `json:"dead"` - CancelDeath bool `json:"cancelDeath"` - DeathTick int64 `json:"deathTick"` + ServerEvents []map[string]any `json:"serverEvents"` + DeathState int `json:"deathState"` Graze int `json:"graze"` - UTCTime float64 `json:"utctime"` }