mirror of
https://github.com/exogen/t2-mapper.git
synced 2026-07-13 07:25:07 +00:00
improve observer mode and disconnect
This commit is contained in:
parent
25b4a53a00
commit
78e791f763
39 changed files with 877 additions and 837 deletions
|
|
@ -4,6 +4,7 @@
|
|||
left: 6px;
|
||||
width: 400px;
|
||||
max-width: 50%;
|
||||
max-height: 25%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
pointer-events: auto;
|
||||
|
|
|
|||
|
|
@ -185,9 +185,6 @@ export function InputConsumer() {
|
|||
const tickMoveZ = useRef(0);
|
||||
const tickTriggers = useRef([false, false, false, false, false, false]);
|
||||
|
||||
// Previous trigger state for edge detection.
|
||||
const prevTriggers = useRef([false, false, false, false, false, false]);
|
||||
|
||||
const isLive =
|
||||
!!adapter &&
|
||||
(gameStatus === "connected" || gameStatus === "authenticating");
|
||||
|
|
@ -241,8 +238,9 @@ export function InputConsumer() {
|
|||
|
||||
// Reset prediction state and mode when liveReady goes false (map cycle).
|
||||
// During map transitions, liveReady is false from MissionStartPhase1 until
|
||||
// the first ghost arrives on the new map. The adapter resets observerMode
|
||||
// to "fly" at phase 1, and we mirror that here.
|
||||
// the first ghost arrives on the new map. Reset to "fly" here; the server-
|
||||
// authoritative mode derivation in useFrame will correct it once the first
|
||||
// camera snapshot arrives on the new map.
|
||||
useEffect(() => {
|
||||
if (!liveReady && activeAdapterRef.current) {
|
||||
log.info("mission change: resetting prediction state and mode");
|
||||
|
|
@ -259,7 +257,6 @@ export function InputConsumer() {
|
|||
tickMoveY.current = 0;
|
||||
tickMoveZ.current = 0;
|
||||
tickTriggers.current.fill(false);
|
||||
prevTriggers.current.fill(false);
|
||||
setMode("fly");
|
||||
}
|
||||
}, [liveReady, setMode]);
|
||||
|
|
@ -285,16 +282,6 @@ export function InputConsumer() {
|
|||
const triggers = [...tickTriggers.current];
|
||||
tickTriggers.current.fill(false);
|
||||
|
||||
// Trigger edge detection.
|
||||
if (triggers[2] && !prevTriggers.current[2]) {
|
||||
activeAdapterRef.current.toggleObserverMode();
|
||||
log.info("observer mode: %s", activeAdapterRef.current.observerMode);
|
||||
setMode(
|
||||
activeAdapterRef.current.observerMode === "follow" ? "follow" : "fly",
|
||||
);
|
||||
}
|
||||
prevTriggers.current = triggers;
|
||||
|
||||
// ── Camera::processTick equivalent ──
|
||||
|
||||
// Quantize rotation to match server's Move::clamp/unclamp round-trip.
|
||||
|
|
@ -556,6 +543,26 @@ export function InputConsumer() {
|
|||
|
||||
predInitialized.current = true;
|
||||
|
||||
// Derive observer mode from the server's camera data — the server
|
||||
// is authoritative. "third-person" with a camera control type means
|
||||
// the server's Camera is in OrbitObject mode (follow). "observer"
|
||||
// means free-fly. This corrects local state if a trigger 2 move
|
||||
// was lost or the server rejected the mode transition.
|
||||
const serverMode = serverCam.mode === "third-person" ? "follow" : "fly";
|
||||
if (serverMode !== mode) {
|
||||
log.info("server corrected observer mode: %s → %s", mode, serverMode);
|
||||
setMode(serverMode);
|
||||
if (activeAdapterRef.current) {
|
||||
activeAdapterRef.current.observerMode = serverMode;
|
||||
}
|
||||
// Reset orbit tracking when leaving follow mode so it re-initializes
|
||||
// cleanly next time we enter follow.
|
||||
if (serverMode === "fly") {
|
||||
orbitTargetInitialized.current = false;
|
||||
lastOrbitSnapshot.current = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize orbit target position on first reconciliation with
|
||||
// orbit data, so follow mode works immediately after mode switch.
|
||||
if (serverCam.orbitTargetId && !orbitTargetInitialized.current) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export function JoinServerButton({
|
|||
// const serverName = useLiveSelector((s) => s.serverName);
|
||||
const ping = useLiveSelector(selectPing);
|
||||
const disconnectServer = useLiveSelector((s) => s.disconnectServer);
|
||||
const disconnectRelay = useLiveSelector((s) => s.disconnectRelay);
|
||||
|
||||
const isLive = gameStatus === "connected";
|
||||
const isConnecting =
|
||||
|
|
@ -34,7 +33,6 @@ export function JoinServerButton({
|
|||
onClick={() => {
|
||||
if (isLive) {
|
||||
disconnectServer();
|
||||
disconnectRelay();
|
||||
} else {
|
||||
onOpenServerBrowser();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ export function LoadDemoButton({
|
|||
// Disconnect from any live server before loading the demo.
|
||||
const liveState = liveConnectionStore.getState();
|
||||
liveState.disconnectServer();
|
||||
liveState.disconnectRelay();
|
||||
// Metadata-first: mission/game-mode sync happens immediately.
|
||||
setRecording(recording);
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@ export function MapInspector() {
|
|||
// clear stream state before loading the new mission in map mode.
|
||||
const liveState = liveConnectionStore.getState();
|
||||
liveState.disconnectServer();
|
||||
liveState.disconnectRelay();
|
||||
engineStore.getState().setRecording(null);
|
||||
gameEntityStore.getState().endStreaming();
|
||||
setCurrentMission(mission);
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@
|
|||
transform: translate(-50%, -50%);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: rgba(200, 240, 230, 0.9);
|
||||
text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
|
||||
pointer-events: none;
|
||||
|
|
@ -149,7 +148,7 @@
|
|||
.TeamScore {
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
text-align: center;
|
||||
padding: 0 10px;
|
||||
border-left: 1px solid rgba(128, 255, 200, 0.15);
|
||||
}
|
||||
|
|
@ -157,6 +156,7 @@
|
|||
.TeamCount {
|
||||
color: rgb(125, 155, 150);
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ export function StreamingMissionInfo() {
|
|||
const handleDisconnect = useCallback(() => {
|
||||
const liveState = liveConnectionStore.getState();
|
||||
liveState.disconnectServer();
|
||||
liveState.disconnectRelay();
|
||||
engineStore.getState().setRecording(null);
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -243,17 +243,9 @@ export const liveConnectionStore = createStore<LiveConnectionStore>(
|
|||
},
|
||||
|
||||
disconnectServer() {
|
||||
const s = get();
|
||||
s._relay?.disconnectServer();
|
||||
s._adapter = null;
|
||||
set({
|
||||
adapter: null,
|
||||
liveReady: false,
|
||||
gameStatus: null,
|
||||
mapName: undefined,
|
||||
serverName: undefined,
|
||||
relayToGameServerPing: null,
|
||||
});
|
||||
// Close the WebSocket — the relay's ws.on("close") handler will
|
||||
// automatically send the disconnect packet to the game server.
|
||||
get().disconnectRelay();
|
||||
},
|
||||
|
||||
sendMoves(moves, moveStartIndex) {
|
||||
|
|
|
|||
|
|
@ -217,7 +217,13 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
protected teamScores: TeamScore[] = [];
|
||||
protected playerRoster = new Map<
|
||||
number,
|
||||
{ name: string; teamId: number; score: number; ping: number; packetLoss: number }
|
||||
{
|
||||
name: string;
|
||||
teamId: number;
|
||||
score: number;
|
||||
ping: number;
|
||||
packetLoss: number;
|
||||
}
|
||||
>();
|
||||
/** Stream time (seconds) when the clock was last set. */
|
||||
protected clockAnchorStreamSec: number | null = null;
|
||||
|
|
@ -548,7 +554,10 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
if (targetId != null && nameTag != null) {
|
||||
const resolved = this.netStrings.get(nameTag);
|
||||
if (resolved) {
|
||||
this.targetNames.set(targetId, stripTaggedStringMarkup(resolved).trim());
|
||||
this.targetNames.set(
|
||||
targetId,
|
||||
stripTaggedStringMarkup(resolved).trim(),
|
||||
);
|
||||
} else {
|
||||
// NetStringEvent hasn't arrived yet — defer resolution.
|
||||
this.pendingNameTags.set(nameTag, targetId);
|
||||
|
|
@ -1921,10 +1930,7 @@ export abstract class StreamEngine implements StreamingPlayback {
|
|||
if (existing) {
|
||||
const score = parseInt(this.resolveNetString(args[3]), 10);
|
||||
const ping = parseInt(this.resolveNetString(args[4]), 10);
|
||||
const packetLoss = parseInt(
|
||||
this.resolveNetString(args[5] ?? ""),
|
||||
10,
|
||||
);
|
||||
const packetLoss = parseInt(this.resolveNetString(args[5] ?? ""), 10);
|
||||
if (!isNaN(score)) existing.score = score;
|
||||
if (!isNaN(ping)) existing.ping = ping;
|
||||
if (!isNaN(packetLoss)) existing.packetLoss = packetLoss;
|
||||
|
|
|
|||
|
|
@ -121,7 +121,13 @@ interface ParsedDemoValues {
|
|||
teamScores: TeamScore[];
|
||||
playerRoster: Map<
|
||||
number,
|
||||
{ name: string; teamId: number; score: number; ping: number; packetLoss: number }
|
||||
{
|
||||
name: string;
|
||||
teamId: number;
|
||||
score: number;
|
||||
ping: number;
|
||||
packetLoss: number;
|
||||
}
|
||||
>;
|
||||
chatMessages: string[];
|
||||
/** Value from clockHud.getTime() — minutes passed to setTime(). */
|
||||
|
|
@ -171,7 +177,13 @@ function parseDemoValues(demoValues: string[]): ParsedDemoValues {
|
|||
const ping = parseInt(fields[6], 10) || 0;
|
||||
const packetLoss = parseInt(fields[7], 10) || 0;
|
||||
if (!isNaN(clientId) && !isNaN(teamId)) {
|
||||
result.playerRoster.set(clientId, { name, teamId, score, ping, packetLoss });
|
||||
result.playerRoster.set(clientId, {
|
||||
name,
|
||||
teamId,
|
||||
score,
|
||||
ping,
|
||||
packetLoss,
|
||||
});
|
||||
}
|
||||
if (!isNaN(teamId) && teamId > 0) {
|
||||
playerCountByTeam.set(teamId, (playerCountByTeam.get(teamId) ?? 0) + 1);
|
||||
|
|
|
|||
|
|
@ -305,23 +305,12 @@ export class LiveStreamAdapter extends StreamEngine {
|
|||
}
|
||||
|
||||
/**
|
||||
* Server-side observer camera mode. In "fly" mode, trigger 0 (fire) would
|
||||
* make the server assign a team — so we must NEVER send fire in fly mode.
|
||||
* Jump (trigger 2) transitions between modes.
|
||||
* Observer camera mode, kept in sync with the server's camera state.
|
||||
* In "fly" mode, trigger 0 (fire) would make the server assign a team —
|
||||
* so we must NEVER send fire in fly mode.
|
||||
*/
|
||||
observerMode: "fly" | "follow" = "fly";
|
||||
|
||||
/** Toggle between follow and free-fly observer modes (local state only). */
|
||||
toggleObserverMode(): void {
|
||||
if (this.observerMode === "fly") {
|
||||
log.info("observer: fly → follow");
|
||||
this.observerMode = "follow";
|
||||
} else {
|
||||
log.info("observer: follow → fly");
|
||||
this.observerMode = "fly";
|
||||
}
|
||||
}
|
||||
|
||||
/** Request updated scores from the server (triggers MsgPlayerScore messages). */
|
||||
requestScores(): void {
|
||||
this.relay.sendCommand("getScores", []);
|
||||
|
|
|
|||
|
|
@ -134,11 +134,6 @@ export class RelayClient {
|
|||
this.send({ type: "joinServer", address, warriorName });
|
||||
}
|
||||
|
||||
/** Disconnect from the current game server. */
|
||||
disconnectServer(): void {
|
||||
this.send({ type: "disconnect" });
|
||||
}
|
||||
|
||||
/** Forward a T2csri auth event to the relay. */
|
||||
sendAuthEvent(command: string, args: string[]): void {
|
||||
this.send({ type: "sendCommand", command, args });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue