Modifying the Source Code
We're happy to answer any basic questions via Discord, but if you need more help, reach out to purchase premium support. Royalty-free licensees get a limited amount of free premium support!
Overview
Here's an overview of what everything does in the source tree:
-
config/
-
docker/
: Dockerfile images used by Redwoodgame-server/
: Dockerfile/.dockerignore for the packaged Linux game serversredwood-chisel/
: Dockerfile to generate theredwoodmmo/redwood-base
image used as a base image forredwood-prod/
redwood-dev/
: Dockerfile/.dockerignore for nonstaging
/production
Kubernetes environments, which uses source TS rather than a bundled imageredwood-prod/
: Dockerfile/.dockerignore forstaging
/production
Kubernetes environments, which uses a bundled image for the microservices
-
node/
: The default Redwood configurations; you can place your custom config-env here or pick a custom location-
cloud-do/
: Config env for DigitalOcean deployment (inherits fromdefault
) -
default/
: The default config env -
kubernetes/
: Config env for all Kubernetes deployments (inherits fromdefault
) -
production/
: Config env for all production deployments (inherits fromstaging
) -
staging/
: Config env for all environments that emulate production, but don't need some production features (e.g. not using an externally managed DB); this is used if you want to test a production environment on your local Kubernetes cluster (inherits fromkubernetes
) -
standard/
: Config env that all closed-source/Standard Licenses should inherit from (inherits fromdefault
) -
test-memurai/
: Config env foryarn test
but using a Memurai Redis server to mimic Kubernetes environments (inherits fromtest
) -
test-mocks/
: Config env used for running the mocked tests in RedwoodPlugins (inherits fromtest
) -
test-standard/
: Config env foryarn test
but using bundled (inherits fromtest
, thenstandard
) -
test/
: Config env foryarn test
using a stubbed Redis implementation (similar to thedev-initiator
, inherits fromdefault
) -
environment.yaml
: Currently unused, but an optional file that can be used to inject environment variables into a process using one of these config environments. You must use theconfig-env
script found atnode_modules/.bin/config-env
. The syntax of the file would be something like this:DATABASE_HOST_ENV_VAR: "director.persistence.database.runtime-access.host"
-
-
-
dist/
: The directory where outputs are stored (including the microservice bundled images), nothing should really be committed here other thangame-servers/.gitignore
to keep the directory around -
packages/
: Where all the source is; each directory is a Yarn Workspacecli/
: All of the CLI interfaces (e.g.yarn cli <command> --arg
)common/
: Various utilities and types used by the Redwood servicescore-runner/
: When we bundle up Redwood as executables for staging & production environments, several microservice share a lot of code. To save on space and extra operations, we bundle these into a single executable & Docker image. This package is the binary entrypoint for running those services in staging & production environments.deployment/
: Docker and Pulumi deployment scripts/Infrastructure-as-Codedev-game-server/
: A wrapper used when you're using thelocal
game server provider (aka via thedev-initiator
) as well as the stubbed servers foryarn test
dev-initiator/
: What runs when you callyarn dev <config-env>
or./dev-initiator-<os> <config-env>
, an application that bootstraps all of the microservices under one process using ts-node, which prevents us from compiling source each time we restart the dev-initiatordirector-backend/
: The Backend for the Director.director-frontend/
: The Frontend for the Director.match-function/
: The Match Function is a service used for the Open Match matchmaking provider to evaluate matchmaking requests figure out which players should be put into a match.persistence/
: This isn't a microservice itself, but it stores the database schema, migrations, and utility functions used by other services.realm-backend/
: The Backend for the Realm.realm-frontend/
: The Frontend for the Realm.sidecar/
: This is the Sidecar service that runs in tandem every game server. It acts as a bridge between the backend and the game server. It communicates to the associated Realm Backend.tests/
: This package is used to store all of our automated tests.ticketing/
: Ticketing runs a "director" (aka master) service and multiple "worker" services to handle queueing and matchmaking.tsconfig.json
:yarn build
in the root references thistsconfig.json
tsconfig.standard.json
: A modifiedtsconfig.json
that we use to distribute the closed-source version; you can ignore this file
-
scripts/
: some helper JS scripts -
types/
: supplemental types for dependencies that have missing or incorrect typings
Typical Player Flow
This is the general flow of how players get into servers:
- Players connect to the Director Frontend via Socket.io/WebSockets
- Players authenticate with the Director Frontend
- Players ask the Director Frontend to list available Realms
- Players request to connect to a Realm
- First they make a request to the Director Frontend, which saves & responds with an auth token and Realm Frontend URL
- Then they connect to the Realm Frontend via Socket.io/WebSockets (maintaining connection to the Director Frontend)
- They authenticate with the Realm Frontend using the auth token
- The Realm Frontend verifies the token with the Director Frontend
- Players ask the Realm Frontend to list GameServerProxies (via
ListServers
) or enter matchmaking- If they called
ListServers
, they can then join them withJoinQueue
- Players can also call
CreateServer
to have the backend start a server with various parameters, optionally keeping it private and/or password protected
- If they called
- When they join matchmaking, create server, or join a queue, the Realm Frontend creates a Ticketing Join "job" in Redis using BullMQ
- A Ticketing Worker service picks up the job, processes it based on the Ticketing config
- If/when the player should join a server (including intermediate updates), a Redis message is published via 2 channels (ticket update and join server)
- The Realm Frontend subscribes to these Redis channels, and if it has a connection with the player, relays the information
- The RedwoodCore UE plugin automatically subscribes for the event to join a server and runs an
open
console command to automatically join the server - If/when servers need to be started, the Realm Backend handles most of this logic; see
packages/realm-backend/src/game-servers/allocate.ts
- There is quite a complex flow for server initialization, retry logic, etc. If you need help understanding this, reach out about purchasing premium support. Most of this will happen between the Realm Backend ⇔ Sidecar ⇔ Game Server (via the Redwood Core UE plugin).
- When the player is told to join a server, it's given a new, one-time-use auth token that's tied to the PlayerIdentity ID, PlayerCharacter ID, and the GameServerInstance ID.
- The
open
console command will include the playerId, characterId, and token as variables, which the game server will use to ask the Sidecar to get the associated PlayerCharacter data - The Sidecar forwards this message to the Realm Backend
- The Realm Backend verifies the token, and if it is provides the character data (or null & error if not) to the Sidecar
- The Sidecar forwards the response to the game server
- If there was an error, the game server will kick the player
Interservice Communication
Services communicate with each other using Socket.io/WebSockets and Redis pubsub channels. Redis is used for any fan out communication where the sender doesn't know the exact replica of the receiver it needs to send to. You can find the various Redis endpoints in packages/common/src/redis.ts
. See below about the Socket.io endpoints.
For Socket.io connections:
- Game server (via the RedwoodCore UE plugin) connects to the Sidecar and doesn't use any Redis
- Sidecar connects to the Realm Backend for it's associated Realm and doesn't use any Redis
- Realm Backend connects to the Director Backend
- Realm Frontend connects to the Director Frontend
- Ticketing only uses Redis communications
- Match Function only uses HTTP methods via the Open Match API
- Player clients connect to the Director and Realm Frontends
API
All API methods for all services can be found in packages/common/src/interfaces.ts
. It's massive, and I've tried breaking it up, but it's the best dev experience I've found. We use yup
for request validation. You'll find a common pattern:
export namespace Namespace {
export namespace Message {
export type Type = RedwoodMessageType<IRequest, IResponse, typeof Name>;
export const Name = "namespace:message";
export const SRequest = object({
// variables, usually with some id of who is requesting to be verified on the receiving service
});
export interface IRequest extends InferType<typeof SRequest> {}
export const SResponse = object({
error: string().defined(), // almost always defined; no error if empty string
result1: string().nullable().defined(), // results are usually nullable().defined() and are null if there is an error
result2: object(/**/).nullable().defined(),
});
export interface IResponse extends InferType<typeof SResponse> {}
}
}
Defining the API here does nothing functionally, you will need to go to packages/<the-receiving-service>/src/routes/
, find the appropriate location and add logic to listen to the new endpoint name. You'll see a common entrypoint in packages/<the-receiving-service>/src/routes/index.ts
, but there are varying patterns used throughout based on the type of connection.
New Service vs Modifying Existing
There isn't a right or wrong answer to how you add new functionality to the backend, but here are some thoughts about whether to write a new service/package or integrate into an existing one:
- If you're adding more endpoints for the clients to use directly, use the appropriate existing Frontend service.
- If you're adding something that purely operates in the backend, you can create a new service. That service can connect to another service via Socket.io/WebSockets (e.g. the Realm Backend) or use Redis to have a communication layer (see the Ticketing service for an example).
- Creating a new package/service does make it easier to handle upstream merge conflicts, but it's not always an ideal setup.
- If your feature is distinctly different than existing code, consider putting your code in a new package in
packages/
but you can still call exported functions from existing services. This would reduce the amount of potential merge conflicts while still having the code execute within the context of an existing service. - Note that new services will need to be added to the
dev-initiator
anddeployment
packages. New Packages may need to be added topackages/tsconfig.json
.
Plugin System
Currently we do not have a plugin system. We're contemplating adding this in the future for source-code users to make modifications more streamlined.
Horizontal Scalability
Keep in mind that other than the Sidecar, every other microservice should be able to scale horizontally with more replicas. This requires extra thought about how to architect things as you can't assume things like "the Director Frontend as a connection with every active player". This would simply not be true; there may be several instances of the Director Frontend running.
Compiling, Packaging, and Testing
The Dev Initiator (packages/dev-initiator
) uses ts-node
to compile changes when it starts up. You will have to restart the Dev Initiator for changes to take place, and there's no file watching functionality to automatically do this. Just run yarn dev <config-env>
or use one of the provided .vscode/launch.json
Launch Initiator: configs (obviously modifying) to debug the dev-initiator in Visual Studio Code (recommended).
If you just want to test compilation without running, you can run an actual compile of all services with yarn build
.
To package/bundle executables of the services run yarn pkg
.
To run the automated tests, run yarn test
. You can also debug the automated tests with the included .vscode/launch.json
Run Tests VSCode launch config.
Deployment
We use Pulumi as the deployment tool for our Infrastructure-as-Code (IaC). You can find all the Pulumi code in packages/deployment
. You should only need to modify these if you're adding a new microservice, which we've written the code to be fairly easy to do that. See packages/deployment/src/redwood/redwood.ts
for an entrypoint of deploying Redwood services; you'll see the Director and Realms being deployed, and those respective classes will deploy the Backend/Frontend.