Sat, 05 Jul 2025 00:35:41 +0300
Add show-buildable mod
14 | 1 | require"sb-util" |
2 | ||
3 | ---@param player LuaPlayer | |
4 | local function get_entity_player_wants_to_place(player) | |
5 | local item_name = nil | |
6 | if player.cursor_stack.valid_for_read | |
7 | then | |
8 | item_name = player.cursor_stack.name | |
9 | elseif player.cursor_ghost | |
10 | then | |
11 | item_name = player.cursor_ghost.name.name | |
12 | end | |
13 | local entity_name = nil | |
14 | local unsupported_types = { | |
15 | ["straight-rail"] = true, | |
16 | ["rail-ramp"] = true, | |
17 | ["rail-support"] = true, | |
18 | ["rail-signal"] = true, | |
19 | ["rail-chain-signal"] = true, | |
20 | ["train-stop"] = true, | |
21 | ["locomotive"] = true, | |
22 | ["cargo-wagon"] = true, | |
23 | ["fluid-wagon"] = true, | |
24 | ["offshore-pump"] = true, | |
25 | ["car"] = true, | |
26 | ["spider-vehicle"] = true, | |
27 | } | |
28 | if item_name | |
29 | then | |
30 | item_proto = prototypes.item[item_name] | |
31 | if item_proto and item_proto.place_result | |
32 | and not item_proto.place_result.flags["placeable-off-grid"] | |
33 | and not unsupported_types[item_proto.place_result.type] | |
34 | then | |
35 | entity_name = item_proto.place_result.name | |
36 | end | |
37 | end | |
38 | return entity_name | |
39 | end | |
40 | ||
41 | -- where to try place the entity inside the grid | |
42 | -- 1×1 entities require 0.5 offset, 2×2 require 1 offset, | |
43 | -- 3×3 entities require 0.5 again, 4×4 require 1. | |
44 | -- and so on... | |
45 | ---@param collision_box data.BoundingBox | |
46 | local function offset_x(collision_box) | |
47 | local x = collision_box.left_top.x | |
48 | local oy = ((x%1) - (x % 0.5)) or 1 | |
49 | return oy > 0 and oy or 1 | |
50 | end | |
51 | ||
52 | ---@param collision_box data.BoundingBox | |
53 | local function offset_y(collision_box) | |
54 | local y = collision_box.left_top.y | |
55 | local oy = ((y%1) - (y % 0.5)) or 1 | |
56 | return oy > 0 and oy or 1 | |
57 | end | |
58 | ||
59 | ---@param proto LuaEntityPrototype | |
60 | --- can x be rotated??? | |
61 | local function can_be_rotated(proto) | |
62 | return proto.supports_direction and proto.collision_box.left_top.x ~= proto.collision_box.left_top.y | |
63 | end | |
64 | ||
65 | local function collision_tester(entity_name) | |
66 | if entity_name and prototypes.entity["collision-tester-"..entity_name] | |
67 | then | |
68 | return "collision-tester-"..entity_name | |
69 | else | |
70 | return entity_name | |
71 | end | |
72 | end | |
73 | ||
74 | ---@param a table | |
75 | ---@param b any | |
76 | local function find(a, b) | |
77 | for k, v in pairs(a) | |
78 | do | |
79 | if v == b | |
80 | then | |
81 | return k | |
82 | end | |
83 | end | |
84 | end | |
85 | ||
86 | local function join(sep, x) | |
87 | result = "" | |
88 | local i = 0 | |
89 | for k, v in pairs(x) | |
90 | do | |
91 | if i > 0 | |
92 | then | |
93 | result = result..sep | |
94 | end | |
95 | result = result..v | |
96 | i = i + 1 | |
97 | end | |
98 | return result | |
99 | end | |
100 | ||
101 | ---@param a MapPosition | |
102 | ---@param b MapPosition | |
103 | local function manh(a, b) | |
104 | return math.abs(b.x - a.x) + math.abs(b.y - a.y) | |
105 | end | |
106 | ||
107 | ---@param surface string | |
108 | ---@param pos MapPosition | |
109 | ---@param proto LuaEntityPrototype | |
110 | function resources_test(surface, pos, proto) | |
111 | if storage.num_memoized_resource_tests == nil or storage.num_memoized_resource_tests > 1000 | |
112 | then | |
113 | storage.num_memoized_resource_tests = 0 | |
114 | ---@type table<string, table<number, table<number, table<string, boolean>>>> | |
115 | storage.memoized_resource_tests = {} | |
116 | end | |
117 | if not storage.memoized_resource_tests[surface] | |
118 | then | |
119 | storage.memoized_resource_tests[surface] = {} | |
120 | end | |
121 | if not storage.memoized_resource_tests[surface][pos.x] | |
122 | then | |
123 | storage.memoized_resource_tests[surface][pos.x] = {} | |
124 | end | |
125 | if not storage.memoized_resource_tests[surface][pos.x][pos.y] | |
126 | then | |
127 | storage.memoized_resource_tests[surface][pos.x][pos.y] = {} | |
128 | end | |
129 | local arr = storage.memoized_resource_tests[surface][pos.x][pos.y] | |
130 | if arr[proto.name] ~= nil | |
131 | then | |
132 | return arr[proto.name] | |
133 | else | |
134 | -- check resources | |
135 | local resources = {} | |
136 | for _, resource in pairs(prototypes.entity) | |
137 | do | |
138 | if resource.type == "resource" | |
139 | and ( | |
140 | resource.resource_category == proto.resource_category | |
141 | or (proto.resource_categories or {})[resource.resource_category] | |
142 | ) | |
143 | then | |
144 | table.insert(resources, resource.name) | |
145 | end | |
146 | end | |
147 | local r = proto.mining_drill_radius | |
148 | local resources_found = game.surfaces[surface].find_entities_filtered{ | |
149 | area = {{pos.x - r, pos.y - r}, {pos.x + r, pos.y + r}}, | |
150 | type = "resource", | |
151 | name = resources, | |
152 | limit = 1 | |
153 | } | |
154 | -- special extra test for pumpjacks because those need to be placed | |
155 | -- directly on top of the resource | |
156 | local found = (#resources_found > 0) | |
157 | if found and proto.mining_drill_radius < 1 | |
158 | and manh(resources_found[1].position, pos) > proto.mining_drill_radius | |
159 | then | |
160 | found = false | |
161 | end | |
162 | arr[proto.name] = found | |
163 | return found | |
164 | end | |
165 | end | |
166 | ||
167 | script.on_nth_tick(30, function() | |
168 | for _, player in pairs(game.players) | |
169 | do | |
170 | if player.game_view_settings.show_entity_info | |
171 | then | |
172 | local entity_name = get_entity_player_wants_to_place(player) | |
173 | local collision_box_table = {} | |
174 | local scale = 1 | |
175 | if entity_name ~= nil | |
176 | then | |
177 | local proto = prototypes.entity[entity_name] | |
178 | scale = 1 | |
179 | if proto.type == "cargo-bay" or proto.type == "cargo-landing-pad" | |
180 | then | |
181 | scale = 2 | |
182 | end | |
183 | local scaled_ceil = function(x) return math.ceil(x / scale) * scale end | |
184 | local rotated_box = { | |
185 | left_top = { | |
186 | x = proto.collision_box.left_top.y, | |
187 | y = proto.collision_box.left_top.x, | |
188 | }, | |
189 | right_bottom = { | |
190 | x = proto.collision_box.right_bottom.y, | |
191 | y = proto.collision_box.right_bottom.x, | |
192 | }, | |
193 | } | |
194 | ---@param x number | |
195 | ---@param y number | |
196 | ---@param direction defines.direction | |
197 | ---@param collision_box data.BoundingBox | |
198 | local function test_placement(x, y, direction, collision_box) | |
199 | local pos = { | |
200 | x = x + offset_x(collision_box) * scale, | |
201 | y = y + offset_y(collision_box) * scale | |
202 | } | |
203 | local can_build = player.can_place_entity{ | |
204 | name = collision_tester(entity_name), | |
205 | position = pos, | |
206 | direction = direction, | |
207 | terrain_building_size = 1, | |
208 | } | |
209 | if can_build and proto.type == "mining-drill" and not resources_test(player.surface.name, pos, proto) | |
210 | then | |
211 | can_build = false | |
212 | end | |
213 | if can_build | |
214 | then | |
215 | local xi = scaled_ceil(collision_box.left_top.x) | |
216 | local xj = scaled_ceil(collision_box.right_bottom.x + 0.5*scale) | |
217 | local yj = scaled_ceil(collision_box.right_bottom.y + 0.5*scale) | |
218 | while xi < xj | |
219 | do | |
220 | local yi = scaled_ceil(collision_box.left_top.y) | |
221 | while yi < yj | |
222 | do | |
223 | if not collision_box_table[x + xi] | |
224 | then | |
225 | collision_box_table[x + xi] = {} | |
226 | end | |
227 | collision_box_table[x + xi][y + yi] = true | |
228 | yi = yi + scale | |
229 | end | |
230 | xi = xi + scale | |
231 | end | |
232 | else | |
233 | if collision_box_table[x][y] == nil | |
234 | then | |
235 | collision_box_table[x][y] = false | |
236 | end | |
237 | end | |
238 | end | |
239 | local x0 = scaled_ceil(player.position.x) | |
240 | local y0 = scaled_ceil(player.position.y) | |
241 | local x = x0 - 12*scale | |
242 | local xn = x0 + 11*scale | |
243 | local yn = y0 + 11*scale | |
244 | while x <= xn | |
245 | do | |
246 | if not collision_box_table[x] | |
247 | then | |
248 | collision_box_table[x] = {} | |
249 | end | |
250 | local y = y0 - 12*scale | |
251 | while y <= yn | |
252 | do | |
253 | local collision_box = proto.collision_box | |
254 | test_placement(x, y, defines.direction.north, collision_box) | |
255 | if proto.type == "asteroid-collector" | |
256 | then | |
257 | test_placement(x, y, defines.direction.east, rotated_box) | |
258 | test_placement(x, y, defines.direction.west, rotated_box) | |
259 | test_placement(x, y, defines.direction.south, collision_box) | |
260 | elseif can_be_rotated(proto) | |
261 | then | |
262 | test_placement(x, y, defines.direction.east, rotated_box) | |
263 | end | |
264 | y = y + scale | |
265 | end | |
266 | x = x + scale | |
267 | end | |
268 | end | |
269 | local function red(a) return {r=a, g=0, b=0, a=a } end | |
270 | local function green(a) return {r=0, g=a, b=0, a=a } end | |
271 | for x, k in pairs(collision_box_table) | |
272 | do | |
273 | for y, can_build in pairs(k) | |
274 | do | |
275 | ---@return LuaRendering.draw_rectangle_param | |
276 | local parms = function(a) | |
277 | return { | |
278 | surface = player.surface, | |
279 | color = (can_build and green or red)(a), | |
280 | filled = true, | |
281 | left_top = {x = x, y = y }, | |
282 | right_bottom = { x = x + scale - 0.05, y = y + scale - 0.05 }, | |
283 | time_to_live = 30, | |
284 | players = {player}, | |
285 | only_in_alt_mode = true, | |
286 | } | |
287 | end | |
288 | rendering.draw_rectangle(parms (0.2)) | |
289 | local parms_2 = parms(0.2) | |
290 | parms_2.draw_on_ground = true | |
291 | rendering.draw_rectangle(parms_2) | |
292 | end | |
293 | end | |
294 | end | |
295 | end | |
296 | end) |