3
Please, think twice before editing this file. Compared to most gadgets, there
4
are some complex things going on. A good understanding of Lua's coroutines is
5
required to make nontrivial modifications to this file.
7
In other words, HERE BE DRAGONS =)
10
- {Query,AimFrom,Aim,Fire}{Primary,Secondary,Tertiary} are not handled.
11
(use {Query,AimFrom,Aim,Fire}{Weapon1,Weapon2,Weapon3} instead!)
12
- Errors in callins which aren't wrapped in a thread do not show a traceback.
13
- Which callins are wrapped in a thread and which aren't is a bit arbitrary.
14
- MoveFinished, TurnFinished and Destroy are overwritten by the framework.
15
- There is no way to reload the script of a single unit. (use /luarules reload)
16
- Error checking is lacking. (In particular for incorrect unitIDs.)
19
- Test real world performance (compared to COB)
22
--------------------------------------------------------------------------------
23
--------------------------------------------------------------------------------
25
function gadget:GetInfo()
27
name = "Lua unit script framework",
28
desc = "Manages Lua unit scripts",
29
author = "Tobi Vollebregt",
30
date = "2 September 2009",
33
enabled = true -- loaded by default?
38
if (not gadgetHandler:IsSyncedCode()) then
49
--"ExtractionRateChanged",
76
local weapon_funcs = {
88
local default_return_values = {
97
--------------------------------------------------------------------------------
98
--------------------------------------------------------------------------------
102
local co_create = coroutine.create
103
local co_resume = coroutine.resume
104
local co_yield = coroutine.yield
105
local co_running = coroutine.running
107
local bit_and = math.bit_and
108
local floor = math.floor
110
local sp_GetGameFrame = Spring.GetGameFrame
111
local sp_GetUnitWeaponState = Spring.GetUnitWeaponState
112
local sp_SetUnitWeaponState = Spring.SetUnitWeaponState
113
local sp_SetUnitShieldState = Spring.SetUnitShieldState
115
local sp_CallAsUnit = Spring.UnitScript.CallAsUnit
116
local sp_WaitForMove = Spring.UnitScript.WaitForMove
117
local sp_WaitForTurn = Spring.UnitScript.WaitForTurn
118
local sp_SetPieceVisibility = Spring.UnitScript.SetPieceVisibility
119
local sp_SetDeathScriptFinished = Spring.UnitScript.SetDeathScriptFinished
122
local UNITSCRIPT_DIR = (UNITSCRIPT_DIR or "scripts/"):lower()
123
local VFSMODE = VFS.ZIP_ONLY
124
if (Spring.IsDevLuaEnabled()) then
125
VFSMODE = VFS.RAW_ONLY
128
VFS.Include('LuaGadgets/system.lua', nil, VFSMODE)
129
VFS.Include('gamedata/VFSUtils.lua', nil, VFSMODE)
131
--------------------------------------------------------------------------------
132
--------------------------------------------------------------------------------
135
Data structure to administrate the threads of each managed unit.
136
We store a set of all threads for each unit, and in two separate tables
137
the threads which are waiting for a turn or move animation to finish.
139
The 'thread' stored in waitingForMove/waitingForTurn/sleepers is the table
140
wrapping the actual coroutine object. This way the signal_mask etc. is
143
The threads table is a weak table. This saves us from having to manually clean
144
up dead threads: any thread which is not sleeping or waiting is in none of
145
(sleepers,waitingForMove,waitingForTurn) => it is only in the threads table
146
=> garbage collector will harvest it because the table is weak.
148
Beware the threads are indexed by thread (coroutine), so careless
149
iteration of threads WILL cause desync!
153
env = {}, -- the unit's environment table
154
waitingForMove = { [piece*3+axis] = thread, ... },
155
waitingForTurn = { [piece*3+axis] = thread, ... },
158
thread = thread, -- the coroutine object
159
signal_mask = number, -- see Signal/SetSignalMask
160
unitID = number, -- 'owner' of the thread
161
onerror = function, -- called after thread died due to an error
173
This is the bed, it stores all the sleeping threads,
174
indexed by the frame in which they need to be woken up.
177
[framenum] = { thread1, thread2, ... },
182
--------------------------------------------------------------------------------
183
--------------------------------------------------------------------------------
185
local function Remove(tab, item)
188
if (tab[i] == item) then
196
local function Destroy()
198
for _,thread in pairs(activeUnit.threads) do
199
if thread.container then
200
Remove(thread.container, thread)
203
units[activeUnit.unitID] = nil
208
local function RunOnError(thread)
209
local fun = thread.onerror
211
local good, err = pcall(fun, err)
213
Spring.Echo("error in error handler: " .. err)
218
local function WakeUp(thread, ...)
219
thread.container = nil
220
local co = thread.thread
221
local good, err = co_resume(co, ...)
224
Spring.Echo(debug.traceback(co))
229
local function AnimFinished(threads, waitingForAnim, piece, axis)
230
local index = piece * 3 + axis
231
local threads = waitingForAnim[index]
233
waitingForAnim[index] = {}
240
local function MoveFinished(piece, axis)
241
return AnimFinished(activeUnit.threads, activeUnit.waitingForMove, piece, axis)
244
local function TurnFinished(piece, axis)
245
return AnimFinished(activeUnit.threads, activeUnit.waitingForTurn, piece, axis)
248
--------------------------------------------------------------------------------
249
--------------------------------------------------------------------------------
251
function Spring.UnitScript.CallAsUnit(unitID, fun, ...)
252
local oldActiveUnit = activeUnit
253
activeUnit = units[unitID]
254
local ret = {sp_CallAsUnit(unitID, fun, ...)}
255
activeUnit = oldActiveUnit
259
local function CallAsUnitNoReturn(unitID, fun, ...)
260
local oldActiveUnit = activeUnit
261
activeUnit = units[unitID]
262
sp_CallAsUnit(unitID, fun, ...)
263
activeUnit = oldActiveUnit
266
local function WaitForAnim(threads, waitingForAnim, piece, axis)
267
local index = piece * 3 + axis
268
local wthreads = waitingForAnim[index]
269
if (not wthreads) then
271
waitingForAnim[index] = wthreads
273
local thread = threads[co_running() or error("not in a thread", 2)]
274
wthreads[#wthreads+1] = thread
275
thread.container = wthreads
276
-- yield the running thread:
277
-- it will be resumed once the wait finished (in AnimFinished).
281
function Spring.UnitScript.WaitForMove(piece, axis)
282
if sp_WaitForMove(piece, axis) then
283
return WaitForAnim(activeUnit.threads, activeUnit.waitingForMove, piece, axis)
287
function Spring.UnitScript.WaitForTurn(piece, axis)
288
if sp_WaitForTurn(piece, axis) then
289
return WaitForAnim(activeUnit.threads, activeUnit.waitingForTurn, piece, axis)
293
function Spring.UnitScript.Sleep(milliseconds)
294
local n = floor(milliseconds / 33)
295
if (n <= 0) then n = 1 end
296
n = n + sp_GetGameFrame()
297
local zzz = sleepers[n]
302
local thread = activeUnit.threads[co_running() or error("not in a thread", 2)]
304
thread.container = zzz
305
-- yield the running thread:
306
-- it will be resumed in frame #n (in gadget:GameFrame).
310
function Spring.UnitScript.StartThread(fun, ...)
311
local co = co_create(fun)
314
-- signal_mask is inherited from current thread, if any
315
signal_mask = (co_running() and activeUnit.threads[co_running()].signal_mask or 0),
316
unitID = activeUnit.unitID,
318
activeUnit.threads[co] = thread
319
-- COB doesn't start thread immediately: it only sets up stack and
320
-- pushes parameters on it for first time the thread is scheduled.
321
-- Here it is easier however to start thread immediately, so we don't need
322
-- to remember the parameters for the first co_resume call somewhere.
323
-- I think in practice the difference in behavior isn't an issue.
324
return WakeUp(thread, ...)
327
local function SetOnError(fun)
328
local thread = activeUnit.threads[co_running()]
334
function Spring.UnitScript.SetSignalMask(mask)
335
local thread = activeUnit.threads[co_running()]
337
thread.signal_mask = mask
341
function Spring.UnitScript.Signal(mask)
342
-- beware, unsynced loop order
343
-- (doesn't matter here as long as all threads get removed)
344
for _,thread in pairs(activeUnit.threads) do
345
if (bit_and(thread.signal_mask, mask) ~= 0) then
346
if thread.container then
347
Remove(thread.container, thread)
353
function Spring.UnitScript.Hide(piece)
354
return sp_SetPieceVisibility(piece, false)
357
function Spring.UnitScript.Show(piece)
358
return sp_SetPieceVisibility(piece, true)
361
function Spring.UnitScript.GetScriptEnv(unitID)
362
return units[unitID].env
365
function Spring.UnitScript.GetLongestReloadTime(unitID)
368
local reloadTime = sp_GetUnitWeaponState(unitID, i, "reloadTime")
369
if (not reloadTime) then break end
370
if (reloadTime > longest) then longest = reloadTime end
372
return 1000 * longest
375
--------------------------------------------------------------------------------
376
--------------------------------------------------------------------------------
378
local scriptHeader = VFS.LoadFile("gamedata/unit_script_header.lua", VFSMODE)
380
scriptHeader = scriptHeader:gsub("%-%-[^\r\n]*", ""):gsub("[\r\n]", " ")
384
Dictionary mapping script name (without path or extension) to a Lua chunk which
385
returns a new closure (read; instance) of this unitscript.
397
for k,v in pairs(System) do
400
script._G = _G -- the global table
401
script.GG = GG -- the shared table (shared with gadgets!)
402
prototypeEnv = script
406
local function Basename(filename)
407
return filename:match("[^\\/:]*$") or filename
411
local function LoadChunk(filename)
412
local text = VFS.LoadFile(filename, VFSMODE)
413
if (text == nil) then
414
Spring.Echo("Failed to load: " .. filename)
417
local chunk, err = loadstring(scriptHeader .. text, filename)
418
if (chunk == nil) then
419
Spring.Echo("Failed to load: " .. Basename(filename) .. " (" .. err .. ")")
426
local function LoadScript(scriptName, filename)
427
local chunk = LoadChunk(filename)
428
scripts[scriptName] = chunk
433
function gadget:Initialize()
434
Spring.Echo(string.format("Loading gadget: %-18s <%s>", ghInfo.name, ghInfo.basename))
436
-- This initialization code has following properties:
437
-- * all used scripts are loaded => early syntax error detection
438
-- * unused scripts aren't loaded
439
-- * files can be arbitrarily ordered in subdirs (like defs)
440
-- * exact path doesn't need to be specified
441
-- * exact path can be specified to resolve ambiguous basenames
442
-- * engine default scriptName (with .cob extension) works
444
-- Recursively collect files below UNITSCRIPT_DIR.
445
local scriptFiles = {}
446
for _,filename in ipairs(RecursiveFileSearch(UNITSCRIPT_DIR, "*.lua", VFSMODE)) do
447
local basename = Basename(filename)
448
scriptFiles[filename] = filename -- for exact match
449
scriptFiles[basename] = filename -- for basename match
452
-- Go through all UnitDefs and load scripts.
453
-- Names are tested in following order:
456
-- * exact match where .cob->.lua
457
-- * basename match where .cob->.lua
459
local unitDef = UnitDefs[i]
460
if (unitDef and not scripts[unitDef.scriptName]) then
461
local fn = UNITSCRIPT_DIR .. unitDef.scriptName:lower()
462
local bn = Basename(fn)
463
local cfn = fn:gsub("%.cob$", "%.lua")
464
local cbn = bn:gsub("%.cob$", "%.lua")
465
local filename = scriptFiles[fn] or scriptFiles[bn] or
466
scriptFiles[cfn] or scriptFiles[cbn]
468
Spring.Echo(" Loading unit script: " .. filename)
469
LoadScript(unitDef.scriptName, filename)
474
-- Fake UnitCreated events for existing units. (for '/luarules reload')
475
local allUnits = Spring.GetAllUnits()
477
local unitID = allUnits[i]
478
gadget:UnitCreated(unitID, Spring.GetUnitDefID(unitID))
482
--------------------------------------------------------------------------------
484
local StartThread = Spring.UnitScript.StartThread
487
local function Wrap_AimWeapon(unitID, callins)
488
local AimWeapon = callins["AimWeapon"]
489
if (not AimWeapon) then return end
491
-- SetUnitShieldState wants true or false, while
492
-- SetUnitWeaponState wants 1 or 0, niiice =)
493
local function AimWeaponThread(weaponNum, heading, pitch)
494
if AimWeapon(weaponNum, heading, pitch) then
495
-- SetUnitWeaponState counts weapons from 0
496
return sp_SetUnitWeaponState(unitID, weaponNum - 1, "aimReady", 1)
500
callins["AimWeapon"] = function(weaponNum, heading, pitch)
501
return StartThread(AimWeaponThread, weaponNum, heading, pitch)
506
local function Wrap_AimShield(unitID, callins)
507
local AimShield = callins["AimShield"]
508
if (not AimShield) then return end
510
-- SetUnitShieldState wants true or false, while
511
-- SetUnitWeaponState wants 1 or 0, niiice =)
512
local function AimShieldThread(weaponNum)
513
local enabled = AimShield(weaponNum) and true or false
514
-- SetUnitShieldState counts weapons from 0
515
return sp_SetUnitShieldState(unitID, weaponNum - 1, enabled)
518
callins["AimShield"] = function(weaponNum)
519
return StartThread(AimShieldThread, weaponNum)
524
local function Wrap_Killed(unitID, callins)
525
local Killed = callins["Killed"]
526
if (not Killed) then return end
528
local function KilledThread(recentDamage, maxHealth)
529
-- It is *very* important the sp_SetDeathScriptFinished is executed, even on error.
530
SetOnError(sp_SetDeathScriptFinished)
531
local wreckLevel = Killed(recentDamage, maxHealth)
532
sp_SetDeathScriptFinished(wreckLevel)
535
callins["Killed"] = function(recentDamage, maxHealth)
536
StartThread(KilledThread, recentDamage, maxHealth)
537
return -- no return value signals Spring to wait for SetDeathScriptFinished call.
542
local function Wrap(callins, name)
543
local fun = callins[name]
544
if (not fun) then return end
546
callins[name] = function(...)
547
return StartThread(fun, ...)
551
--------------------------------------------------------------------------------
554
Storage for MemoizedInclude.
555
Format: { [filename] = chunk }
557
local include_cache = {}
560
local function ScriptInclude(filename)
561
--Spring.Echo(" Loading include: " .. UNITSCRIPT_DIR .. filename)
562
local chunk = LoadChunk(UNITSCRIPT_DIR .. filename)
564
include_cache[filename] = chunk
570
local function MemoizedInclude(filename, env)
571
local chunk = include_cache[filename] or ScriptInclude(filename)
573
--overwrite environment so it access environment of current unit
579
--------------------------------------------------------------------------------
581
function gadget:UnitCreated(unitID, unitDefID)
582
local ud = UnitDefs[unitDefID]
583
local chunk = scripts[ud.scriptName]
584
if (not chunk) then return end
586
-- Global variables in the script are still per unit.
587
-- Set up a new environment that is an instance of the prototype
588
-- environment, so we don't need to copy all globals for every unit.
590
-- This means of course, that global variable accesses are a bit more
591
-- expensive inside unit scripts, but this can be worked around easily
592
-- by localizing the necessary globals.
594
local pieces = Spring.GetUnitPieceMap(unitID)
597
unitDefID = unitDefID,
598
script = {}, -- will store the callins
601
env.include = function(f)
602
return MemoizedInclude(f, env)
605
env.piece = function(...)
607
for _,name in ipairs{...} do
608
p[#p+1] = pieces[name] or error("piece not found: " .. tostring(name), 2)
613
setmetatable(env, { __index = prototypeEnv })
616
-- Execute the chunk. This puts the callins in env.script
617
CallAsUnitNoReturn(unitID, chunk)
618
local callins = env.script
620
-- Add framework callins.
621
callins.MoveFinished = MoveFinished
622
callins.TurnFinished = TurnFinished
623
callins.Destroy = Destroy
625
-- AimWeapon/AimShield is required for a functional weapon/shield,
626
-- so it doesn't hurt to not check other weapons.
627
if ((not callins.AimWeapon and callins.AimWeapon1) or
628
(not callins.AimShield and callins.AimShield1)) then
629
for j=1,#weapon_funcs do
630
local name = weapon_funcs[j]
633
for i=1,ud.weapons.n do
634
local fun = callins[name .. i]
640
if (n == ud.weapons.n) then
642
callins[name] = function(w, ...)
643
return dispatch[w](...)
646
-- needed for QueryWeapon / AimFromWeapon to return -1
647
-- while AimWeapon / AimShield should return false, etc.
648
local ret = default_return_values[name]
649
callins[name] = function(w, ...)
650
local fun = dispatch[w]
651
if fun then return fun(...) end
658
-- Wrap certain callins in a thread and/or safety net.
659
for i=1,#thread_wrap do
660
Wrap(callins, thread_wrap[i])
662
Wrap_AimWeapon(unitID, callins)
663
Wrap_AimShield(unitID, callins)
664
Wrap_Killed(unitID, callins)
666
-- Wrap everything so activeUnit get's set properly.
667
for k,v in pairs(callins) do
668
local fun = callins[k]
669
callins[k] = function(...)
670
activeUnit = units[unitID]
675
-- Register the callins with Spring.
676
Spring.UnitScript.CreateScript(unitID, callins)
678
-- Register (must be last: it shouldn't be done in case of error.)
684
threads = setmetatable({}, {__mode = "kv"}), -- weak table
687
-- Now it's safe to start a thread which will run Create().
688
-- (Spring doesn't run it, and if it did, it would do so too early to be useful.)
689
if callins.Create then
690
CallAsUnitNoReturn(unitID, StartThread, callins.Create)
695
function gadget:GameFrame(n)
696
local zzz = sleepers[n]
699
-- Wake up the lazy bastards.
701
local unitID = zzz[i].unitID
702
activeUnit = units[unitID]
703
sp_CallAsUnit(unitID, WakeUp, zzz[i])
708
--------------------------------------------------------------------------------
709
--------------------------------------------------------------------------------