Skip to content

Commit ee94fb9

Browse files
feat: world trigger zones, E-key interaction, and config persistence (v1.0.1.0)
feat: world trigger zones, E-key interaction, and config persistence (v1.0.1.0)
2 parents 4cb5015 + 7ceb7ec commit ee94fb9

13 files changed

Lines changed: 656 additions & 115 deletions

build.sh

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ set -e
99
MOD_NAME="FS25_CustomTriggerCreator"
1010
DEPLOY_DIR="C:/Users/tison/Documents/My Games/FarmingSimulator2025/mods"
1111
OUT_ZIP="${MOD_NAME}.zip"
12+
PYTHON="C:/Users/tison/AppData/Local/Programs/Python/Python313/python.exe"
1213

1314
INCLUDE=(
1415
main.lua
@@ -31,7 +32,32 @@ for item in "${INCLUDE[@]}"; do
3132
fi
3233
done
3334

34-
zip -r "${OUT_ZIP}" "${EXISTING[@]}"
35+
# Use native zip if available; fall back to Python zipfile (forward-slash paths)
36+
if command -v zip &>/dev/null; then
37+
zip -r "${OUT_ZIP}" "${EXISTING[@]}"
38+
else
39+
echo " (zip not found — using Python zipfile fallback)"
40+
"${PYTHON}" - "${OUT_ZIP}" "${EXISTING[@]}" <<'PYEOF'
41+
import sys, os, zipfile
42+
43+
out = sys.argv[1]
44+
args = sys.argv[2:]
45+
46+
def add(zf, path):
47+
if os.path.isfile(path):
48+
zf.write(path, path.replace("\\", "/"))
49+
elif os.path.isdir(path):
50+
for root, dirs, files in os.walk(path):
51+
for f in files:
52+
fp = os.path.join(root, f)
53+
zf.write(fp, fp.replace("\\", "/"))
54+
55+
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zf:
56+
for arg in args:
57+
add(zf, arg)
58+
PYEOF
59+
fi
60+
3561
echo "==> Built: ${OUT_ZIP} ($(du -sh "${OUT_ZIP}" | cut -f1))"
3662

3763
if [[ "$1" == "--deploy" ]]; then

main.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ source(modDirectory .. "src/core/TriggerRegistry.lua")
3838
source(modDirectory .. "src/core/TriggerSerializer.lua")
3939
source(modDirectory .. "src/core/TriggerExecutor.lua")
4040
source(modDirectory .. "src/core/CTTriggerExporter.lua")
41+
source(modDirectory .. "src/core/CTTriggerActivatable.lua")
42+
source(modDirectory .. "src/core/CTWorldManager.lua")
4143

4244
-- =========================================================
4345
-- Triggers

modDesc.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
22
<modDesc descVersion="105">
33
<author>TisonK</author>
4-
<version>1.0.0.1</version>
4+
<version>1.0.1.0</version>
55
<modName>FS25_CustomTriggerCreator</modName>
66
<title>
77
<en>Custom Trigger Creator</en>

src/CustomTriggerCreator.lua

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ function CustomTriggerCreator.new(mission, modDirectory, modName)
2424
self.notificationHUD = CTNotificationHUD.new(self.settings)
2525
self.triggerExecutor = TriggerExecutor.new()
2626
self.hotspotManager = CTHotspotManager.new()
27+
self.worldManager = CTWorldManager.new()
2728
self.triggerExporter = CTTriggerExporter.new(self.triggerRegistry)
2829

2930
-- External script callback registry (for FIRE_EVENT / CUSTOM_SCRIPT triggers)
@@ -50,14 +51,15 @@ function CustomTriggerCreator:onMissionLoaded()
5051
self.triggerExecutor:initialize()
5152

5253
self.initialized = true
53-
Logger.info("Initialized — ready (Phase 4)")
54+
Logger.info("Initialized — ready")
5455
end
5556

5657
function CustomTriggerCreator:update(dt)
5758
if not self.initialized or not self.settings.enabled then return end
5859
self.markerDetector:update(dt)
5960
self.notificationHUD:update(dt)
6061
self.triggerExecutor:update(dt)
62+
self.worldManager:update(dt)
6163
self:_updateProximityHint()
6264
end
6365

@@ -78,6 +80,7 @@ function CustomTriggerCreator:delete()
7880
if self.markerDetector then self.markerDetector:delete() end
7981
if self.notificationHUD then self.notificationHUD:delete() end
8082
if self.hotspotManager then self.hotspotManager:delete() end
83+
if self.worldManager then self.worldManager:delete() end
8184
self.initialized = false
8285
Logger.info("Deleted — cleanup complete")
8386
end
@@ -108,6 +111,10 @@ function CustomTriggerCreator:loadFromXML(xmlFile)
108111
Logger.setDebug(self.settings.debugMode)
109112
self.triggerSerializer:load(xmlFile)
110113
Logger.module("CTC", "Loaded from XML — " .. self.triggerRegistry:count() .. " trigger(s)")
114+
115+
-- Rebuild world zones and map hotspots from loaded triggers
116+
self.worldManager:refresh(self.triggerRegistry)
117+
self.hotspotManager:refreshFromRegistry(self.triggerRegistry)
111118
end
112119

113120
-- ---------------------------------------------------------------------------

src/core/CTTriggerActivatable.lua

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
-- =========================================================
2+
-- CTTriggerActivatable.lua — FS25_CustomTriggerCreator
3+
-- Implements the FS25 Activatable interface for CTC trigger
4+
-- zones. Registered with ActivatableObjectsSystem when the
5+
-- player enters a trigger's interaction radius; pressing E
6+
-- calls run() which executes the trigger.
7+
-- =========================================================
8+
9+
CTTriggerActivatable = {}
10+
CTTriggerActivatable._mt = { __index = CTTriggerActivatable }
11+
12+
---Create a CTTriggerActivatable from a trigger registry record.
13+
---@param record table Trigger record from TriggerRegistry
14+
---@return CTTriggerActivatable
15+
function CTTriggerActivatable.new(record)
16+
local self = setmetatable({}, CTTriggerActivatable._mt)
17+
self.record = record
18+
self.activateText = "Activate: " .. (record.name or "Trigger")
19+
return self
20+
end
21+
22+
-- ---------------------------------------------------------------------------
23+
-- Activatable interface (required by FS25 ActivatableObjectsSystem)
24+
-- ---------------------------------------------------------------------------
25+
26+
---Whether the trigger can be activated right now.
27+
---Called every frame; controls "Press E" prompt visibility.
28+
---@return boolean
29+
function CTTriggerActivatable:getIsActivatable()
30+
if not self.record or not self.record.enabled then return false end
31+
-- Don't allow activation while any GUI is open
32+
if g_gui and g_gui.currentGui ~= nil then return false end
33+
-- Require a running career mission
34+
if not g_currentMission or not g_currentMission.isMissionStarted then return false end
35+
return true
36+
end
37+
38+
---Distance from this trigger's world position to a given point.
39+
---Used by the system to sort and prioritise nearby activatables.
40+
---@param x number
41+
---@param y number
42+
---@param z number
43+
---@return number
44+
function CTTriggerActivatable:getDistance(x, y, z)
45+
local cfg = self.record and self.record.config
46+
if not cfg or not cfg.worldX or not cfg.worldZ then return math.huge end
47+
local dx = x - cfg.worldX
48+
local dy = y - (cfg.worldY or 0)
49+
local dz = z - cfg.worldZ
50+
return math.sqrt(dx * dx + dy * dy + dz * dz)
51+
end
52+
53+
---Called when the player presses E while this activatable is active.
54+
function CTTriggerActivatable:run()
55+
if not g_CTCSystem or not g_CTCSystem.triggerExecutor then return end
56+
g_CTCSystem.triggerExecutor:executeById(self.record.id)
57+
end

src/core/CTWorldManager.lua

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
-- =========================================================
2+
-- CTWorldManager.lua — FS25_CustomTriggerCreator
3+
-- Manages world-space proximity zones for player-created
4+
-- triggers that have a world position stored in config.
5+
--
6+
-- Each positioned trigger gets a CTTriggerActivatable that
7+
-- is added to / removed from FS25's ActivatableObjectsSystem
8+
-- as the player enters / leaves the interaction radius.
9+
-- This is what shows "Press [E] to ..." and handles E-key.
10+
-- =========================================================
11+
12+
CTWorldManager = {}
13+
CTWorldManager._mt = { __index = CTWorldManager }
14+
15+
CTWorldManager.INTERACT_RADIUS = 3.0 -- metres
16+
CTWorldManager.INTERACT_RADIUS_SQ = 9.0 -- radius^2, avoids sqrt in update
17+
18+
---Create a new CTWorldManager.
19+
---@return CTWorldManager
20+
function CTWorldManager.new()
21+
local self = setmetatable({}, CTWorldManager._mt)
22+
-- id -> { activatable: CTTriggerActivatable, inRange: bool, record: table }
23+
self._zones = {}
24+
return self
25+
end
26+
27+
-- ---------------------------------------------------------------------------
28+
-- Public API
29+
-- ---------------------------------------------------------------------------
30+
31+
---Rebuild zones to match the current registry state.
32+
---Call after trigger create, delete, or savegame load.
33+
---@param registry TriggerRegistry
34+
function CTWorldManager:refresh(registry)
35+
if not registry then return end
36+
local all = registry:getAll()
37+
local active = {}
38+
39+
for _, t in ipairs(all) do
40+
active[t.id] = true
41+
local cfg = t.config
42+
if cfg and cfg.worldX and cfg.worldZ then
43+
local zone = self._zones[t.id]
44+
if not zone then
45+
-- New positioned trigger
46+
self._zones[t.id] = {
47+
activatable = CTTriggerActivatable.new(t),
48+
inRange = false,
49+
record = t,
50+
}
51+
Logger.debug("CTWorldManager: zone registered for " .. t.id)
52+
else
53+
-- Trigger updated (toggle/rename) — refresh live references
54+
zone.record = t
55+
zone.activatable.record = t
56+
zone.activatable.activateText = "Activate: " .. (t.name or "Trigger")
57+
end
58+
end
59+
end
60+
61+
-- Remove zones for deleted triggers
62+
for id, zone in pairs(self._zones) do
63+
if not active[id] then
64+
self:_removeZone(id, zone)
65+
end
66+
end
67+
end
68+
69+
---Per-frame proximity check. Adds/removes activatables as player moves.
70+
---@param dt number Delta time in ms (FS25 convention)
71+
function CTWorldManager:update(dt)
72+
if not g_localPlayer or not g_localPlayer.rootNode then return end
73+
local activSys = g_currentMission and g_currentMission.activatableObjectsSystem
74+
if not activSys then return end
75+
76+
local px, py, pz = getWorldTranslation(g_localPlayer.rootNode)
77+
78+
for id, zone in pairs(self._zones) do
79+
local cfg = zone.record and zone.record.config
80+
local wx = cfg and cfg.worldX
81+
local wz = cfg and cfg.worldZ
82+
if wx and wz then
83+
local wy = cfg.worldY or 0
84+
local dx = px - wx
85+
local dy = py - wy
86+
local dz = pz - wz
87+
local distSq = dx * dx + dy * dy + dz * dz
88+
local near = distSq <= CTWorldManager.INTERACT_RADIUS_SQ
89+
90+
if near ~= zone.inRange then
91+
zone.inRange = near
92+
if near then
93+
activSys:addActivatable(zone.activatable)
94+
Logger.debug("CTWorldManager: player entered zone " .. id)
95+
else
96+
activSys:removeActivatable(zone.activatable)
97+
Logger.debug("CTWorldManager: player left zone " .. id)
98+
end
99+
end
100+
end
101+
end
102+
end
103+
104+
---Clean up all zones on mod unload.
105+
function CTWorldManager:delete()
106+
local activSys = g_currentMission and g_currentMission.activatableObjectsSystem
107+
for id, zone in pairs(self._zones) do
108+
if zone.inRange and activSys then
109+
activSys:removeActivatable(zone.activatable)
110+
end
111+
end
112+
self._zones = {}
113+
Logger.module("CTWorldManager", "Cleaned up")
114+
end
115+
116+
-- ---------------------------------------------------------------------------
117+
-- Internal
118+
-- ---------------------------------------------------------------------------
119+
120+
function CTWorldManager:_removeZone(id, zone)
121+
if zone.inRange then
122+
local activSys = g_currentMission and g_currentMission.activatableObjectsSystem
123+
if activSys then
124+
activSys:removeActivatable(zone.activatable)
125+
end
126+
end
127+
self._zones[id] = nil
128+
Logger.debug("CTWorldManager: zone removed for " .. tostring(id))
129+
end

src/core/TriggerExecutor.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ function TriggerExecutor:update(dt)
9191
if self._activeTrigger.updateChain then
9292
self._activeTrigger:updateChain(dt)
9393
end
94-
-- Clear when chain is complete
95-
if self._activeTrigger._activeChain == nil then
94+
-- Clear when chain is complete (re-check nil: updateChain could invalidate)
95+
if not self._activeTrigger or self._activeTrigger._activeChain == nil then
9696
self._activeTrigger = nil
9797
end
9898
end

0 commit comments

Comments
 (0)