1. Please be advised of a few specific rules and guidelines for this section.

RELEASED Lua Hooks 0.2

Run code inside vanilla starbound files without modifying them

  1. olsonpm

    olsonpm Void-Bound Voyager

    olsonpm submitted a new mod:

    Lua Hooks - Run code inside vanilla starbound files without modifying them

    Read more about this mod...
     
  2. bk3k

    bk3k Oxygen Tank

    This is an interesting way to go about it, but I think the current implementation is problematic. The problem being you are replacing /stats/player_primary.lua and that's going to break lots of other mods which handle this another way - unless your mod loads first. That means you break FU(several hooks), some of my mods, and a good number of other mods. Adding priority in your metadata can mitigate this
    Code:
    "priority" : -9998,
    As the base game assets are -9999 so your mod would load right after. The default priority is 0.

    As you say, you really cannot patch LUA but you can use patching with most things in Starbound to do something that's close enough. In the case of the player's _ENV you make a /player.config.patch file
    Code:
    [
      [
        {
          "op" : "test",
          "path" : "/modEnvironment",
          "inverse" : true
        },
        {
          "op" : "add",
          "path" : "/modEnvironment",
          "value" : {}
        }
      ],
    
      [
        {
          "op" : "test",
          "path" : "/modEnvironment/on_shipDamage",
          "inverse" : true
        },
        {
          "op" : "add",
          "path" : "/modEnvironment/on_shipDamage",
          "value" : true
        },
        {
          "op" : "add",
          "path" : "/statusControllerSettings/primaryScriptSources/-",
          "value" : "/scripts/bk3k/bk3k_shipDamage.lua"
        }
      ]
    ]
    
    Now some of that is extra. Really just the last patch is necessary. The rest just adds an easy way to detect this mod via root.assetJson("/player.config")

    Anyhow this adds another script to the end of the player's primary script source array/list. For some other things I add them to a companion script array/list. These scripts will load after the previous scripts in the list - functionally as though you used requires at the end of the last script.

    Here's /scripts/bk3k/bk3k_shipDamage.lua
    Code:
    local bk3k_shipDamage_init = init
    bk3k_shipDamage_applyDamageRequest = applyDamageRequest
    
    --Apparently much of the environment I expect (such as root) doesn't exist prior to object initialization
    --therefore I needed to change my approach for multiversion compatibility
    
    function init(...)
    
      if root.elementalResistance then
        if mcontroller.atWorldLimit then
          require("/scripts/bk3k/shipDamageVersions/bk3k_shipDamage(1.3.0).lua")
        else
          require("/scripts/bk3k/shipDamageVersions/bk3k_shipDamage(1.2.0).lua")
        end
      else
        require("/scripts/bk3k/shipDamageVersions/bk3k_shipDamage(1.1.1).lua")
      end
    
      bk3k_shipDamage_init(...)
    end
    Now some of that code isn't necessary anymore since we aren't in between versions and nightly builds are no longer "a thing." But it doesn't hurt anything.
    And here's /scripts/bk3k/shipDamageVersions/bk3k_shipDamage(1.3.0).lua
    Code:
    applyDamageRequest = function(damageRequest)
    
      if world.getProperty("ship.fuel") then
        --blank but will run the rest of the function
      else
        return bk3k_shipDamage_applyDamageRequest(damageRequest)
      end
    
      status.addEphemeralEffects(damageRequest.statusEffects, damageRequest.sourceEntityId)
    
      if damageRequest.damageSourceKind == "applystatus" or damageRequest.damageSourceKind ~= "falling" and (self.hitInvulnerabilityTime > 0) then
        return {}
      end
    
      local damage = 0
      if damageRequest.damageType == "Damage" or damageRequest.damageType == "Knockback" then
        damage = damage + root.evalFunction2("protection", damageRequest.damage, status.stat("protection"))
      elseif damageRequest.damageType == "IgnoresDef" or damageRequest.damageType == "Environment" then
        damage = damage + damageRequest.damage
      end
    
      if status.resourcePositive("damageAbsorption") then
        local damageAbsorb = math.min(damage, status.resource("damageAbsorption"))
        status.modifyResource("damageAbsorption", -damageAbsorb)
        damage = damage - damageAbsorb
      end
    
      if damageRequest.hitType == "ShieldHit" then
        if self.shieldHitInvulnerabilityTime == 0 then
          local preShieldDamageHealthPercentage = damage / status.resourceMax("health")
          self.shieldHitInvulnerabilityTime = status.statusProperty("shieldHitInvulnerabilityTime") * math.min(preShieldDamageHealthPercentage, 1.0)
    
          if not status.resourcePositive("perfectBlock") then
            status.modifyResource("shieldStamina", -damage / status.stat("shieldHealth"))
          end
        end
    
        status.setResourcePercentage("shieldStaminaRegenBlock", 1.0)
        damage = 0
        damageRequest.statusEffects = {}
        damageRequest.damageSourceKind = "shield"
      end
    
      local elementalStat = root.elementalResistance(damageRequest.damageSourceKind)
      local resistance = status.stat(elementalStat)
      damage = damage - (resistance * damage)
    
      local healthLost = math.min(damage, status.resource("health"))
      if healthLost > 0 and damageRequest.damageType ~= "Knockback" then
        status.modifyResource("health", -healthLost)
        animator.playSound("ouch")
    
        local damageHealthPercentage = damage / status.resourceMax("health")
        if damageHealthPercentage > status.statusProperty("hitInvulnerabilityThreshold") then
          self.hitInvulnerabilityTime = status.statusProperty("hitInvulnerabilityTime")
        end
      end
    
    
      local knockbackFactor = (1 - status.stat("grit"))
    
      local knockbackMomentum = vec2.mul(damageRequest.knockbackMomentum, knockbackFactor)
      local knockback = vec2.mag(knockbackMomentum)
      if knockback > status.stat("knockbackThreshold") then
        mcontroller.setVelocity({0,0})
        local dir = knockbackMomentum[1] > 0 and 1 or -1
        mcontroller.addMomentum({dir * knockback / 1.41, knockback / 1.41})
      end
    
      local hitType = damageRequest.hitType
      if not status.resourcePositive("health") then
        hitType = "kill"
      end
      return {{
        sourceEntityId = damageRequest.sourceEntityId,
        targetEntityId = entity.id(),
        position = mcontroller.position(),
        damageDealt = damage,
        healthLost = healthLost,
        hitType = hitType,
        damageSourceKind = damageRequest.damageSourceKind,
        targetMaterialKind = status.statusProperty("targetMaterialKind")
      }}
    end
    
    And in case you're curious, I've hooked the script that gets called when we should take damage. The original checks if you're on a ship and returns if so. My copy removes this so you do take damage on ships. But if you are NOT on a ship, then I call the original function (which I backed up). Only it might not be the original - it could be another hook such as from a mod reducing fall damage. I don't know, and I don't need to know. If that isn't clear, at least the first script should be. whatever was saved under the init namespace is preserved and called.

    Wait I probably don't need to explain that much since you get hooking already. Better to over-explain than under though. No insult intended. The main point is you can just add another script to the player scripts via patching instead of replacing the file.

    You can do the same thing for objects by patching the object and adding to the end of "scripts". The only thing - and I hate this - certain things in Starbound don't use an array but just point to a single script. Quests for example.

    I see you have something for quests which I haven't checked out quite yet(but for now presume is similar). Tragically you really do need to replace the script in this case. You could point it to an entirely other script, but that's the same thing in the end. In those cases alone your method is probably the best way forward.

    Although I would recommend an alteration. Rather than making your own file(/luahooks/initscripts.json), the solution becomes a bit more modular to add data into an existing vanilla JSON file by the same method you see me patching above.
    Code:
    [
      [
        {
          "op" : "test",
          "path" : "/quest_initScripts",
          "inverse" : true
        },
        {
          "op" : "add",
          "path" : "/quest_initScripts",
          "value" : []
        }
      ],
    
      [
        {
          "op" : "add",
          "path" : "/quest_initScripts/-",
          "value" : "/scripts/bk3k/questThingy.lua"
        }
      ]
    ]
    
    The patch tests for "initscripts" and creates it if not present. The next batch adds a script to the end of "initscripts". The advantage here is another mod may or may not have already added "initscripts" and their own scripts. Thanks to the test condition and the separate batches, you don't really have to care because it works either way. Now what file to use? Ideally something small for performance reasons - "/emotes.config" should be a good target.

    So edit /luahooks/hooks.lua lines 43-44 to this -
    Code:
          local config = root.assetJson("/emotes.config")
          for _, aScript in ipairs(config.quest_initScripts) do
    
    and you're good to go still. Alternatively edit lines 43 - 44 to this
    Code:
          local config = root.assetJson("/emotes.config:quest_initScripts")
          for _, aScript in ipairs(config) do
    
    That probably runs a tiny bit faster.

    And maybe that's not quite right for quests since you might want a different one per quest. But that's the idea. You just build more JSON objects within "quest_initScripts" that are per quests, or per edited file. Whatever the system is. Guess I should check out your quest mod. Or more like I should go to bed since I'm supposed to wake up in an hour.
     
  3. olsonpm

    olsonpm Void-Bound Voyager

    Hey thanks so much for all your feedback and especially the code examples. What you say makes a lot of sense and I'll try to make some of your suggested changes in the coming week.


    About the alternative method to hooks which you proposed, I did notice that in one of the mods when I was browsing - I didn't like how it only worked for scripts which were appendable via configured arrays wereas my approach works for all vanilla files (as you noted in your comment). Now I didn't realize to the extent which contexts were able to be appended, e.g. it didn't occur to me that I could override object scripts. I think the best course will be for me to do what you mention - follow your approach where it works and mine where it doesn't.


    Don't worry about over-explaining, I know what you mean how it can come across as condescending because I have a bad habit of doing that at my work, but I
    1. don't perceive it that way
    2. am new to lua and starbound modding and can use all the explanation I can get haha.


    By the way, thanks for the comment
    That's how I assumed it worked but wasn't sure


    I lol'd at
    Code:
    -- Apparently much of the environment I expect (such as root) doesn't exist
    
    because I had the same issue with my lua script initialization.


    Regarding your comment about modifying an existing json file rather than adding my own
    My approach was file namespacing e.g. /<mod name>/<mod files here>. You're right I'm taking a slight risk that nobody will overwrite my files, but I'm okay with that. Placing my initscripts array anywhere else seemed dirty/weird, even if functionally it's more compatible. Should it cause problems I'll patch a vanilla file like you suggest.


    Thanks for the note about the file directive
    Code:
    local config = root.assetJson("/emotes.config:quest_initScripts")
    for _, aScript in ipairs(config) do
    
    I completely forgot about it. I wish the api they exposed was a little more friendly e.g.
    Code:
    local config = root.assetJson(
      "/emotes.config",
      { path = "/quest_initScripts" }
    )
    
    I could write my own wrapper but that doesn't help with all the directives used throughout the json files. Those will forever be cryptic.
     
    Last edited: Jan 17, 2018
  4. olsonpm

    olsonpm Void-Bound Voyager

    Wow so I'm mostly a javascript programmer and `this` is one of the most bug-prone constructs in that language. So I avoided `self` in lua so I wouldn't have to learn the details of how it works, however the starbound devs made it more confusing by creating a `self` table which is completely different from the `self` lua construct. It's like overriding a keyword :(

    /rant
     
  5. olsonpm

    olsonpm Void-Bound Voyager

Share This Page