Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/GlueballBuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
types:
- created
workflow_dispatch: {}
push:

permissions:
id-token: write
Expand Down
2 changes: 1 addition & 1 deletion fission/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"fmt:fix": "bunx biome format --write",
"style": "bunx biome check",
"style:fix": "bunx biome check --write",
"assetpack": "git lfs pull && (rm -rf public/Downloadables;tar -xf public/assetpack.zip -C public/)",
"assetpack": "git lfs pull && (rm -rf public/Downloadables;(unzip public/assetpack.zip -d public/ || tar -xf public/assetpack.zip -C public/))",
"assetpack:update": "bun update_manifest.ts && cd public && zip -FS -r assetpack.zip Downloadables -x '**/.*' -x '**/__MACOSX'",
"assetpack:merge": "git checkout --theirs public/assetpack.zip && rm -rf public/Downloadables && tar -xf public/assetpack.zip -C public/ && git checkout --ours public/assetpack.zip && tar -xf public/assetpack.zip -C public/ && bun run assetpack:update",
"playwright:install": "bun x playwright install",
Expand Down
14 changes: 8 additions & 6 deletions fission/src/Synthesis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ import { ThemeProvider } from "./ui/ThemeProvider.tsx"
import { UIProvider } from "./ui/UIProvider.tsx"
import CommandPalette from "@/ui/components/CommandPalette.tsx"
import SessionStorage, { applyAutoToast } from "@/util/SessionStorage.ts"
import MultiplayerWebsocket from "@/systems/multiplayer/MultiplayerWebsocket.ts"
import { globalOpenModal } from "@/components/GlobalUIControls.ts"
import { startMultiplayerWorld } from "@/ui/helpers/StartMultiplayerWorld.ts"
import { Stack } from "@mui/material"
import MultiplayerWebtransport from "@/systems/multiplayer/MultiplayerWebtransport.ts"

