--[[
The MIT License (MIT)
Copyright (c) 2016 Christoph Kubisch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
]]
local cmdlineargs = {...}
--local cmdlineargs = {"-addrace", "./results/test3.lua", "Race2.json", "-makehtml", "./results/test3.lua", "./results/test3.html"}
local cfg = {}
local function execEnvString(string, env)
local fn,err = loadstring(string)
assert(fn, err)
fn = setfenv(fn, env)
fn()
end
local function execEnv(filename, env)
local fn,err = loadfile(filename)
assert(fn, err)
fn = setfenv(fn, env)
fn()
end
execEnv("config.lua", cfg)
-------------------------------------------------------------------------------------
--
local printlog = print
local function tableFlatCopy(tab,fields)
local tout = {}
if (fields) then
for i,v in pairs(fields) do
tout[v] = tab[v]
end
else
for i,v in pairs(tab) do
tout[i] = v
end
end
return tout
end
local function tableLayerCopy(tab,fields)
local tout = {}
for i,v in pairs(tab) do
tout[i] = tableFlatCopy(v,fields)
end
return tout
end
local function quote(str)
return str and '"'..tostring(str)..'"' or "nil"
end
local function ParseTime(str)
if (not str) then return end
local h,m,s = str:match("(%d+):(%d+):([0-9%.]+)")
if (h and m and s) then return h*60*60 + m*60 + s end
local m,s = str:match("(%d+):([0-9%.]+)")
if (m and s) then return m*60 + s end
end
local function MakeTime(s, sep, fmt)
local fmt = fmt or "%#07.4f"
local sep = sep or ":"
local h = math.floor(s/3600)
s = s - h*3600
local m = math.floor(s/60)
s = s - m*60
return (h > 0 and tostring(h)..sep or "")..tostring(m)..sep..string.format(fmt,s)
end
local function DiffTime(stra, strb)
local ta = ParseTime(stra)
local tb = ParseTime(strb)
if (not (ta and tb)) then return end
local diff = tb-ta
local absdiff = math.abs(diff)
local h = math.floor(absdiff/3600)
absdiff = absdiff - h * 3600
local m = math.floor(absdiff/60)
absdiff = absdiff - m * 60
local s = absdiff
local sign = (diff >= 0 and "+" or "-")
if (h > 0) then
return sign..string.format(" %2d:%2d:%.3f", h,m,s)
elseif (m > 0) then
return sign.. string.format(" %2d:%.3f", m, s)
else
return sign.. string.format(" %.3f", s)
end
end
-------------------------------------------------------------------------------------
--
local function outputTime(time)
return string.format("%.2f", time)
end
local function computeTime(times)
local avgtime = 0
local variance = 0
local num = times and #times or 0
if (num < 1) then return 0,0,0 end
for i=1,num do
avgtime = tonumber(times[i]) + avgtime
end
avgtime = avgtime / num
local variance = 0
for i=1,num do
local diff = tonumber(times[i]) - avgtime
variance = variance + diff*diff
end
variance = math.sqrt(variance)
return num, avgtime, variance
end
-------------------------------------------------------------------------------------
--
local function parseJson(filename)
printlog("parsing:",filename)
local f = io.open(filename,"rt")
if (not f) then return nil end
local txt = f:read("*a")
f:close()
local cjson = require "cjson"
local json = cjson.decode(txt)
local numClasses = 0
local classes = {}
local classesSorted = {}
for _,v in pairs(json.classes) do
local id = tostring(v.Id)
local tab = {name=v.Name, id=id}
table.insert(classesSorted, tab)
classes[id] = tab
numClasses = numClasses + 1
end
local tracks = {}
local tracksSorted = {}
local numTracks = 0
for _,v in pairs(json.tracks) do
for _,layout in pairs(v.layouts) do
local name = v.Name.." - "..layout.Name
local id = tostring(layout.Id)
local tab = {name=name, id=id}
table.insert(tracksSorted, tab)
tracks[id] = tab
numTracks = numTracks + 1
end
end
json = nil
table.sort(classesSorted, function(a,b) return a.name < b.name end)
table.sort(tracksSorted, function(a,b) return a.name < b.name end)
printlog("Classes")
for i,v in ipairs(classesSorted) do
printlog(v.id,v.name)
end
printlog(numClasses)
printlog("Tracks")
for i,v in ipairs(tracksSorted) do
printlog(v.id,v.name)
end
printlog(numTracks)
return {
classes=classes,
classesSorted=classesSorted,
tracks=tracks,
tracksSorted=tracksSorted,
numClasses=numClasses,
numTracks=numTracks
}
end
local jsonFile = "r3e-data.json"
local r3egamedir = cfg.r3egamedir
if (not r3egamedir) then
local winapi = require("winapi")
local key = winapi.open_reg_key [[HKEY_CURRENT_USER\Software\Classes\rrre\shell\open\command]]
if (key) then
local value = key:get_value()
if (value) then
r3egamedir = value:match('%b""'):sub(2,-11)
end
key:close()
end
end
if (r3egamedir) then
r3egamedir = r3egamedir:gsub("\\","/")
local f = io.open(r3egamedir.."/GameData/General/r3e-data.json")
if (f) then
f:close()
jsonFile = r3egamedir.."/GameData/General/r3e-data.json"
end
end
local assets = parseJson(jsonFile)
if (not assets) then
printlog("ERROR: could not find r3e-data.json")
printlog(" manually set config.lua - r3egamedir")
printlog(" or put r3e-data.json into app's directory")
os.execute("pause")
os.exit()
end
-------------------------------------------------------------------------------------
--
do
local database = {
classes = {
minAI = 120,
maxAI = 80,
-- id
tracks = {
-- id
ailevels = {
{ },-- level times in seconds
},
},
}
}
end
-------------------------------------------------------------------------------------
--
local function GenerateStatsHTML(outfilename,database,fmt)
assert(outfilename and database)
printlog("generate HTML",outfilename)
local f = io.open(outfilename,"wt")
f:write([[
]])
if (cfg.embedStylesheet) then
local sf = io.open(cfg.embedStylesheet, "rt")
local str = sf:read("*a")
f:write([[
]])
sf:close()
else
f:write([[
]])
end
f:write([[
Icons are linked directly from the game's official website
R3E AI Database
]])
local trackEntries = 0
local totalEntries = 0
local totalTimes = 0
local function writeTrack(track, trackasset, entry, minAI, maxAI)
f:write([[
]]..trackasset.name.." ("..trackasset.id..[[)
]])
local found = 0
for ai = minAI, maxAI do
local times = track.ailevels[ai] or {}
local num,avgtime,variance = computeTime(times)
local aitime
if (num > 0) then
aitime = MakeTime(avgtime, ":", fmt)..' '..string.format("%.3f / %d", variance, num)..""
totalTimes = totalTimes + num
totalEntries = totalEntries + 1
found = 1
else
aitime = ""
end
f:write([[
]]..aitime..[[
]])
end
trackEntries = trackEntries + found
end
local function writeClass(class, classasset)
f:write([[
]]..classasset.name.." ("..classasset.id..[[)
Track
]])
local minAI = math.max(cfg.minAI,class.minAI)
local maxAI = math.min(cfg.maxAI,class.maxAI)
for ai = minAI, maxAI do
f:write([[
]]..ai..[[
]])
end
f:write([[
]])
local tracks = {}
local i = 0
for _,trackasset in ipairs(assets.tracksSorted) do
local track = class.tracks[trackasset.id]
if (track) then
writeTrack(track, trackasset, i, minAI, maxAI)
i = i + 1
end
end
f:write([[
]])
end
for _,classasset in ipairs(assets.classesSorted) do
local class = database.classes[classasset.id]
if (class) then
writeClass(class, classasset)
end
end
f:write([[
Total (track * car * ai) Entries: ]]..totalEntries..string.format(" (%.2f%%)", totalEntries*100/(assets.numClasses*assets.numTracks*(cfg.maxAI-cfg.minAI)) )..[[ Times: ]]..totalTimes..[[
Track (track * car) Entries: ]]..trackEntries..string.format(" (%.2f%%)", trackEntries*100/(assets.numClasses*assets.numTracks) )..[[
]])
f:close()
end
----------------------------------------------------------------------------------------------------------------
-- Internals
local lxml = dofile("xml.lua")
local function labellink(obj)
for i,v in ipairs(obj) do
if (type(v) == "table" and v.label) then
obj[v.label] = v
labellink(v)
end
end
end
local function parseAdaptive(filename, database, playertimes)
local f = io.open(filename,"rt")
if (not f) then
printlog("adaptive file not openable")
return
else
printlog("apdative file parsing", filename)
end
local txt = f:read("*a")
f:close()
local xml = lxml.parse(txt)
labellink(xml)
if (not xml) then
printlog("could not decode")
return
end
--[[
0263253108.74433136115.84943390123.27467346100108.444274902
...
]]
local function iterate3(tab, fn)
local num = tab and #tab or 0
for i=1,num,3 do
fn(tab[i], tab[i+1], tab[i+2])
end
end
local function iterate2(tab, fn)
local num = tab and #tab or 0
for i=1,num,2 do
fn(tab[i], tab[i+1])
end
end
local tracklist = xml.AiAdaptation.aiAdaptationData
local added = false
iterate3(tracklist, function(trackindex, trackkey, trackvalue)
local trackid = trackkey[1]
if (assets.tracks[trackid]) then
iterate3( trackvalue, function(classindex, classkey, classcustom)
local classid = classkey[1]
local playerentries = classcustom[1]
local aientries = classcustom[2]
if (assets.classes[classid]) then
if (playertimes and playerentries and #playerentries > 0) then
local class = playertimes.classes[classid] or {tracks={}}
playertimes.classes[classid] = class
local track = class.tracks[trackid] or {playertime=nil,}
class.tracks[trackid] = track
local mintime = 1000000
iterate2(playerentries, function(playerindex, playercustom)
local playertime = tonumber(playercustom[1])
mintime = math.min(playertime, mintime)
end)
track.playertime = mintime
printlog("playertime found", assets.classes[classid].name, assets.tracks[trackid].name, mintime)
end
if (aientries and #aientries > 0) then
local class = database.classes[classid] or {tracks={}}
local track = class.tracks[trackid] or {ailevels={}}
iterate3(aientries, function(aiindex, aikey, aicustom)
local aitime = aicustom[1][1]
-- filter out values that were generated by the tool/manual
if (aitime:match("%.%d%d$")) then
printlog("skipping: generated", trackid, classid, aitime)
return
end
local ailevel = tonumber(aikey[1])
class.minAI = math.min(ailevel, class.minAI or ailevel)
class.maxAI = math.max(ailevel, class.maxAI or ailevel)
track.minAI = math.min(ailevel, track.minAI or ailevel)
track.maxAI = math.max(ailevel, track.maxAI or ailevel)
if (false and classid == "3375") then
printlog(trackid, classid, ailevel, aitime)
printlog(class.minAI, class.maxAI)
end
local times = track.ailevels[ailevel] or {}
track.ailevels[ailevel] = times
local num = #times
local found = false
for i=1,num do
if (times[i] == aitime) then
found = true
end
end
if not found then
added = true
table.insert(times, aitime)
else
--printlog("skipping: found", trackid, classid, aitime)
end
end)
if (track.maxAI) then
class.tracks[trackid] = track
database.classes[classid] = class
end
end
end
end)
end
end)
return added
end
local function clearAdaptive(filename)
local f = io.open(filename,"rt")
assert(f,"file not found: "..filename)
local xml = f:read("*a")
f:close()
--[[
9792.45950
]]
local xml,num = xml:gsub(
'[^\n]+%s+'..
'%d+%s+'..
'%s+'..
' %d?%d%d%.%d%d%s+'..
' %d+%s+'..
'\n'
, function(str)
--printlog(str)
return ""
end)
if (num > 0) then
printlog("cleared generated ai file", filename, num)
local f = io.open(filename,"wt")
f:write(xml)
f:close()
end
end
local function clearAdaptiveAll(filename)
local f = io.open(filename,"rt")
assert(f,"file not found: "..filename)
local xml = f:read("*a")
f:close()
--[[
9792.459500050
]]
local xml,num = xml:gsub(
'[^\n]+%s+'..
'%d+%s+'..
'%s+'..
' %d+%.%d+%s+'..
' %d+%s+'..
'\n'
, function(str)
--printlog(str)
return ""
end)
if (num > 0) then
printlog("cleared all ai file", filename, num)
local f = io.open(filename,"wt")
f:write(xml)
f:close()
end
end
local function resetAll(filename)
local f = io.open(filename,"rt")
assert(f,"file not found: "..filename)
local xml = f:read("*a")
f:close()
--[[
9792.459500050
]]
local xml,num = xml:gsub(
'([^\n]+%s+'..
'%d+%s+'..
'%s+'..
' %d+%.%d+%s+'..
' )(%d+)(%s+'..
'\n)'
, function(s,m,e)
return s.."0"..e
end)
if (num > 0) then
printlog("reset all ai file", filename, num)
local f = io.open(filename,"wt")
f:write(xml)
f:close()
end
end
local function modifyAdaptive(filename, processed, trackid, classid, aifrom, aito, aispacing)
local class = processed.classes[classid]
if (not class) then
printlog("processed class not found", classid)
return
end
local track = class.tracks[trackid]
if (not track) then
printlog("processed track not found", trackid)
return
end
--[[
0263253108.74433136115.84943390123.27467346100108.444274902
...
]]
local f = io.open(filename,"rt")
assert(f,"file not found: "..filename)
local xml = f:read("*a")
f:close()
local found = false
local xmlnew = xml:gsub('('..trackid..'%s*)(.-)()',
function(tpre,tracks,tpost)
printlog("found track", trackid)
local tracks = tracks:gsub('('..classid..'\n%s*\n)(.-)(\n )',
function(cpre,class,cpost)
local class = class:gsub('(%s*)(.*)(\n%s*)$',
function(apre,aold,apost)
local anew = ""
local indent = string.rep(' ',10)
found = true
local idx = 0
for ai=aifrom,aito,aispacing do
local num,time = computeTime(track.ailevels[ai])
if (num > 0) then
anew = anew.."\n"
anew = anew..indent..'\n'
anew = anew..indent..''..ai..'\n'
anew = anew..indent..'\n'
anew = anew..indent..' '..outputTime(time)..'\n'
anew = anew..indent..' 0\n'
anew = anew..indent..''
idx = idx + 1
end
end
return apre..anew..apost
end)
return cpre..class..cpost
end)
return tpre..tracks..tpost
end)
if (found) then
printlog("modified ai file", "track",trackid,"class", classid, filename)
local f = io.open(filename,"wt")
f:write(xmlnew)
f:close()
else
printlog("could not find","track", trackid, "class", classid)
end
end
local matrix = require "matrix"
-- function to get the results
local function getresults( mtx )
assert( #mtx+1 == #mtx[1], "Cannot calculate Results" )
mtx:dogauss()
-- tresults
local cols = #mtx[1]
local tres = {}
for i = 1,#mtx do
tres[i] = mtx[i][cols]
end
return unpack( tres )
end
-- fit.linear ( x_values, y_values )
-- fit a straight line
-- model ( y = a + b * x )
-- returns a, b
local fit = {}
function fit.linear( x_values,y_values )
-- x_values = { x1,x2,x3,...,xn }
-- y_values = { y1,y2,y3,...,yn }
-- values for A matrix
local a_vals = {}
-- values for Y vector
local y_vals = {}
for i,v in ipairs( x_values ) do
a_vals[i] = { 1, v }
y_vals[i] = { y_values[i] }
end
-- create both Matrixes
local A = matrix:new( a_vals )
local Y = matrix:new( y_vals )
local ATA = matrix.mul( matrix.transpose(A), A )
local ATY = matrix.mul( matrix.transpose(A), Y )
local ATAATY = matrix.concath(ATA,ATY)
return getresults( ATAATY )
end
-- fit.parabola ( x_values, y_values )
-- Fit a parabola
-- model ( y = a + b * x + c * x² )
-- returns a, b, c
function fit.parabola( x_values,y_values )
-- x_values = { x1,x2,x3,...,xn }
-- y_values = { y1,y2,y3,...,yn }
-- values for A matrix
local a_vals = {}
-- values for Y vector
local y_vals = {}
for i,v in ipairs( x_values ) do
a_vals[i] = { 1, v, v*v }
y_vals[i] = { y_values[i] }
end
-- create both Matrixes
local A = matrix:new( a_vals )
local Y = matrix:new( y_vals )
local ATA = matrix.mul( matrix.transpose(A), A )
local ATY = matrix.mul( matrix.transpose(A), Y )
local ATAATY = matrix.concath(ATA,ATY)
return getresults( ATAATY )
end
local function trackGenerator(classid, trackid, track)
if (not track.maxAI or (track.maxAI - track.minAI < cfg.testMinAIdiffs)) then return end
local minNum,minTime,minVar = computeTime(track.ailevels[ track.minAI ])
local x = {}
local y = {}
if (cfg.fitAll) then
for i= track.minAI,track.maxAI do
local times = track.ailevels[ i ]
local num = times and #times or 0
for t=1,num do
table.insert(x, i)
table.insert(y, times[t])
end
end
else
for i= track.minAI,track.maxAI do
local num,time,var = computeTime(track.ailevels[ i ])
if (num > 0) then
table.insert(x, i)
table.insert(y, time)
end
end
end
local a,b,c = fit.linear(x,y)
c = c or 0
local function generator(t)
return a + b * t + c * (t*t)
end
local function printfail(...)
printlog("track fails fit", ...)
printlog(" class", classid, assets.classes[classid].name)
printlog(" track", trackid, assets.tracks[trackid].name)
end
local tested = 0
local passed = 0
local threshold = minTime * cfg.testMaxTimePct
if (cfg.fitAll) then
local lasttime
for i= track.minAI,track.maxAI do
local base = generator(i)
local num,time,var = computeTime(track.ailevels[ i ])
if (num > 0) then
tested = tested + 1
local diff = math.abs(base - time)
if (diff < threshold) then
passed = passed + 1
end
end
if (base > (lasttime or base)) then
printfail("not monotonically decreasing")
return
end
lasttime = base
end
else
for i= track.minAI,track.maxAI do
local base = generator(i)
local times = track.ailevels[ i ]
local num = times and #times or 0
for t=1,num do
local time = times[t]
tested = tested + 1
local diff = math.abs(base - time)
if (diff < threshold) then
passed = passed + 1
end
end
if (base > (lasttime or base)) then
printfail("not monotonically decreasing")
return
end
lasttime = base
end
end
local accepted = tested - passed <= math.max(1,tested * cfg.testMaxFailsPct)
if (not accepted) then
printfail("outliers", tested - passed)
end
return accepted and generator
end
local function processDatabase(database)
-- find track/car combos where we can derive ailevels
local filtered = {classes ={} }
for classid,class in pairs(database.classes) do
for trackid,track in pairs(class.tracks) do
local gen = trackGenerator(classid, trackid, track)
if (gen) then
local classf = filtered.classes[classid] or {tracks={}}
filtered.classes[classid] = classf
classf.minAI = 80
classf.maxAI = 120
local ailevels = {}
for i=80,120 do
ailevels[i] = { outputTime(gen(i)) }
end
local trackf = {}
classf.tracks[trackid] = trackf
trackf.minAI = 80
trackf.maxAI = 120
trackf.ailevels = ailevels
end
end
end
return filtered
end
---------------------------------------------
require("wx")
local serpent = require("serpent")
local database = {classes = {}}
local playertimes = {classes = {}}
do
local f = io.open(cfg.outdir..cfg.databasefile,"rt")
if (f) then
local dbstr = f:read("*a")
f:close()
local ok,db = serpent.load(dbstr)
if (ok and db and db.classes) then
database = db
end
end
end
local function specialFilename(filename)
local replacedirs = {
USER_DOCUMENTS = wx.wxStandardPaths.Get():GetDocumentsDir(),
}
filename = filename:gsub("%$([%w_]+)%$", replacedirs)
return filename
end
local function appendSeeds()
printlog("appending seeds")
-- iterate lua files
local path = wx.wxGetCwd().."/"..cfg.seeddir
local dir = wx.wxDir(path)
local found, file = dir:GetFirst("*.xml", wx.wxDIR_FILES)
local dirty = false
local targetfile = specialFilename(cfg.targetfile)
dirty = parseAdaptive(targetfile, database, playertimes)
while found do
dirty = parseAdaptive(cfg.seeddir..file, database) or dirty
found, file = dir:GetNext()
end
if (dirty) then
GenerateStatsHTML(cfg.outdir..cfg.reportfile, database, "%#07.4f")
local f = io.open(cfg.outdir..cfg.databasefile,"wt")
f:write( serpent.dump(database,{indent=' '}) )
f:close()
end
end
appendSeeds()
local processed = processDatabase(database)
GenerateStatsHTML(cfg.outdir..cfg.processedfile, processed, "%#05.2f")
local editenv = {
specialFilename = specialFilename,
modifyAdaptive = modifyAdaptive,
clearAdaptive = clearAdaptive,
clearAdaptiveAll = clearAdaptiveAll,
processed = processed,
database = database,
print = printlog,
}
local argcnt = #cmdlineargs
if (argcnt > 1) then
for i=1,argcnt do
local arg = cmdlineargs[i]
if arg:find("r3e-adaptive-ai-primer.lua",1,true) then
elseif arg:match(".lua$") then
execEnv(arg, editenv)
end
end
return
end
-- debug
if (false) then
clearAdaptive(specialFilename(cfg.targetfile))
return
end
function main()
-- create the frame window
local ww = 820
local wh = 840
frame = wx.wxFrame( wx.NULL, wx.wxID_ANY, "R3E Apdative AI Primer",
wx.wxDefaultPosition, wx.wxSize(ww+16, wh),
wx.wxDEFAULT_FRAME_STYLE )
-- show the frame window
frame:Show(true)
local panel = wx.wxPanel ( frame, wx.wxID_ANY)
frame.panel = panel
local targetfile = specialFilename(cfg.targetfile)
if not targetfile or not wx.wxFileName(targetfile):FileExists() then
local label = wx.wxStaticText(panel, wx.wxID_ANY, "Could not find R3E adaptive AI file:\n"..tostring(targetfile).."\n\nEdit config.lua targetfile entry for proper file path.",
wx.wxPoint(8,8))
frame.label = label
panel:Fit()
frame:Fit()
printlog("error")
return
end
local winUpper = wx.wxWindow ( panel, wx.wxID_ANY)
local winLower = wx.wxWindow ( panel, wx.wxID_ANY)
-- Give the scrollwindow enough size so sizer works when calling Fit()
--winLower:SetScrollbars(15, 15, 400, 1000, 0, 0, false)
local sizer = wx.wxBoxSizer(wx.wxVERTICAL)
sizer:Add(winUpper, 0, wx.wxEXPAND)
sizer:Add(winLower, 0, wx.wxEXPAND)
panel:SetSizer(sizer)
frame.sizer = sizer
frame.winUpper = winUpper
frame.winLower = winLower
local lblfile = wx.wxStaticText(winUpper, wx.wxID_ANY, "R3E adaptive AI file found:\n"..targetfile, wx.wxPoint(8,8), wx.wxSize(ww,30) )
local lblmod = wx.wxStaticText(winUpper, wx.wxID_ANY, "Modification:", wx.wxPoint(8,50), wx.wxSize(ww-8,16) )
local btnapply = wx.wxButton(winUpper, wx.wxID_ANY, "Apply Selected Modification", wx.wxPoint(8,70), wx.wxSize(200,20))
local btnremgen = wx.wxButton(winUpper, wx.wxID_ANY, "Remove likely generated", wx.wxPoint(ww-8-240-240-4,70), wx.wxSize(240,20))
local btnreset = wx.wxButton(winUpper, wx.wxID_ANY, "Reset all AI times", wx.wxPoint(ww-8-240,70), wx.wxSize(240,20))
winUpper.lblfile = lblfile
winUpper.lblmod = lblmod
winUpper.btnapply = btnapply
winUpper.binremove = btnremove
winUpper.btnreset = btnreset
local class
local classid
local trackid
local ailevel
local aifrom
local aito
local aiNumLevels = cfg.aiNumLevels
local aiSpacing = cfg.aiSpacing
btnremgen:Connect( wx.wxEVT_COMMAND_BUTTON_CLICKED, function(event)
clearAdaptive(targetfile)
end)
btnreset:Connect( wx.wxEVT_COMMAND_BUTTON_CLICKED, function(event)
resetAll(targetfile)
end)
btnapply:Connect( wx.wxEVT_COMMAND_BUTTON_CLICKED, function(event)
if (classid and trackid and ailevel) then
modifyAdaptive(targetfile, processed, trackid, classid, aifrom, aito, aiSpacing)
end
end)
local ctrlClass = wx.wxListCtrl(winLower, wx.wxID_ANY,
wx.wxPoint(8,8), wx.wxSize(200,700),
wx.wxLC_REPORT)
ctrlClass:InsertColumn(0, "Classes")
ctrlClass:SetColumnWidth(0,190)
local ctrlTrack = wx.wxListCtrl(winLower, wx.wxID_ANY,
wx.wxPoint(8+200,8), wx.wxSize(450,700),
wx.wxLC_REPORT)
ctrlTrack:InsertColumn(0, "Tracks")
ctrlTrack:InsertColumn(1, "Player Best")
ctrlTrack:SetColumnWidth(0,340)
ctrlTrack:SetColumnWidth(1,100)
local ctrlAI = wx.wxListCtrl(winLower, wx.wxID_ANY,
wx.wxPoint(8+200+450,8), wx.wxSize(150,700),
wx.wxLC_REPORT)
ctrlAI:InsertColumn(0, "AI")
ctrlAI:InsertColumn(1, "Time")
ctrlAI:SetColumnWidth(0,30)
ctrlAI:SetColumnWidth(1,110)
local classids = {}
local trackids = {}
local ailevels = {}
local function updateClasses()
classid = nil
classids = {}
local i = 0
ctrlClass:DeleteAllItems()
for _,classasset in ipairs(assets.classesSorted) do
local class = processed.classes[classasset.id]
local palyerclass = playertimes and playertimes.classes[classasset.id]
if (class or palyerclass) then
ctrlClass:InsertItem(i, classasset.name)
table.insert(classids, classasset.id)
i = i + 1
classid = classid or classasset.id
end
end
ctrlClass:SetItemState(0, wx.wxLIST_STATE_SELECTED, wx.wxLIST_STATE_SELECTED)
end
local function updateTracks()
trackid = nil
trackids = {}
local i = 0
ctrlTrack:DeleteAllItems()
for _,trackasset in ipairs(assets.tracksSorted) do
local class = processed.classes[classid]
local track = class and class.tracks[trackasset.id]
local palyerclass = playertimes and playertimes.classes[classid]
local playertrack = palyerclass and palyerclass.tracks[trackasset.id]
if (track or playertrack) then
ctrlTrack:InsertItem(i, trackasset.name)
table.insert(trackids, trackasset.id)
if (playertrack and playertrack.playertime) then
ctrlTrack:SetItem(i, 1, MakeTime(playertrack.playertime, " : "))
end
i = i + 1
trackid = trackid or trackasset.id
end
end
ctrlTrack:SetItemState(0, wx.wxLIST_STATE_SELECTED, wx.wxLIST_STATE_SELECTED)
end
local function updateAI()
if (not trackid) then return end
ailevel = nil
ailevels = {}
local i = 0
ctrlAI:DeleteAllItems()
local class = processed.classes[classid]
local track = class and class.tracks[trackid]
if (track) then
for ai=track.minAI,track.maxAI do
local num,time = computeTime(track.ailevels[ai])
if (num > 0) then
ctrlAI:InsertItem(i, tostring(ai))
ctrlAI:SetItem(i, 1, MakeTime(time, " : "))
table.insert(ailevels, ai)
i = i + 1
ailevel = ailevel or ai
end
end
ctrlAI:SetItemState(0, wx.wxLIST_STATE_SELECTED, wx.wxLIST_STATE_SELECTED)
end
end
local function updateSelection()
if (not ailevel) then return end
aifrom = math.max( 80,ailevel - math.floor(aiNumLevels/2))
aito = math.min(120,aifrom + aiNumLevels - 1)
lblmod:SetLabel("Modification: "..assets.classes[classid].name.." - "..assets.tracks[trackid].name.." : "..aifrom.." - "..aito.." step: "..aiSpacing)
end
updateClasses()
updateTracks()
updateAI()
updateSelection()
ctrlClass:Connect(wx.wxEVT_COMMAND_LIST_ITEM_SELECTED, function (event)
local idx = event:GetIndex()
classid = classids[idx + 1]
updateTracks()
end)
ctrlTrack:Connect(wx.wxEVT_COMMAND_LIST_ITEM_SELECTED, function (event)
local idx = event:GetIndex()
trackid = trackids[idx + 1]
updateAI()
end)
ctrlAI:Connect(wx.wxEVT_COMMAND_LIST_ITEM_SELECTED, function (event)
local idx = event:GetIndex()
ailevel = ailevels[idx + 1]
updateSelection()
end)
sizer:Fit(panel)
end
main()
wx.wxGetApp():MainLoop()