Return to Level1Techs.com

Devember - Screep Studio

code
devember2k18

#41

Unfortunately long polling is not supported by the HTTP implementation used by the server, it’s just a basic set of REST endpoints.
The live data is served from a specifically implemented web socket endpoint.

The work around should be fine for development, hopefully the bug will be fixed by the time I commit my code to GitHub and I can list Qt 5.12 as a requirement if anyone wants to build the code on Linux.


#42

Day 4

Today I will be mostly extracting a network protocol from JavaScript soup

Still useful because at some point I will need to write some Creep A.I to test against and I will eventually get back to playing the actual game so a little JavaScript refresher will come in handy.


#43

I’m about to stress test my database by making a 16 GiB worth of records.


#44

Day 5

TL;DR - Skip to that section please.

Not much new to show today as I’ve been working on figuring out the Websocket protocol used by the Screeps Sever. But I have made progress.

I figured out the basics of the protocol and wrote some test code to make sure my findings where correct, so far I can do the following:-

  1. Connect to the web socket endpoint (ws://host:port/socket/websocket/). Once you open the connection the server immediately sends two messages

    time [numeric milliseconds since Unix epoc]
    protocol [numeric version number]

    Then it happily sits there with the socket open … there doesn’t seem to be any time out or keep alive messages that I need to handle.

  2. I can successfully authenticate with the server. This involves sending a simple text based command to the server containing the authorisation token received from the /api/auth/signin REST endpoint.

    auth [token]

    The auth command returns either of the following

    auth ok [token] - A new token is returned, not sure what this is used for yet
    auth failed

  3. Subscribe/unsubscribe to event channels

    subscribe [channel]
    unsubscribe [channel]

  4. Using the above subscription command I can now subscribe to a room and recover all the information about the entities in the room including updates as they move or change state.

    subscribe room:[room_name]
    unsubscribe room:[room_name]

    Unfortunately I think this part of the protocol was designed late on a Friday afternoon and the developers could taste the after work pints as it looks like they rushed things and didn’t think the protocol through. The return is simply a JSON array as follows:-

    [[email protected]:[room_name], 'error message text'] - On failure
    ['room:[room_name], { ... massive JSON object here }]

    Now this isn’t ideal, as up to now, every reply contained the original command as the first parameter so it’s easy to match commands to responses when you’re firing them off asynchronously. It would have been much nicer if they had kept that consistency. Something like: -

    subscribe room:[room_name] { ... massive JSON object here } - For the initial channel recovery
    event room:[room_name] { … JSON list of updated objects here } - For subsequent updates

    But, it was late on a Friday and pints were in order so it was quick and dirty. I guess it means I will have to just check the if the first character of every reply is a ‘[’ to determine if its a normal command reply OR special subscription command reply or channel event. I might use a regular expression to at least check to see if I received a JSON array.

  5. The server also supports GZip compression. It’s disabled by default which is good because I don’t need to worry about it now, but future me might add that support (especially considering the maaaaahooooosive JSON objects coming from room subscriptions). GZip support is toggled with a simple command.

    gzip [on|off]

    When GZip is enabled for the Websocket connection all data from the server will be in the following format:-

    gz:[base64 encoded data]

One of the things I wasn’t sure about was how ‘shards’ work. I’ve seen some unofficial documentation mention that the websocket API expected all room names to be prefixed with the shard if the server supported them but I was unable to find any mention of shards in the private server source code. So today I decided to have a poke around on the live server and revisit the REST API using the existing web client and Chromium’s developer tools.

This turned out to be very useful because not only did I find out that only the live server supports shards, but I was also able to find where in the REST replies the shard information should be as well as fill in some gaps for endpoints that were missing from other peoples documentation.

{
  "ok": 1,
  "package": 145,
  "protocol": 13,
  "serverData": {
    "historyChunkSize": 100,
    "shards": [
      "shard0",
      "shard1",
      "shard2",
      "shard3"
    ]
  }
}

So all in all, even though not much code has written, it was a very productive bit of analysis (and I got to refresh my limited JavaScript knowledge a bit)

TL;DR - More research was done.

Tomorrow I will start writing code again … after I fix my test server … because it shat itself :poop: and somehow managed to delete the scripts for the test bot so now they’re all dead :frowning:

Shecks


#45

Day 6

Nothing to see here today again, only got couple of hours sleeps last night (and the dog ate my homework™) so didn’t get any code for the websocket interface done today as planned, my brain is just not working. So I just did some bits and pieces.

  1. Fixed my test server so I have a bot back playing in one of my rooms to generating websocket traffic

  2. Discovered that, although the Screeps socket protocol doesn’t require any “keep alive” messages, the actual websocket protocol support PING/PONG messages and other Screeps APIs and tools seem to suggest that it should be used (The Qt QWebsocket implementation supports sending PING and PONGs so I will just send periodic PINGs on a timer from my WebsocketManager when I get around to writing it).

  3. Did a little refresher on Regular Expressions so I can parse and match the protocol messages a little easier and more elegantly than checking for specific characters in the messages.

That’s about it … I’m off to bed.

Shecks zzZZZZZzzzZZZZZZzzzzz :sleeping:


#46

Still Day 6

I felt guilty …

class WebsocketManager : public QObject {
    typedef QObject _super;

    Q_OBJECT

public:
    explicit WebsocketManager(QObject* parent = nullptr);
    virtual ~WebsocketManager();

    void openConnection(const QUrl& host, int port = -1, bool isSecure = false);
    void closeConnection();

    void sendCommand(const QString& command, const QString& parameters) const;
    void sendCommand(const QString& command, const QStringList& parameters) const;

    void subscribe(const QString& channel, const QString& objectName = QString()) const;
    void unsubscribe(const QString& channel, const QString& objectName = QString()) const;

    bool isConnected() const;

signals:
    void connected();
    void disconnected();

private slots:

private:
    typedef QMap<QString, int> TSubscriptionMap;

    QWebSocket _socket;
    QTimer _keepAliveTimer;

    TSubscriptionMap _subscriptions;
};

The header counts as code right?

I’ll implement it tomrrow :smiley:

Shecks


#47

:grin:

When following this thread I feel really bad how little I get done in an hour. :frowning:

You have a really nice log here :slightly_smiling_face:


#48

You shouldn’t feel bad, I do a lot more than an hour each day most days so my dev logs don’t really reflect the amount of work achieved in an hour :innocent:

At the moment I am just trying to get all the networking code out of the way (which has turned out to be a lot more than expected) so I can get on to the UI and graphics work which is more interesting so I am forcing myself to do a bit more. I am also not learning a new programming language (at least not C++, been reading a lot of JavaScript/Node.js though), just learning how the game server works and figuring out how to communicate with it.

Because I am an experienced programmer I can get a decent bit of code written in an hour but so far I’ve spent several hours just trying to figure out why my server died and how to get it back up and running or looking through the server code looking for hints to fill in gaps in my knowledge about the protocol :thinking:


#49

Day 7

Yet another day of not much progress, it seems I’ve been struck down with a Norovirus (not fun at all, I was very close to calling for a priest last night, 100x worse than “Man-flu” I reckon)

Anyway, I couldn’t face working on the Websocket network code today so I wimped out and did some graphics instead. I discovered that the Screeps developers have open sourced the JS renderer and it seems that some of the game assets are also opensource. So had a go at adding the tiled background graphics to my room scene.

It’s quite subtle and I am not sure if I have done it correctly because there are two images, ground and ground_mask and my theory is that they render the ground first then the fixed terrain (just black blocks for walls and green blocks for swamp at the moment) and finally the ground_mask over the top.

ground
Ground.

ground-mask
Ground Mask

Now it’s either my old eyes or my current feverish state, but I can’t see the difference between rendering just the ground and rendering the ground with the ground mask over the top.

Here’s an example room for the official Screeps web client:-

The background looks a lot darker too so I think they might be filling the scene with a dark background colour first then blending the rest … I’ll try to figure it out when I my code stops jumping around on the screen.

Shecks :nauseated_face:


#50

And still you managed to get something done? :open_mouth:

image


#51

Just about, it took me the best part of an hour just to figure out the math to calculate the width and height of the partial tiles on the right hand-side and bottom of the room. I kept writing overly complex code then looking at it and thinking “that’s not right, there’s got to be something simpler” :thinking:


#52

Day 8

Made some good progress on the Websocket networking code today. I managed to come up with a way to hide the inconsistencies in the protocol from the upper layers of my code by manually packaging the command responses as JSON objects. Command responses come back from the server as simple space delimited text whereas subscription and stream events are always JSON arrays with channel name and associated (event specific) data as a JSON object.

I didn’t want to have to deal with the differences inside my models so as part of my WebsocketManager I package the command responses into the same format JSON arrays using the fake channel called “system”, the command name as the “stream” and the command results as a JSON array.

So everything emitted from my WebsocketManager is neatly packaged into a SocketResponse object with a consistent interface: -

class SocketResponse {
    friend class WebsocketManager;

public:
    virtual ~SocketResponse() {

    }

    const QString& channel() const              { return _channel; }
    const QString& stream() const               { return _stream; }
    const QJsonDocument& json() const           { return _json; }

    QString toString() const                    { return _json.toJson(); }

private:
    QString _channel;
    QString _stream;
    QJsonDocument _json;

    SocketResponse(const QString& channel, const QString& stream, const QJsonDocument& json);
};

Commands all come through the “system” channel with the command name as the stream/instance, room events from the “room” channel with the room name as the stream/instance etc.

I can now subscribe to any of the “channels” and receive live data. The room channel spits out a huge JSON object which is going to take a fair bit of work to parse to the point where I will have something to show in the client (I will need to write graphics entities for each of the games entities or at least some place holders) so I decided to test my websocket code by subscribing to the user console channel.

So, I now have live console output:

As far as I can tell, console supports 3 different types of message from the server:

  1. General log messages - These either come from the server itself or the result of using JavaScript Console.log() from you A.I scripts (Shown in gray text above)

  2. Command results - These are the returned result from any commands issued at the console prompt. I have yet to implement the console input but I can input commands through the Steam Screeps Client and they are pushed down the console stream to my app. (Shown in cyan in the above)

  3. Errors - These are errors either as a result of errors in the bot A.I JavaScript as it’s executing or from invalid input to the console. (Shown in red above)

So it looks the basics are working, I will probably need to tweak things a little since I am learning more about the protocol as I go there may well be some channel/streams that don’t fit the model I am using, but I have enough now to push on with the room stream.

My plan will be to create a base class for the room entities (since they all have a type and x/y position and some other common attributes). Then create a simple QGraphicsViewItem to display them in my RoomGraphicsScene (just as different coloured shapes for the moment).

Once I have that working I’ll probably implement the room item selection so I can do the properties panel.

If I get that far I will start to think about the mammoth task of writing type specific classes for each game entity type to draw and animate them.

Shecks


#53

Day 9

TL;DR - Nothing to see here, more refactoring and fun with state machines.

Nothing new and interesting to show today, the UI looks the same as yesterday. The code on the other hand has been almost completely refactored (again), but I’m really happy with where it is now. I cleaned up all the test code and did a lot of restructuring. It will be a lot easier to support opening multiple rooms now (possibly using a tabbed interface) which was one of the main things I wanted to support.

Another major change was to the networking code. As I’ve mentioned before Screeps has two client APIs, a REST API for static data and a WebSocket API for live data (room entity updates, console update etc) both APIs are required to build a usable client.

I didn’t want any of the application data models or UI code to be concerned with having to build HTTP requests for the REST API, format data for the WebSocket API or parse JSON packets so I wrote a wrapper class for each API (RestNetworkManager and SocketNetworkManager) with interfaces and nicley packaged PODs that hid all the gory networking details from the upper layers.

But, once I completed the SocketNetworkManager I realised, they’re not mutually exclusive … I need both of them available for the client to function and they both need to be authenticated, so how will the connect sequence work? Eventually the user will be presented with a login dialog to enter the server endpoint and authentication credential … but then what?

If the HTTP server hosting the rest API is up but the websocket endpoint isn’t working what do I do? What if the websocket connection is dropped while the client is running. I don’t really want to be juggling two different network managers. Also the two APIs are linked, before you can send any commands over the websocket API you need to send an authentication token … but you first have to get this authentication token by authenticating with the REST API…

So I needed to think about the connection sequence … enter a new level of abstraction … NetworkModel. This new model acts as the interface between the UI and both API network managers and most importantly it manages the connection making sure that both APIs are available before the UI can make any requests.

To handle the connection sequence I implemented a state machine with the following states: -

  1. Disconnected - Nothing interesting to see here, we can’t do anything yet. When NetworkModel::openConnection() is called I attempt to open a connection to the websocket API. On success we move to the next state …

  2. SocketConnected - We have an websocket connection and we know we at least have a working network connection and the websocket is available. But we can’t doing anything with it until we authenticate so we now need to check the HTTP endpoint for the REST API. Now I could just POST /api/auth/signin to authenticate the user and get the authentication token but I just want to check if the HTTP endpoint is available and since there’s an endpoint to get information about the server which doesn’t required authentication, I figure I might as well just grab that now (it also returns some information about the server that I will need later anyway). So a call to /api/version REST endpoint is made and on success we move to the next state.

  3. RestConnected - Ok it’s not really a “connection” but now we know the REST endpoint is available, time to get that authentication token so a call to the /api/auth/signin REST endpoint is made using the the users username and password … on success we move to the next state…

  4. RestAuthenticated - Excellent … at this point we have received an authentication token form the REST API … time to get the websocket API authenticated so we send the “auth [token]” command down the websocket … and on success … you guessed it … we move to the next state.

  5. SocketAuthenticated - Technically both “connections” are in a state where the client can now make any requests or execute any commands but since I’ve just authenticated the user I figure this is as good a place as any to get information about the signed in users account so a call to the /api/auth/me REST endpoint is made and … on success … we go to yet another state …

  6. Connected - Yay! Finally! The was much rejoicing … as far as the UI is concerned we are now connected.

It looks like a lot when written down but from the UI layers point of view it’s all hidden. It’s also asynchronous, so now instead of juggling the two different network managers the UI only needs to make the following calls:

NetworkModel:setAuthentication([username], [password])
NetworkModel::openConnection([host], [port], [secure_flag])

And wait for the NetworkModel::connected signal to be fired (or NetworkModel::connectError if there’s any errors). I also added NetworkModel::connectionStateChanged([oldState], [newState]) in case I want to show progress (everything happens pretty quickly though) or do anthing in intermediate states.

Anyway, that’s what I did today (took a bit more than an hour :innocent:) … sorry there’s no new UI or fancy graphics to look at yet, but I think this needed to be done before moving on.

Shecks


#54

Day 10

I’ll try too keep this entry short since I don’t have any fancy graphics and pictures to show. Today I’ve been working on parsing the JSON for the room update events (from the websocket)

Iv’e had to figure out how the server notifies the client about the current state of the room objects, newly added objects, updated objects and deleted objects then get this into my RoomModel in a usable form.

This has been written and is working, everything I need to render the game entities in the QGraphicsScene is now available in my RoomModel but since I am just logging to the debug console there’s no point in posting any screenshots … it’s just more text.

Shecks


#55

Still Day 10

Just to break up the wall of text I’ve been posting … here’s some fancy graphics …

Check out the detail on those little yellow entities … I’m getting better at pixel art :stuck_out_tongue:

And for comparison … even though, I’m sure you’ll agree, it’s virtually indistinguishable from the Screeps Steam client … here’s how the same room looks

Shecks


#56

Day 11

Since things are starting to get a bit more graphical now I thought I’d post a short video showing my Screep Studio clients current functionality.

As I can now retrieve the live room entities from the server I have started adding the code to display them in the RoomGraphicsScene. They are still only place holders but I’ve made a couple of simple different types for testing purposes so I can identify easier during development.

At the moment I have graphics for the following types:

  1. Sources - These are the rooms energy sources used by the Creeps to harvest the energy needed for building, they are shown as yellow rounded rectangles with a thin darker yellow border

  2. Construction Sites - These are unfinished buildings (storage containers, roads, walls etc). They remain as constructions sites until they have been completely built then are replaced by the target infrastructure type. They are shown here as gray circles with a darker gray inner circle.

  3. Creeps - Creeps are the little A.I bots that move about the room and do all the work (harvesting energy, building, repairing, upgrading etc). Here they are shown as little yellow circles with a blue inner circle.

  4. Unknown Entity - Currently all other entities are displayed using a simple placeholder QGraphicsItem. They are shown as rounded green rectangles with a black border.

The “unknown entity” placeholder turned out to be a very useful addition. Using them helped me observe how the game server handles the completion of construction sites. Thankfully, instead of changing the entity type when a construction site is completed, the game actually removes the construction site and creates a new infrastructure entity in its place. You can see where this has happened in the video where there are some placeholder entities showing up in the line of constructions sites. These are roads that have been completed by the Creeps but the road graphics are not support yet.

Also, the client supports scalling/zooming of the view via Ctrl + Mouse Wheel. Finally, the console is dockable but I am not sure how to get OBS to record the applications child windows so it looks a little strange in the video where I tried to demo detaching it and floating over the main window.

EDIT I forgot to mention there’s also single and multiple item selections support too. This will be used later to update the properties view with information about the selected entity.

Plans for today
I’d like to implement the update code for the entities so that I can get the Creeps moving about the room.

Shecks


#57

Still Day 11 (kind of)

Just a quick update.

I have written the code to update the game entities in response to the server events. As a test I have implemented a very crude movement mechanism for the Creeps, when I get a position update I just move them directly to the new location. The official Screeps clients does a tween motion so that the Creeps glide smoothly between the grid positions so I need to figure out how to do that.

I’ve also added some “placeholder” graphics for some of the other game entities (Spawn and Controller). To help with debugging I added tooltips to each of the entities but OBS doesn’t record them when recording in window mode.

Not sure what am going to tackle next … maybe figure out how to render the roads, those will be tricky :thinking:

Shecks


#58

Day 12

Well that’s the end of than then, might as well write my blog entry now because my Screeps Server just shat itself again :poop: and corrupted the room database. My lovely test room is no more :frowning:

I am still trying to figure out if I can salvage it but it’s very strange. The Steam Screeps Client seems to still be able to render the room correctly but on initial inspection it looks like the server is not sending me the room entities on the initial recovery. The JSON array of objects is empty. So either the Steam Screeps Client is working form a cache or it’s not using the same websocket interface I am :thinking:

Up until this apocalyptic event I was working on implementing the graphical objects for more of the game entities and getting some of them to react to game events and reflect changes to their properties.

Anyway, I’m off to do some debugging or to reset my server DB and get another test room up and running :frowning:

Shecks


#59

Day 12-ish

Got my favourite test room back … we will rebuild!!! (Creeps are going to work the night shift …)

I’m off to sleep.

Shecks


#60

Day 13

While writing the scene graphics classes for the individual game entities I found myself having to dig through all the debug data I am spitting out to the Qt Creator console log looking for the raw JSON corresponding to the entity I was working on so that I could sanity check values (the joys of working with an undocumented API). Assuming I found the right object, I’d then copy/paste it into Atom and run a JSON beautifier on it so it was readable.

This starts to get a bit tedious after a while … so I wrote an “Object Inspector” and added it to Screep Studio.

Now all I have to do is click the entity in the scene view and I get an nice little display of the raw JSON in a panel on the right :slight_smile:

Pretty happy with that, should be really useful when implementing code for new entities and when working on the entity animations.

Shecks