BAR Headless

What is this?

This is a very basic writeup of what I discovered on how to run a headless game between AI’s and how to run a headless replay. The information here is not 100% accurate; it’s just what worked for me at the time. Take it with a grain of salt!

Running a Headless AI Game

When running a headless game between two AI’s I wanted to accomplish the following:

  • Be able to choose the map, AI’s, settings, etc. of the game that would be run
  • Have the game run quickly and with as few resources as possible
  • Generate a replay from which I could watch the game and use for further projects

Choosing the game

This was accomplished by simply setting up a game using the normal BAR UI.

  • Open the game as you normally would
  • Go to the Skirmish menu
  • Choose Spectate above the Start button (this will remove you from the game)

Spectate Button

  • Choose the AI(s), map, teams, and settings that you wish
  • Start the game
  • Exit the game once it is loaded (there is no need to wait for this game to play out, you are only getting the settings)

Run a headless game quickly

Now if you go to your BAR data directory (probably either C:\Program Files\Beyond-All-Reason\data or %LocalAppData%\Programs\Beyond-All-Reason\data) there should be file called _script.txt.

Example _script.txt:

Example _script.txt

  • This file contains all the settings from the game you just set up. In that file replace
1
2
3
[modoptions]
{
}

with

1
2
3
4
5
[modoptions]
{
	MinSpeed = 9999;
	MaxSpeed = 9999;
}

so that the game will run as fast as possible (otherwise it will run in real time, which is pointless if you are not watching or playing the game).

  • Go to the engine folder under the BAR data folder and find the most recent engine

Example (in this case it would be 105.1.1-2511-g747f18b bar):

Example _script.txt

  • Open a command prompt and go to that engine directory using cd

cd "C:\Program Files\Beyond-All-Reason\data\engine\105.1.1-2511-g747f18b bar"

  • Run a headless game using the _script.txt file you generated

.\spring-headless.exe --write-dir <full path to the BAR/data directory> _script.txt

Example: .\spring-headless.exe --write-dir "C:\Program Files\Beyond-All-Reason\data" _script.txt

Getting the replay from our headless game

Thankfully BAR automatically generates a replay of our game in the demo folder under the main BAR data folder. This means we can watch it using BAR’s built-in replay viewer or use it in whatever projects we would normally use a replay.

Running a Headless Replay

Here we want to be able to run any replay headlessly in order to generate statistics or other data from the replay that may not be output when the game is initially run. We need to

  • Get the map
  • Get a replay
  • Get the game version
  • Get the engine
  • Inject a widget (to export data)
  • Run the replay headlessly

Getting the Stuff

Replays can be downloaded easily from the BAR website, but there is also a replay API that allows you to download the replay programmatically. This API gives responses in JSON format, so you can easily code a project to pull these replays.

To get what you need:

Example: cd C:\Program Files\Beyond-All-Reason\bin

  • Run the pr-downloader to download the game version by running: .\pr-downloader.exe --filesystem-writepath <BAR data folder> --download-game <gameVersion>

Example: .\pr-downloader.exe --filesystem-writepath "..\data" --download-game "Beyond All Reason test-26380-9194139"

  • Download the engineVersion from Github, REMOVING THE BAR105 FROM THE VERSION, using this URL: https://github.com/beyond-all-reason/spring/releases/download/spring_bar_%7BBAR105%7D{engineVersion}/spring_bar_.BAR105.{engineVersion}_windows-64-minimal-portable.7z

Example: https://github.com/beyond-all-reason/spring/releases/download/spring_bar_%7BBAR105%7D105.1.1-2511-g747f18b/spring_bar_.BAR105.105.1.1-2511-g747f18b_windows-64-minimal-portable.7z

  • Extract and move this engine folder into the engine directory (keep it in its own, named folder).

Example _script.txt

Injecting a Widget

In order to extract data, we need to inject a widget into the engine that will run as the replay runs. This is a little touch-and-go, and is the one part of this process that I’m pretty unsure of. What I have gotten to work is by spoofing another widget’s widget:GetInfo function. If this doesn’t work, try spoofing a different widget? It’s worked before!

Take the following widget file and drop it into <BAR Engine>\LuaUI\Widgets folder (create the folder if you need to) and name it whatever you wish.

Example: C:\Program Files\Beyond-All-Reason\data\engine\105.1.1-2511-g747f18b bar\LuaUI\Widgets\headless_stat_generator.lua

This widget file will speed up the replay when running in headless mode, and will output statistic and unit information into the BAR data directory as CSV’s.

Widget File:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
-- function widget:GetInfo()
--  return {
--     name    = "StatGenerator",
--     desc    = "Generates stats from replays",
--     author  = "Neek-sss",
--     date    = "Apr. 2024",
--     license = "Apache 2.0/MIT",
--     layer   = -99990,
--     enabled = true,
--  }
-- end  

file = io.open("test.txt", "w")  
io.output(file)  
io.write("test\n")  
io.close(file)  
  
function widget:GetInfo()  
    return {  
       name = "BuildETA",  
       desc = "Displays estimated time of arrival for builds",  
       author = "trepan (modified by jK)",  
       date = "2007",  
       license = "GNU GPL, v2 or later",  
       layer = -9,  
       enabled = true  
    }  
end  
  
function dump(o)  
   if type(o) == 'table' then  
      local s = '{ '  
      for k,v in pairs(o) do  
         if type(k) ~= 'number' then k = '"'..k..'"' end  
         s = s .. '['..k..'] = ' .. dump(v) .. ','  
      end  
      return s .. '} '  
   else  
      return tostring(o)  
   end  
end  
  
local startingSpeed = 9999  
local timer  
local headless  
local frame = 0  
local def_names = {}  
  
