EXSII featured a truly interactive live event, enabled by Synapse, a cutting-edge mod that brings players from around the world together to engage with innovative new maps in real-time. Participants compete against each other on a shared leaderboard and can communicate seamlessly using the in-game chat feature.
- Takeover the main menu with your own prefabs during the countdown to build hype.
- A countdown and custom banner from the main menu, along with notifications from anywhere in-game that your event is live.
- A 1-hour grace period to automatically download required mods for an event.
- Multiple divisions so all players can enjoy their preferred difficulty.
- A fully featured chatroom where players can interact with each other, complete with moderation tools like banning malicious users, as well as toggleable options like a profanity filter or opting-out of chat entirely.
- Replace the lobby with a custom prefab themed around your event, which also includes custom cinematics that can play as an intro or outro.
- Download and synchronously start maps for all players to experience maps at the same time.
- An event leaderboard where players can compete with each other, as well as the ability to run tournament formats which can eliminate players each round.
- All dockerized to be easily portable.
- Seamlessly runs, even with 1900+ players, as shown during EXSII.
Interested in running an event using Synapse? Contact me and I can help config and list your event using the official API.
By default, Synapse will check https://synapse.totalbs.dev/api/v1/directory for an active listing.
This can be changed by going to UserData/Synapse.json and editing the URL to point to your own API.
The backend for Synapse is made up of two projects, the listing and the server.
- The listing is a simple API that the client mod will ping everytime the game is started. It contains necessary information such where to connect to the server, when the event starts, and any required assets needed to join.
- The server is what the clients actually connect to. It runs the entire event.
The recommended way to run the server is using docker.
Example docker-compose.yml:
services:
synapse-listing:
image: ghcr.io/aeroluna/synapse-listing
container_name: synapse-listing
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://synapse-listing:1000
- ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
volumes:
- ./synapse/listing/wwwroot:/app/wwwroot
- ./synapse/listing/appsettings.Production.json:/app/appsettings.Production.json
restart: unless-stopped
synapse-server:
image: ghcr.io/aeroluna/synapse-server
container_name: synapse-server
depends_on:
- synapse-listing
environment:
- DOTNET_ENVIRONMENT=Production
volumes:
- ./synapse/server/appsettings.Production.json:/app/appsettings.Production.json
- ./synapse/server/config:/config
ports:
- 1001:1001
stdin_open: true
tty: trueThe configuration is done using two appsettings.ENVIRONMENT.json for the listing and server. This will typically be appsettings.Production.json. Example configs can be found at Synapse.Listing/appsettings.json.sample and Synapse.Server/appsettings.json.sample
Although assets can be hosted from wwwroot/ using the Listing project, it is recommended to use a CDN. Example assets can be found in the SampleAssets directory.
Listing appsettings:
{
"Listing": {
"Title": "Extra Sensory II", // Name shown to players
"Time": "2025-01-26T22:00:00Z", // UTC time to start the event
"IpAddress": "127.0.0.1:1001", // IP address of the server. Clients should be able to connect to this. IP:PORT
"BannerImage": "http://localhost:5033/images/banner.png", // Banner image shown in menu 200x500.
"BannerColor": "#341552", // Hex code of banner in main menu
"GameVersion": "1.29.1,1.34.2,1.37.1,1.39.1,1.40.0", // Game versions that can connect. Comma separated exact versions
"Divisions": [ // Divisions for event. If empty, no divisions will be used.
{
"Name": "Casual",
"Description": "For those looking to enjoy the game at a more casual pace, However, you may still find later maps challenging. Notes per second is lower, but note gimmicks are unaffected."
},
{
"Name": "Experienced",
"Description": "Experience the maps as they were originally intended. Higher notes per second for those looking to put their skills to the test"
}
],
"Takeover": { // Main menu takeover during lead up to event
"DisableDust": true, // Disables global dust when true
"DisableLogo": true, // Disables default logo when true
"CountdownTMP": "Logo/EXSII_COUNTDOWN", // Path to TextMeshPro to update with countdown
"Bundles": [
{
"GameVersion": "1.29.1", // Comma separated game versions that use this bundle
"Url": "https://localhost:5033/bundle/takeover_windows2019_3913474686",
"Hash": 3913474686 // CRC hash of bundle. Bundle will not load if this is wrong
},
{
"GameVersion": "1.34.2,1.37.1,1.39.1,1.40.0",
"Url": "https://localhost:5033/bundle/takeover_windows2021_2910218729",
"Hash": 2910218729
}
]
},
"Lobby": {
"DisableDust": true, // Disables global dust when true
"DisableSmoke": true, // Disables global smoke when true
"DepthTextureMode": 1, // Bitmask to force camera depth texture mode. https://docs.unity3d.com/ScriptReference/DepthTextureMode.html
"Bundles": [
{
"GameVersion": "1.29.1",
"Url": "https://localhost:5033/bundle/bundle_windows2019_853015874",
"Hash": 853015874
},
{
"GameVersion": "1.34.2,1.37.1,1.39.1,1.40.0",
"Url": "https://localhost:5033/bundle/bundle_windows2021_1876858522",
"Hash": 1876858522
}
]
},
"RequiredMods": [ // Mods that will be downloaded when trying to join event. These will also be downloaded an hour early. Be sure to include all mod dependencies as well
{
"GameVersion": "1.29.1",
"Mods": [
{
"Id": "Vivify",
"Version": "^1.0.1", // Version range to check for. If player's version does not match, will try to install
"Url": "https://localhost:5033/mods/Vivify-1.0.1%2B1.29.1-bs1.29.1-d32ee3d.zip",
"Hash": "2e3250b635f5d8c5711ba8d7fd996b4a" // MD5 hash of zip
}
]
}
]
}
}Server appsettings:
{
"Port": 1001, // Port to listen on
"Listing": "http://synapse-listing:1000/api/v1/directory", // URL of the listing
"MaxPlayers": -1, // Max players allowed to connect. Set to -1 for infinite
"Directory": "/config", // Where to save persistent files such as scores/logs
"Auth": {
"Test": { // Used by TestClient
"Enabled": false
},
"Steam": {
"Enabled": true,
"APIKey": "" // Generate an API key at https://steamcommunity.com/dev
},
"Oculus": {
"Enabled": true
}
},
"Event": {
"Title": "Extra Sensory II", // Name used for logs
"Format": "Showcase", // Currently supported: [None, Showcase]
"Intro": { // Event goes through three stages, Intro, Play, and Finish
"Motd": "<color=#ff6464><size=120%><mspace=0.6em>//// <b><color=#ffffff>CONNECTION ESTABLISHED</color></b> ////", // MOTDs are displayed when moving between maps/stages and when joining
"Intermission": "00:05:00", // Wait period before starting the intro
"Duration": "00:01:00", // Duration of intro until changing stages
"Url": "http://localhost:5033/images/intro.png" // Image shown while waiting for intro. 400x700
},
"Finish": {
"Motd": "<color=#ff6464><size=120%><mspace=0.6em>//// <b><color=#ffffff>CONNECTION TERMINATED</color></b> ////<br><color=#ffffff>YOUR COOPERATION IS APPRECIATED",
"Url": "http://localhost:5033/images/finish.png" // Image shown after outro. 400x700
},
"Maps": [
{
"Name": "Breezer", // Name shown on the leaderboard
"AltCoverUrl": "https://localhost:5033/maps/alternative_breezer.png", // Alternative cover to show. Setting this will show "???" before playing the song
"Motd": "<size=120%><#FFFFFF>[<#FF2121><b>ERROR</b><#FFFFFF> @ <#3171E8>02:18:23<#FFFFFF> | Vivify] Map file <#45B543>'breezer.zip'<#FFFFFF> has been breached by unknown source",
"Intermission": "00:10:00", // Wait period before map start
"Duration": "00:10:00", // Duration of song before changing maps. Should be more than the map's duration to make sure scores can be submitted before ending
"Ruleset": {
"AllowResubmission": true, // Allows players to retry for a better score
"Modifiers": ["noEnergy"] // Modifiers to use. Synapse adds the "noEnergy" modifier, a special modifier where the energy bar is disabled
},
"Keys": [ // Should be a key for every division. This tells the game which difficulty to use
{
"Characteristic": "Standard",
"Difficulty": 1 // 0 = Easy, 1 = Normal, 2 = Hard, 3 = Expert, 4 = Expert+
},
{
"Characteristic": "Standard",
"Difficulty": 2
}
],
"Downloads": [
{
"GameVersion": "1.29.1", // Comma separated game versions
"Url": "https://localhost:5033/maps/Breezer_2019_3d3304c.zip",
"Hash": "3d3304c27e4cd48cb0f9da768ce833a2" // MD5 hash of zip file
},
{
"GameVersion": "1.34.2,1.37.1,1.39.1,1.40.0",
"Url": "https://localhost:5033/maps/Breezer_2021_2c170c1.zip",
"Hash": "2c170c14544b25b055029d2ca67b932c"
}
]
}
]
}
}Roles can grant the following permissions:
- 1 = Coordinator: Can control the flow of the event, i.e. start/stop maps manually, change motd, etc.
- 2 = Moderator: Can moderate other users, i.e. ban or kick users
- 4 = NoQualify: Users with this permission are unable to qualify
Example
roles.json:
[
{
"name": "coordinator",
"priority": 99, // Can only affect users with a lower priority than themselves
"color": "red", // Username color in chat. See https://digitalnativestudios.com/textmeshpro/docs/rich-text/#color. Will use role with highest priority
"permission": 1 // Bitmask of permissions
}
]Example admins.json:
[
{
"roles": [
"moderator",
"mapper",
"coordinator"
],
"id": "76561198301904113_Steam",
"username": "Aeroluna"
}
]All users that connect will be given an ID in the following format: PLATFORMID_PLATFORM, e.g. 76561172306506184_Steam or 2026218196532328_OculusRift.
Commands be sent through the server, or from a client with appropriate permissions by beginning a chat message with /, e.g. /say hello!
Commands which need an ID/username can use the start of a name instead. i.e. kick aero will find the user Aeroluna.
Commands that use options can be combined, i.e. -e -f is the same as -ef.
Nested lists represent subcommands, e.g. scores backup reload.
motd [message]Prints the motd again or sets a new one. Allows rich text. Requirescoordinatorto set an motdroll [min] [max]Rolls a random number. Rolls between 1-100 with no parameters, and between 1-MAX with one parameter, and MIN-MAX with two parameters.tell [player] [message](t,whisper,w) Privately message another player. Messages will still be logged by the server.who [options] [player]Prints how many players are currently connected. May specify a name to find all players whose name starts with that name.-eto print more names.-vto print IDs (requiresmoderator).pingPrints current latency between client and server. (Client only)
say [message]Sends a priority message to everyone with the format[Server] MESSAGE. Allows rich text. Requirescoordinator.sayraw [message]Sends a priority message without formatting. Allows rich text. Requirescoordinator.
allow [player]Adds a user to the whitelist. Requiresmoderator.ban [player] [reason] [time]Bans a user. Optionally set a reason and/or duration. Requiresmoderator.banip [player]Bans a user by ip. Requiresmoderator.kick [player]Kicks a user. Requiresmoderator.blacklistRequiresmoderator.reloadReloadsblacklist.jsonfrom disk.listLists currently banned users.add [id] [username]Manually add an ID/username to the blacklist.remove [options] [username]Remove a user from the blacklist.-ito search by ID instead.
bannedipsRequiresmoderator.reloadReloadsbannedips.jsonfrom disk.listLists currently banned ips.add [ip]Manually add an IP to the blacklist.remove [ip]Remove an IP from the blacklist.
rolesRequirescoordinator.reloadReloadsroles.jsonandadmins.jsonfrom disk.listLists all admins and their roles.listrolesLists all roles.add [options] [username] [role]Add a role to a user.-ito search by ID instead.remove [options] [username] [role]Remove a role to a user.-ito search by ID instead.
whitelistRequiresmoderator.reloadReloadswhitelist.jsonfrom disk.listLists all whitelisted users.add [id] [username]Manually add an ID/username to the whitelist.remove [options] [username] [role]Remove a user from the whitelist.-ito search by ID instead.
scoresRequirescoordinator. Refer to divisions by index, i.e. 0 = Casual, 1 = Experienced.refresh [map index]Resends map's leaderboard to all players. Uses current map index if not specified.remove [options] [division] [map index] [username]Removes a score. Uses current map index if not specified.-ito search by ID instead.drop [division] [map index]Drops all scores for a map. Uses current map index if not specified.resubmit [map index]Resubmit scores for the map to the tournament format.list [options] [division] [map index]List all submitted scores. Uses current map index if not specified.-vto print the scores,-eto print more.testSubmit fake scores for the current map.backupreloadReload score backups from disk.
eventRequirescoordinator.statusDisplays the current status of the event.start [seconds]Starts the intermission for the current stage. Uses time from config if not specified.play [seconds]Plays the current stage. Uses time from config if not specified.stopStops the current stage. Will kick users out of intros, outros and levels.stage [stage index]Changes the stage. Can usenorpinstead of an index for "next" or "previous" respectively.index [options] [map index]Changes the map. Can usenorpinstead of an index for "next" or "previous" respectively.-sto additionally submit scores to the tournament format.-ato auto-start the next with the time from the config.
Synapse.TestClient is a simple client designed to emulate chatting and setting scores. Input the URL to the listing in the appsettings.ENVIRONMENT.json.
Comes with the following commands:
stopDisconnect all clients and close.deploy [count]Deploys a specific amount of clients to connect to the server.scoreCommand all clients to submit a random score for the current map.sendCommand all clients to start sending random chat messages.rollCommand all clients to send the roll command to the server.