Skip to content

Multiplayer Match Mode & Testing [AARD-2023] #1260

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 38 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0aaa439
init: multiplayer subsystem
azaleacolburn Jul 29, 2025
9ec9239
feat: update multiplayer system to use fission types
azaleacolburn Jul 29, 2025
02bbb4f
feat: base multiplayer lobby system
rutmanz Jul 29, 2025
21ebb2d
fix: make initWorld use broadcast
rutmanz Jul 29, 2025
cf5330c
Merge
azaleacolburn Jul 30, 2025
7149d29
feat: handle peer info and world initialization
azaleacolburn Jul 30, 2025
4baad96
feat(wip): move multiplayer to world
azaleacolburn Jul 30, 2025
c99d6f0
feat: add multiplayer start menu
rutmanz Jul 30, 2025
2d56d2a
feat: collision handling hook
azaleacolburn Jul 30, 2025
30a11ba
feat: add multiplayer hud
rutmanz Jul 30, 2025
46168b1
refactor: use discriminated union tpye
rutmanz Jul 30, 2025
a0947ea
feat: show info nicely on HUD
rutmanz Jul 30, 2025
a0e6d67
feat: add host transitioning
rutmanz Jul 30, 2025
652f294
feat: new object handling
azaleacolburn Jul 30, 2025
990df6d
chore: format
azaleacolburn Jul 31, 2025
f1afdd0
feat: newObject message works
azaleacolburn Jul 31, 2025
3ede77f
feat: persistent client ids and names, increased room security, check…
rutmanz Jul 31, 2025
e884eea
feat: updating object velocity, position, rotation works
azaleacolburn Aug 1, 2025
10ca6d7
feat: add multiplayer nametags
rutmanz Aug 1, 2025
74328e4
feat: send metadata between multiplayer clients
rutmanz Aug 1, 2025
f70da18
feat: allow multiple objects per client
rutmanz Aug 1, 2025
e65fc5a
feat: intaking and ejecting game pieces across multiplayer
azaleacolburn Aug 4, 2025
7089985
feat: use field cache if possible in mulitplayer
azaleacolburn Aug 4, 2025
11a170d
chore: remove comments in handlePeerUpdate
azaleacolburn Aug 4, 2025
7663a13
feat: send initial configuration with new objects
azaleacolburn Aug 5, 2025
6ea074b
feat: preferences are sent on update
azaleacolburn Aug 5, 2025
714b11c
Merge remote-tracking branch 'origin/dev' into colbura/2024/multiplayer
rutmanz Aug 5, 2025
12cb3e2
feat: finish migrating to UI refactor, fix circular import
rutmanz Aug 5, 2025
8f1b742
fix: import correctly
rutmanz Aug 5, 2025
f7ce472
feat: add multiplayer tests
rutmanz Aug 5, 2025
800aa6b
Merge branch 'dev' of github.com:Autodesk/synthesis into colbura/2024…
azaleacolburn Aug 6, 2025
08667b3
feat: remove printing in tests and make server exit gracefully
rutmanz Aug 6, 2025
a4f0a72
Merge remote-tracking branch 'origin/dev' into colbura/2024/multiplayer
rutmanz Aug 6, 2025
55760d9
Merge remote-tracking branch 'origin/dev' into colbura/2024/multiplayer
rutmanz Aug 6, 2025
0437128
Merge branch 'colbura/2024/multiplayer' into zachr/2023/multiplayer-m…
rutmanz Aug 6, 2025
de50ba7
feat: add singleplayer button back, fix default scene order and filen…
rutmanz Aug 7, 2025
f898e4f
feat: add match mode timer support
rutmanz Aug 7, 2025
e2a4f02
Merge branch 'colbura/2024/multiplayer' into zachr/2023/multiplayer-m…
rutmanz Aug 7, 2025
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
271 changes: 175 additions & 96 deletions fission/bun.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion fission/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"electron-squirrel-startup": "^1.0.1",
"framer-motion": "^10.18.0",
"lygia": "^1.3.3",
"peerjs": "^1.5.5",
"msw": "^2.10.4",
"notistack": "^3.0.2",
"playwright": "^1.54.2",
Expand Down Expand Up @@ -95,11 +96,11 @@
"eslint-plugin-react-refresh": "^0.4.20",
"jsdom": "^24.1.3",
"pako": "^2.1.0",
"peer": "^1.0.2",
"postcss": "^8.5.6",
"protobufjs": "^7.5.3",
"protobufjs-cli": "^1.1.3",
"rollup": "^4.46.2",
"sirv": "^3.0.1",
"tailwindcss": "^3.4.17",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.9.2",
Expand Down
54 changes: 40 additions & 14 deletions fission/src/Synthesis.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { AnimatePresence } from "framer-motion"
import { SnackbarProvider } from "notistack"
import { useCallback, useEffect, useRef, useState } from "react"
import { globalAddToast } from "@/components/GlobalUIControls.ts"
import MainHUD from "@/components/MainHUD"
import MultiplayerHUD from "@/components/MultiplayerHUD.tsx"
import Scene from "@/components/Scene.tsx"
import MultiplayerStartModal from "@/modals/MultiplayerStartModal.tsx"
import MultiplayerSystem from "@/systems/multiplayer/MultiplayerSystem.ts"
import World from "@/systems/World.ts"
import { UIRenderer } from "@/ui/UIRenderer.tsx"
import PreferencesSystem from "./systems/preferences/PreferencesSystem.ts"
Expand All @@ -23,34 +27,55 @@ function Synthesis() {
const [consentPopupDisable, setConsentPopupDisable] = useState<boolean>(true)

const mainLoopHandle = useRef(0)
const startMainLoop = async () => {
await World.initWorld()
if (!PreferencesSystem.getGlobalPreference("ReportAnalytics") && !import.meta.env.DEV) {
setConsentPopupDisable(false)
}

const mainLoop = () => {
mainLoopHandle.current = requestAnimationFrame(mainLoop)
World.updateWorld()
}

mainLoop()
}
useEffect(() => {
const urlParams = new URLSearchParams(document.location.search)
if (urlParams.has("code")) {
window.opener.convertAuthToken(urlParams.get("code"))
window.close()
return
}
const startSingleplayerCallback = () => {
World.initWorld()

if (!PreferencesSystem.getGlobalPreference("ReportAnalytics") && !import.meta.env.DEV) {
setConsentPopupDisable(false)
}

const mainLoop = () => {
mainLoopHandle.current = requestAnimationFrame(mainLoop)
World.updateWorld()
}

mainLoop()
}
globalOpenModal(MainMenuModal, { startSingleplayerCallback })
globalOpenModal(MainMenuModal, {
startSingleplayerCallback: async () => await startMainLoop(),
startMultiplayerCallback: () => {
globalOpenModal(MultiplayerStartModal, {
startWorldCallback: async (name, room) => {
const isHost = room == null
if (room == null) {
room = Math.random().toString(10).substring(2, 8)
globalAddToast("info", "Room code", room)
}
PreferencesSystem.setGlobalPreference("MultiplayerUsername", name)
PreferencesSystem.savePreferences()
const success = await MultiplayerSystem.setup(room, name, isHost)
if (success) {
await startMainLoop()
return true
}
return false
},
})
},
})
// Cleanup
return () => {
// TODO: Teardown literally everything
cancelAnimationFrame(mainLoopHandle.current)
World.destroyWorld()
World.multiplayerSystem?.destroy()
// World.SceneRenderer.RemoveAllSceneObjects();
}
}, [])
Expand All @@ -75,6 +100,7 @@ function Synthesis() {
<Scene useStats={import.meta.env.DEV} key="scene-in-toast-provider" />
<SceneOverlay />
<ContextMenu />
<MultiplayerHUD />
<MainHUD key={"main-hud"} />
<UIRenderer />
<ProgressNotifications key={"progress-notifications"} />
Expand Down
1 change: 1 addition & 0 deletions fission/src/Window.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
declare interface Window {
convertAuthToken(code: string): void
world?: unknown // for development
gtag?: (command: "config" | "set" | "get" | "event" | "consent", ...args: unknown[]) => void
dataLayer?: unknown[][]
}
18 changes: 11 additions & 7 deletions fission/src/mirabuf/EjectableSceneObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type MirabufSceneObject from "./MirabufSceneObject"
import ScoringZoneSceneObject from "./ScoringZoneSceneObject"

class EjectableSceneObject extends SceneObject {
private _parentAssembly: MirabufSceneObject
private _parentSceneObject: MirabufSceneObject
private _gamePieceBodyId?: Jolt.BodyID

private _parentBodyId?: Jolt.BodyID
Expand Down Expand Up @@ -44,25 +44,29 @@ class EjectableSceneObject extends SceneObject {
return this._parentBodyId
}

public get parentSceneObject(): MirabufSceneObject {
return this._parentSceneObject
}

public constructor(parentAssembly: MirabufSceneObject, gamePieceBody: Jolt.BodyID) {
super()

console.debug("Trying to create ejectable...")

this._parentAssembly = parentAssembly
this._parentSceneObject = parentAssembly
this._gamePieceBodyId = gamePieceBody
}

public setup(): void {
if (this._parentAssembly.ejectorPreferences && this._gamePieceBodyId) {
this._parentBodyId = this._parentAssembly.mechanism.nodeToBody.get(
this._parentAssembly.ejectorPreferences.parentNode ?? this._parentAssembly.rootNodeId
if (this._parentSceneObject.ejectorPreferences && this._gamePieceBodyId) {
this._parentBodyId = this._parentSceneObject.mechanism.nodeToBody.get(
this._parentSceneObject.ejectorPreferences.parentNode ?? this._parentSceneObject.rootNodeId
)

this._deltaTransformation = convertArrayToThreeMatrix4(
this._parentAssembly.ejectorPreferences.deltaTransformation
this._parentSceneObject.ejectorPreferences.deltaTransformation
)
this._ejectVelocity = this._parentAssembly.ejectorPreferences.ejectorVelocity
this._ejectVelocity = this._parentSceneObject.ejectorPreferences.ejectorVelocity

// Record start transform at the game piece center of mass
const gpBody = World.physicsSystem.getBody(this._gamePieceBodyId)
Expand Down
7 changes: 4 additions & 3 deletions fission/src/mirabuf/MirabufLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,9 +508,10 @@ class MirabufCachingService {
): Promise<MirabufCacheInfo | undefined> {
try {
const backupID = Date.now().toString()
if (!miraType) {
console.debug("Double loading")
miraType = this.assemblyFromBuffer(miraBuff).dynamic ? MiraType.ROBOT : MiraType.FIELD
if (miraType == null || name == null) {
const assembly = this.assemblyFromBuffer(miraBuff)
miraType ??= assembly.dynamic ? MiraType.ROBOT : MiraType.FIELD
name ??= assembly.info?.name ?? undefined
}

// Local cache map
Expand Down
102 changes: 91 additions & 11 deletions fission/src/mirabuf/MirabufSceneObject.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type Jolt from "@azaleacolburn/jolt-physics"
import * as THREE from "three"
import type { mirabuf } from "@/proto/mirabuf"
import { BodyAssociate } from "@/systems/physics/BodyAssociate.ts"
import { OnContactAddedEvent } from "@/systems/physics/ContactEvents"
import type Mechanism from "@/systems/physics/Mechanism"
import { BodyAssociate, type LayerReserve } from "@/systems/physics/PhysicsSystem"
import type { LayerReserve } from "@/systems/physics/PhysicsSystem"
import PreferencesSystem from "@/systems/preferences/PreferencesSystem"
import type {
Alliance,
Expand All @@ -30,6 +31,7 @@ import ConfigurePanel from "@/ui/panels/configuring/assembly-config/ConfigurePan
import AutoTestPanel from "@/ui/panels/simulation/AutoTestPanel"
import JOLT from "@/util/loading/JoltSyncLoader"
import { convertJoltMat44ToThreeMatrix4, convertJoltVec3ToThreeVector3 } from "@/util/TypeConversions"
import type { FieldConfiguration, MetadataUpdateData, RobotConfiguration } from "../systems/multiplayer/types"
import SceneObject from "../systems/scene/SceneObject"
import EjectableSceneObject from "./EjectableSceneObject"
import FieldMiraEditor from "./FieldMiraEditor"
Expand Down Expand Up @@ -86,6 +88,7 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
private _scoringZones: ScoringZoneSceneObject[] = []
private _protectedZones: ProtectedZoneSceneObject[] = []

private _nameOverride?: string
private _nameTag: SceneOverlayTag | undefined
private _centerOfMassIndicator: THREE.Mesh | undefined
private _intakeActive = false
Expand All @@ -97,6 +100,23 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
private _collision?: (event: OnContactAddedEvent) => void
private _cacheId?: string

public get multiplayerInfo(): MetadataUpdateData {
return {
sceneObjectKey: this.id,
alliance: this._alliance,
station: this._station,
}
}

public set multiplayerInfo(info: MetadataUpdateData) {
this._alliance = info.alliance
this._station = info.station
console.log({ info })
}

public set nameOverride(name: string | undefined) {
this._nameOverride = name
}
public get intakeActive() {
return this._intakeActive
}
Expand All @@ -109,6 +129,12 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
public set ejectorActive(a: boolean) {
this._ejectorActive = a
}
public set mechanism(a: Mechanism) {
this._mechanism = a
}
public set mirabufInstance(a: MirabufInstance) {
this.mirabufInstance = a
}

get mirabufInstance() {
return this._mirabufInstance
Expand Down Expand Up @@ -207,12 +233,14 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {

if (this.miraType === MiraType.ROBOT) {
// creating nametag for robots
this._nameTag = new SceneOverlayTag(() =>
this._brain instanceof SynthesisBrain
? this._brain.inputSchemeName
: this._brain instanceof WPILibBrain
? "Magic"
: "Not Configured"
this._nameTag = new SceneOverlayTag(
() =>
this._nameOverride ??
(this._brain instanceof SynthesisBrain
? this._brain.inputSchemeName
: this._brain instanceof WPILibBrain
? "Magic"
: "Not Configured")
)

// Detects when something collides with the robot
Expand Down Expand Up @@ -546,9 +574,7 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {

// 3) avoid duplicates
const key = bodyId.GetIndexAndSequenceNumber()
if (this._ejectables.some(e => e.gamePieceBodyId!.GetIndexAndSequenceNumber() === key)) {
return false
}
if (this._ejectables.some(e => e.gamePieceBodyId!.GetIndexAndSequenceNumber() === key)) return false

const ejectable = new EjectableSceneObject(this, bodyId)
this._ejectables.push(ejectable)
Expand Down Expand Up @@ -650,7 +676,11 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
*
* @returns the object containing the width (x), height (y), and depth (z) dimensions in meters.
*/
public getDimensionsWithoutRotation(): { width: number; height: number; depth: number } {
public getDimensionsWithoutRotation(): {
width: number
height: number
depth: number
} {
const rootNodeId = this.getRootNodeId()
if (!rootNodeId) {
console.warn("No root node found for robot, using regular dimensions")
Expand Down Expand Up @@ -781,6 +811,32 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
}
}

public getPreferenceData(): FieldConfiguration | RobotConfiguration {
return this.miraType == MiraType.FIELD
? {
fieldPreferences: JSON.stringify(this._fieldPreferences),
protectedZones: JSON.stringify(this._protectedZones),
scoringZones: JSON.stringify(this._scoringZones),
}
: {
intakePreferences: JSON.stringify(this._intakePreferences),
ejectorPreferences: JSON.stringify(this._ejectorPreferences),
}
}

public setPreferenceData(preferences: FieldConfiguration | RobotConfiguration) {
if (this.miraType == MiraType.FIELD) {
const config = preferences as FieldConfiguration
this._fieldPreferences = JSON.parse(config.fieldPreferences)
this._protectedZones = JSON.parse(config.protectedZones)
this._scoringZones = JSON.parse(config.scoringZones)
} else {
const config = preferences as RobotConfiguration
this._intakePreferences = JSON.parse(config.intakePreferences)
this._ejectorPreferences = JSON.parse(config.ejectorPreferences)
}
}

public updateSimConfig(config: SimConfigData | undefined) {
const robotPrefs = PreferencesSystem.getRobotPreferences(this.assemblyName)
if (robotPrefs) {
Expand Down Expand Up @@ -890,12 +946,36 @@ class MirabufSceneObject extends SceneObject implements ContextSupplier {
data.items.push({
name: "Remove",
func: () => {
World.multiplayerSystem?.broadcast({ type: "deleteObject", data: this.id })
World.sceneRenderer.removeSceneObject(this.id)
},
})

return data
}
public getUpdateData() {
const rootBodyId = this.getRootNodeId()
if (!rootBodyId) return
const rootBody = World.physicsSystem.getBody(rootBodyId)

const sceneObject = World.sceneRenderer.sceneObjects.get(this.id) as MirabufSceneObject
const gamePiecesControlled: number[] = sceneObject.activeEjectables.map(bodyId =>
bodyId.GetIndexAndSequenceNumber()
)
const linearVelocity = rootBody.GetLinearVelocity()
const angularVelocity = rootBody.GetAngularVelocity()
const position = rootBody.GetPosition()
const rotation = rootBody.GetRotation()

return {
sceneObjectKey: this.id,
gamePiecesControlled,
linearVelocityStr: `{"x": ${linearVelocity.GetX()}, "y": ${linearVelocity.GetY()}, "z": ${linearVelocity.GetZ()}}`,
angularVelocityStr: `{"x": ${angularVelocity.GetX()}, "y": ${angularVelocity.GetY()}, "z": ${angularVelocity.GetZ()}}`,
positionStr: `{"x": ${position.GetX()}, "y": ${position.GetY()}, "z": ${position.GetZ()}}`,
rotationStr: `{"x": ${rotation.GetX()}, "y": ${rotation.GetY()}, "z": ${rotation.GetZ()}, "w": ${rotation.GetW()}}`,
}
}

private recordRobotCollision(collision: Jolt.BodyID) {
const objectCollidedWith = <RigidNodeAssociate>World.physicsSystem.getBodyAssociation(collision)
Expand Down
Loading