Writing a backend for my s&box project

TLDR;

I write about how I write a Go server to act as a middle man between my database and
the S&box server.

Writing a backend for a s&box gamemode

I know the key bragging project showcases with no real effort or progress
get old fast, but I wish to actually contribute to the community, as I believe
many of you might want to consider something similar to this.

This post is mainly going to outline:

  • How I would write a REST API in Go, with the Echo framework.
  • How to structure a database for both efficiency and seperation of concerns.

Coming from a roleplay background, I want to create a better type of roleplay
gamemode. My main influences probably root back to San Andreas Multiplayer
and having built these kind of gamemodes in essentially 3 games so far (Garry’s Mod, sa-mp, and GTA V with RageMP). I used to be a lead developer on what’s now gta.world

My coding background is a mix of languages, but mainly being focused on strongly typed languages
such as Go, C#, Java/Kotlin, etc.

My reasons for picking Go for this project:

  • Go is pretty fast.
  • go build compiles to a single binary without external dependencies; making for very simple deployment.
  • Huge library + IDE support and active development community.
  • Subjectively feels lightweight compared to .NET and JVM applications.

The Project

The idea for this project is to create a REST API which interacts with my game
database, and have the game server call on this to retrieve and modify data.
This drops the need for an ORM running
in the sandboxed (pun) C# engine running in S&box.

Database

Let’s write a short feature list so we can figure out what needs to go where.

  • Players should be able to have items in an inventory.
  • We must distinguish between players as they can have different permissions.
  • Players may purchase or rent in game properties.
  • They should also be able to place down furniture in their properties. This should be loaded in
    when they join the server.
  • All administrative punishments etc. should end up on something equivalent to a criminal record.
  • Permanent entities for NPCs, waypoints, etc. I may want to expand this to use a seperate “Location” table later.

With this short feature list, we can start laying out the models with Gorm.
We’ll make the following models:

  • Account; player account
  • AdminAction; administrative record of players
  • FurnitureDefinition; define our furniture types, and their attributes
  • InventoryItems; player’s inventory
  • ItemDefinitions; what items exist and their attributes
  • PermanentEntity; permanent entities (ie. NPCs, etc)
  • Permission; what permissions an usergroup can have
  • Property; define properties such as businesses and apartments
  • PropertyFurniture; what furniture is in each property?
  • PropertyTenant; who rents or owns a property, and for how long?

This list is pretty much the first edition of the database itself. These tables are related to each other and use
foreign keys to ensure that we don’t delete a player but not their
inventory, and so on.

This is our Account model, minus all the unrelated fields. It contains a field where the ORM inserts the related table
data.

account.go

// Account represents a player's stored data
type Account struct {
    gorm.Model
    
    // SteamID in the format STEAM_0:1:42177812.
    // Used to connect a player to their database account.
    SteamID string
    
    // More properties here, but these are the relevant ones.
    
    AdminActions []AdminAction
    Usergroups   []*Usergroup `gorm:"many2many:account_usergroups;"`
    Items        []InventoryItems
    Tenants      []PropertyTenant
}

The AdminAction model contains some fields so we can see who did what, and why. You’ll notice I’m not including any
timestamp fields and that’s because gorm.Model has a created, updated and deleted timestamp along with a primary key ID.

admin_action.go

type AdminActionType string

const (
	AdminAction_Ban            AdminActionType = "ban"
	AdminAction_Kick                           = "kick"
	AdminAction_Mute                           = "mute"
	AdminAction_Note                           = "note"
	AdminAction_Administrative                 = "administrative"
)

// Represents that an admin has performed some kind of administrative action towards a player.
// Can be both punishments and other kind of "logs" or whatever.
type AdminAction struct {
	gorm.Model

	AccountID uint
	AdminID   uint

	Type AdminActionType

	Reason      string
	BanDuration time.Duration
}

The Rest API

Let’s serve our Account model on /accounts/:id. I’m using a middleware that looks for a header that contains an API
key (which is generated and stored in the server configuration file). This prevents the API being accessed from outside
the game server if we accidentally expose it on the internet for some reason.

In my Echo route definitions, I’ve made a group for the /accounts endpoints, and created an endpoint to retrieve an
account by ID; accounts.GET("/:id", endpoint.GetAccount)

Serving the model itself is pretty simple:

func GetAccount(c echo.Context) error {
    // The AccountRepository is a data access object which contains some predefined accessors for the database.
	dao := repository.NewAccountRepository(database.Db)

	accountId, err := strconv.ParseUint(c.Param("id"), 10, 32)
	if err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, "bad request data")
	}

	account, err := dao.GetAccountById(uint(accountId))
	if err != nil {
        // We can return the error text itself here because our Dao only returns "friendly" error messages.
		return echo.NewHTTPError(http.StatusNotFound, err.Error())
	}

	return c.JSON(http.StatusOK, account)
}

