diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..2815cfc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "nulllib"] + path = nulllib + url = https://gitea.protogen.io/nullbite/nulllib-lua.git diff --git a/TODO.md b/TODO.md index 5a320c9..b2e93ca 100644 --- a/TODO.md +++ b/TODO.md @@ -10,6 +10,7 @@ ## Rewrite - [ ] fix data api + - [ ] ConfigAPI - [ ] reimplement avatar settings - [ ] fix sound - [ ] fix Blockbench animations @@ -24,9 +25,9 @@ - [ ] fix UVManager with matrices or something - [ ] add swap to skin texture - [ ] split off large snippets of code into separate files - - [ ] PartsManager - - [ ] UVManager - - [ ] utility functions + - [.] PartsManager + - [.] UVManager + - [.] utility functions - [ ] cleanup tick functions, timers - [ ] cleanup armor code to make less redundant diff --git a/nulllib b/nulllib new file mode 160000 index 0000000..c2c1bc3 --- /dev/null +++ b/nulllib @@ -0,0 +1 @@ +Subproject commit c2c1bc372d17cdda06f0971f94c334e011e4de72 diff --git a/script.lua b/script.lua index 174c84f..06878c0 100644 --- a/script.lua +++ b/script.lua @@ -14,317 +14,16 @@ ping=pings TEXTURE_WIDTH = 256 TEXTURE_HEIGHT = 256 -LOG_LEVEL="INFO" +util = require("nulllib.util") +logging = require("nulllib.logging") +timers=require("nulllib.timers") +nmath=require("nulllib.math") +PartsManager=require("nulllib.PartsManager") +UVManager=require("nulllib.UVManager") --- Basic logging --{{{ - -do - logging = {} - local loglevels={ - ["SILENT"]=0, - ["FATAL"]=1, - ["ERROR"]=2, - ["WARN"]=3, - ["INFO"]=4, - ["DEBUG"]=5, - ["TRACE"]=6 - } - - -- default log level - local loglevel="INFO" - - function setLogLevel(level) - loglevel=loglevels[level] and level or loglevel - end - - setLogLevel(LOG_LEVEL) - - local function printLog(severity, message) - if (loglevels[loglevel]) >= severity then - log("[" .. loglevel .. "] " .. message) - end - end - - function logging.fatal(message) printLog(1, message) end - function logging.error(message) printLog(2, message) end - function logging.warn(message) printLog(3, message) end - function logging.info(message) printLog(4, message) end - function logging.debug(message) printLog(5, message) end - function logging.trace(message) printLog(6, message) end -end - --- }}} - --- utility functions -- {{{ - ---- Create a string representation of a table ---- @param o table -function dumpTable(o) - if type(o) == 'table' then - local s = '{ ' - local first_loop=true - for k,v in pairs(o) do - if not first_loop then - s = s .. ', ' - end - first_loop=false - if type(k) ~= 'number' then k = '"'..k..'"' end - s = s .. '['..k..'] = ' .. dumpTable(v) - end - return s .. '} ' - else - return tostring(o) - end -end - -do - local function format_any_value(obj, buffer) - local _type = type(obj) - if _type == "table" then - buffer[#buffer + 1] = '{"' - for key, value in next, obj, nil do - buffer[#buffer + 1] = tostring(key) .. '":' - format_any_value(value, buffer) - buffer[#buffer + 1] = ',"' - end - buffer[#buffer] = '}' -- note the overwrite - elseif _type == "string" then - buffer[#buffer + 1] = '"' .. obj .. '"' - elseif _type == "boolean" or _type == "number" then - buffer[#buffer + 1] = tostring(obj) - else - buffer[#buffer + 1] = '"???' .. _type .. '???"' - end - end - --- Dumps object as UNSAFE json, i stole this from stackoverflow so i could use json.tool to format it so it's easier to read - function dumpJSON(obj) - if obj == nil then return "null" else - local buffer = {} - format_any_value(obj, buffer) - return table.concat(buffer) - end - end -end - - --- TODO: accept model part to determine texture width and height ----@param uv table -function UV(uv) - return vec( - uv[1]/TEXTURE_WIDTH, - uv[2]/TEXTURE_HEIGHT - ) -end - - ----@param inputstr string ----@param sep string -function splitstring (inputstr, sep) - if sep == nil then - sep = "%s" - end - local t={} - for str in string.gmatch(inputstr, "([^"..sep.."]+)") do - table.insert(t, str) - end - return t -end - ----@param input string -function unstring(input) - if input=="nil" then - return nil - elseif input == "true" or input == "false" then - return input=="true" - elseif tonumber(input) ~= nil then - return tonumber(input) - else - return input - end -end - ----@param func function ----@param table table -function map(func, table) - local t={} - for k, v in pairs(table) do - t[k]=func(v) - end - return t -end - - ----@param func function ----@param table table -function filter(func, table) - local t={} - for k, v in pairs(table) do - if func(v) then - t[k]=v - end - end - return t -end - ----@param tbl table ----@param val any -function has_value(tbl, val) - for _, v in pairs(tbl) do - if v==val then return true end - end - return false -end - ---- Unordered reduction, only use when working with dictionaries and ---- execution order does not matter ----@param tbl table Table to reduce ----@param func function Function used to reduce table ----@param init any Initial operand for reduce function -function reduce(func, tbl, init) - local result = init - local first_loop = true - for _, v in pairs(tbl) do - if first_loop and init == nil then - result=v - else - result = func(result, v) - end - first_loop=false - end - return result -end - ---- Ordered reduction, does not work with dictionaries ----@param tbl table Table to reduce ----@param func function Function used to reduce table ----@param init any Initial operand for reduce function -function ireduce(func, tbl, init) - local result = init - local first_loop = true - for _, v in ipairs(tbl) do - if first_loop and init == nil then - result=v - else - result = func(result, v) - end - first_loop=false - end - return result -end - ---- Merge two tables. First table value takes precedence when conflict occurs. ----@param tb1 table ----@param tb2 table -function mergeTable(tb1, tb2) - local t={} - for k, v in pairs(tb1) do - t[k]=v - end - for k, v in pairs(tb2) do - if type(k)=="number" then - table.insert(t, v) - else - if t[k]==nil then - t[k]=v - end - end - end - return t -end - -function debugPrint(var) - print(var) - return var -end - -function debugPrintTable(var) - printTable(var) - return var -end - ---- Recursively walk a model tree and return a table containing the group and each of its sub-groups ---- @param group table The group to recurse ---- @return table Resulting table -function recurseModelGroup(group) - local t={} - table.insert(t, group) - if group:getType()=="GROUP" then - for k, v in pairs(group:getChildren()) do - for _, v2 in pairs(recurseModelGroup(v)) do - table.insert(t, v2) - end - end - end - return t -end --- }}} - --- Timer (not mine lol) -- {{{ --- TODO investigate if events can replace some of this -do - local timers = {} - function wait(ticks,next) - table.insert(timers, {t=world.getTime()+ticks,n=next}) - end - local function tick() - for key,timer in pairs(timers) do - if world.getTime() >= timer.t then - timer.n() - table.remove(timers,key) - end - end - end - - events.TICK:register(function() if player then tick() end end, "timer") -end - --- named timers (this one is mine but heavily based on the other) -- --- if timer is armed twice before expiring it will only be called once) -- -do - local timers = {} - function namedWait(ticks, next, name) - -- main difference, this will overwrite an existing timer with - -- the same name - timers[name]={t=world.getTime()+ticks,n=next} - end - local function tick() - for key, timer in pairs(timers) do - if world.getTime() >= timer.t then - timer.n() - timers[key]=nil - end - end - end - events.TICK:register(function() if player then tick() end end, "named_timer") -end - --- named cooldowns -do - local timers={} - function cooldown(ticks, name) - if timers[name] == nil then - timers[name]={t=world.getTime()+ticks} - return true - end - return false - end - local function tick() - for key, timer in pairs(timers) do - if world.getTime() >= timer.t then - timers[key]=nil - end - end - end - events.TICK:register(function() if player then tick() end end, "cooldown") -end - -function rateLimit(ticks, next, name) - if cooldown(ticks+1, name) then - namedWait(ticks, next, name) - end -end - --- }}} +-- math functions +lerp=math.lerp -- this is implemented in figura now +wave=nmath.wave -- syncState {{{ do @@ -369,15 +68,6 @@ function ping.syncState(tbl) end -- }}} --- Math {{{ ---- Sine function with period and amplitude ---- @param x number Input value ---- @param period number Period of sine wave ---- @param amp number Peak amplitude of sine wave -function wave(x, period, amp) return math.sin((2/period)*math.pi*(x%period))*amp end -function lerp(a, b, t) return a + ((b - a) * t) end --- }}} - -- Master and local state variables -- {{{ -- Local State (these are copied by pings at runtime) -- local_state={} @@ -417,8 +107,8 @@ do end savedData=data.loadAll() end - skin_state=mergeTable( - map(unstring,data.loadAll()), + skin_state=util.mergeTable( + util.map(util.parse,data.loadAll()), defaults) else skin_state=defaults @@ -448,164 +138,6 @@ end -- }}} --- PartsManager -- {{{ -do - PartsManager={} - local pm={} - - --- ensure part is initialized - local function initPart(part) - local part_key=part - if pm[part_key] == nil then - pm[part_key]={} - end - pm[part_key].part=part - if pm[part_key].functions == nil then - pm[part_key].functions = {} - end - if pm[part_key].init==nil then - pm[part_key].init="true" - end - end - --- Add function to part in PartsManager. - --- @param part table Any object with a setEnabled() method. - --- @param func function Function to add to model part's function chain. - --- @param init? boolean Default value for chain. Should only be set once, subsequent uses overwrite the entire chain's initial value. - function PartsManager.addPartFunction(part, func, init) - initPart(part) - local part_key=part - if init ~= nil then - pm[part_key].init=init - end - table.insert(pm[part_key]["functions"], func) - end - - --- Set initial value for chain. - --- @param part table Any object with a setEnabled() method. - --- @param init? boolean Default value for chain. Should only be set once, subsequent uses overwrite the entire chain's initial value. - function PartsManager.setInitialValue(part, init) - assert(init~=nil) - initPart(part) - local part_key=part - pm[part_key].init=init - end - - --- Set initial value for chain on all objects in table. - --- @param group table A table containing objects with a setEnabled() method. - --- @param init? boolean Default value for chain. Should only be set once, subsequent uses overwrite the entire chain's initial value. - function PartsManager.setGroupInitialValue(group, init) - assert(init~=nil) - for _, v in pairs(group) do - PartsManager.setInitialValue(v, init) - end - end - - --- Evaluate a part's chain to determine if it should be visible. - --- @param part table An object managed by PartsManager. - function PartsManager.evaluatePart(part) - local part_key=part - assert(pm[part_key] ~= nil) - - local evalFunc=function(x, y) return y(x) end - local init=pm[part_key].init - return ireduce(evalFunc, pm[part_key].functions, true) - end - local evaluatePart=PartsManager.evaluatePart - - --- Refresh (enable or disable) a part based on the result of it's chain. - --- @param part table An object managed by PartsManager. - function PartsManager.refreshPart(part) - local part_enabled=evaluatePart(part) - part:setVisible(part_enabled) - return part_enabled - end - - --- Refresh all parts managed by PartsManager. - function PartsManager.refreshAll() - for _, v in pairs(pm) do - PartsManager.refreshPart(v.part) - end - end - - --- Add function to list of parts in PartsManager - --- @param group table A table containing objects with a setEnabled() method. - --- @param func function Function to add to each model part's function chain. - --- @param default? boolean Default value for chain. Should only be set once, subsequent uses overwrite the entire chain's initial value. - function PartsManager.addPartGroupFunction(group, func, default) - for _, v in ipairs(group) do - PartsManager.addPartFunction(v, func, default) - end - end -end --- }}} - --- UVManager {{{ --- --- TODO: accept model part for built-in UV management, automatic texture size -do - local mt={} - --- @class UVManager - UVManager = { - step=vec(0,0), - offset=vec(0,0), - positions={}, - part=nil, - dimensions=nil - } - mt.__index=UVManager - --- @return UVManager - --- @param step Vector2 A vector representing the distance between UVs - --- @param offset Vector2 A vector represnting the starting point for UVs, or nil - --- @param positions table A dictionary of names and offset vectors - --- @param part ModelPart Model part to manage - function UVManager.new(self, step, offset, positions, part) - local t={} - if step ~= nil then t.step=step end - if offset ~= nil then t.offset=offset end - if positions ~= nil then t.positions=positions end - - if part ~= nil then - UVManager.setPart(t, part) - end - - t=setmetatable(t, mt) - return t - end - - --- @param part ModelPart Model part to manage - function UVManager.setPart(self, part) - self.part=part - self.dimensions=part:getTextureSize() - end - - function UVManager.getUV(self, input) - local vect={} - local stp=self.step - local offset=self.offset - if type(input) == "string" then - if self.positions[input] == nil then return nil end - vect=self.positions[input] - else - vect=vectors.of(input) - end - local u=offset.x+(vect.x*stp.x) - local v=offset.y+(vect.y*stp.y) - if self.dimensions ~= nil then - -- TODO override for my specific texture, replace this with matrix stuff - -- (get rid of division once you figure out how setUVMatrix works) - return vec(u/(self.dimensions.x/2), v/(self.dimensions.y/2)) - else - return UV{u, v} - end - end - - function UVManager.setUV(self, input) - if self.part == nil then return false end - self.part:setUV(self:getUV(input)) - end -end --- }}} - -- Parts, groups, other constants -- {{{ HEAD=model.Head.Head FACE=model.Head.Face @@ -680,7 +212,7 @@ BODY_EMISSIVES={ FACE_EMISSIVES={ model.Head.Face } -EMISSIVES=mergeTable(BODY_EMISSIVES, FACE_EMISSIVES) +EMISSIVES=util.mergeTable(BODY_EMISSIVES, FACE_EMISSIVES) COLORS={} COLORS.neutral=vec(127/255,127/255,255/255) COLORS.hurt= vec(1, 0, 63/255) @@ -724,11 +256,11 @@ do PM.addPartFunction(vanilla_model.CAPE, function(last) return last and not local_state.tail_enabled end) --- Custom state - -- local tail_parts=mergeTable({model.Body.TailBase}, TAIL_BONES) + -- local tail_parts=util.mergeTable({model.Body.TailBase}, TAIL_BONES) local tail_parts={model.Body.MTail1, model.Body.TailBase} -- TODO: old vanilla_partial groups, use these for texture swap - -- local vanilla_partial_disabled=mergeTable(MAIN_GROUPS, {model.Body.Body, model.Body.BodyLayer}) + -- local vanilla_partial_disabled=util.mergeTable(MAIN_GROUPS, {model.Body.Body, model.Body.BodyLayer}) -- local vanilla_partial_enabled={model.Head, model.Body} -- Show shattered only at low health @@ -743,7 +275,7 @@ do PM.addPartGroupFunction(tail_parts, function(last) return last and aquaticTailVisible() end) --- Armor state - local all_armor=reduce(mergeTable, {VANILLA_GROUPS.ARMOR, TAIL_LEGGINGS, TAIL_BOOTS}) + local all_armor=util.reduce(util.mergeTable, {VANILLA_GROUPS.ARMOR, TAIL_LEGGINGS, TAIL_BOOTS}) PM.addPartGroupFunction(all_armor, function(last) return last and local_state.armor_enabled end) -- Only show armor if equipped PM.addPartFunction(model.Body.MTail1.MTail2.MTail3.Boot, function(last) return last and armor_state.boots end) @@ -804,7 +336,7 @@ do -- This one is for more explicit "flashes" such as player hurt -- animations, get color explicitly setColor(COLORS[expression]) - namedWait(ticks, resetExpression, "resetExpression") + timers.namedWait(ticks, resetExpression, "resetExpression") end function resetExpression() lock_color=false @@ -1113,7 +645,7 @@ function player_init() old_state.health=local_state.health -- TODO possibly reconsider if this should be redone -- actually it's probably fine, it's jsut here because i forget visibility settings - -- local all_parts=recurseModelGroup(model) + -- local all_parts=util.recurseModelGroup(model) -- for k, v in pairs(all_parts) do -- v:setVisible(nil) @@ -1156,7 +688,7 @@ function tick() if world.getTimeOfDay() % 20 == 0 then if player:getPose() == "SLEEPING" then - if cooldown(20*4, "snore") then + if timers.cooldown(20*4, "snore") then snore() end end