1586 lines
41 KiB
Lua
1586 lines
41 KiB
Lua
-- Enable strict mode for this file
|
|
-- See template/std.xml for more details
|
|
--setfenv(1, xero.strict)
|
|
|
|
|
|
-- ===================================================================== --
|
|
|
|
-- Convenience shortcuts / options
|
|
|
|
local max_pn = 8 -- default: `8`
|
|
local debug_print_applymodifier_input = false -- default: `false`
|
|
local debug_print_mod_targets = false -- default: `false`
|
|
|
|
local foreground = xero.foreground
|
|
local copy = xero.copy
|
|
local clear = xero.clear
|
|
local stringbuilder = xero.stringbuilder
|
|
local stable_sort = xero.stable_sort
|
|
local perframe_data_structure = xero.perframe_data_structure
|
|
local instant = xero.instant
|
|
|
|
|
|
|
|
-- ===================================================================== --
|
|
|
|
-- The global data structures used in the program
|
|
-- These are declared early so that other functions can easily fill them in
|
|
|
|
-- eases :: list of {beat/time, len, ease_, *args, pn = number, start_time = number} and a couple of other optional string keys
|
|
-- table for eases/add/set/acc/reset
|
|
--
|
|
-- pn must be present, and must be a number.
|
|
-- start_time must be present, and is the time that the effect starts *in seconds*
|
|
--
|
|
-- Before ready_command is run:
|
|
-- * Is in the order that the mods have been inserted
|
|
-- * You can insert into this table
|
|
-- After ready_command is run:
|
|
-- * is sorted by start_time with insertion order as tiebreaker
|
|
-- * ease/add/set/acc should not insert anymore, because it will ruin the sort order.
|
|
local eases = {}
|
|
|
|
-- funcs :: list of {beat, len?, fn}, and a couple of other optional string keys
|
|
-- the table of scheduled functions and perframes
|
|
--
|
|
-- if the len is present, it will be treated as a perframe, otherwise a func.
|
|
local funcs = {}
|
|
|
|
-- auxes :: table where auxes[modname] = true when modname is auxed
|
|
-- auxilliary variables
|
|
--
|
|
-- when a mod is "auxed", it won't be applied via ApplyModifiers to the game.
|
|
-- This usually means that the mod has an in-template implementation in lua.
|
|
-- When a mod isn't auxed, it will be handled by the c++ game engine source code.
|
|
local auxes = {}
|
|
|
|
-- nodes :: list of {list<string> inputs, function(inputs) -> outputs, list<string> outputs}
|
|
-- stores nodes / definemods
|
|
--
|
|
-- After the nodes are compiled, it changes format to something different.
|
|
local nodes = {}
|
|
|
|
-- aliases :: table where aliases[old] = new
|
|
-- mods that should be renamed.
|
|
--
|
|
-- The replacement happens in the resolve_aliases function.
|
|
-- This table is stored in lowercase.
|
|
local aliases = {}
|
|
|
|
-- touched_mods :: table where touched_mods[pn][mod] = true
|
|
-- mods whose values have changed that need to be applied
|
|
local touched_mods = {}
|
|
for pn = 1, max_pn do
|
|
touched_mods[pn] = {}
|
|
end
|
|
|
|
-- default_mods :: table where default_mods[mod] = number
|
|
-- store the default values for every mod
|
|
local default_mods = {}
|
|
|
|
-- use metatables to prefill the default_mods table with 0
|
|
setmetatable(default_mods, {
|
|
__index = function(self, i)
|
|
self[i] = 0
|
|
return 0
|
|
end
|
|
})
|
|
|
|
|
|
local song = GAMESTATE:GetCurrentSong()
|
|
|
|
-- ===================================================================== --
|
|
|
|
-- Functions
|
|
|
|
|
|
-- the `plr=` system
|
|
local default_plr = {1, 2}
|
|
|
|
-- for reading the `plr` variable from the xero environment
|
|
-- without falling back to the global table
|
|
local function get_plr()
|
|
return rawget(xero, 'plr') or default_plr
|
|
end
|
|
|
|
local banned_chars = {}
|
|
local banned_chars_string = '\'\\{}(),;* '
|
|
for i = 1, #banned_chars_string do
|
|
banned_chars[string.sub(banned_chars_string, i, i)] = true
|
|
end
|
|
|
|
-- A mod name isn't valid if it would cause problems when put into
|
|
-- the "*-1 100 {}" format that GAMESTATE:ApplyModifiers expects.
|
|
-- For example, the space in 'invert ' means the game engine would treat
|
|
-- it identically to regular 'invert', which is why 'invert ' should be denied.
|
|
local function ensure_mod_name_is_valid(name)
|
|
if banned_chars[string.sub(name, 1, 1)] or banned_chars[string.sub(name, -1, -1)] then
|
|
error(
|
|
'You have a typo in your mod name. '..
|
|
'You wrote \''..name..'\', but you probably meant '..
|
|
'\''..string.gsub(name, '[\'\\{}(),;* ]', '')..'\''
|
|
)
|
|
end
|
|
if string.find(name, '^c[0-9]+$') then
|
|
error(
|
|
'You can\'t name your mod \''..name..'\'.\n'..
|
|
'Use \'cmod\' if you want to set a cmod.'
|
|
)
|
|
end
|
|
if string.find(name, '^[0-9.]+x$') then
|
|
error(
|
|
'You can\'t name your mod \''..name..'\'.\n'..
|
|
'Use \'xmod\' if you want to set an xmod.'
|
|
)
|
|
end
|
|
end
|
|
|
|
local function normalize_mod_no_checks(name)
|
|
name = string.lower(name)
|
|
return aliases[name] or name
|
|
end
|
|
|
|
-- convert a mod to its lowercase dealiased name
|
|
local function normalize_mod(name)
|
|
if not auxes[name] then ensure_mod_name_is_valid(name) end
|
|
return normalize_mod_no_checks(name)
|
|
end
|
|
|
|
|
|
-- ease {start, len, eas, percent, 'mod'}
|
|
-- adds an ease to the ease table
|
|
local function ease(self)
|
|
|
|
-- Welcome to Ease!
|
|
--
|
|
-- -- Flags set by the user
|
|
-- * plr: number[] or number or nil
|
|
-- * mode: 'end' or nil (also m could be set to 'e')
|
|
-- * time: true or nil
|
|
--
|
|
-- -- Flags set by the user (but only from `reset`)
|
|
-- * only: mod[] or nil
|
|
-- * exclude: mod[] or nil
|
|
--
|
|
-- -- Set by the other ease functions
|
|
-- * relative: true or nil
|
|
-- * makes this entry act like `add`
|
|
-- * reset: true or nil
|
|
-- * activates special reset code later
|
|
--
|
|
-- [1]: start beat (or time)
|
|
-- [2]: length
|
|
-- [3]: the ease
|
|
-- [4 + 2*n]: the target mod value
|
|
-- [5 + 2*n]: the mod name
|
|
|
|
-- convert mode into a regular true or false
|
|
self.mode = self.mode == 'end' or self.m == 'e'
|
|
|
|
-- convert the ease into relative
|
|
if self.mode then
|
|
self[2] = self[2] - self[1]
|
|
end
|
|
|
|
-- convert the start beat into time and store it in start_time
|
|
self.start_time = self.time and self[1] or song:GetElapsedTimeFromBeat(self[1])
|
|
|
|
-- future steps assume that plr is a number, so if it's a table,
|
|
-- we need to duplicate the entry once for each player number
|
|
-- The table is then stored into `eases` for later
|
|
local plr = self.plr or get_plr()
|
|
if type(plr) == 'table' then
|
|
for _, plr in ipairs(plr) do
|
|
local new = copy(self)
|
|
new.plr = plr
|
|
table.insert(eases, new)
|
|
end
|
|
else
|
|
self.plr = plr
|
|
table.insert(eases, self)
|
|
end
|
|
|
|
end
|
|
|
|
-- add {start, len, eas, percent, mod}
|
|
-- adds an ease to the ease table
|
|
local function add(self)
|
|
self.relative = true
|
|
ease(self)
|
|
end
|
|
|
|
-- set {start, percent, mod}
|
|
-- adds a set to the ease table
|
|
local function set(self)
|
|
table.insert(self, 2, 0)
|
|
table.insert(self, 3, instant)
|
|
ease(self)
|
|
end
|
|
|
|
|
|
-- acc {start, percent, mod}
|
|
-- adds a relative set to the ease table
|
|
local function acc(self)
|
|
self.relative = true
|
|
table.insert(self, 2, 0)
|
|
table.insert(self, 3, instant)
|
|
ease(self)
|
|
end
|
|
|
|
-- reset {start, [len, eas], [exclude = {mod list}]}
|
|
-- adds a reset to the ease table
|
|
local function reset(self)
|
|
-- if a length and ease aren't provided, use `0, instant` to make it act like `set`
|
|
self[2] = self[2] or 0
|
|
self[3] = self[3] or instant
|
|
|
|
-- set flag for the `run_eases` function to know that this is a reset entry
|
|
self.reset = true
|
|
|
|
if self.only then
|
|
-- you can pass `only` to reset only a specific set of mods
|
|
-- later code assumes this is a table if present, so here,
|
|
-- single values need to get wrapped in a table.
|
|
if type(self.only) == 'string' then
|
|
self.only = {self.only}
|
|
end
|
|
elseif self.exclude then
|
|
-- you can pass `exclude` to exclude a specific set of mods
|
|
-- later code assumes this is a table if present, so here,
|
|
-- single values need to get wrapped in a table.
|
|
if type(self.exclude) == 'string' then
|
|
self.exclude = {self.exclude}
|
|
end
|
|
|
|
-- When exclude is passed in, each mod is a value
|
|
-- but it needs to become a table where each mod is a key
|
|
local exclude = {}
|
|
for _, v in ipairs(self.exclude) do
|
|
exclude[v] = true
|
|
end
|
|
|
|
-- store it back
|
|
self.exclude = exclude
|
|
end
|
|
|
|
-- just use ease to insert it into the ease table
|
|
ease(self)
|
|
end
|
|
|
|
-- func helper for scheduling a function
|
|
local function func(self)
|
|
-- func {5, 'P1:xy', 2, 3}
|
|
if type(self[2]) == 'string' then
|
|
local args, syms = {}, {}
|
|
for i = 1, #self - 2 do
|
|
syms[i] = 'arg' .. i
|
|
args[i] = self[i + 2]
|
|
end
|
|
local symstring = table.concat(syms, ', ')
|
|
local code = 'return function('..symstring..') return function() '..self[2]..'('..symstring..') end end'
|
|
self[2] = xero(assert(loadstring(code, 'func_generated')))()(unpack(args))
|
|
while self[3] do
|
|
table.remove(self)
|
|
end
|
|
end
|
|
self[2], self[3] = nil, self[2]
|
|
local persist = self.persist
|
|
-- convert mode into a regular true or false
|
|
self.mode = self.mode == 'end' or self.m == 'e'
|
|
|
|
if type(persist) == 'number' and self.mode then
|
|
persist = persist - self[1]
|
|
end
|
|
if persist == false then
|
|
persist = 0.5
|
|
end
|
|
if type(persist) == 'number' then
|
|
local fn = self[3]
|
|
local final_time = self[1] + persist
|
|
self[3] = function(beat)
|
|
if beat < final_time then
|
|
fn(beat)
|
|
end
|
|
end
|
|
end
|
|
self.priority = (self.defer and -1 or 1) * (#funcs + 1)
|
|
self.start_time = self.time and self[1] or song:GetElapsedTimeFromBeat(self[1])
|
|
table.insert(funcs, self)
|
|
end
|
|
|
|
local disallowed_poptions_perframe_persist = setmetatable({}, {__index = function(_)
|
|
error('you cannot use poptions and persist at the same time. </3')
|
|
end})
|
|
|
|
-- func helper for scheduling a perframe
|
|
local function perframe(self, deny_poptions)
|
|
-- convert into relative
|
|
if self.mode then
|
|
self[2] = self[2] - self[1]
|
|
end
|
|
if not deny_poptions then
|
|
self.mods = {}
|
|
for pn = 1, max_pn do
|
|
self.mods[pn] = {}
|
|
end
|
|
end
|
|
self.priority = (self.defer and -1 or 1) * (#funcs + 1)
|
|
self.start_time = self.time and self[1] or song:GetElapsedTimeFromBeat(self[1])
|
|
|
|
local persist = self.persist
|
|
if persist then
|
|
if type(persist) == 'number' and self.mode then
|
|
persist = persist - self[1] - self[2]
|
|
end
|
|
func {
|
|
self[1] + self[2],
|
|
function()
|
|
self[3](GAMESTATE:GetSongBeat(), disallowed_poptions_perframe_persist)
|
|
end,
|
|
persist = self.persist,
|
|
}
|
|
end
|
|
|
|
table.insert(funcs, self)
|
|
end
|
|
|
|
-- func helper for function eases
|
|
local function func_ease(self)
|
|
-- convert mode into a regular true or false
|
|
self.mode = self.mode == 'end' or self.m == 'e'
|
|
-- convert into relative
|
|
if self.mode then
|
|
self[2] = self[2] - self[1]
|
|
end
|
|
local fn = table.remove(self)
|
|
local eas = self[3]
|
|
local start_percent = #self >= 5 and table.remove(self, 4) or 0
|
|
local end_percent = #self >= 4 and table.remove(self, 4) or 1
|
|
local end_beat = self[1] + self[2]
|
|
|
|
if type(fn) == 'string' then
|
|
fn = xero(assert(loadstring('return function(p) '..fn..'(p) end', 'func_generated')))()
|
|
end
|
|
|
|
self[3] = function(beat)
|
|
local progress = (beat - self[1]) / self[2]
|
|
fn(start_percent + (end_percent - start_percent) * eas(progress))
|
|
end
|
|
|
|
-- it's a function-ease variant, so make it persist
|
|
if self.persist ~= false then
|
|
local final_percent = eas(1) > 0.5 and end_percent or start_percent
|
|
func {
|
|
end_beat,
|
|
function()
|
|
fn(final_percent)
|
|
end,
|
|
persist = self.persist,
|
|
defer = self.defer,
|
|
}
|
|
end
|
|
self.persist = false
|
|
perframe(self, true)
|
|
end
|
|
|
|
-- alias {'old', 'new'}
|
|
-- aliases a mod
|
|
local function alias(self)
|
|
local a, b = self[1], self[2]
|
|
a, b = string.lower(a), string.lower(b)
|
|
aliases[a] = b
|
|
end
|
|
|
|
-- setdefault {percent, 'mod'}
|
|
-- set the default value of a mod
|
|
local function setdefault(self)
|
|
for i = 1, #self, 2 do
|
|
default_mods[self[i + 1]] = self[i]
|
|
end
|
|
return setdefault
|
|
end
|
|
|
|
-- aux {'mod'}
|
|
-- mark a mod as an aux, which won't get sent to `ApplyModifiers`
|
|
local function aux(self)
|
|
if type(self) == 'string' then
|
|
local v = self
|
|
auxes[v] = true
|
|
elseif type(self) == 'table' then
|
|
for i = 1, #self do
|
|
aux(self[i])
|
|
end
|
|
end
|
|
return aux
|
|
end
|
|
|
|
-- node {'inputs', function(inputs) return outputs end, 'outputs'}
|
|
-- create a listener that gets run whenever a mod value gets changed
|
|
local function node(self)
|
|
|
|
if type(self[2]) == 'number' then
|
|
-- transform the shorthand into the full version
|
|
local multipliers = {}
|
|
local i = 2
|
|
while self[i] do
|
|
local amt = string.format('p * %f', table.remove(self, i) * 0.01)
|
|
table.insert(multipliers, amt)
|
|
i = i + 1
|
|
end
|
|
local ret = table.concat(multipliers, ', ')
|
|
local code = 'return function(p) return '..ret..' end'
|
|
local fn = loadstring(code, 'node_generated')()
|
|
table.insert(self, 2, fn)
|
|
end
|
|
|
|
local i = 1
|
|
local inputs = {}
|
|
while type(self[i]) == 'string' do
|
|
table.insert(inputs, self[i])
|
|
i = i + 1
|
|
end
|
|
local fn = self[i]
|
|
i = i + 1
|
|
local out = {}
|
|
while self[i] do
|
|
table.insert(out, self[i])
|
|
i = i + 1
|
|
end
|
|
local result = {inputs, out, fn}
|
|
result.priority = (self.defer and -1 or 1) * (#nodes + 1)
|
|
table.insert(nodes, result)
|
|
return node
|
|
end
|
|
|
|
-- definemod{'mod', function(mod, pn) end}
|
|
-- calls aux and node on the provided arguments
|
|
local function definemod(self)
|
|
for i = 1, #self do
|
|
if type(self[i]) ~= 'string' then
|
|
break
|
|
end
|
|
aux(self[i])
|
|
end
|
|
node(self)
|
|
return definemod
|
|
end
|
|
|
|
|
|
-- ===================================================================== --
|
|
|
|
-- Runtime
|
|
|
|
|
|
-- mod targets are the values that the mod would be at if the current eases finished
|
|
local targets = {}
|
|
local targets_mt = {__index = default_mods}
|
|
for pn = 1, max_pn do
|
|
targets[pn] = setmetatable({}, targets_mt)
|
|
end
|
|
|
|
-- the live value of the current mods. Gets recomputed each frame
|
|
local mods = {}
|
|
local mods_mt = {}
|
|
for pn = 1, max_pn do
|
|
mods_mt[pn] = {__index = targets[pn]}
|
|
mods[pn] = setmetatable({}, mods_mt[pn])
|
|
end
|
|
|
|
-- a stringbuilder of the modstring that is being applied
|
|
local mod_buffer = {}
|
|
for pn = 1, max_pn do
|
|
mod_buffer[pn] = stringbuilder()
|
|
end
|
|
|
|
-- data structure for nodes
|
|
local node_start = {}
|
|
|
|
-- keep track of which players are awake
|
|
local last_seen_awake = {}
|
|
|
|
-- poptions table
|
|
local poptions = {}
|
|
local poptions_logging_target
|
|
for pn = 1, max_pn do
|
|
local pn = pn
|
|
local mods_pn = mods[pn]
|
|
local mt = {
|
|
__index = function(_, k)
|
|
return mods_pn[normalize_mod_no_checks(k)]
|
|
end,
|
|
__newindex = function(_, k, v)
|
|
k = normalize_mod_no_checks(k)
|
|
mods_pn[k] = v
|
|
if v then
|
|
poptions_logging_target[pn][k] = true
|
|
end
|
|
end,
|
|
}
|
|
poptions[pn] = setmetatable({}, mt)
|
|
end
|
|
|
|
local function touch_mod(mod, pn)
|
|
-- run metatables to ensure that the mod gets applied this frame
|
|
if pn then
|
|
mods[pn][mod] = mods[pn][mod]
|
|
else
|
|
-- if no player is provided, run for every player
|
|
for pn = 1, max_pn do
|
|
touch_mod(mod, pn)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function touch_all_mods(pn)
|
|
for mod in pairs(default_mods) do
|
|
touch_mod(mod)
|
|
end
|
|
if pn then
|
|
for mod in pairs(targets[pn]) do
|
|
touch_mod(mod, pn)
|
|
end
|
|
else
|
|
for pn = 1, max_pn do
|
|
for mod in pairs(targets[pn]) do
|
|
touch_mod(mod, pn)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- runs once during OnCommand
|
|
-- takes Name= actors and places them in the xero table
|
|
local function scan_named_actors()
|
|
local mt = {}
|
|
function mt.__index(self, k)
|
|
self[k] = setmetatable({}, mt)
|
|
return self[k]
|
|
end
|
|
local actors = setmetatable({}, mt)
|
|
local list = {}
|
|
local code = stringbuilder()
|
|
local function sweep(actor, skip)
|
|
if actor.GetNumChildren then
|
|
for i = 0, actor:GetNumChildren() - 1 do
|
|
sweep(actor:GetChildAt(i))
|
|
end
|
|
end
|
|
if skip then
|
|
return
|
|
end
|
|
local name = actor:GetName()
|
|
if name and name ~= '' then
|
|
if loadstring('t.'..name..'=t') then
|
|
table.insert(list, actor)
|
|
code'actors.'(name)' = list['(#list)']\n'
|
|
else
|
|
SCREENMAN:SystemMessage('invalid actor name: \''..name..'\'')
|
|
end
|
|
end
|
|
end
|
|
|
|
code'return function(list, actors)\n'
|
|
sweep(foreground, true)
|
|
code'end'
|
|
|
|
local load_actors = xero(assert(loadstring(code:build())))()
|
|
load_actors(list, actors)
|
|
|
|
local function clear_metatables(tab)
|
|
setmetatable(tab, nil)
|
|
for _, obj in pairs(tab) do
|
|
if type(obj) == 'table' and getmetatable(obj) == mt then
|
|
clear_metatables(obj)
|
|
end
|
|
end
|
|
end
|
|
clear_metatables(actors)
|
|
|
|
for name, actor in pairs(actors) do
|
|
xero[name] = actor
|
|
end
|
|
|
|
end
|
|
|
|
local function on_command(self)
|
|
scan_named_actors()
|
|
self:queuecommand('Ready')
|
|
end
|
|
|
|
-- runs once during ScreenReadyCommand, before the user code is loaded
|
|
-- hides various actors that are placed by the theme
|
|
local function hide_theme_actors()
|
|
for _, element in ipairs {
|
|
'Overlay', 'Underlay',
|
|
'ScoreP1', 'ScoreP2',
|
|
'LifeP1', 'LifeP2',
|
|
} do
|
|
local child = SCREENMAN(element)
|
|
if child then child:hidden(1) end
|
|
end
|
|
end
|
|
|
|
-- runs once during ScreenReadyCommand, before the user code is loaded
|
|
-- sets up the player tables
|
|
local P = {}
|
|
local function prepare_variables()
|
|
for pn = 1, max_pn do
|
|
local player = SCREENMAN('PlayerP' .. pn)
|
|
xero['P' .. pn] = player
|
|
P[pn] = player
|
|
end
|
|
xero.P = P
|
|
end
|
|
|
|
-- runs once during ScreenReadyCommand, after the user code is loaded
|
|
-- sorts the mod tables so that things can be processed in order
|
|
-- after the mod tables are sorted, no more calls to table-inserting functions are allowed
|
|
local function sort_tables()
|
|
-- sort eases by their start time, with resets running first if there's a tie break
|
|
-- it's a stable sort, so other ties are broken based on insertion order
|
|
stable_sort(eases, function(a, b)
|
|
if a.start_time == b.start_time then
|
|
return a.reset and not b.reset
|
|
else
|
|
return a.start_time < b.start_time
|
|
end
|
|
end)
|
|
|
|
-- sort the funcs by their start time and priority
|
|
-- the priority matches the insertion order unless the user added `defer = true`,
|
|
-- in which case the priority will be negative
|
|
stable_sort(funcs, function(a, b)
|
|
if a.start_time == b.start_time then
|
|
local x, y = a.priority, b.priority
|
|
return x * x * y < x * y * y
|
|
else
|
|
return a.start_time < b.start_time
|
|
end
|
|
end)
|
|
|
|
-- sort the nodes by their priority
|
|
-- the priority matches the insertion order unless the user added `defer = true`,
|
|
-- in which case the priority will be negative
|
|
stable_sort(nodes, function(a, b)
|
|
local x, y = a.priority, b.priority
|
|
return x * x * y < x * y * y
|
|
end)
|
|
end
|
|
|
|
-- runs once during ScreenReadyCommand, after the user code is loaded
|
|
-- replaces aliases with their respective mods
|
|
local function resolve_aliases()
|
|
-- aux
|
|
local old_auxes = copy(auxes)
|
|
clear(auxes)
|
|
for mod, _ in pairs(old_auxes) do
|
|
-- auxes bypass name checks
|
|
auxes[normalize_mod_no_checks(mod)] = true
|
|
end
|
|
-- ease
|
|
for _, e in ipairs(eases) do
|
|
for i = 5, #e, 2 do
|
|
e[i] = normalize_mod(e[i])
|
|
end
|
|
if e.exclude then
|
|
local exclude = {}
|
|
for k, v in pairs(e.exclude) do
|
|
exclude[normalize_mod(k)] = v
|
|
end
|
|
e.exclude = exclude
|
|
end
|
|
if e.only then
|
|
for i = 1, #e.only do
|
|
e.only[i] = normalize_mod(e.only[i])
|
|
end
|
|
end
|
|
end
|
|
-- node
|
|
for _, node_entry in ipairs(nodes) do
|
|
local input = node_entry[1]
|
|
local output = node_entry[2]
|
|
for i = 1, #input do input[i] = normalize_mod(input[i]) end
|
|
for i = 1, #output do output[i] = normalize_mod(output[i]) end
|
|
end
|
|
-- default_mods
|
|
local old_default_mods = copy(default_mods)
|
|
clear(default_mods)
|
|
for mod, percent in pairs(old_default_mods) do
|
|
local normalized = normalize_mod(mod)
|
|
default_mods[normalized] = percent
|
|
for pn = 1, max_pn do
|
|
touched_mods[pn][normalized] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- runs once during ReadyCommand
|
|
local function compile_nodes()
|
|
local terminators = {}
|
|
for _, nd in ipairs(nodes) do
|
|
for _, mod in ipairs(nd[2]) do
|
|
terminators[mod] = true
|
|
end
|
|
end
|
|
local priority = -1 * (#nodes + 1)
|
|
for k, _ in pairs(terminators) do
|
|
table.insert(nodes, {{k}, {}, nil, nil, nil, nil, nil, true, priority = priority})
|
|
end
|
|
local start = node_start
|
|
local locked = {}
|
|
local last = {}
|
|
for _, nd in ipairs(nodes) do
|
|
-- struct node {
|
|
-- list<string> inputs;
|
|
-- list<string> out;
|
|
-- lua_function fn;
|
|
-- list<struct node> children;
|
|
-- list<list<struct node>> parents; // the inner lists also have a [0] field that is a boolean
|
|
-- lua_function real_fn;
|
|
-- list<map<string, float>> outputs;
|
|
-- bool terminator;
|
|
-- int seen;
|
|
-- }
|
|
local terminator = nd[8]
|
|
if not terminator then
|
|
nd[4] = {} -- children
|
|
nd[7] = {} -- outputs
|
|
for pn = 1, max_pn do
|
|
nd[7][pn] = {}
|
|
end
|
|
end
|
|
nd[5] = {} -- parents
|
|
local inputs = nd[1]
|
|
local out = nd[2]
|
|
local fn = nd[3]
|
|
local parents = nd[5]
|
|
local outputs = nd[7]
|
|
local reverse_in = {}
|
|
for i, v in ipairs(inputs) do
|
|
reverse_in[v] = true
|
|
start[v] = start[v] or {}
|
|
parents[i] = {}
|
|
if not start[v][locked] then
|
|
table.insert(start[v], nd)
|
|
end
|
|
if start[v][locked] then
|
|
parents[i][0] = true
|
|
end
|
|
for _, pre in ipairs(last[v] or {}) do
|
|
table.insert(pre[4], nd)
|
|
table.insert(parents[i], pre[7])
|
|
end
|
|
end
|
|
for _, v in ipairs(out) do
|
|
if reverse_in[v] then
|
|
start[v][locked] = true
|
|
last[v] = {nd}
|
|
elseif not last[v] then
|
|
last[v] = {nd}
|
|
else
|
|
table.insert(last[v], nd)
|
|
end
|
|
end
|
|
|
|
local function escapestr(s)
|
|
return '\'' .. string.gsub(s, '[\\\']', '\\%1') .. '\''
|
|
end
|
|
local function list(code, i, sep)
|
|
if i ~= 1 then code(sep) end
|
|
end
|
|
|
|
local code = stringbuilder()
|
|
local function emit_inputs()
|
|
for i, mod in ipairs(inputs) do
|
|
list(code, i, ',')
|
|
for j = 1, #parents[i] do
|
|
list(code, j, '+')
|
|
code'parents['(i)']['(j)'][pn]['(escapestr(mod))']'
|
|
end
|
|
if not parents[i][0] then
|
|
list(code, #parents[i] + 1, '+')
|
|
code'mods[pn]['(escapestr(mod))']'
|
|
end
|
|
end
|
|
end
|
|
local function emit_outputs()
|
|
for i, mod in ipairs(out) do
|
|
list(code, i, ',')
|
|
code'outputs[pn]['(escapestr(mod))']'
|
|
end
|
|
return out[1]
|
|
end
|
|
code
|
|
'return function(outputs, parents, mods, fn)\n'
|
|
'return function(pn)\n'
|
|
if terminator then
|
|
code'mods[pn]['(escapestr(inputs[1]))'] = ' emit_inputs() code'\n'
|
|
else
|
|
if emit_outputs() then code' = ' end code 'fn(' emit_inputs() code', pn)\n'
|
|
end
|
|
code
|
|
'end\n'
|
|
'end\n'
|
|
|
|
local compiled = assert(loadstring(code:build(), 'node_generated'))()
|
|
nd[6] = compiled(outputs, parents, mods, fn)
|
|
if terminator then
|
|
for i, mod in ipairs(inputs) do
|
|
touch_mod(mod)
|
|
end
|
|
else
|
|
for pn = 1, max_pn do
|
|
nd[6](pn)
|
|
end
|
|
end
|
|
end
|
|
|
|
for mod, v in pairs(start) do
|
|
v[locked] = nil
|
|
end
|
|
end
|
|
|
|
local function apply_modifiers(str, pn)
|
|
GAMESTATE:ApplyModifiers(str, pn)
|
|
end
|
|
|
|
-- this if statement won't run unless you are mirin
|
|
if debug_print_applymodifier_input then
|
|
-- luacov: disable
|
|
local old_apply_modifiers = apply_modifiers
|
|
apply_modifiers = function(str, pn)
|
|
if debug_print_applymodifier_input == true or debug_print_applymodifier_input < GAMESTATE:GetSongBeat() then
|
|
print('PLAYER ' .. pn .. ': ' .. str)
|
|
if debug_print_applymodifier_input ~= true then
|
|
apply_modifiers = old_apply_modifiers
|
|
end
|
|
end
|
|
old_apply_modifiers(str, pn)
|
|
end
|
|
-- luacov: enable
|
|
end
|
|
|
|
local eases_index = 1
|
|
local active_eases = {}
|
|
|
|
local function run_eases(beat, time)
|
|
-- {start_beat, len, ease, p0, m0, p1, m1, p2, m2, p3, m3}
|
|
-- `eases` is the full sorted timeline of every ease
|
|
-- `eases_index` is pointing to the next ease in the timeline that hasn't started yet
|
|
while eases_index <= #eases do
|
|
local e = eases[eases_index]
|
|
-- The ease measures timings by beat by default, or time if time=true was set
|
|
local measure = e.time and time or beat
|
|
-- if it's not ready, break out of the loop
|
|
-- the eases table is sorted, so none of the later eases will be done either
|
|
if measure < e[1] then break end
|
|
|
|
-- At this point, we've already decided we need to add the ease to the active_eases table
|
|
-- The next step is to prepare the entry to go into the `active_eases` table
|
|
-- The problem is that the `active_eases` code makes a bunch of assumptions (so it can run faster), so
|
|
-- the ease entry needs to be normalized.
|
|
-- A "normalized" ease is basically of the form:
|
|
-- {beat, len, ease, offset1, mod1, offset2, mod2, ...}
|
|
--
|
|
-- Here are some important things that need to be made true for an active ease:
|
|
-- * It always lists out all mods being affected
|
|
-- * even a 'reset' one
|
|
-- * It always has relative numbers for its mods
|
|
-- * this makes the logic just work when there's more than one ease touching the same mod
|
|
-- * It is relative to the end point, not the start point.
|
|
-- * This one is kind of complicated.
|
|
-- the ease "commits" its changes to the targets table instantly
|
|
-- and the display value only lags behind visually.
|
|
|
|
-- plr is just a number at this point, because of the code in `ease`
|
|
local plr = e.plr
|
|
|
|
-- special cases for reset
|
|
if e.reset then
|
|
if e.only then
|
|
-- Reset only these mods, because only= was set.
|
|
for _, mod in ipairs(e.only) do
|
|
table.insert(e, default_mods[mod])
|
|
table.insert(e, mod)
|
|
end
|
|
else
|
|
-- Reset any mod that isn't excluded and isn't at its default value.
|
|
-- The goal is to normalize the reset into a regular ease entry
|
|
-- by just inserting the default values.
|
|
for mod in pairs(targets[plr]) do
|
|
if not(e.exclude and e.exclude[mod]) and targets[plr][mod] ~= default_mods[mod] then
|
|
table.insert(e, default_mods[mod])
|
|
table.insert(e, mod)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- If the ease value ends with 0.5 or more, the ease should "stick".
|
|
-- Ie, if you use outExpo, the value should stay on until turned off.
|
|
-- this is a poor quality comment
|
|
local ease_ends_at_different_position = e[3](1) >= 0.5
|
|
e.offset = ease_ends_at_different_position and 1 or 0
|
|
|
|
for i = 4, #e, 2 do
|
|
-- If it isn't using relative already, it needs to be adjusted to be relative
|
|
-- (ie, like 'add', not like 'ease')
|
|
-- Adjusted based on what the current target is set to
|
|
-- This is the reason why the sorting the eases table needs to be stable.
|
|
if not e.relative then
|
|
local mod = e[i + 1]
|
|
e[i] = e[i] - targets[plr][mod]
|
|
end
|
|
|
|
-- Update the target if it needs to be updated
|
|
if ease_ends_at_different_position then
|
|
local mod = e[i + 1]
|
|
targets[plr][mod] = targets[plr][mod] + e[i]
|
|
end
|
|
end
|
|
-- activate the ease and move to the next one
|
|
table.insert(active_eases, e)
|
|
eases_index = eases_index + 1
|
|
end
|
|
|
|
-- Every ease that's active needs to be animated
|
|
local active_eases_index = 1
|
|
while active_eases_index <= #active_eases do
|
|
local e = active_eases[active_eases_index]
|
|
local plr = e.plr
|
|
local measure = e.time and time or beat
|
|
if measure < e[1] + e[2] then
|
|
-- For every active ease, calculate the current magnitude of the ease
|
|
local e3 = e[3]((measure - e[1]) / e[2]) - e.offset
|
|
-- Go through all of the mods in the ease and write the temporary changes
|
|
-- to the mods table.
|
|
for i = 4, #e, 2 do
|
|
local mod = e[i + 1]
|
|
mods[plr][mod] = mods[plr][mod] + e3 * e[i]
|
|
end
|
|
active_eases_index = active_eases_index + 1
|
|
else
|
|
-- If the ease is done, the change to the targets table has already been made
|
|
-- so the ease only needs to be removed from the active_eases table.
|
|
-- First, we mark the mods as touched, so that eases with length 0
|
|
-- will still apply, even while being active for 0 frames.
|
|
for i = 4, #e, 2 do
|
|
local mod = e[i + 1]
|
|
touch_mod(mod, plr)
|
|
end
|
|
-- Then, the ease needs to be tossed out.
|
|
if active_eases_index ~= #active_eases then
|
|
-- Since the order of the active eases table doesn't matter,
|
|
-- we can remove an item by moving the last item to the current index.
|
|
-- For example, turning the list [1, 2, 3, 4, 5] into [1, 5, 3, 4] removes item 2
|
|
-- This strategy is used because it's faster than calling table.remove with an index
|
|
active_eases[active_eases_index] = table.remove(active_eases)
|
|
else
|
|
-- If it's already at the end of the list, just remove the item with table.remove.
|
|
table.remove(active_eases)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local funcs_index = 1
|
|
local active_funcs = perframe_data_structure(function(a, b)
|
|
local x, y = a.priority, b.priority
|
|
return x * x * y < x * y * y
|
|
end)
|
|
local function run_funcs(beat, time)
|
|
while funcs_index <= #funcs do
|
|
local e = funcs[funcs_index]
|
|
local measure = e.time and time or beat
|
|
if measure < e[1] then break end
|
|
if not e[2] then
|
|
e[3](measure)
|
|
elseif measure < e[1] + e[2] then
|
|
active_funcs:add(e)
|
|
end
|
|
funcs_index = funcs_index + 1
|
|
end
|
|
|
|
while true do
|
|
local e = active_funcs:next()
|
|
if not e then break end
|
|
local measure = e.time and time or beat
|
|
if measure < e[1] + e[2] then
|
|
poptions_logging_target = e.mods
|
|
e[3](measure, poptions)
|
|
else
|
|
if e.mods then
|
|
for pn = 1, max_pn do
|
|
for mod, _ in pairs(e.mods[pn]) do
|
|
touch_mod(mod, pn)
|
|
end
|
|
end
|
|
end
|
|
active_funcs:remove()
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
local seen = 1
|
|
local active_nodes = {}
|
|
local active_terminators = {}
|
|
local propagateAll, propagate
|
|
function propagateAll(nodes_to_propagate)
|
|
if nodes_to_propagate then
|
|
for _, nd in ipairs(nodes_to_propagate) do
|
|
propagate(nd)
|
|
end
|
|
end
|
|
end
|
|
function propagate(nd)
|
|
if nd[9] ~= seen then
|
|
nd[9] = seen
|
|
if nd[8] then
|
|
table.insert(active_terminators, nd)
|
|
else
|
|
propagateAll(nd[4])
|
|
table.insert(active_nodes, nd)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function run_nodes()
|
|
for pn = 1, max_pn do
|
|
if P[pn] and P[pn]:IsAwake() then
|
|
if not last_seen_awake[pn] then
|
|
last_seen_awake[pn] = true
|
|
for mod, _ in pairs(touched_mods[pn]) do
|
|
touch_mod(mod, pn)
|
|
touched_mods[pn][mod] = nil
|
|
end
|
|
end
|
|
seen = seen + 1
|
|
for k in pairs(mods[pn]) do
|
|
-- identify all nodes to execute this frame
|
|
propagateAll(node_start[k])
|
|
end
|
|
for _ = 1, #active_nodes do
|
|
-- run all the nodes
|
|
table.remove(active_nodes)[6](pn)
|
|
end
|
|
for _ = 1, #active_terminators do
|
|
-- run all the nodes marked as 'terminator'
|
|
table.remove(active_terminators)[6](pn)
|
|
end
|
|
else
|
|
last_seen_awake[pn] = false
|
|
for mod, _ in pairs(mods[pn]) do
|
|
mods[pn][mod] = nil
|
|
touched_mods[pn][mod] = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function run_mods()
|
|
-- each player is processed separately
|
|
for pn = 1, max_pn do
|
|
-- if the player is active
|
|
if P[pn] and P[pn]:IsAwake() then
|
|
local buffer = mod_buffer[pn]
|
|
-- toss everything that isn't an aux into the buffer
|
|
for mod, percent in pairs(mods[pn]) do
|
|
if not auxes[mod] then
|
|
buffer('*-1 '..percent..' '..mod)
|
|
end
|
|
mods[pn][mod] = nil
|
|
end
|
|
-- if the buffer has at least 1 item in it
|
|
-- then pass it to ApplyModifiers
|
|
if buffer[1] then
|
|
apply_modifiers(buffer:build(','), pn)
|
|
buffer:clear()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- this if statement won't run unless you are mirin
|
|
if debug_print_mod_targets then
|
|
-- luacov: disable
|
|
func {0, 9e9, function(beat)
|
|
if debug_print_mod_targets == true or debug_print_mod_targets < beat then
|
|
for pn = 1, max_pn do
|
|
if P[pn] and P[pn]:IsAwake() then
|
|
local outputs = {}
|
|
local i = 0
|
|
for k, v in pairs(targets[pn]) do
|
|
if v ~= default_mods[k] then
|
|
i = i + 1
|
|
outputs[i] = tostring(k)..': '..tostring(v)
|
|
end
|
|
end
|
|
print('Player '..pn..' at beat '..beat..' --> '..table.concat(outputs, ', '))
|
|
else
|
|
print('Player '..pn..' is asleep or missing')
|
|
end
|
|
end
|
|
debug_print_mod_targets = (debug_print_mod_targets == true)
|
|
end
|
|
end}
|
|
-- luacov: enable
|
|
end
|
|
|
|
local is_beyond_load_command = false
|
|
|
|
local function ready_command(self)
|
|
hide_theme_actors()
|
|
prepare_variables()
|
|
foreground:hidden(0)
|
|
|
|
-- loads both the plugins and the layout.xml due to propagation
|
|
foreground:playcommand('Load')
|
|
-- loads mods.lua
|
|
--xero.require 'mods'
|
|
|
|
sort_tables()
|
|
resolve_aliases()
|
|
compile_nodes()
|
|
|
|
for i = 1, max_pn do
|
|
mod_buffer[i]:clear()
|
|
end
|
|
|
|
-- load command has happened
|
|
-- Set this variable so that ease{}s get denied past this point
|
|
is_beyond_load_command = true
|
|
|
|
-- make sure nodes are up to date
|
|
run_nodes()
|
|
run_mods()
|
|
|
|
self:luaeffect('Update')
|
|
end
|
|
|
|
local function update_command(self)
|
|
self:hidden(1)
|
|
|
|
local beat = GAMESTATE:GetSongBeat()
|
|
local time = self:GetSecsIntoEffect()
|
|
|
|
run_eases(beat, time)
|
|
run_funcs(beat, time)
|
|
run_nodes()
|
|
run_mods()
|
|
|
|
-- if no errors have occurred, unhide self
|
|
-- to make the update_command run again next frame
|
|
self:hidden(0)
|
|
end
|
|
|
|
---------------------------------------------------------------------------------------
|
|
GAMESTATE:ApplyModifiers('clearall')
|
|
|
|
|
|
-- zoom
|
|
aux 'zoom'
|
|
node {
|
|
'zoom', 'zoomx', 'zoomy',
|
|
function(zoom, x, y)
|
|
local m = zoom * 0.01
|
|
return m * x, m * y
|
|
end,
|
|
'zoomx', 'zoomy',
|
|
defer = true,
|
|
}
|
|
|
|
setdefault {
|
|
100, 'zoom',
|
|
100, 'zoomx',
|
|
100, 'zoomy',
|
|
100, 'zoomz',
|
|
}
|
|
|
|
setdefault {400, 'grain'}
|
|
|
|
-- movex
|
|
local function repeat8(a)
|
|
return a, a, a, a, a, a, a, a
|
|
end
|
|
|
|
for _, a in ipairs {'x', 'y', 'z'} do
|
|
definemod {
|
|
'move' .. a,
|
|
repeat8,
|
|
'move'..a..'0', 'move'..a..'1', 'move'..a..'2', 'move'..a..'3',
|
|
'move'..a..'4', 'move'..a..'5', 'move'..a..'6', 'move'..a..'7',
|
|
defer = true,
|
|
}
|
|
end
|
|
|
|
-- xmod
|
|
setdefault {1, 'xmod'}
|
|
definemod {
|
|
'xmod', 'cmod',
|
|
function(xmod, cmod, pn)
|
|
if cmod == 0 then
|
|
mod_buffer[pn](string.format('*-1 %fx', xmod))
|
|
else
|
|
mod_buffer[pn](string.format('*-1 %fx,*-1 c%f', xmod, cmod))
|
|
end
|
|
end,
|
|
defer = true,
|
|
}
|
|
|
|
|
|
-- ===================================================================== --
|
|
|
|
-- Error checking
|
|
|
|
|
|
local function is_valid_ease(eas)
|
|
local err = type(eas) ~= 'function' and (not getmetatable(eas) or not getmetatable(eas).__call)
|
|
if not err then
|
|
local result = eas(1)
|
|
err = type(result) ~= 'number'
|
|
end
|
|
return not err
|
|
end
|
|
|
|
local function check_ease_errors(self, name)
|
|
if type(self) ~= 'table' then
|
|
return 'curly braces expected'
|
|
end
|
|
if type(self[1]) ~= 'number' then
|
|
return 'beat missing'
|
|
end
|
|
local is_set = name == 'set' or name == 'acc'
|
|
if not is_set then
|
|
if type(self[2]) ~= 'number' then
|
|
return 'len / end missing'
|
|
end
|
|
if not is_valid_ease(self[3]) then
|
|
return 'invalid ease function'
|
|
end
|
|
end
|
|
local i = is_set and 2 or 4
|
|
while self[i] do
|
|
if type(self[i]) ~= 'number' then
|
|
return 'invalid mod percent'
|
|
end
|
|
if type(self[i + 1]) ~= 'string' then
|
|
return 'invalid mod'
|
|
end
|
|
i = i + 2
|
|
end
|
|
assert(self[i + 1] == nil, 'invalid mod percent: '..tostring(self[i]))
|
|
local plr = self.plr or get_plr()
|
|
if type(plr) ~= 'number' and type(plr) ~= 'table' then
|
|
return 'invalid plr'
|
|
end
|
|
if is_beyond_load_command then
|
|
return 'cannot call '..name..' after LoadCommand finished'
|
|
end
|
|
end
|
|
|
|
local function check_reset_errors(self, name)
|
|
if type(self) ~= 'table' then
|
|
return 'curly braces expected'
|
|
end
|
|
if type(self[1]) ~= 'number' then
|
|
return 'the first argument needs to be a number in beats'
|
|
end
|
|
if self[2] and self[3] then
|
|
if type(self[2]) ~= 'number' then
|
|
return 'invalid length'
|
|
end
|
|
if not is_valid_ease(self[3]) then
|
|
return 'invalid ease'
|
|
end
|
|
elseif self[2] or self[3] then
|
|
return 'needs both length and ease'
|
|
end
|
|
if type(self.exclude) ~= 'nil' and type(self.exclude) ~= 'string' and type(self.exclude) ~= 'table' then
|
|
return 'invalid `exclude=` value: ' .. tostring(self.exclude)
|
|
end
|
|
if type(self.only) ~= 'nil' and type(self.only) ~= 'string' and type(self.only) ~= 'table' then
|
|
return 'invalid `only=` value: `' .. tostring(self.only)
|
|
end
|
|
if self.exclude and self.only then
|
|
return 'exclude= and only= are mutually exclusive'
|
|
end
|
|
if is_beyond_load_command then
|
|
return 'cannot call '..name..' after LoadCommand finished'
|
|
end
|
|
end
|
|
|
|
local valid_func_signatures = {
|
|
['number, function'] = true,
|
|
['number, number, function'] = true,
|
|
['number, number, ease, function'] = true,
|
|
['number, number, ease, string'] = true,
|
|
['number, number, ease, number, function'] = true,
|
|
['number, number, ease, number, string'] = true,
|
|
['number, number, ease, number, number, function'] = true,
|
|
['number, number, ease, number, number, string'] = true,
|
|
['number, string, ?'] = true,
|
|
}
|
|
|
|
local function check_func_errors(self, name)
|
|
if type(self) ~= 'table' then
|
|
return 'curly braces expected'
|
|
end
|
|
local beat, fn = self[1], self[2]
|
|
if type(beat) ~= 'number' then
|
|
return 'the first argument needs to be a number in beats'
|
|
end
|
|
if type(fn) ~= 'function' and type(fn) ~= 'string' then
|
|
return 'the second argument needs to be a function\n(maybe try using func_ease or perframe instead)'
|
|
end
|
|
if is_beyond_load_command then
|
|
return 'cannot call '..name..' after LoadCommand finished'
|
|
end
|
|
end
|
|
|
|
local function check_func_ease_errors(self, name)
|
|
if type(self) ~= 'table' then
|
|
return 'curly braces expected'
|
|
end
|
|
local beat, len, eas, fn = self[1], self[2], self[3], self[#self]
|
|
if type(beat) ~= 'number' then
|
|
return 'the first argument needs to be a number in beats'
|
|
end
|
|
if type(len) ~= 'number' then
|
|
return 'the second argument needs to be a number in beats'
|
|
end
|
|
if not is_valid_ease(eas) then
|
|
return 'the third argument needs to be an ease'
|
|
end
|
|
if #self > 5 and type(self[4]) ~= 'number' then
|
|
return 'the fourth argument needs to be a percentage'
|
|
end
|
|
if type(fn) ~= 'function' and type(fn) ~= 'string' then
|
|
return 'the last argument needs to be a function to be eased'
|
|
end
|
|
if #self > 4 and type(self[#self - 1]) ~= 'number' then
|
|
return 'the second-to-last argument needs to be a number'
|
|
end
|
|
if is_beyond_load_command then
|
|
return 'cannot call '..name..' after LoadCommand finished'
|
|
end
|
|
end
|
|
|
|
local function check_perframe_errors(self, name)
|
|
if type(self) ~= 'table' then
|
|
return 'curly braces expected'
|
|
end
|
|
local beat, len, fn = self[1], self[2], self[3]
|
|
if type(beat) ~= 'number' then
|
|
return 'the first argument needs to be a number in beats'
|
|
end
|
|
if type(len) ~= 'number' then
|
|
return 'the second argument needs to be a number in beats'
|
|
end
|
|
if type(fn) ~= 'function' and type(fn) ~= 'string' then
|
|
return 'the third argument needs to be a function'
|
|
end
|
|
if is_beyond_load_command then
|
|
return 'cannot call '..name..' after LoadCommand finished'
|
|
end
|
|
end
|
|
|
|
local function check_alias_errors(self, name)
|
|
if type(self) ~= 'table' then
|
|
return 'curly braces expected'
|
|
end
|
|
local a, b = self[1], self[2]
|
|
if type(a) ~= 'string' then
|
|
return 'argument 1 should be a string'
|
|
end
|
|
if type(b) ~= 'string' then
|
|
return 'argument 2 should be a string'
|
|
end
|
|
if is_beyond_load_command then
|
|
return 'cannot call '..name..' after LoadCommand finished'
|
|
end
|
|
end
|
|
|
|
local function check_setdefault_errors(self, name)
|
|
if type(self) ~= 'table' then
|
|
return 'curly braces expected'
|
|
end
|
|
for i = 1, #self, 2 do
|
|
if type(self[i]) ~= 'number' then
|
|
return 'invalid mod percent'
|
|
end
|
|
if type(self[i + 1]) ~= 'string' then
|
|
return 'invalid mod name'
|
|
end
|
|
end
|
|
if is_beyond_load_command then
|
|
return 'cannot call '..name..' after LoadCommand finished'
|
|
end
|
|
end
|
|
|
|
local function check_aux_errrors(self, name)
|
|
if type(self) ~= 'string' and type(self) ~= 'table' then
|
|
return 'expecting curly braces'
|
|
end
|
|
if type(self) == 'table' then
|
|
for _, v in ipairs(self) do
|
|
if type(v) ~= 'string' then
|
|
return 'invalid mod to aux: '.. tostring(v)
|
|
end
|
|
end
|
|
end
|
|
if is_beyond_load_command then
|
|
return 'cannot call '..name..' after LoadCommand finished'
|
|
end
|
|
end
|
|
|
|
local function check_node_errors(self, name)
|
|
if type(self) ~= 'table' then
|
|
return 'curly braces expected'
|
|
end
|
|
if type(self[2]) == 'number' then
|
|
-- the shorthand version
|
|
for i = 2, #self, 2 do
|
|
if type(self[i]) ~= 'number' then
|
|
return 'invalid mod percent'
|
|
end
|
|
if type(self[i + 1]) ~= 'string' then
|
|
return 'invalid mod name'
|
|
end
|
|
end
|
|
else
|
|
-- the function form
|
|
local i = 1
|
|
while type(self[i]) == 'string' do
|
|
i = i + 1
|
|
end
|
|
if i == 1 then
|
|
return 'the first argument needs to be the mod name'
|
|
end
|
|
if type(self[i]) ~= 'function' then
|
|
return 'mod definition expected'
|
|
end
|
|
i = i + 1
|
|
while self[i] do
|
|
if type(self[i]) ~= 'string' then
|
|
return 'unexpected argument '..tostring(self[i])..', expected a string'
|
|
end
|
|
i = i + 1
|
|
end
|
|
end
|
|
if is_beyond_load_command then
|
|
return 'cannot call '..name..' after LoadCommand finished'
|
|
end
|
|
end
|
|
|
|
|
|
-- ===================================================================== --
|
|
|
|
-- Exports
|
|
|
|
|
|
local function export(fn, check_errors, name)
|
|
local function inner(self)
|
|
local err = check_errors(self, name)
|
|
if err then
|
|
error(name..': '..err, 2)
|
|
else
|
|
fn(self)
|
|
end
|
|
return inner
|
|
end
|
|
xero[name] = inner
|
|
end
|
|
|
|
export(ease, check_ease_errors, 'ease')
|
|
export(add, check_ease_errors, 'add')
|
|
export(set, check_ease_errors, 'set')
|
|
export(acc, check_ease_errors, 'acc')
|
|
export(reset, check_reset_errors, 'reset')
|
|
export(func, check_func_errors, 'func')
|
|
export(perframe, check_perframe_errors, 'perframe')
|
|
export(func_ease, check_func_ease_errors, 'func_ease')
|
|
export(alias, check_alias_errors, 'alias')
|
|
export(setdefault, check_setdefault_errors, 'setdefault')
|
|
export(aux, check_aux_errrors, 'aux')
|
|
export(node, check_node_errors, 'node')
|
|
export(definemod, check_node_errors, 'definemod')
|
|
xero.get_plr = get_plr
|
|
xero.touch_mod = touch_mod
|
|
xero.touch_all_mods = touch_all_mods
|
|
|
|
xero.max_pn = max_pn
|
|
|
|
|
|
xero()
|
|
|
|
scx = SCREEN_CENTER_X
|
|
scy = SCREEN_CENTER_Y
|
|
sw = SCREEN_WIDTH
|
|
sh = SCREEN_HEIGHT
|
|
|
|
dw = DISPLAY:GetDisplayWidth()
|
|
dh = DISPLAY:GetDisplayHeight()
|
|
|
|
e = 'end'
|
|
|
|
function sprite(self)
|
|
self:basezoomx(sw / dw)
|
|
self:basezoomy(-sh / dh)
|
|
self:x(scx)
|
|
self:y(scy)
|
|
end
|
|
|
|
function aft(self)
|
|
self:SetWidth(dw)
|
|
self:SetHeight(dh)
|
|
self:EnableDepthBuffer(false)
|
|
self:EnableAlphaBuffer(false)
|
|
self:EnableFloat(false)
|
|
self:EnablePreserveTexture(true)
|
|
self:Create()
|
|
end
|
|
|
|
|
|
-- UNDOCUMENTED
|
|
xero.mod_buffer = mod_buffer
|
|
|
|
---@diagnostic disable-next-line: lowercase-global
|
|
function xero.aftsprite(aft, sprite)
|
|
sprite:SetTexture(aft:GetTexture())
|
|
end
|
|
|
|
-- end UNDOCUMENTED
|
|
|
|
-- This is the entry point of the template.
|
|
-- It sets up all of the commands used to run the template.
|
|
function xero.init_command(self)
|
|
-- This sets up a trick to get the Song time during the update command
|
|
self:effectclock('music')
|
|
|
|
-- Register the commands to the actor
|
|
|
|
-- OnCommand is for resolving Name= on all the actors
|
|
self:addcommand('On', on_command)
|
|
|
|
-- ReadyCommand is called after OnCommand, and does all of the loading
|
|
-- at the end of ReadyCommand, the tables are sorted and prepared for UpdateCommand
|
|
self:addcommand('Ready', ready_command)
|
|
|
|
-- Update command is called every frame. It is what sets the mod values every frame,
|
|
-- and reads through everything that's been queued by the user.
|
|
-- Delay one frame because the escape menu issue
|
|
self:addcommand('Update', function()
|
|
self:removecommand('Update')
|
|
self:addcommand('Update', update_command)
|
|
end)
|
|
|
|
-- NotITG and OpenITG have a long standing bug where the InitCommand on an actor can run twice in certain cases.
|
|
-- By removing the command here (at the end of init_command), we prevent it from being run again.
|
|
self:removecommand('Init')
|
|
|
|
-- init_command is the only one that was in the xero table, so clean it up
|
|
xero.init_command = nil
|
|
end
|