Result:

Here I’m running a GET against our new endpoint, and we get some random seed data. I’m purposefully only including
the AdminActions struct, and not the Usergroups, Items or Tenants because this will be called when the player joins the
server, and we only have to check that the player doesn’t have any active bans. All the other structs will be loaded on
demand with seperate endpoints, to keep the request count down.

GET http://127.0.0.1:8080/accounts/1

{
  "ID": 1,
  "CreatedAt": "2021-05-16T03:12:05.221+02:00",
  "UpdatedAt": "2021-05-16T03:12:05.239+02:00",
  "DeletedAt": null,
  "SteamID": "yTlbV2or1GQ1",
  "LastNick": "VgSEd4ZO2BODvVHC",
  "RoleplayName": "FdOdcyaNMl41loeq",
  "Money": 1337,
  "Bank": 30000,
  "Playtime": 6468543220896000,
  "LastSeen": "2021-05-16T03:12:05.221+02:00",
  "AdminActions": [
    {
      "ID": 1,
      "CreatedAt": "2021-05-16T03:12:05.271+02:00",
      "UpdatedAt": "2021-05-16T03:12:05.271+02:00",
      "DeletedAt": null,
      "AccountID": 1,
      "AdminID": 2,
      "Type": "administrative",
      "Reason": "seeding test",
      "BanDuration": 60000000000
    }
  ],
  "Usergroups": null,
  "Items": null,
  "Tenants": null
}

The project’s structure after all this:

project structure

Database map:

Questions

My discord is Kingstone#4375 for any questions about this project, if you’re interested in hearing more about it.
I’m likely going to keep updating this post as I make more progress. It’s unfortunately not flashy and full of pictures
or anything, but it does serve a purpose down the line for me.

12 Likes

Very cool! This would be sbox Player <-> sbox server <-> go backend right? Have you considered allowing player to backend directly? Would be interesting where you would place what parts of the logic (go backend vs sbox C#)

3 Likes

That is something I’ve thought about a bit and I think that it’d be useful to allow the client to directly fetch certain resources, like the Item definitions and so on. It really depends how restrictive the S&box C# API is with it, and of course what kind of validation I need to do.

3 Likes

Awesome post - the in-depth rundown was great. I’ve been wanting to learn Go for a while, and this certainly makes me more inclined to do so (much better raw performance than node, which is what I currently use)

Also, would this be something you would open source? It would be a great example for anybody wanting to create something similar.

Keen for future updates :smiley:

4 Likes

Awesome post - the in-depth rundown was great. I’ve been wanting to learn Go for a while, and this certainly makes me more inclined to do so (much better raw performance than node, which is what I currently use)
Also, would this be something you would open source? It would be a great example for anybody wanting to create something similar.
Keen for future updates

Yeah I might open source this component, or parts of it eventually. Otherwise, you can always ask me and I’ll happily show you parts of it.

4 Likes

Good project :clap:

3 Likes

Nice work but curious as to why you would do this rather than just interacting with a database directly via C#?

3 Likes

Main reason being that external DLLs currently aren’t even supported, and may not be supported in the near future. Another reason being that sandboxing makes things like Entity Framework basically out of the option.
I think that seperating data manipulation into a seperate API will make serverside code cleaner as well.

3 Likes

Interesting, I think it would be quite surprising if we won’t be able to interact with databases via C# whether that’s an approved external DLL or something shipped with the game. And to your point about separating logic, I think having the database interaction code wrapped in its own class/namespace within the game mode makes more sense.

Having a rest API to manage real time game play data seems quite convoluted to me. I’ve never really come across this in game development before. I wonder the implications this would have on speed/load.

2 Likes

i wish there could be websocket support (also on client), that would just feel better for me at least than always requesting or pushing updates from/to the api every <time unit here> as it would be instant

2 Likes

I doubt that there won’t be support for websockets at some point, at least serverside. If it’s not supported clientside, you could just proxy it or use long-polling for example as a workaround.

3 Likes

Yeah, I’m not too worried about the amount of requests, since the API is very fast, and I’ll cache pretty much all of the things that don’t change too often

1 Like

Love the logical approach to your design, which is something many GMod addons sorely lacked. Do you anticipate a need for asynchronous transactions in your s&box project? I’m curious if you would implement a task model in the API itself or handle it server side.

2 Likes

I think that if I find that requests are backing up under higher player counts and so on, I might need to add a “data update” queue in addition to simply fetching data from the API with async

3 Likes