Skip to content

Commit a94d868

Browse files
authored
Improve the creation of chunk templates (#6674)
1 parent ae2ab54 commit a94d868

File tree

5 files changed

+165
-75
lines changed

5 files changed

+165
-75
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
- (#6674) Make it easier to create chunk templates for AIs
2+
3+
Chunk templates is a concept that we're toying with to see if we can help the AI build better infrastructure. With these changes we make it easier to use and understand.
4+
5+
- 1) The output (a template) is now copied to the clipboard instead of to the logs.
6+
- 2) What is happening is now printed to screen via the global `print`.
7+

lua/keymap/debugKeyActions.lua

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -290,23 +290,23 @@ local keyActionsDebugAI = {
290290
category = 'ai'
291291
},
292292
['create_build_template_02'] = {
293-
action = 'UI_Lua import("/lua/ui/game/aichunktemplates.lua").PopulateChunkTemplate(2)',
293+
action = 'UI_Lua import("/lua/ui/game/aichunktemplates.lua").AddUnitSelectionToEmptyChunkTemplate(2)',
294294
category = 'ai'
295295
},
296296
['create_build_template_04'] = {
297-
action = 'UI_Lua import("/lua/ui/game/aichunktemplates.lua").PopulateChunkTemplate(4)',
297+
action = 'UI_Lua import("/lua/ui/game/aichunktemplates.lua").AddUnitSelectionToEmptyChunkTemplate(4)',
298298
category = 'ai'
299299
},
300300
['create_build_template_08'] = {
301-
action = 'UI_Lua import("/lua/ui/game/aichunktemplates.lua").PopulateChunkTemplate(8)',
301+
action = 'UI_Lua import("/lua/ui/game/aichunktemplates.lua").AddUnitSelectionToEmptyChunkTemplate(8)',
302302
category = 'ai'
303303
},
304304
['create_build_template_16'] = {
305-
action = 'UI_Lua import("/lua/ui/game/aichunktemplates.lua").PopulateChunkTemplate(16)',
305+
action = 'UI_Lua import("/lua/ui/game/aichunktemplates.lua").AddUnitSelectionToEmptyChunkTemplate(16)',
306306
category = 'ai'
307307
},
308308
['create_build_template_32'] = {
309-
action = 'UI_Lua import("/lua/ui/game/aichunktemplates.lua").PopulateChunkTemplate(32)',
309+
action = 'UI_Lua import("/lua/ui/game/aichunktemplates.lua").AddUnitSelectionToEmptyChunkTemplate(32)',
310310
category = 'ai'
311311
},
312312
}

lua/keymap/keydescriptions.lua

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,4 +562,11 @@ keyDescriptions = {
562562

563563
['select_surface_bombers'] = '<LOC key_desc_0407>Select all Bombers (Normal)',
564564
['select_torpedo_bombers'] = '<LOC key_desc_0408>Select all Bombers (Torpedo)',
565+
566+
['create_build_template_02'] = '<LOC key_desc_create_build_template_02>Create a 2x2 chunk template',
567+
['create_build_template_04'] = '<LOC key_desc_create_build_template_02>Create a 4x4 chunk template',
568+
['create_build_template_08'] = '<LOC key_desc_create_build_template_02>Create a 8x8 chunk template',
569+
['create_build_template_16'] = '<LOC key_desc_create_build_template_02>Create a 16x16 chunk template',
570+
['create_build_template_32'] = '<LOC key_desc_create_build_template_02>Create a 32x32 chunk template',
571+
565572
}

lua/shared/aichunktemplates.lua

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
--******************************************************************************************************
2+
--** Copyright (c) 2025 Willem 'Jip' Wijnia
3+
--**
4+
--** Permission is hereby granted, free of charge, to any person obtaining a copy
5+
--** of this software and associated documentation files (the "Software"), to deal
6+
--** in the Software without restriction, including without limitation the rights
7+
--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
--** copies of the Software, and to permit persons to whom the Software is
9+
--** furnished to do so, subject to the following conditions:
10+
--**
11+
--** The above copyright notice and this permission notice shall be included in all
12+
--** copies or substantial portions of the Software.
13+
--**
14+
--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
--** SOFTWARE.
21+
--******************************************************************************************************
122

