@@ -42,7 +42,7 @@ function getEntityExportString(entity)
4242 -- Likely a player ship
4343 for k , v in pairs (__ship_templates ) do
4444 if v .__type == " playership" and v .typename .type_name == entity .components .typename .type_name then
45- return " PlayerSpaceship():setTemplate('" .. k .. " ')" .. __exportShipChanges (entity , v )
45+ return " PlayerSpaceship():setTemplate('" .. k .. " ')" .. __exportShipChanges (entity , v , " full " )
4646 end
4747 end
4848 end
@@ -120,10 +120,15 @@ function getEntityExportString(entity)
120120 if entity .components .allow_radar_link then
121121 return " ScanProbe()" .. __exportScanProbe (entity )
122122 end
123+
124+ -- No matching API function
123125 return " "
124126end
125127
126- function __exportBasics (entity )
128+ -- default_scan_state: the scan state the entity type has by default (before any explicit setScanState call).
129+ -- PlayerSpaceship() initialises all factions to "fullscan"; everything else defaults to "none".
130+ function __exportBasics (entity , default_scan_state )
131+ default_scan_state = default_scan_state or " none"
127132 local x , y = entity :getPosition ()
128133 local extras = string.format (" :setPosition(%.0f, %.0f)" , x , y )
129134 local rotation = entity :getRotation ()
@@ -135,7 +140,8 @@ function __exportBasics(entity)
135140 extras = extras .. " :setFaction('" .. faction .. " ')"
136141 end
137142 if entity .components .callsign then
138- extras = extras .. " :setCallSign('" .. entity .components .callsign .callsign .. " ')"
143+ local cs = entity .components .callsign .callsign :gsub (" \\ " , " \\\\ " ):gsub (" '" , " \\ '" )
144+ extras = extras .. " :setCallSign('" .. cs .. " ')"
139145 end
140146 local ss = entity .components .scan_state
141147 if ss then
@@ -161,11 +167,11 @@ function __exportBasics(entity)
161167 end
162168 end
163169 if # states > 0 then
164- if all_same and first_state ~= " none " then
170+ if all_same and first_state ~= default_scan_state then
165171 extras = extras .. " :setScanState('" .. first_state .. " ')"
166172 elseif not all_same then
167173 for _ , entry in ipairs (states ) do
168- if entry .state ~= " none " then
174+ if entry .state ~= default_scan_state then
169175 extras = extras .. " :setScanStateByFaction('" .. entry .name .. " ', '" .. entry .state .. " ')"
170176 end
171177 end
@@ -175,8 +181,8 @@ function __exportBasics(entity)
175181 return extras
176182end
177183
178- function __exportShipChanges (entity , template )
179- local extras = __exportBasics (entity )
184+ function __exportShipChanges (entity , template , default_scan_state )
185+ local extras = __exportBasics (entity , default_scan_state )
180186
181187 -- Hull: export max if changed from template, current if damaged, allow_destruction if changed
182188 local hull = entity .components .hull
@@ -252,9 +258,203 @@ function __exportShipChanges(entity, template)
252258 end
253259 end
254260
261+ -- Impulse engine
262+ local ie = entity .components .impulse_engine
263+ local t_ie = template .impulse_engine
264+ if ie then
265+ if not t_ie or ie .max_speed_forward ~= t_ie .max_speed_forward or ie .max_speed_reverse ~= t_ie .max_speed_reverse then
266+ if ie .max_speed_forward == ie .max_speed_reverse then
267+ extras = extras .. string.format (" :setImpulseMaxSpeed(%.1f)" , ie .max_speed_forward )
268+ else
269+ extras = extras .. string.format (" :setImpulseMaxSpeed(%.1f, %.1f)" , ie .max_speed_forward , ie .max_speed_reverse )
270+ end
271+ end
272+ if not t_ie or ie .acceleration_forward ~= t_ie .acceleration_forward or ie .acceleration_reverse ~= t_ie .acceleration_reverse then
273+ if ie .acceleration_forward == ie .acceleration_reverse then
274+ extras = extras .. string.format (" :setAcceleration(%.1f)" , ie .acceleration_forward )
275+ else
276+ extras = extras .. string.format (" :setAcceleration(%.1f, %.1f)" , ie .acceleration_forward , ie .acceleration_reverse )
277+ end
278+ end
279+ if ie .sound and ie .sound ~= " " and (not t_ie or ie .sound ~= t_ie .sound ) then
280+ extras = extras .. " :setImpulseSoundFile('" .. ie .sound .. " ')"
281+ end
282+ end
283+
284+ -- Maneuvering thrusters
285+ local man_thrusters = entity .components .maneuvering_thrusters
286+ local t_man_thrusters = template .maneuvering_thrusters
287+ if man_thrusters and (not t_man_thrusters or man_thrusters .speed ~= t_man_thrusters .speed ) then
288+ extras = extras .. string.format (" :setRotationMaxSpeed(%.1f)" , man_thrusters .speed )
289+ end
290+
291+ -- Combat maneuvering thrusters
292+ local cmt = entity .components .combat_maneuvering_thrusters
293+ local t_cmt = template .combat_maneuvering_thrusters
294+ if cmt and (not t_cmt or cmt .boost_speed ~= t_cmt .boost_speed or cmt .strafe_speed ~= t_cmt .strafe_speed ) then
295+ extras = extras .. string.format (" :setCombatManeuver(%.0f, %.0f)" , cmt .boost_speed , cmt .strafe_speed )
296+ end
297+
298+ -- Warp drive (C++ default speed_per_level=1000; setWarpDrive(true) without setWarpSpeed leaves it nil in template)
299+ local wd = entity .components .warp_drive
300+ local t_wd = template .warp_drive
301+ if wd then
302+ if not t_wd then
303+ extras = extras .. " :setWarpDrive(true)"
304+ end
305+ local t_warp_speed = (t_wd and t_wd .speed_per_level ) or 1000
306+ if wd .speed_per_level ~= t_warp_speed then
307+ extras = extras .. string.format (" :setWarpSpeed(%.0f)" , wd .speed_per_level )
308+ end
309+ end
310+
311+ -- Jump drive (C++ defaults min=5000, max=50000; setJumpDrive(true) without setJumpDriveRange leaves them nil)
312+ local jd = entity .components .jump_drive
313+ local t_jd = template .jump_drive
314+ if jd then
315+ if not t_jd then
316+ extras = extras .. " :setJumpDrive(true)"
317+ end
318+ local t_jd_min = (t_jd and t_jd .min_distance ) or 5000
319+ local t_jd_max = (t_jd and t_jd .max_distance ) or 50000
320+ if jd .min_distance ~= t_jd_min or jd .max_distance ~= t_jd_max then
321+ extras = extras .. string.format (" :setJumpDriveRange(%.0f, %.0f)" , jd .min_distance , jd .max_distance )
322+ end
323+ end
324+
325+ -- Beam weapons (0-based index for setters, 1-based for ECS component array)
326+ local bw = entity .components .beam_weapons
327+ local t_bw = template .beam_weapons
328+ if bw then
329+ for i = 1 , # bw do
330+ local b = bw [i ]
331+ local tb = t_bw and t_bw [i ]
332+ local idx = i - 1
333+ if not tb or b .arc ~= tb .arc or b .direction ~= tb .direction or b .range ~= tb .range
334+ or b .cycle_time ~= tb .cycle_time or b .damage ~= tb .damage
335+ then
336+ extras = extras .. string.format (" :setBeamWeapon(%d, %.1f, %.1f, %.0f, %.1f, %.1f)" ,
337+ idx , b .arc , b .direction , b .range , b .cycle_time , b .damage )
338+ end
339+ if b .turret_arc and b .turret_arc ~= 0 then
340+ if not tb or b .turret_arc ~= tb .turret_arc or b .turret_direction ~= tb .turret_direction
341+ or b .turret_rotation_rate ~= tb .turret_rotation_rate
342+ then
343+ extras = extras .. string.format (" :setBeamWeaponTurret(%d, %.1f, %.1f, %.1f)" ,
344+ idx , b .turret_arc , b .turret_direction , b .turret_rotation_rate )
345+ end
346+ end
347+ -- For properties not stored in the Lua template, fall back to C++ struct defaults
348+ local tb_texture = (tb and tb .texture ) or " texture/beam_orange.png"
349+ if b .texture and b .texture ~= " " and b .texture ~= tb_texture then
350+ extras = extras .. string.format (" :setBeamWeaponTexture(%d, '%s')" , idx , b .texture )
351+ end
352+ local tb_energy = (tb and tb .energy_per_beam_fire ) or 3.0
353+ if b .energy_per_beam_fire and b .energy_per_beam_fire ~= tb_energy then
354+ extras = extras .. string.format (" :setBeamWeaponEnergyPerFire(%d, %.2f)" , idx , b .energy_per_beam_fire )
355+ end
356+ local tb_heat = (tb and tb .heat_per_beam_fire ) or 0.02
357+ if b .heat_per_beam_fire and math.abs (b .heat_per_beam_fire - tb_heat ) > 1e-5 then
358+ extras = extras .. string.format (" :setBeamWeaponHeatPerFire(%d, %.3f)" , idx , b .heat_per_beam_fire )
359+ end
360+ -- arc_color is u8vec4 {r,g,b,a} (0-255); setBeamWeaponArcColor takes floats 0-1.
361+ -- ShipTemplate has no setBeamWeaponArcColor, so compare against C++ defaults:
362+ -- arc_color default={255,0,0,128}, arc_color_fire default={255,255,0,128}
363+ local ac = b .arc_color
364+ local acf = b .arc_color_fire
365+ if ac and (ac [1 ] ~= 255 or ac [2 ] ~= 0 or ac [3 ] ~= 0
366+ or (acf and (acf [1 ] ~= 255 or acf [2 ] ~= 255 or acf [3 ] ~= 0 )))
367+ then
368+ local fr = acf and acf [1 ] / 255.0 or 1.0
369+ local fg = acf and acf [2 ] / 255.0 or 1.0
370+ local fb = acf and acf [3 ] / 255.0 or 0.0
371+ extras = extras .. string.format (" :setBeamWeaponArcColor(%d, %.3f, %.3f, %.3f, %.3f, %.3f, %.3f)" ,
372+ idx , ac [1 ] / 255.0 , ac [2 ] / 255.0 , ac [3 ] / 255.0 , fr , fg , fb )
373+ end
374+ if b .damage_type and b .damage_type ~= " energy" and (not tb or b .damage_type ~= tb .damage_type ) then
375+ extras = extras .. string.format (" :setBeamWeaponDamageType(%d, '%s')" , idx , b .damage_type )
376+ end
377+ end
378+ end
379+
380+ -- Docking bay: DockingBay uses a bitfield (default flags=0), so unset template fields are nil
381+ -- while entity fields are false. Normalise both to boolean with == true before comparing.
382+ local db = entity .components .docking_bay
383+ local t_db = template .docking_bay
384+ if db then
385+ local e_share = db .share_energy == true
386+ local e_repair = db .repair == true
387+ local e_probes = db .restock_probes == true
388+ local e_missiles = db .restock_missiles == true
389+ local t_share = t_db and t_db .share_energy == true
390+ local t_repair = t_db and t_db .repair == true
391+ local t_probes = t_db and t_db .restock_probes == true
392+ local t_missiles = t_db and t_db .restock_missiles == true
393+ if e_share ~= t_share then
394+ extras = extras .. " :setSharesEnergyWithDocked(" .. tostring (e_share ) .. " )"
395+ end
396+ if e_repair ~= t_repair then
397+ extras = extras .. " :setRepairDocked(" .. tostring (e_repair ) .. " )"
398+ end
399+ if e_probes ~= t_probes then
400+ extras = extras .. " :setRestocksScanProbes(" .. tostring (e_probes ) .. " )"
401+ end
402+ if e_missiles ~= t_missiles then
403+ extras = extras .. " :setRestocksMissilesDocked(" .. tostring (e_missiles ) .. " )"
404+ end
405+ end
406+
407+ -- Repair crew: only export if we found actual crew entities and the count differs from template.
408+ -- internal_crew entities are not always instantiated, so a count of 0 is not reliable.
409+ local crew_count = __countShipCrew (entity )
410+ local template_crew_count = template .__repair_crew_count or 0
411+ if crew_count > 0 and crew_count ~= template_crew_count then
412+ extras = extras .. string.format (" :setRepairCrewCount(%d)" , crew_count )
413+ end
414+
415+ -- AI controller (CPU ships only): export AI name and non-entity-targeted orders
416+ local ai = entity .components .ai_controller
417+ local t_ai = template .ai_controller
418+ if ai then
419+ if ai .new_name and ai .new_name ~= " " and ai .new_name ~= " default" and ai .new_name ~= t_ai .new_name then
420+ extras = extras .. " :setAI('" .. ai .new_name .. " ')"
421+ end
422+ local orders = ai .orders
423+ local loc = ai .order_target_location
424+ if orders == " idle" then
425+ extras = extras .. " :orderIdle()"
426+ elseif orders == " roaming" then
427+ if loc and (loc [1 ] ~= 0 or loc [2 ] ~= 0 ) then
428+ extras = extras .. string.format (" :orderRoamingAt(%.0f, %.0f)" , loc [1 ], loc [2 ])
429+ else
430+ extras = extras .. " :orderRoaming()"
431+ end
432+ elseif orders == " stand_ground" then
433+ extras = extras .. " :orderStandGround()"
434+ elseif orders == " defend_location" and loc then
435+ extras = extras .. string.format (" :orderDefendLocation(%.0f, %.0f)" , loc [1 ], loc [2 ])
436+ elseif orders == " fly_towards" and loc then
437+ extras = extras .. string.format (" :orderFlyTowards(%.0f, %.0f)" , loc [1 ], loc [2 ])
438+ elseif orders == " fly_towards_blind" and loc then
439+ extras = extras .. string.format (" :orderFlyTowardsBlind(%.0f, %.0f)" , loc [1 ], loc [2 ])
440+ -- entity-targeted orders (defend_target, attack, dock, fly_formation) cannot be serialized
441+ end
442+ end
443+
255444 return extras
256445end
257446
447+ -- Returns the number of internal repair crew belonging to `entity`.
448+ function __countShipCrew (entity )
449+ local n = 0
450+ for _ , crew in ipairs (getEntitiesWithComponent (" internal_crew" )) do
451+ if crew .components .internal_crew and crew .components .internal_crew .ship == entity then
452+ n = n + 1
453+ end
454+ end
455+ return n
456+ end
457+
258458function __exportPlanet (entity )
259459 local extras = __exportBasics (entity )
260460 local pr = entity .components .planet_render
0 commit comments