function widget:Initialize()  
    headless = (Spring.GetConfigInt('Headless', 0) ~= 0)  
    if (headless) then  
        Spring.Echo('Prepping for headless...')  
        Spring.SendCommands(  
        string.format('setmaxspeed %i', startingSpeed),  
        string.format('setminspeed %i', startingSpeed),  
        'hideinterface'  
        )  
    end  
  
    file = io.open("defs.csv", "w")  
    io.output(file)  
    io.write("id,name,translatedTooltip,translatedHumanName\n")  
    for k,v in pairs(UnitDefs) do  
        id = k  
        name = ""  
        tooltip = ""  
        human_name = ""  
        for k1, v1 in v:pairs() do  
            if k1 == "name" then  
                name = v1  
                def_names[id] = name  
            elseif k1 == "translatedTooltip" then  
                tooltip = v1:gsub(",", "")  
            elseif k1 == "translatedHumanName" then  
                human_name = v1  
            end  
        end        io.write(id)  
        io.write(",")  
        io.write(name)  
        io.write(",")  
        io.write(tooltip)  
        io.write(",")  
        io.write(human_name)  
        io.write("\n")  
    end  
    io.close(file)  
  
    file = io.open("stats.csv", "w")  
    io.output(file)  
  
    for team,stats in pairs(Spring.GetTeamStatsHistory(0,0)) do  
       for name,value in pairs(stats) do  
          io.write(name)  
          io.write(",")  
       end  
       io.write("team")  
       break  
    end  
    io.write("\n")  
    io.close(file)  
  
    file = io.open("units.csv", "w")  
    io.output(file)  
    io.write("frame,unit_def_name,unit_id,team_id,action\n")  
    io.close(file)  
  
    file = io.open("positions.csv", "w")  
    io.output(file)  
    io.write("player_id,x,y,z\n")  
    io.close(file)  
end  
  
function widget:GameFrame(n)  
    frame = n  
    -- Open file and set as output    file = io.open("stats.csv", "a")  
    io.output(file)  
  
    num_teams = #Spring.GetAllyTeamList() - 1  
    for i=0,num_teams do  
       hist_len = Spring.GetTeamStatsHistory(i)  
       for key,stats in pairs(Spring.GetTeamStatsHistory(i, hist_len)) do  
          for name,value in pairs(stats) do  
             io.write(value)  
             io.write(",")  
          end  
          io.write(i)  
          io.write("\n")  
       end  
    end  
    -- Close file    io.close(file)  
end  
  
local position_count = 0  
  
function widget:UnitCreated(unitID, unitDefID, teamID)  
    file = io.open("units.csv", "a")  
    io.output(file)  
    io.write(frame)  
    io.write(",")  
    io.write(def_names[unitDefID])  
    io.write(",")  
    io.write(unitID)  
    io.write(",")  
    io.write(teamID)  
    io.write(",add\n")  
    io.close(file)  
  
    if position_count < #Spring.GetAllyTeamList() - 1 then  
        local x,y,z = Spring.GetUnitPosition(unitID)  
        file = io.open("positions.csv", "a")  
        io.output(file)  
        io.write(teamID)  
        io.write(",")  
        io.write(x)  
        io.write(",")  
        io.write(y)  
        io.write(",")  
        io.write(z)  
        io.write("\n")  
        io.close(file)  
  
        position_count = position_count + 1  
    end  
end  
  
function widget:UnitDestroyed(unitID, unitDefID, teamID)  
    file = io.open("units.csv", "a")  
    io.output(file)  
    io.write(frame)  
    io.write(",")  
    io.write(def_names[unitDefID])  
    io.write(",")  
    io.write(unitID)  
    io.write(",")  
    io.write(teamID)  
    io.write(",remove\n")  
    io.close(file)  
end  
  
function widget:UnitGiven(unitID, unitDefID, newTeamID, teamID)  
    file = io.open("units.csv", "a")  
    io.output(file)  
    io.write(frame)  
    io.write(",")  
    io.write(def_names[unitDefID])  
    io.write(",")  
    io.write(unitID)  
    io.write(",")  
    io.write(teamID)  
    io.write(",remove\n")  
    io.close(file)  
end  
  
function widget:UnitTaken(unitID, unitDefID, oldTeamID, teamID)  
    file = io.open("units.csv", "a")  
    io.output(file)  
    io.write(frame)  
    io.write(",")  
    io.write(def_names[unitDefID])  
    io.write(",")  
    io.write(unitID)  
    io.write(",")  
    io.write(teamID)  
    io.write(",add\n")  
    io.close(file)  
end  
  
function widget:GameStart()  
   Spring.Echo('Game started... starting timer.')  
   timer = Spring.GetTimer()  
end  
  
function widget:GameOver()  
   local time = Spring.DiffTimers(Spring.GetTimer(), timer)  
   Spring.Echo(string.format('Game over, realtime: %i seconds, gametime: %i seconds', time, Spring.GetGameSeconds()))  
end

Running the Replay

In order to run the replay we need to create a _script.txt file that points to the replay, and then run that replay headlessly.

  • Create a _script.txt file in the BAR data folder with contents like the following:
1
2
3
4
[game]  
{  
demofile=<REPLAY FILE NAME>;  
}

Example:

1
2
3
4
[game]  
{  
demofile=2024-07-01_14-03-07-946_Cirolata 1_105.1.1-2511-g747f18b BAR105.sdfz;  
}
  • Open a command prompt and go to that engine directory using cd

cd "C:\Program Files\Beyond-All-Reason\data\engine\105.1.1-2511-g747f18b bar"

  • Run a headless game using the _script.txt file you generated

.\spring-headless.exe --write-dir <full path to the BAR/data directory> _script.txt

Example: .\spring-headless.exe --write-dir "C:\Program Files\Beyond-All-Reason\data" _script.txt

0%