223
---@class AIChunkOffset
324
---@field [1] number
@@ -26,7 +47,13 @@
2647
---@field BuildAreas AIChunkOffset[][]
2748
---@field Faction FactionCategory
2849

29-
--- Verifies the chunk so that it
50+
local TableGetn = table.getn
51+
local TableInsert = table.insert
52+
local TableConcat = table.concat
53+
54+
local StringFormat = string.format
55+
56+
--- Verifies the chunk so that it
3057
---@param template AIChunkTemplate
3158
function VerifyChunkTemplate(template)
3259
if not template.Faction then
@@ -44,19 +71,19 @@ function VerifyChunkTemplate(template)
4471
return false
4572
end
4673

47-
local count = table.getn(template.BuildAreas)
74+
local count = TableGetn(template.BuildAreas)
4875
if count < 16 then
4976
WARN("AIChunkTemplates - not sufficient offsets in the 'BuildAreas' field: should be at least 16")
5077
return false
5178
end
5279

53-
for k = 1, table.getn(template.BuildAreas) do
80+
for k = 1, TableGetn(template.BuildAreas) do
5481
local offsets = template.BuildAreas[k]
55-
for i = 1, table.getn(offsets) do
82+
for i = 1, TableGetn(offsets) do
5683
local offset = offsets[i]
5784

5885
if not (offset[1] or offset[2]) then
59-
WARN(string.format("AIChunkTemplates - invalid offset at size %d at index %d: (%f, %f) ", k, i, unpack(offset)))
86+
WARN(StringFormat("AIChunkTemplates - invalid offset at size %d at index %d: (%f, %f) ", k, i, unpack(offset)))
6087
end
6188
end
6289
end
@@ -70,19 +97,19 @@ end
7097
function CreateChunkTemplate(size, faction)
7198

7299
if size < 1 then
73-
WARN(string.format("AIChunkTemplates - size is too small: %s", tostring(size)))
100+
WARN(StringFormat("AIChunkTemplates - size is too small: %s", tostring(size)))
74101
return nil
75102
end
76103

77104
if size > 256 then
78-
WARN(string.format("AIChunkTemplates - size is too large: %s", tostring(size)))
105+
WARN(StringFormat("AIChunkTemplates - size is too large: %s", tostring(size)))
79106
return nil
80107
end
81108

82109
---@type AIChunkTemplate
83110
local template = {
84111
Faction = faction,
85-
BuildAreas = { },
112+
BuildAreas = {},
86113
Size = size,
87114
}
88115

@@ -96,33 +123,27 @@ function CreateChunkTemplate(size, faction)
96123
return template
97124
end
98125

99-
--- Copies the template
100-
---@param template AIChunkTemplate
101-
function CopyChunkTemplate(template)
102-
103-
end
104-
105-
--- Turns the template into a stringified Lua table
126+
--- Turns the template into a stringified Lua table. Useful for debugging
106127
---@param template AIChunkTemplate
107128
function StringifyChunkTemplate(template)
108129

109-
local lines = { }
130+
local lines = {}
110131

