Using OpenTTD to create a realistic data stream

A while back I was going through a refresher course on Azure Stream Analytics (https://www.pluralsight.com/courses/azure-stream-analytics-understanding by Alan Smith) and found the method used to generate data fascinating.

Basically, Alan used an opensource C# racing game to build a whole course on Azure Stream Analytics and the processing of telemetry data generated by it.

I took inspiration from it and manipulated a C++ opensource game OpenTTD to output JSON messages to disk which is then streamed to Azure via an Eventhub.

After scouring the code I found a procedure called OnNewDay which is called for each vehicle when a day is over.

OpenTTD source code showing OnNewDay procedure

This procedure was perfect for adding a function that would log out the vehicle information

LogVehicleEvent function call added to OnNewDay

Here is the full listing of LogVehicleEvent (bear in mind I primarily code in C#)

LogEvents.cpp

#include "stdafx.h"
#include <string>
#include "vehicle_base.h"
#include "station_base.h"
#include "strings_func.h"
#include "town_map.h"
#include "town.h"
#include "townname_func.h"
#include <iostream>
#include <fstream>
#include "LogEvents.h"
#include <ctime>

bool LogVehicleEvent(Vehicle *v)
{
Rect coord = v->coord;
Money value = v->value;
Money profit_this_year = v->profit_this_year;
Money profit_last_year = v->profit_last_year;
Year build_year = v->build_year;
Date max_age = v->max_age;
Date age = v->age;
UnitID unitnumber = v->unitnumber;
uint16 cur_speed = v->cur_speed;
Date date_of_last_service = v->date_of_last_service;
byte breakdowns_since_last_service = v->breakdowns_since_last_service;
Order current_order = v->current_order;
StationID last_station_visited = v->last_station_visited;
StationID last_loading_station = v->last_loading_station;

if (unitnumber != 0)
{
string name;
string type;
time_t now = time(0);

switch (v->type) {
case VEH_TRAIN:    type = "Train"; break;
case VEH_ROAD:     type = "Road Vehicle"; break;
case VEH_SHIP:     type = "Ship"; break;
case VEH_AIRCRAFT: type = "Aircraft"; break;
default:           type = "Unknown Vehicle";
}

if (v->name != NULL)
{
name = v->name;
}
else
{
name = type + " " + to_string(unitnumber);
}

string lastStationVisited = GetTheStationName(last_station_visited);
string lastLoadingStation = GetTheStationName(last_loading_station);

string message = "{";
message = message + "\"Name\":\"" + name + "\"";
message = message + ",";
message = message + "\"TimeStamp\":\"" + to_string(now) + "\"";
message = message + ",";
message = message + "\"Type\":\"" + type + "\"";
message = message + ",";
message = message + "\"CurrentSpeed\":" + to_string(cur_speed);
message = message + ",";
message = message + "\"LastStationVisited\":\"" + lastStationVisited + "\"";
message = message + ",";
message = message + "\"LastLoadingStation\":\"" + lastLoadingStation + "\"";
message = message + ",";
message = message + "\"ProfitLastYear\":" + to_string(profit_last_year);
message = message + ",";
message = message + "\"ProfitThisYear\":" + to_string(profit_this_year);
message = message + ",";
message = message + "\"Value\":" + to_string(value);
message = message + ",";
message = message + "\"BuildYear\":" + to_string(build_year);
message = message + ",";
message = message + "\"Age\":" + to_string(age);
message = message + ",";
message = message + "\"MaxAge\":" + to_string(max_age);
message = message + ",";
message = message + "\"DateOfLastService\":" + to_string(date_of_last_service);
message = message + ",";
message = message + "\"BreakdownsSinceLastService\":" + to_string(breakdowns_since_last_service);
message = message + "}";

ofstream myfile;
string tempFolder = getenv("TEMP");
myfile.open(tempFolder + "\\" + GetRandomString(15, "abcdefghijklmnaoqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") + ".txt");
myfile << message + "\n";
myfile.close();

return true;
}
}

string GetTheStationName(StationID s)
{
string lastVisitedStation = "";
try
{
BaseStation *station = BaseStation::Get(s);

Town *town = station->town;

char buf1[(MAX_LENGTH_TOWN_NAME_CHARS + 1) * MAX_CHAR_LENGTH];
char buf2[(MAX_LENGTH_TOWN_NAME_CHARS + 1) * MAX_CHAR_LENGTH];

const char *buf = town->name;
if (buf == NULL) {
GetTownName(buf2, town, lastof(buf2));
buf = buf2;
}

StringID stationStringID = station->string_id;
string stationNamePart = GetStringPtr(stationStringID);
stationNamePart = stationNamePart.substr(stationNamePart.find(" ") + 1);

string townNameString(buf);

lastVisitedStation = townNameString + " " + stationNamePart;
}
catch (...)
{

}
return lastVisitedStation;
}

string GetRandomString(uint l = 15, std::string charIndex = "abcdefghijklmnaoqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
{
uint length = rand() % l + 1;

uint ri[15];

for (uint i = 0; i < length; ++i)
ri[i] = rand() % charIndex.length();

std::string rs = "";

for (uint i = 0; i < length; ++i)
rs += charIndex[ri[i]];

if (rs.empty()) GetRandomString(l, charIndex);
else return rs;
}

LogEvents.h

#pragma once

#include "stdafx.h"
#include <string>
#include "vehicle_base.h"
#include "station_base.h"
#include "strings_func.h"
#include "town_map.h"
#include "town.h"
#include "townname_func.h"
#include <iostream>
#include <fstream>

using namespace std;

bool LogVehicleEvent(Vehicle *v);
string GetTheStationName(StationID s);
string GetRandomString(uint l, std::string charIndex);

The result is that a file is created every game day for each vehicle

JSON telemetry files generated per vehicle per game day

Sample JSON vehicle telemetry data content

And finally, here is an example after it passed through Azure Stream Analytics

Query

SELECT Name, Type, avg(CurrentSpeed) as AvgSpeed, max(CurrentSpeed) as MaxSpeed INTO [OpenTTDOutput] FROM [OpenTTDInput] GROUP BY Name, Type, TumblingWindow(minute,10)

Result

{"name":"Road 13","type":"Road","avgspeed":83.92307692307692,"maxspeed":176.0}
{"name":"Train 39","type":"Train","avgspeed":0.0,"maxspeed":0.0}
{"name":"Road 37","type":"Road","avgspeed":80.88,"maxspeed":176.0}
{"name":"Road 67","type":"Road","avgspeed":0.0,"maxspeed":0.0}
{"name":"Road 12","type":"Road","avgspeed":111.23076923076923,"maxspeed":176.0}
{"name":"Train 10","type":"Train","avgspeed":146.53846153846155,"maxspeed":264.0}
{"name":"Train 47","type":"Train","avgspeed":0.0,"maxspeed":0.0}
{"name":"Train 30","type":"Train","avgspeed":0.0,"maxspeed":0.0}
{"name":"Road 5","type":"Road","avgspeed":75.07692307692308,"maxspeed":176.0}
{"name":"Road 16","type":"Road","avgspeed":79.384615384615387,"maxspeed":176.0}
{"name":"Road 52","type":"Road","avgspeed":134.72,"maxspeed":176.0}
{"name":"Road 49","type":"Road","avgspeed":132.73076923076923,"maxspeed":176.0}
{"name":"Train 2","type":"Train","avgspeed":80.6,"maxspeed":264.0}
{"name":"Road 75","type":"Road","avgspeed":122.19230769230769,"maxspeed":176.0}
{"name":"Road 20","type":"Road","avgspeed":139.5,"maxspeed":176.0}
{"name":"Road 68","type":"Road","avgspeed":0.0,"maxspeed":0.0}
{"name":"Train 18","type":"Train","avgspeed":102.07692307692308,"maxspeed":198.0}
{"name":"Road 72","type":"Road","avgspeed":127.96153846153847,"maxspeed":176.0}
{"name":"Train 13","type":"Train","avgspeed":144.26923076923077,"maxspeed":264.0}
{"name":"Train 35","type":"Train","avgspeed":0.0,"maxspeed":0.0}

Leave a Reply