Implementation Server-side

Introduction

Contents are hosted on our community hub. Proposed urls for that are api.inexor.org or community.inexor.org.

The content distribution service should be dezentralized in the future, to match with the ideas of Distributed Network. For now, we have a centralized approach, which can be further broken apart in the future. Maybe in a similar fashion how torrents work.

Current Implementation

Under the hood we use Strapi as content management framework. This allows us to quickly organize schemas and extend the API. API endpoints are generated by strapi and can quickly be expanded on demand. Mysql, Postgres, Mongodb or SQLite can be used as database (the framework is db agnostic). For now we decide to go with mongoDB, as it comes with the best feature parity for strapi. Switching to MySQL (as we have most experience with that) is thinkable though.

Relationships are easily maintainable via strapi:

Client

Strapi comes with a HTTP Restful interface and a GraphQL interface.

There only exists a JavaScript client offered by strapi. However since everything is based on HTTP, for our C++ side we could use curl or whatever client we are comfortable using.

Read-only for public access is enabled by default, only logged in users are allowed to POST and PUT their own content. PUT for contents where they are registered as co-authors is also allowed.

More details on how to search for items of an API/table/list is documented in Strapis filter documentation. This way, we could query for e.g. GET community.inexor.org/contents?starred=true to retrieve all starred content for initial setup on game start.

Schema

This is our current schema implementation with fields and descriptions.

  • Comment
  • Content
  • Gamemode
  • License
  • Server
  • Team
  • User

There are more strapi-internal models like groups and permissions, to assign public/user/admin permissions.

Comment

We may want to support nested comments, in this case we need to define parent/children.

Fields

{
    "text": {
        "default": "",
        "type": "text"
    },
    "content": {
        "model": "content",
        "via": "comments"
    }
}

Content

Packaging

The most important field here is package which contains the file, which is important for client-side use e.g. in the Entity-System. There are JSON based contents (prefab, entity, map) and binary based contents (model, texture, sound) Collections are a special type of content. It is yet to decide how exactly we package contents.

Option A: Use dependencies as field to recursively resolve all required sub-packages and have the package field only contain the original content + references to other contents. This results in more client-work, (a complex package manager to retrieve and resolve dependencies is required), more HTTP overhead for recursive resolving but less harddisk storage usage. Using a package manager, we could also easily pin-point

Option B: Have package include the entire required original content of all sub-dependencies when being uploaded. Which results in less client-work, less HTTP overhead, but more harddisk storage usage.

Current problems with schema

This schema is likely to change, because dependencies are currently linked as contents. This makes it impossible to implement a version control for every kind of content. Users should be able to define the versions of their dependencies. This version control has to work both offline and online (push/pull)

Secondly, the gamemode is expected to be both a content (entity) but also be able to work as its own content node, to be used for relationship analysis (who is playing what gamemode on which server?)

This is a preliminary preview of the schema:

Fields

{
    "title": {
        "type": "string"
    },
    "description": {
        "type": "text"
    },
    "authors": {
        "collection": "user",
        "dominant": true,
        "via": "contents",
        "plugin": "users-permissions"
    },
    "package": {
        "collection": "file",
        "via": "related",
        "plugin": "upload",
        "required": false
    },
    "license": {
        "model": "license",
        "via": "contents"
    },
    "rating": {
        "type": "float"
    },
    "gamemodes": {
        "collection": "gamemode",
        "via": "contents",
        "dominant": true
    },
    "comments": {
        "collection": "comment",
        "via": "content"
    },
    "type": {
        "default": "",
        "type": "enumeration",
        "enum": [
            "collection",
            "map",
            "prefab",
            "model",
            "texture",
            "sound",
            "entity"
        ]
    },
    "dependencies": {
        "collection": "content",
        "via": "dependents",
        "dominant": true
    },
    "dependents": {
        "collection": "content",
        "via": "dependencies"
    },
    "teamHome": {
        "model": "team",
        "via": "homeMap"
    },
    "servers": {
        "collection": "server",
        "via": "map"
    }
}

Gamemode

Fields

{
    "name": {
        "default": "",
        "type": "string"
    },
    "contents": {
        "collection": "content",
        "via": "gamemodes"
    },
    "servers": {
        "collection": "server",
        "via": "gamemode"
    }
}

License

Fields

{
    "name": {
        "default": "",
        "type": "string"
    },
    "contents": {
        "collection": "content",
        "via": "license"
    }
}

Server

Fields

{
    "name": {
        "default": "",
        "type": "string"
    },
    "creator": {
        "model": "user",
        "via": "serversCreated",
        "plugin": "users-permissions"
    },
    "players": {
        "collection": "user",
        "dominant": true,
        "via": "serversPlaying",
        "plugin": "users-permissions"
    },
    "maxPlayers": {
        "default": 12,
        "max": 1024,
        "min": 1,
        "type": "integer",
        "required": true
    },
    "gamemode": {
        "model": "gamemode",
        "via": "servers"
    },
    "map": {
        "model": "content",
        "via": "servers"
    }
}

Team

Comparable to usergroup or clan as a different name. Allows the user to organize themselves for playing and mapping.

Fields

{
    "name": {
        "default": "",
        "type": "string"
    },
    "homeMap": {
        "model": "content",
        "via": "teamHome"
    },
    "description": {
        "default": "",
        "type": "text"
    },
    "tag": {
        "default": "",
        "type": "string"
    },
    "founder": {
        "model": "user",
        "via": "teamsFounded",
        "plugin": "users-permissions"
    },
    "members": {
        "collection": "user",
        "dominant": true,
        "via": "teams",
        "plugin": "users-permissions"
    }
}

User

Instead of maintaining an configfile of user-settings, common things like selected playermodel can be saved to the online profile.

Fields

{
    "username": {
        "type": "string",
        "minLength": 3,
        "unique": true,
        "configurable": false,
        "required": true
    },
    "email": {
        "type": "email",
        "minLength": 6,
        "configurable": false,
        "required": true
    },
    "password": {
        "type": "password",
        "minLength": 6,
        "configurable": false,
        "private": true
    },
    "confirmed": {
        "type": "boolean",
        "default": false,
        "configurable": false
    },
    "blocked": {
        "type": "boolean",
        "default": false,
        "configurable": false
    },
    "role": {
        "model": "role",
        "via": "users",
        "plugin": "users-permissions",
        "configurable": false
    },
    "teamsFounded": {
        "collection": "team",
        "via": "founder"
    },
    "teams": {
        "collection": "team",
        "via": "members"
    },
    "contents": {
        "collection": "content",
        "via": "authors"
    },
    "comments": {
        "collection": "comment",
        "via": "author"
    },
    "avatar": {
        "model": "file",
        "via": "related",
        "plugin": "upload",
        "required": false
    },
    "model": {
        "model": "content"
    }
}