111-
table.insert(lines, "{\r\n")
112-
table.insert(lines, string.format(" Faction = %s, \r\n", tostring(template.Faction)))
113-
table.insert(lines, string.format(" Size = %d, \r\n", tostring(template.Size)))
114-
table.insert(lines, string.format(" BuildAreas = { \r\n", tostring(template.Size)))
115-
for k = 1, table.getn(template.BuildAreas) do
132+
TableInsert(lines, "{\r\n")
133+
TableInsert(lines, StringFormat(" Faction = %s, \r\n", tostring(template.Faction)))
134+
TableInsert(lines, StringFormat(" Size = %d, \r\n", tostring(template.Size)))
135+
TableInsert(lines, StringFormat(" BuildAreas = { \r\n", tostring(template.Size)))
136+
for k = 1, TableGetn(template.BuildAreas) do
116137
local buildOffsets = template.BuildAreas[k]
117-
local content = { }
118-
for l = 1, table.getn(buildOffsets) do
138+
local content = {}
139+
for l = 1, TableGetn(buildOffsets) do
119140
local offset = buildOffsets[l]
120-
content[l] = string.format("{ %.2f, %.2f }, ", offset[1], offset[2])
141+
content[l] = StringFormat("{ %.2f, %.2f }, ", offset[1], offset[2])
121142
end
122-
table.insert(lines, string.format(" { %s }, \r\n", table.concat(content, "")))
143+
TableInsert(lines, StringFormat(" { %s }, \r\n", TableConcat(content, "")))
123144
end
124-
table.insert(lines, " }, \r\n")
125-
table.insert(lines, "} \r\n")
145+
TableInsert(lines, " }, \r\n")
146+
TableInsert(lines, "} \r\n")
126147

127-
return table.concat(lines, "")
128-
end
148+
return TableConcat(lines, "")
149+
end

lua/ui/game/aichunktemplates.lua

Lines changed: 96 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,52 @@
1+
--******************************************************************************************************
2+
--** Copyright (c) 2025 Willem 'Jip' Wijnia
3+
--**
4+
--** Permission is hereby granted, free of charge, to any person obtaining a copy
5+
--** of this software and associated documentation files (the "Software"), to deal
6+
--** in the Software without restriction, including without limitation the rights
7+
--** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
--** copies of the Software, and to permit persons to whom the Software is
9+
--** furnished to do so, subject to the following conditions:
10+
--**
11+
--** The above copyright notice and this permission notice shall be included in all
12+
--** copies or substantial portions of the Software.
13+
--**
14+
--** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
--** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
--** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
--** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
--** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
--** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
--** SOFTWARE.
21+
--******************************************************************************************************
22+
23+
local TableGetn = table.getn
24+
local TableEmpty = table.empty
25+
local TableInsert = table.insert
26+
27+
local MathFloor = math.floor
28+
local MathMin = math.min
29+
local MathMax = math.max
130

231
local UserDecal = import("/lua/user/userdecal.lua").UserDecal
332
local CreateChunkTemplate = import("/lua/shared/aichunktemplates.lua").CreateChunkTemplate
433
local VerifyChunkTemplate = import("/lua/shared/aichunktemplates.lua").VerifyChunkTemplate
534
local StringifyChunkTemplate = import("/lua/shared/aichunktemplates.lua").StringifyChunkTemplate
635

36+
--- Adds the given units to the given template.
37+
---@param template AIChunkTemplate
38+
---@param units UserUnit[]
739
---@param size number
8-
function PopulateChunkTemplate (size)
9-
-- sanity check
10-
local template = CreateChunkTemplate(size, 'UEF')
11-
if not template then
12-
return
13-
end
14-
15-
-- sanity check
16-
local selection = GetSelectedUnits()
17-
if not selection or table.empty(selection) then
18-
return
19-
end
20-
40+
---@return boolean
41+
function AddToChunkTemplate(template, units, size)
2142
local buildAreas = template.BuildAreas
2243

23-
-- for debugging
24-
local decals = { }
25-
ForkThread(
26-
function()
27-
WaitSeconds(10)
28-
for k, v in decals do
29-
v:Destroy()
30-
end
31-
end
32-
)
33-
34-
local c = table.getn(selection)
44+
-- compute the center
45+
local c = TableGetn(units)
3546
local cx, cz = 0, 0
3647
for k = 1, c do
3748
-- compute center
38-
local unit = selection[k]
49+
local unit = units[k]
3950
local position = unit:GetPosition()
4051
cx = cx + position[1]
4152
cz = cz + position[3]
@@ -46,18 +57,18 @@ function PopulateChunkTemplate (size)
4657
---@type string | number
4758
-- local id = "Walls"
4859
-- if not EntityCategoryContains(categories.WALL, unit) then
49-
id = math.min(math.max(blueprint.Physics.SkirtSizeX, blueprint.Physics.SkirtSizeZ), 16)
60+
id = MathMin(MathMax(blueprint.Physics.SkirtSizeX, blueprint.Physics.SkirtSizeZ), 16)
5061
-- end
5162