const Synthesis = () => {
const [consentPopupDisable, setConsentPopupDisable] = useState<boolean>(true)
Expand Down Expand Up @@ -54,11 +54,13 @@ const Synthesis = () => {
if (urlParams.has("autojoin")) {
const room = urlParams.get("autojoin")!
const name = PreferencesSystem.getUserPreference("MultiplayerUsername") ?? "TestUser"
const ws = new MultiplayerWebsocket(
`ws${PreferencesSystem.getUserPreference("MultiplayerSecure") ? "s" : ""}://${PreferencesSystem.getUserPreference("MultiplayerHost") || "127.0.0.1"}:${PreferencesSystem.getUserPreference("MultiplayerPort")}`
)
MultiplayerWebsocket.init(room || null, name, ws)
setTimeout(() => startMultiplayerWorld({ displayName: name, ws, keepAssets: false, isHost: false }))
MultiplayerWebtransport.create(
`https://${PreferencesSystem.getUserPreference("MultiplayerHost") || "127.0.0.1"}:${PreferencesSystem.getUserPreference("MultiplayerPort")}`
).then(ws => {
if (ws === null) return
MultiplayerWebtransport.init(room || null, name, ws)
setTimeout(() => startMultiplayerWorld({ displayName: name, ws, keepAssets: false, isHost: false }))
})
}

applyAutoToast()
Expand Down
8 changes: 6 additions & 2 deletions fission/src/mirabuf/MirabufSceneObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1374,10 +1374,14 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
return data
}

public getUpdateData(): UpdateObjectData {
const gamePiecesControlled: RigidNodeId[] = this.activeEjectables.map(
public getGamePiecesControlled(): RigidNodeId[] {
return this.activeEjectables.map(
bodyId => (<RigidNodeAssociate>World.physicsSystem.getBodyAssociation(bodyId)).rigidNodeId
)
}

public getUpdateData(): UpdateObjectData {
const gamePiecesControlled = this.getGamePiecesControlled()

const bodies = this.getAllBodies()
.map(body => World.physicsSystem.getBodyUpdateData(body))
Expand Down
30 changes: 22 additions & 8 deletions fission/src/systems/multiplayer/MessageHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function handleUpdateMessage(data: UpdateBody, peerId: string, timestamp: number

// Handle Scene object update

const { sceneObjectKey, gamePiecesControlled, bodies } = data.sceneObject
const { objId: sceneObjectKey, gamePiecesControlled } = data

const sceneObject = World.sceneRenderer.sceneObjects.get(sceneObjectKey)
if (sceneObject == null) {
Expand Down Expand Up @@ -131,22 +131,36 @@ function handleUpdateMessage(data: UpdateBody, peerId: string, timestamp: number
return sceneObject.setEjectable(bodyId)
})
}
}

handleUpdateObjectPhysics(sceneObject, bodies, peerId)
/// Last applied timestamp per body, keyed by scene object then rigid node. Bodies
/// now arrive as independent messages, so two updates for the same body can turn up
/// out of order and a stale one must not overwrite a fresher one.
const bodyToUpdateMap = new Map<SceneObjectId, Map<string, number>>()

data.touchedBodies.forEach(data => handleUpdatePhysicsBody(data, peerId, timestamp))
}
function handleUpdatePhysicsBody(data: UpdatePhysicsBodyData, peerId: string, timestamp: number) {
const [objId, body] = data
const rnId = body[0]

let bodyTimestamps = bodyToUpdateMap.get(objId)
if (bodyTimestamps == null) {
bodyTimestamps = new Map()
bodyToUpdateMap.set(objId, bodyTimestamps)
}

const lastTimestamp = bodyTimestamps.get(rnId)
if (lastTimestamp != null && lastTimestamp > timestamp) return
bodyTimestamps.set(rnId, timestamp)

function handleUpdatePhysicsBody(data: UpdatePhysicsBodyData, peerId: string, _timestamp: number) {
// We only want to send it through the mapping if it's not a game piece we own
const sceneObject = World.sceneRenderer.sceneObjects.get(data.sceneObjectId) as MirabufSceneObject
const bodyId = sceneObject.mechanism.getBodyByNodeId(data.rigidNodeId)
const sceneObject = World.sceneRenderer.sceneObjects.get(objId) as MirabufSceneObject | undefined
const bodyId = sceneObject?.mechanism.getBodyByNodeId(rnId)
if (bodyId == null) {
console.error(`BodyId: ${bodyId} sent by ${peerId} does not exist in bodyMap`)
return
}

applyPhysicsBodyData(bodyId, data)
applyPhysicsBodyData(bodyId, body)
}

function handleCollisionMessage() {
Expand Down
43 changes: 30 additions & 13 deletions fission/src/systems/multiplayer/MultiplayerMessageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,28 +80,45 @@ export type NeedAssemblyBody = {
assemblyHash: string
}

/**
* The part of a robot's state that isn't attached to any one body. Its bodies
* travel separately, as one `updatePhysicsBody` per body.
*/
export type UpdateBody = {
sceneObject: UpdateObjectData
touchedBodies: UpdatePhysicsBodyData[]
objId: SceneObjectId
gamePiecesControlled: RigidNodeId[] // rnIds within the field, since there's only one
}

export type CollisionBody = UpdateObjectData[]
export type UpdateObjectData = {
sceneObjectKey: SceneObjectId
gamePiecesControlled: RigidNodeId[] // rnIds within the field, since there's only one
bodies: PhysicsBodyData[]
}

export type UpdatePhysicsBodyData = {
sceneObjectId: SceneObjectId
} & PhysicsBodyData
/**
* One body's state, addressed by the scene object owning it.
*
* Each of these is sent as its own message so it fits in a datagram, and so that
* losing one costs a single body for a single tick.
*/
export type UpdatePhysicsBodyData = [objId: SceneObjectId, body: PhysicsBodyData]

export type PhysicsBodyData = {
rigidNodeId: RigidNodeId // rnIds are relative to their scene object, so be sure to send the id for that too
// {x, y, z, w?}
linearVelocityStr: string
angularVelocityStr: string
positionStr: string
rotationStr: string
}
/**
* One body's physics state.
*
* Packed as a tuple rather than an object: these go out per physics tick, and the
* field names cost more on the wire than the numbers they label. The order below
* *is* the wire schema, so a change here has to land on both the producer
* (`PhysicsSystem`) and the consumer (`UpdatePhysicsData`) at once.
*/
export type PhysicsBodyData = [
// rnIds are relative to their scene object, so be sure to send the id for that too
rnId: RigidNodeId,
linVel: [x: number, y: number, z: number],
rotVel: [x: number, y: number, z: number],
pos: [x: number, y: number, z: number],
rot: [x: number, y: number, z: number, w: number],
]

export type LatencyInfoBody = { latencyMS: number }
24 changes: 10 additions & 14 deletions fission/src/systems/multiplayer/MultiplayerSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import type { ClientAndLatencyInfo, ClientInfo, Message, MessageWithTimestamp }
import EventSystem from "@/systems/EventSystem.ts"
import type { ServerToClientMessage } from "@/systems/multiplayer/bindings/ServerToClientMessage.ts"
import { consolePrefixer } from "console-prefixer"
import type MultiplayerWebsocket from "@/systems/multiplayer/MultiplayerWebsocket.ts"
import { hashBuffer } from "@/util/Utility.ts"
import { mirabuf } from "@/proto/mirabuf"
import type { SceneObjectId } from "@/systems/scene/SceneRenderer.ts"
import MatchMode from "../match_mode/MatchMode.ts"
import type { MultiplayerTransport } from "@/systems/multiplayer/MultiplayerTransport.ts"

export const COLLISION_TIMEOUT = 500

Expand All @@ -24,7 +24,7 @@ export const multiplayerLogger = consolePrefixer({
const console = multiplayerLogger

class MultiplayerSystem {
public readonly client: MultiplayerWebsocket
public readonly client: MultiplayerTransport
public roomId: string = ""
public clientId: string = ""
private _initializationPromise: Promise<boolean>
Expand All @@ -47,7 +47,7 @@ class MultiplayerSystem {

public sinceLastUpdate = 0

public static async setup(ws: MultiplayerWebsocket, displayName: string, isHost: boolean): Promise<boolean> {
public static async setup(ws: MultiplayerTransport, displayName: string, isHost: boolean): Promise<boolean> {
MatchMode.getInstance().sandboxModeStart()

console.group("Multiplayer initialization")
Expand All @@ -61,7 +61,7 @@ class MultiplayerSystem {
return initResult
}

private constructor(ws: MultiplayerWebsocket, displayName: string, isHost: boolean) {
private constructor(ws: MultiplayerTransport, displayName: string, isHost: boolean) {
this.isHost = isHost
this.client = ws

Expand Down Expand Up @@ -136,7 +136,6 @@ class MultiplayerSystem {
}

async handleServerMessage(message: ServerToClientMessage) {
console.debug(`Incoming server message ${message.type}`)
switch (message.type) {
case "sendinfo":
this.roomId = message.room_id
Expand Down Expand Up @@ -172,15 +171,12 @@ class MultiplayerSystem {
console.info("Ignoring message for", message.recipientId)
return
}
if (message.type != "update") {
console.info(`Receiving Message ${message.type}`, message)
const baseHandler = peerMessageHandlers[message.type]
if (baseHandler == null) {
console.error("Unknown message recieved:", message.type, message.data)
return message.type
}

const handler = peerMessageHandlers[message.type].bind(this) as (
data: unknown,
peerid: string,
time: number
) => Promise<void> | void
const handler = baseHandler.bind(this) as (data: unknown, peerid: string, time: number) => Promise<void> | void
await handler(message.data, message.clientId, message.timestamp)
return message.type
}
Expand All @@ -196,7 +192,7 @@ class MultiplayerSystem {
if (message.type == "newObject" && message.data.miraType == MiraType.FIELD) {
this.fieldTransferLock = { ts: message.timestamp, id: message.data.sceneObjectId }
}
this.client.sendPeer(message as MessageWithTimestamp)
this.client.sendPeer(message as MessageWithTimestamp, message.type == "update")
}

async introduceSelf(requestIntroductions: boolean, peerID?: string) {
Expand Down
46 changes: 46 additions & 0 deletions fission/src/systems/multiplayer/MultiplayerTransport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { MessageWithTimestamp } from "@/systems/multiplayer/MultiplayerTypes.ts"
import type { ClientToServerMessage } from "@/systems/multiplayer/bindings/ClientToServerMessage.ts"
import type { ServerToClientMessage } from "@/systems/multiplayer/bindings/ServerToClientMessage.ts"

export const CLIENT_PREFIX = 0b00000001
export const SERVER_PREFIX = 0b00000011

export abstract class MultiplayerTransport {
public onServerMessage?: (msg: ServerToClientMessage) => void
public onPeerMessage?: (msg: MessageWithTimestamp) => void
public onOpen?: ((this: MultiplayerTransport, ev: Event | null) => unknown) | null
public onClose?: ((this: MultiplayerTransport, ev: CloseEvent | null) => unknown) | null
public onError?: ((this: MultiplayerTransport, ev: Event) => unknown) | null

public abstract get ready(): boolean

public static init(roomId: string | null, displayName: string, ws: MultiplayerTransport): MultiplayerTransport {
const initialMessage: ClientToServerMessage = {
type: "initializeconnection",
room_id: roomId,
name: displayName,
}

if (ws.ready) {
ws.sendServer(initialMessage)
} else {
ws.onOpen = () => {
ws.sendServer(initialMessage)
}
}

return ws
}

protected abstract send(prefix: number, msg: MessageWithTimestamp | ClientToServerMessage, datagram: boolean): void

public sendPeer(msg: MessageWithTimestamp, datagram: boolean = false): void {
return this.send(CLIENT_PREFIX, msg, datagram)
}

public sendServer(msg: ClientToServerMessage, datagram: boolean = false): void {
return this.send(SERVER_PREFIX, msg, datagram)
}

public abstract close(code?: number, reason?: string): void
}
52 changes: 9 additions & 43 deletions fission/src/systems/multiplayer/MultiplayerWebsocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import type { MessageWithTimestamp } from "@/systems/multiplayer/MultiplayerType
import { Encoder, Decoder } from "@msgpack/msgpack"
import type { ClientToServerMessage } from "@/systems/multiplayer/bindings/ClientToServerMessage.ts"
import type { ServerToClientMessage } from "@/systems/multiplayer/bindings/ServerToClientMessage.ts"

const CLIENT_PREFIX = 0b00000001
const SERVER_PREFIX = 0b00000011
import { MultiplayerTransport, SERVER_PREFIX } from "@/systems/multiplayer/MultiplayerTransport.ts"

const console = consolePrefixer({
defaultPrefix: {
Expand All @@ -14,24 +12,19 @@ const console = consolePrefixer({
},
})

class MultiplayerWebsocket {
class MultiplayerWebsocket extends MultiplayerTransport {
private readonly _ws: WebSocket

private readonly _encoder: Encoder<never> = new Encoder()
private readonly _decoder: Decoder<never> = new Decoder()
private readonly _encoder: Encoder = new Encoder({ forceFloat32: true })
private readonly _decoder: Decoder = new Decoder()
private _prefixBuf = new Uint8Array(1)

public onServerMessage?: (msg: ServerToClientMessage) => void
public onPeerMessage?: (msg: MessageWithTimestamp) => void
public onOpen?: ((this: MultiplayerWebsocket, ev: Event) => unknown) | null
public onClose?: ((this: MultiplayerWebsocket, ev: CloseEvent) => unknown) | null
public onError?: ((this: MultiplayerWebsocket, ev: Event) => unknown) | null

public get ready() {
public override get ready() {
return this._ws.readyState === WebSocket.OPEN
}

constructor(url: string) {
super()
this._ws = new WebSocket(url)
console.log("Connecting to", url)
this._ws.onopen = e => {
Expand Down Expand Up @@ -59,7 +52,7 @@ class MultiplayerWebsocket {
const data = msg.slice(1).stream()
const decoded = (await this._decoder.decodeAsync(data)) as ServerToClientMessage | MessageWithTimestamp
const isServer = headerByte == SERVER_PREFIX
if (decoded.type != "update") console.debug("Recieving", isServer ? "server" : "client", decoded)
// if (decoded.type != "update") console.debug("Recieving", isServer ? "server" : "client", decoded)
if (isServer) {
this.onServerMessage?.(decoded as ServerToClientMessage)
} else {
Expand All @@ -68,40 +61,13 @@ class MultiplayerWebsocket {
}
}

public static init(roomId: string | null, displayName: string, ws: MultiplayerWebsocket): MultiplayerWebsocket {
const initialMessage: ClientToServerMessage = {
type: "initializeconnection",
room_id: roomId,
name: displayName,
}

if (ws._ws.readyState == WebSocket.OPEN) {
ws.sendServer(initialMessage)
} else {
ws.onOpen = () => {
ws.sendServer(initialMessage)
}
}

return ws
}

private send(prefix: number, msg: MessageWithTimestamp | ClientToServerMessage): void {
if (msg.type != "update") console.debug("Sending", msg)
override send(prefix: number, msg: MessageWithTimestamp | ClientToServerMessage): void {
const encoded = this._encoder.encodeSharedRef(msg)
this._prefixBuf[0] = prefix
return this._ws.send(new Blob([this._prefixBuf, encoded]))
}

public sendPeer(msg: MessageWithTimestamp): void {
return this.send(CLIENT_PREFIX, msg)
}

public sendServer(msg: ClientToServerMessage): void {
return this.send(SERVER_PREFIX, msg)
}

public close(code?: number, reason?: string) {
override close(code?: number, reason?: string) {
return this._ws.close(code, reason)
}
}
Expand Down
Loading
Loading