--[[ 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..[[)

]]) local minAI = math.max(cfg.minAI,class.minAI) local maxAI = math.min(cfg.maxAI,class.maxAI) for ai = minAI, maxAI do f:write([[ ]]) 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([[
Track]]..ai..[[

]]) 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 --[[ 0 263 253 108.74433136 115.84943390 123.27467346 100 108.44427490 2 ... ]] 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() --[[ 97 92.4595 0 ]] 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() --[[ 97 92.45950005 0 ]] 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() --[[ 97 92.45950005 0 ]] 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 --[[ 0 263 253 108.74433136 115.84943390 123.27467346 100 108.44427490 2 ... ]] 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()