52-
table.insert(buildAreas[id], unit)
63+
TableInsert(buildAreas[id], unit)
5364
end
5465

55-
cx = math.floor((cx / c) / size) * size + 0.5 * size
56-
cz = math.floor((cz / c) / size) * size + 0.5 * size
66+
cx = MathFloor((cx / c) / size) * size + 0.5 * size
67+
cz = MathFloor((cz / c) / size) * size + 0.5 * size
5768

58-
for k = 1, table.getn(buildAreas) do
69+
for k = 1, TableGetn(buildAreas) do
5970
local buildOffsets = buildAreas[k]
60-
for u = 1, table.getn(buildOffsets) do
71+
for u = 1, TableGetn(buildOffsets) do
6172
local unit = buildOffsets[u] --[[@as UserUnit]]
6273
local position = unit:GetPosition()
6374
buildOffsets[u] = {
@@ -69,29 +80,73 @@ function PopulateChunkTemplate (size)
6980

7081
--#region debugging
7182

83+
-- add a decal underneath each unit that we processed
84+
85+
local decals = {}
86+
ForkThread(
87+
function()
88+
WaitSeconds(10)
89+
for k, v in decals do
90+
v:Destroy()
91+
end
92+
end
93+
)
94+
7295
local decal = UserDecal()
7396
decal:SetTexture("/textures/ui/common/game/AreaTargetDecal/nuke_icon_inner.dds")
7497
decal:SetScale({ 20, 1, 20 })
75-
decal:SetPosition({cx, 0, cz})
76-
table.insert(decals, decal)
98+
decal:SetPosition({ cx, 0, cz })
99+
TableInsert(decals, decal)
77100

78-
for k = 1, table.getn(buildAreas) do
101+
for k = 1, TableGetn(buildAreas) do
79102
local buildOffsets = buildAreas[k]
80-
for u = 1, table.getn(buildOffsets) do
103+
for u = 1, TableGetn(buildOffsets) do
81104
local offset = buildOffsets[u]
82105

83106
local decal = UserDecal()
84107
decal:SetTexture("/textures/ui/common/game/AreaTargetDecal/nuke_icon_inner.dds")
85108
decal:SetScale({ 1, 1, 1 })
86109
decal:SetPosition({ cx + offset[1], 0, cz + offset[2] })
87-
table.insert(decals, decal)
110+
TableInsert(decals, decal)
88111
end
89112
end
90113

91114
--#endregion
92115

93-
LOG(StringifyChunkTemplate(template))
94-
VerifyChunkTemplate(template)
116+
if not VerifyChunkTemplate(template) then
117+
print("Unable to create a valid chunk template")
118+
return false
119+
end
120+
121+
return true
122+
end
123+
124+
--- Creates a new chunk template and populates it with the unit selection.
125+
---@param size number
126+
---@return AIChunkTemplate?
127+
function AddUnitSelectionToEmptyChunkTemplate(size)
128+
-- sanity check
129+
local template = CreateChunkTemplate(size, 'UEF')
130+
if not template then
131+
print("Unable to create a new chunk template")
132+
return
133+
end
134+
135+
-- sanity check
136+
local selection = GetSelectedUnits()
137+
if not selection or TableEmpty(selection) then
138+
print("Unable to populate a new chunk template with no unit selection")
139+
return
140+
end
141+
142+
-- populate the template
143+
local ok = AddToChunkTemplate(template, selection, size)
144+
if not ok then
145+
print("Unable to populate a new chunk template with the given unit selection")
146+
return
147+
end
95148

96-
return template
97-
end
149+
-- copy the template to the clipboard
150+
CopyToClipboard(StringifyChunkTemplate(template))
151+
print("Template copied to clipboard")
152+
end

0 commit comments

Comments
 (0)