UnofficialCrusaderPatch / UnofficialCrusaderPatch3

Development for the dll Injection approach
GNU General Public License v3.0
11 stars 2 forks source link

[CORE/CODE] Create system that tracks original code, claims, and prevents influencing code #125

Open gynt opened 3 days ago

gynt commented 3 days ago

The modular system of UCP3 comes with the potential issue that code from different modules affects each other in an unintended way. This lua code is meant to address it.

There are three components:

The framework has a couple of ramifications:


-- Since code memory is global, and the same bytes should only have a change applied to them once...
-- Keep track of an index of code memory that checks for overlaps but also stores the original bytes.
-- If a code modification involves multiple segments/slices, multiple changes should be applied.
-- If the modification kills a branch of code (e.g. zaps an if-else statement), and no other code uses that branch of code, 
-- then it should be nopped
local GameCode = {

    --[[

        Original code storage

    --]]

    -- Gets size bytes from database from address
    get = function(address, size)
        local data = {}

        for i=0,(size-1),1 do
            table.insert(data, self.bytes[address+i])
        end

        return data
    end,

    -- Sets bytes into database at address
    set = function(address, bytes)
        for i=0,(size-1),1 do
            -- lua is 1-based
            if self.bytes[address + i] == nil then
                self.bytes[address + i] = bytes[i+1]
            end
        end
    end,

    -- Stores the original bytes
    store = function(self, address, size)
        if self.bytes[address] ~= nil then return end

        local bytes = readBytes(address, size)
        self:set(address, bytes)
    end,

    --[[

        Claim management

    --]]
    claimer = function(self, address, size)
        for i=0,(size-1),1 do 
            local claimer = self.claims[address+i]
            if claimer ~= nil then 
                return claimer
            end
        end

        return nil
    end,    

    -- Checks if the range is not claimed
    available = function(self, address, size)
        return self:claimer(address, size) == nil
    end,

    -- Claim 
    claim = function(self, address, size, claimer)
        for i=0,(size-1),1 do
            self.claims[address+i] = claimer
        end
    end,

    unclaim = function(self, address, size, claimer)
        for i=0,(size-1),1 do
            local offset = address+i
            local c = self.claims[offset]

            if c == nil then
                error(string.format("Error: unclaim failed: %X was never claimed", offset))
            end

            if c ~= claimer then
                error(string.format("Error: unclaim failed: %X was claimed by %s, not by %s", offset, tostring(c), tostring(claimer)))
            end

            self.claims[offset] = nil
        end
    end,

    --[[

        Interface

    --]]

    -- This is what the user only needs to interface with (hide the rest?)
    applyChange = function(self, address, bytes, claimer)
        local size = #bytes

        local holder = self:claimer(address, size)
        if holder ~= nil then
            error(string.format("Error: apply_change failed: claim by %s failed: [%X-%X] is already claimed by %s", tostring(claimer), address, address + size, tostring(holder)))
        end

        self:claim(address, size, claimer)

        self:store(address, size)
        writeCode(address, bytes)

        return self
    end,

    unapplyChange = function(self, address, size, claimer)
        local size = #bytes

        local holder = self:claimer(address, size)
        if holder ~= claimer then
            error(string.format("Error: unapply_change failed: claim by %s failed: [%X-%X] is not claimed by %s", tostring(claimer), address, address + size, tostring(holder)))
        end

        self:unclaim(address, size, claimer)

        -- get original bytes
        local o = self:get(address, size)
        writeCode(address, o)        

        return self
    end,

    allocate = function(self, size, claimer)
        local address = core.allocateCode(size, true) -- make sure it is all zeroes? or 0xCC?

        local holder = self:claimer(address, size)
        if holder ~= nil then
            error(string.format("Error: allocate failed: claim by %s failed: [%X-%X] is already claimed by %s", tostring(claimer), address, address + size, tostring(holder)))
        end

        self:claim(address, size, claimer)

        return address
    end,

    deallocate = function(self, address, size, claimer)
        local holder = self:claimer(address, size)
        if holder ~= claimer then
            error(string.format("Error: deallocate failed: claim by %s failed: [%X-%X] is not claimed by %s", tostring(claimer), address, address + size, tostring(holder)))
        end

        self:unclaim(address, size, claimer)
        core.deallocateCode(address)

    end,

    -- Alternative for this?
    writeCode = function(self, address, bytes, claimer)
        local size = #bytes
        local holder = self:claimer(address, size)
        if holder ~= claimer then
            error(string.format("Error: writeCode failed: claim by %s failed: [%X-%X] is not claimed by %s", tostring(claimer), address, address + size, tostring(holder)))
        end

        core.writeCode(address, bytes)
    end,

}
TheRedDaemon commented 3 days ago

So is this meant as a new code modification interface?
I like the idea in general, even if it brings a bit of overhead.
I can see an issue with C++ modules, but I think at least I actually never bound logic code via C++. I certainly modify data in it, though.

gynt commented 3 days ago

I hope overhead is minimal as code changes are only done at runtime.

C++ modules are not supposed to modify code by writing or overriding the game's machine code. The Lua side of things is responsible for that.

Modifying data is absolutely fine of course and we don't need to track that