-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathac_extras_ini.lua
346 lines (310 loc) · 13.9 KB
/
ac_extras_ini.lua
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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
---@diagnostic disable: redundant-parameter
__source 'lua/api_extras_ini.cpp'
local _iniss
ac.INIConfig = class('ac.INIConfig', function (format, sections, filename)
return { format = format or 0, sections = sections or {}, filename = filename }
end)
---Pass this as a `defaultValue` to `:get()` (or use it as a value in `:mapSection()`) to get either a boolean or, if it’s missing, `nil`.
ac.INIConfig.OptionalBoolean = {}
---Pass this as a `defaultValue` to `:get()` (or use it as a value in `:mapSection()`) to get either a number or, if it’s missing, `nil`.
ac.INIConfig.OptionalNumber = {}
---Pass this as a `defaultValue` to `:get()` (or use it as a value in `:mapSection()`) to get either a string or, if it’s missing, `nil`.
ac.INIConfig.OptionalString = {}
---Pass this as a `defaultValue` to `:get()` (or use it as a value in `:mapSection()`) to get either a list of original values or, if it’s missing, `nil`.
ac.INIConfig.OptionalList = {}
---Parse INI config from a string.
---@param data string @Serialized INI data.
---@param format ac.INIFormat? @Format to parse. Default value: `ac.INIFormat.Default`.
---@param includeFolders ('@cars'|'@tracks'|string)[]? @Optional folders to include files from (only for `ac.INIFormat.ExtendedIncludes` format). Use special values `'@cars'` and `'@tracks'` for car or track configs.
---@return ac.INIConfig
function ac.INIConfig.parse(data, format, includeFolders)
-- ffi.C.lj_parse_ini(data and tostring(data) or nil, tonumber(format) or 0, includeFolders and table.concat(includeFolders, '\n') or nil)
-- local ret = __util.result()
-- if ret == nil then error('Failed to parse data', 2) end
return ac.INIConfig(tonumber(format) or 0, __util.native('lj_parse_ini', data, format, includeFolders))
end
---Load INI file, optionally with includes.
---@param filename string @INI config filename.
---@param format ac.INIFormat? @Format to parse. Default value: `ac.INIFormat.Default`.
---@param includeFolders ('@cars'|'@tracks'|string)[]? @Optional folders to include files from (only for `ac.INIFormat.ExtendedIncludes` format). If not set, parent folder for config filename is used. Use special values `'@cars'` and `'@tracks'` for car or track configs.
---@return ac.INIConfig
function ac.INIConfig.load(filename, format, includeFolders)
-- ffi.C.lj_load_ini(filename and tostring(filename) or nil, tonumber(format) or 0, includeFolders and table.concat(includeFolders, '\n') or nil)
-- local ret = __util.result()
-- if ret == nil then error('Failed to parse data', 2) end
return ac.INIConfig(tonumber(format) or 0, __util.native('lj_load_ini', filename, format, includeFolders), filename)
end
---Load car data INI file. Supports “data.acd” files as well. Returned files might be tweaked by
---things like custom physics virtual tyres. To get original file, use `ac.INIConfig.load()`.
---
---Returned file can’t be saved.
---@param carIndex number @0-based car index.
---@param fileName string @Car data file name, such as `'tyres.ini'`.
---@return ac.INIConfig
function ac.INIConfig.carData(carIndex, fileName)
if not _iniss then _iniss = {} end
local k = 'c'..tostring(carIndex)..tostring(fileName)
if _iniss[k] then return _iniss[k] end
_iniss[k] = ac.INIConfig(1, __util.native('lj_load_cardata_ini', carIndex, fileName), nil)
_iniss[k].__car = { carIndex, fileName }
return _iniss[k]
end
---Load track data INI file. Can be used by track scripts which might not always have access to those files directly.
---
---Returned file can’t be saved.
---@param fileName string @Car data file name, such as `'tyres.ini'`.
---@return ac.INIConfig
function ac.INIConfig.trackData(fileName)
if not _iniss then _iniss = {} end
local k = 't'..tostring(fileName)
if _iniss[k] then return _iniss[k] end
_iniss[k] = ac.INIConfig(1, __util.native('lj_load_trackdata_ini', fileName), nil)
return _iniss[k]
end
---Returns CSP config for a car. Might be slow: some of those configs are huge. Make sure to cache the resulting value if you need to reuse it.
---
---Returned file can’t be saved.
---@param carIndex number @0-based car index.
---@return ac.INIConfig
function ac.INIConfig.carConfig(carIndex)
return ac.INIConfig(10, __util.native('lj_loadcspcarconfig_ini', carIndex), nil)
end
---Returns CSP config for a track. Might be slow: some of those configs are huge. Make sure to cache the resulting value if you need to reuse it.
---
---Returned file can’t be saved.
---@return ac.INIConfig
function ac.INIConfig.trackConfig()
return ac.INIConfig(10, __util.native('lj_loadcsptrackconfig_ini'), nil)
end
---Returns config with extra online options, the ones that can be set with Content Manager.
---
---Returned file can’t be saved.
---@return ac.INIConfig|nil @If not an online session, returns `nil`.
function ac.INIConfig.onlineExtras()
local ret = __util.native('lj_loadonlineextras_ini')
return ret and ac.INIConfig(10, ret, nil)
end
---Returns race config (`cfg/race.ini`). Password and online GUID won’t be included.
---
---Returned file can’t be saved.
---@return ac.INIConfig
function ac.INIConfig.raceConfig()
return ac.INIConfig(10, __util.native('lj_load_race_ini'), nil)
end
---Returns video config (`cfg/video.ini`).
---
---Returned file can’t be saved.
---@return ac.INIConfig
function ac.INIConfig.videoConfig()
return ac.INIConfig(10, __util.native('lj_load_video_ini'), nil)
end
---Returns controls config (`cfg/controls.ini`).
---
---Returned file can’t be saved.
---@return ac.INIConfig
function ac.INIConfig.controlsConfig()
return ac.INIConfig(10, __util.native('lj_load_controls_ini'), nil)
end
---Load config of a CSP module by its name.
---@param cspModuleID ac.CSPModuleID @Name of a CSP module.
---@return ac.INIConfig
function ac.INIConfig.cspModule(cspModuleID)
if cspModuleID == nil then return ac.INIConfig(ac.INIFormat.Default, {}) end
-- ffi.C.lj_loadconfig_ini(tostring(cspModuleID)..'.ini')
-- local ret = __util.result()
-- if ret == nil then error('Failed to parse data', 2) end
return ac.INIConfig(ac.INIFormat.Extended, __util.native('lj_loadconfig_ini', tostring(cspModuleID)..'.ini'), ac.getFolder(ac.FolderID.ExtCfgUser)..'/'..cspModuleID..'.ini')
end
---Load config of the current Lua script (“settings.ini” in script directory and settings overriden by user, meant to be customizable with Content Manager).
---@return ac.INIConfig
function ac.INIConfig.scriptSettings()
if not _iniss then _iniss = {} end
if _iniss.ss then return _iniss.ss end
local ret, filename = __util.native('lj_loadscriptconfig_ini')
if ret == nil then error('Script of this type can’t have settings', 2) end
_iniss.ss = ac.INIConfig(ac.INIFormat.Extended, ret, filename)
return _iniss.ss
end
local function _indv(defaultValue)
if defaultValue == ac.INIConfig.OptionalList
or defaultValue == ac.INIConfig.OptionalString
or defaultValue == ac.INIConfig.OptionalNumber
or defaultValue == ac.INIConfig.OptionalBoolean then return nil end
return defaultValue
end
function ac.INIConfig:get(section, key, defaultValue, offset)
offset = offset or 1
if offset < 1 then return _indv(defaultValue) end
local s = self.sections[section]
local v = s and s[key]
if not v or offset > #v then return _indv(defaultValue) end
if defaultValue == nil or type(defaultValue) == 'table' then
if defaultValue == ac.INIConfig.OptionalString then return v[offset] end
if defaultValue == ac.INIConfig.OptionalNumber then return tonumber(v[offset]) end
if defaultValue == ac.INIConfig.OptionalBoolean then return v[offset] ~= '0' end
return offset > 1 and table.slice(v, offset) or v
end
if type(defaultValue) == 'string' then return v[offset] end
if type(defaultValue) == 'number' then return tonumber(v[offset]) or defaultValue end
if type(defaultValue) == 'boolean' then return v[offset] ~= '0' end
if vec2.isvec2(defaultValue) then return vec2(tonumber(v[offset]) or 0, tonumber(v[offset + 1]) or 0) end
if vec3.isvec3(defaultValue) then return vec3(tonumber(v[offset]) or 0, tonumber(v[offset + 1]) or 0, tonumber(v[offset + 2]) or 0) end
if rgb.isrgb(defaultValue) then return rgb(tonumber(v[offset]) or 0, tonumber(v[offset + 1]) or 0, tonumber(v[offset + 2]) or 0) end
if vec4.isvec4(defaultValue) then return vec4(tonumber(v[offset]) or 0, tonumber(v[offset + 1]) or 0, tonumber(v[offset + 2]) or 0, tonumber(v[offset + 3]) or 0) end
if rgbm.isrgbm(defaultValue) then return rgbm(tonumber(v[offset]) or 0, tonumber(v[offset + 1]) or 0, tonumber(v[offset + 2]) or 0, tonumber(v[offset + 3]) or 0) end
error('Unknown type', 2)
end
function ac.INIConfig:tryGetLut(section, key)
local data = self:get(section, key, '')
if not data then return nil end
data = data:trim()
if data == '' then return nil end
if data:startsWith('(') and data:endsWith(')') then
return ac.DataLUT11.parse(data)
end
if self.__car then
return ac.DataLUT11.carData(self.__car[1], data)
end
if self.filename then
return ac.DataLUT11.load(self.filename..'/../'..data)
end
return nil
end
function ac.INIConfig:tryGet2DLut(section, key)
local data = self:get(section, key, '')
if not data then return nil end
data = data:trim()
if data == '' then return nil end
if data:startsWith('(') and data:endsWith(')') then
return ac.DataLUT21.parse(data)
end
if self.__car then
return ac.DataLUT21.carData(self.__car[1], data)
end
if self.filename then
return ac.DataLUT21.load(self.filename..'/../'..data)
end
return nil
end
function ac.INIConfig:mapSection(section, defaults)
return table.map(defaults, function (v, k, s) return s:get(section, k, v), k end, self)
end
function ac.INIConfig:mapConfig(defaults)
return table.map(defaults, function (v, k, s) return s:mapSection(k, v), k end, self)
end
function ac.INIConfig:set(section, key, value)
local s = self.sections[section]
if s == nil then
s = {}
self.sections[section] = s
end
if value == nil then s[key] = value
elseif type(value) == 'table' then
if value == ac.INIConfig.OptionalList or value == ac.INIConfig.OptionalString
or value == ac.INIConfig.OptionalNumber or value == ac.INIConfig.OptionalBoolean then
s[key] = nil
else
s[key] = value
end
elseif type(value) == 'string' then s[key] = { value }
elseif type(value) == 'number' then s[key] = { tostring(value) }
elseif type(value) == 'boolean' then s[key] = { value and '1' or '0' }
elseif vec2.isvec2(value) then s[key] = { tostring(value.x), tostring(value.y) }
elseif vec3.isvec3(value) then s[key] = { tostring(value.x), tostring(value.y), tostring(value.z) }
elseif rgb.isrgb(value) then s[key] = { tostring(value.r), tostring(value.g), tostring(value.b) }
elseif vec4.isvec4(value) then s[key] = { tostring(value.x), tostring(value.y), tostring(value.z), tostring(value.w) }
elseif rgbm.isrgbm(value) then s[key] = { tostring(value.r), tostring(value.g), tostring(value.b), tostring(value.mult) }
else error('Unknown type', 2) end
return self
end
local function callbackIterateSort(a, b)
return a:alphanumCompare(b) < 0
end
function ac.INIConfig:iterate(prefix, noPostfixForFirst)
if self.format >= 10 then
local ret = {}
local key
local pattern = '^'..prefix..'_%d.*$'
while true do
key = next(self.sections, key)
if not key then break end
if not key:endsWith('_') and key:find(pattern) then ret[#ret + 1] = key end
end
table.sort(ret, callbackIterateSort)
return ipairs(ret)
else
local i = 0
return function ()
local k = i == 0 and noPostfixForFirst and prefix or prefix..'_'..tostring(i)
i = i + 1
if self.sections[k] then return i, k end
end
end
end
function ac.INIConfig:iterateValues(section, prefix, digitsOnly)
local data = self.sections[section]
if not data then return function () end end
local ret = {}
local key
local pattern = '^'..prefix..(digitsOnly and '_%d+$' or '_%d.*$')
while true do
key = next(data, key)
if not key then break end
if not key:endsWith('_') and key:find(pattern) then ret[#ret + 1] = key end
end
table.sort(ret, callbackIterateSort)
return ipairs(ret)
end
function ac.INIConfig:setAndSave(section, key, value)
if not self.filename then error('Filename is not set', 2) end
local old = self:get(section, key, ac.INIConfig.OptionalString)
self:set(section, key, value)
local new = self:get(section, key, ac.INIConfig.OptionalString)
if new == old then return false end
__util.native("lj_write_value_ini", self.filename, self.format, section, key, new)
return true
end
local _iemap = { ['\''] = '\\\'', ['\\'] = '\\\\', ['\n'] = '\\n', ['\t'] = '\\t' }
local function _iechar(c)
return _iemap[c] or c
end
function ac.INIConfig:__tostring()
local r, i = {}, 1
local q = self.format >= 10
for k, v in pairs(self.sections) do
r[i], i = '[', i + 1
r[i], i = k, i + 1
r[i], i = ']\n', i + 1
for k0, v0 in pairs(v) do
r[i], i = k0, i + 1
r[i], i = '=', i + 1
for j = 1, #v0 do
if j > 1 then r[i], i = ',', i + 1 end
if q and string.match(v0[j], '[\\\'\",\n\t=$@]') then
r[i], i = '\'', i + 1
r[i], i = string.gsub(v0[j], '[\'\n\t]', _iechar), i + 1
r[i], i = '\'', i + 1
else
r[i], i = v0[j], i + 1
end
end
r[i], i = '\n', i + 1
end
r[i], i = '\n', i + 1
end
return table.concat(r)
end
---Serializes data in INI format using format specified on INIConfig creation. You can also use `tostring()` function.
---@return string
function ac.INIConfig:serialize()
return tostring(self)
end
---Saves contents to a file in INI form.
---@param filename string? @Filename. If filename is not set, saves file with the same name as it was loaded. Updates `filename` field.
---@return ac.INIConfig @Returns itself for chaining several methods together.
function ac.INIConfig:save(filename)
self.filename = filename or self.filename
if not self.filename then error('Filename is not set', 2) end
io.save(self.filename, tostring(self))
return self
end