Repository navigation
Expand file tree
/
Copy pathconfiguration.lua
More file actions
256 lines (249 loc) · 12.9 KB
/
Copy pathconfiguration.lua
File metadata and controls
256 lines (249 loc) · 12.9 KB
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
-- Validate the whole configuration before resolving or patching native code.
local constants = require('constants')
local M = {}
-- Inspect effective profiles after validate() has merged sparse overrides.
-- Initialization consumers share this traversal so conditional capabilities
-- include fortification and decoration profiles consistently.
function M.any_profile(cfg, predicate)
if predicate(cfg) then return true end
if cfg.on_fortification and predicate(cfg.on_fortification) then return true end
for _, rule in ipairs(cfg.near_decorations or {}) do
if predicate(rule.ground) or predicate(rule.fortified) then return true end
end
return false
end
M.numbers = {
count = {1, 64, 1}, cow_count = {1, 64, 1}, spread = {0, 800, 0}, inaccuracy = {0, 800, 0},
spread_tiles = {0, 100, 0}, inaccuracy_tiles = {0, 100, 0},
interval = {1, 60000, 100}, interval_moving = {0, 60000, 100},
interval_standing = {0, 60000, 100}, range = {1, 100, 20},
wall_min_distance = {0, 100, 3}, require_manned = {0, 4, 0},
shoot_height = {0, 500, 0}, stagger_min = {1, 60000, 1},
stagger_max = {0, 60000, 0}, density_min = {1, 256, 1},
density_radius = {1, 100, 5}, attached_interval = {0, 60000, 100},
attached_board_radius = {0, 100, 2}, preload_poll = {1, 60000, 5},
sync_max_wait = {1, 60000, 40},
}
M.booleans = {
suppress_default = true, random_targets = true, attached_ignore_crew = true,
attached_stop_when_boarded = true, ai_cow_vs_units = true, ai_only = true,
preload = true, sync_to_animation = true, turn_before_shot = true,
strict_range = true, auto_targeting = true,
}
M.units = {}
for id, name in ipairs(constants.unit_names) do
M.units[name] = id
end
local projectile_ids = {}
for _, id in pairs(constants.projectile_names) do projectile_ids[id] = true end
local function fail(path, reason)
error('[custom-projectiles] ' .. path .. ': ' .. reason, 0)
end
local function object(value, path)
if type(value) ~= 'table' then fail(path, 'expected a mapping') end
end
local function validate_flat(config, variants)
object(config, 'config')
for key in pairs(config) do
if key ~= 'units' and key ~= 'projectiles' and key ~= 'decorations' then fail(tostring(key), 'unknown section; expected units, projectiles or decorations') end
end
local units = config.units
if units == nil then units = {} end
object(units, 'units')
local result = {units = {}}
for name, cfg in pairs(units) do
local path = 'units.' .. tostring(name)
if not M.units[name] then fail(path, 'unknown unit name') end
object(cfg, path)
if cfg.target_bias_tiles ~= nil and cfg.threat_priority ~= nil then
fail(path, 'use only one of target_bias_tiles and threat_priority')
end
local out = {}
for key, value in pairs(cfg) do
local field = path .. '.' .. tostring(key)
local bounds = M.numbers[key]
if value == 'native' and (bounds or M.booleans[key] or key == 'targets'
or key == 'threat_priority' or key == 'target_bias_tiles'
or ((key == 'projectile' or key == 'cow_projectile') and not (variants and variants.native))) then
-- Explicit inheritance uses the same path as omission. Keep
-- native state-dependent behavior with its existing owner.
elseif bounds then
if key == 'require_manned' and type(value) == 'boolean' then value = value and 1 or 0 end
if type(value) ~= 'number' or value ~= value or value % 1 ~= 0
or value < bounds[1] or value > bounds[2] then
fail(field, 'expected an integer from ' .. bounds[1] .. ' to ' .. bounds[2])
end
out[key] = value
elseif M.booleans[key] then
if type(value) ~= 'boolean' then fail(field, 'expected true or false') end
out[key] = value
elseif key == 'projectile' or key == 'cow_projectile' then
local variant = variants and variants[value]
local id = variant and variant.id or (type(value) == 'string' and constants.projectile_names[value] or value)
if not variant and not projectile_ids[id] then fail(field, 'unknown or unsafe projectile type') end
out[key] = id
elseif key == 'targets' then
if type(value) == 'string' then value = {value} end
object(value, field)
local count, seen, targets = 0, {}, {}
for index in pairs(value) do
if type(index) ~= 'number' or index % 1 ~= 0 or index < 1 or index > #value then
fail(field, 'expected a list of target kinds')
end
count = count + 1
end
if count < 1 or count > 4 or count ~= #value then fail(field, 'use one to four target kinds') end
for _, kind in ipairs(value) do
if not constants.target_kinds[kind] or seen[kind] then fail(field, 'unknown or duplicate target kind') end
seen[kind] = true
targets[#targets+1] = kind
end
out[key] = targets
elseif key == 'threat_priority' or key == 'target_bias_tiles' then
object(value, field)
local priorities = {}
local any_bonus = false
for target, bonus in pairs(value) do
if not M.units[target] then fail(field .. '.' .. tostring(target), 'unknown target unit name') end
if type(bonus) ~= 'number' or bonus ~= bonus or bonus % 1 ~= 0
or bonus < 0 or bonus > 255 then
fail(field .. '.' .. target, 'expected an integer from 0 to 255')
end
priorities[target] = bonus
if bonus > 0 then any_bonus = true end
end
-- Preserve the normalized key for runtime and saved-config identity.
if any_bonus then out.threat_priority = priorities end
else
fail(field, 'unknown setting')
end
end
for _, key in ipairs({'spread', 'inaccuracy'}) do
if out[key] ~= nil and out[key .. '_tiles'] ~= nil then fail(path, 'use only one of ' .. key .. ' and ' .. key .. '_tiles') end
end
if (out.stagger_max or 0) > 0 and (out.stagger_min or 1) > out.stagger_max then
fail(path, 'stagger_min must not exceed stagger_max')
end
if out.stagger_min and not out.stagger_max then fail(path, 'stagger_min requires stagger_max') end
-- Each state interval enables automatic fire independently. Without a
-- fallback, unconfigured states hold fire. Keep the native scheduler's
-- positive enable value internal; chooseInterval selects the state rate.
if out.interval == nil and (out.interval_moving ~= nil or out.interval_standing ~= nil
or out.attached_interval ~= nil) then
out.interval_moving = out.interval_moving or 0
out.interval_standing = out.interval_standing or 0
out.interval = math.max(1, out.interval_standing, out.interval_moving, out.attached_interval or 0)
end
if not out.interval and out.stagger_max ~= nil then
fail(path .. '.stagger_max', 'requires an automatic-fire interval: interval, interval_moving, interval_standing or attached_interval')
end
if out.interval then
-- Native reload must not bypass the engine's normal crew gate.
-- Explicit 0/false still deliberately permits unmanned fire.
if out.require_manned == nil and out.sync_to_animation ~= false then
out.require_manned = constants.native_reload_crews[name]
end
-- Independent timers may be explicitly combined with native shots.
if out.suppress_default == nil then out.suppress_default = true end
if out.projectile == nil then
out.projectile = constants.native_projectiles[name] or constants.projectile_names.arrow
end
end
if next(out) ~= nil then result.units[name] = out end
end
return result
end
-- One merge owner for fortification and decoration profiles. An explicit
-- native clears a configured parent value, while a concrete compatibility
-- alias in the same override wins independent of Lua table iteration order.
local function merge_fields(parent, fields, path)
if fields.target_bias_tiles ~= nil and fields.threat_priority ~= nil then
fail(path, 'use only one of target_bias_tiles and threat_priority')
end
local merged = {}
for key, value in pairs(parent) do merged[key] = value end
for key, value in pairs(fields) do merged[key] = value end
if fields.target_bias_tiles ~= nil then merged.threat_priority = nil end
if fields.threat_priority ~= nil then merged.target_bias_tiles = nil end
for _, key in ipairs({'spread', 'inaccuracy'}) do
local alias = key .. '_tiles'
if fields[key] ~= nil and (fields[alias] == nil or fields[alias] == 'native') then
merged[alias] = nil
end
if fields[alias] ~= nil and (fields[key] == nil or fields[key] == 'native') then
merged[key] = nil
end
end
return merged
end
function M.validate(config)
object(config, 'config')
local variants = require('sprite_resources').definitions(config.projectiles)
local decorations = require('decorations')
local definitions = decorations.definitions(config.decorations)
local units = config.units
if units == nil then units = {} end
object(units, 'units')
local plain, fortified = {}, {}
for name, settings in pairs(units) do
object(settings, 'units.' .. tostring(name))
local base = {}
for key, value in pairs(settings) do
if key ~= 'on_fortification' and key ~= 'near_decorations' then base[key] = value end
end
plain[name] = base
if settings.on_fortification ~= nil then
object(settings.on_fortification, 'units.' .. tostring(name) .. '.on_fortification')
for _, key in ipairs({'spread', 'inaccuracy'}) do
if settings.on_fortification[key] ~= nil and settings.on_fortification[key] ~= 'native'
and settings.on_fortification[key .. '_tiles'] ~= nil and settings.on_fortification[key .. '_tiles'] ~= 'native' then
fail('units.' .. name .. '.on_fortification', 'use only one of ' .. key .. ' and ' .. key .. '_tiles')
end
end
if settings.on_fortification.on_fortification ~= nil then
fail('units.' .. name, 'nested fortification overrides are not supported')
end
local merged = merge_fields(base, settings.on_fortification,
'units.' .. name .. '.on_fortification')
if next(settings.on_fortification) ~= nil then fortified[name] = merged end
end
end
local copied = {}
for key, value in pairs(config) do copied[key] = value end
copied.units = plain
local result = validate_flat(copied, variants)
local alternates = validate_flat({units=fortified}, variants)
if next(variants) then result.projectiles = variants end
if next(definitions) then result.decorations = definitions end
for name in pairs(fortified) do
result.units[name] = result.units[name] or {}
result.units[name].on_fortification = alternates.units[name] or {}
end
for name, settings in pairs(units) do
local rules = decorations.rules(settings.near_decorations, definitions, name)
if #rules > 0 then
local function effective(parent, fields)
local merged = merge_fields(parent, fields,
'units.' .. name .. '.near_decorations')
return validate_flat({units={[name]=merged}}, variants).units[name] or {}
end
for _, rule in ipairs(rules) do
-- Validate sparse fields as part of their effective profile,
-- so inherited interval/stagger dependencies remain meaningful.
for _, key in ipairs({'spread', 'inaccuracy'}) do
if rule.fields[key] ~= nil and rule.fields[key] ~= 'native'
and rule.fields[key .. '_tiles'] ~= nil and rule.fields[key .. '_tiles'] ~= 'native' then
fail('units.' .. name .. '.near_decorations', 'use only one accuracy/spread unit system')
end
end
rule.ground = effective(plain[name], rule.fields)
rule.fortified = effective(fortified[name] or plain[name], rule.fields)
rule.fields = nil
end
result.units[name] = result.units[name] or {}
result.units[name].near_decorations = rules
end
end
return result
end
return M