Skip to content
bobsayshilol edited this page Oct 18, 2013 · 15 revisions

Tutorial/example

For an example I will show you how to convert one of the files from Double Fine's Spacebase DF-9. The file I have chosen is "Door.lua" which is located in the "Data\Scripts\UI" folder - I chose this file because it has no use in the current game and must have been left over between the AF version and the current release (try removing it - there will be no difference!) and hence I am not posting any source code for either game online.

I will assume you have already done the steps provided on the main page and have the 2 files "test.asm" and "out.lua", and that you know the basics of coding (this is not a coding tutorial).

Now open "out.lua" and look at the mess contained within. Currently the script creates this file on a line-by-line basis and hence it can include things that are meaningless (for example you will probably see a lot of randomNo's. Here are the basics to understanding how to turn this into a nice lua file that will actually run:

First you will need to look at the last function (not intuitive I know) since LuaJIT is encoded backwards - to see this properly look for function calls in any compiled script through something like notepad and you'll see they appear in the opposite direction to your script. This last function is infact the whole script with a wrapper on it, so the first thing to do is remove the wrapper (ie function someFunction() and end). I will now briefly explain how the functions are generated:

Imagine you have code that looks like this:

function a()
   local tester
   function tester.new()
      b()
   end
end

function b()
   doStuff()
end

b()
a()

Then the script would list the functions in this order:

function tester.new()
end

function a()
   randomFunction0 = tester.new
end

function b()
end

function someFunction()
   randomFunction1 = a
   randomFunction2 = b
end

You can see 2 main things from this:

  1. the script itself is considered a function and is always the last function
  2. functions that are in other functions are always defined in the same order as they appear inside their parent function

Another way to think of point 2 is that you can move functions referenced in another function into that function and they keep their order. Now that you have an understanding of how that works you can see why we removed the wrapper on the last function, and now we can start to name the remaining functions. The list of functions from the unwrapped last function should look like this:

local randomFunction7 = function() end -- starts at  test.lua:0
randomTable1.create = randomFunction7
local randomFunction8 = function() end -- starts at  test.lua:0
randomTable1.new = randomFunction8

From this we can deduce that the 2 functions before this are called "randomTable1.create" and "randomTable1.new" respectively, so go back and rename these and then delete these 4 lines. We can also do a bit of tidying up and move the rest of the unwrapped function to the top (except the final line return randomTable1) that so the functions get "placed" where they were defined.

As you may have noticed the output of the script is incredibly verbose (ie it will create a new variable for anything) and we will tidy up the bit we just moved to the top now. You should already have it looking like this:

local randomString20 = "UI.Gui"
local randomOut87 = require(randomString20)
local randomString21 = "DFCommon.Util"
local randomOut88 = require(randomString21)
local randomString22 = "DFCommon.Input"
local randomOut89 = require(randomString22)
local randomString23 = "DFCommon.Math"
local randomOut90 = require(randomString23)
local randomString24 = "UI.UIElement"
local randomOut91 = require(randomString24)
local randomString25 = "Renderer"
local randomOut92 = require(randomString25)
local randomString26 = "ObjectList"
local randomOut93 = require(randomString26)
local randomString27 = "SoundManager"
local randomOut94 = require(randomString27)
local randomTable1 = {}

To simplify this all we need to do is check that each randomString only appears twice (once to be defined and once again to be used). If it appears more than twice then don't delete it, otherwise just move the string that it holds into where it should be. You should end up with this (I have renamed them to what they reference to make it easier to work with later, but you can name them whatever you want since they only appear once):

local Gui = require("UI.Gui")
local Util = require("DFCommon.Util")
local Input = require("DFCommon.Input")
local Math = require("DFCommon.Math")
local UIElement = require("UI.UIElement")
local Renderer = require("Renderer")
local ObjectList = require("ObjectList")
local SoundManager = require("SoundManager")
local randomTable1 = {}

That's the first part done.

Now let's go back down to the last function again (LuaJIT is backwards remember):

function randomTable1.new()
   local randomOut86 = uget_0.create()
   --VARG appeared here - that usually means that the input for this function should be (...) and anywhere you see var0 or var1 or var2 ect. in this function should be replaced by (...)
   randomOut86.init(randomOut86, var3)
   return randomOut86
end

Now you can see a comment about "VARG" in the centre so let's do what it says to get this:

function randomTable1.new(...)
   local randomOut86 = uget_0.create()
   randomOut86.init(randomOut86, ...)
   return randomOut86
end

Now we have something called "uget_0" which isn't referenced anywhere before it appears - these are things that are defined outside of the function they are in (ie the "require"s and the randomTable). There is no definite way to tell what these are without trial and error, but in most cases you can make a good guess from what the "uget" has done to it. In most cases it is best to leave these until you have done everything else, but in this case we know it has the function "create" and the result of that has the function "init" - we will see in a minute that the function "randomTable1.create" has a function in it called "init" - so we can safely assume that "uget_0" is in fact "randomTable1" (which is defined outside of this function) and we can replace all entries of "uget_0" in this function with "randomTable1" to get:

function randomTable1.new(...)
   local randomOut86 = randomTable1.create()
   randomOut86.init(randomOut86, ...)
   return randomOut86
end

One final thing to point out about this function is the third line and how it should be written: function calls of the form someObject.doStuff(someObject) should be written as someObject:doStuff() not just for aesthetic reasons, but because LuaJIT compiles these to different bytecodes for some reason. This extends to any number of arguments (ie a.b(a,c,d) becomes a:b(c,d)) so we can write that function as:

function randomTable1.new(...)
   local randomOut86 = randomTable1.create()
   randomOut86:init(...)
   return randomOut86
end

Now let's move onto the next function (I swear this gets easier!).

We can see that most of this function is references to other functions, so we can drag the functions it mentions into this function and rename them appropriately. This leaves us with 3 lines of code (I told you it became easier):

local randomOut84 = uget_1.create() -- replace all occurrences of randomOut84 with this function and remove this line
local randomOut85 = uget_0.createSubclass(randomOut84)
-- lots of functions go here (called randomOut85.something)
return randomOut85

Already we have been told to replace "randomOut84" so let's do that:

local randomOut85 = uget_0.createSubclass(uget_1.create())
-- lots of functions go here (called randomOut85.something)
return randomOut85

We can't easily guess the "uget"s at the moment so we will come back to these later.

Now we move onto the next function.

function randomOut85.refresh()
   var0.updateSelected(var0)
   return
end

In this case a new thing has appeared with being defined again called "var0". These are input variables to the function. In this case it looks like only 1 variable is sent when "randomOut85.refresh" is called so we get:

function randomOut85.refresh(var0)
   var0.updateSelected(var0)
   return
end

We can also simplify this as stated before to get:

function randomOut85.refresh(var0)
   var0:updateSelected()
   return
end

See? Much easier.

Now onto the next function.

function randomOut85.cycleDoor()
   local randomOut67, randomOut68 = uget_0.getObjType(var0.currentDoor)
   if var0.currentDoor then
   for randomOut69, randomOut70 in (randomOut67s calling function) do --if (randomOut67s calling function) isn't on the line before then remove this line
   --jump to 0042 (if previous if statement is false)
   if randomOut68 ==  "Airlock" then
   for randomOut71, randomOut72 in (var0s calling function) do --if (var0s calling function) isn't on the line before then remove this line
   --jump to 0016 (if previous if statement is false)
   local randomOut73 = var0.currentDoor.getScriptController(var0.currentDoor)
   if not randomOut73 then
   for randomOut74, randomOut75 in (var0s calling function) do --if (var0s calling function) isn't on the line before then remove this line
   --jump to 0042 (if previous if statement is false)
   --location 0016
   --location 0019
   var0.currentDoor.cycle(var0.currentDoor)
   if var0.currentDoor.operation == 0 then
   for randomOut76, randomOut77 in (var0s calling function) do --if (var0s calling function) isn't on the line before then remove this line
   --jump to 0029 (if previous if statement is false)
   local randomString17 = "doorforcedopen"
   uget_1.playSfx(randomString17)
   for randomOut78, randomOut79 in (var0s calling function) do --if (var0s calling function) isn't on the line before then remove this line
   --jump to 0042 (if previous if statement is false)
   --location 0029
   if var0.currentDoor.operation == 1 then
   for randomOut80, randomOut81 in (var0s calling function) do --if (var0s calling function) isn't on the line before then remove this line
   --jump to 0038 (if previous if statement is false)
   --location 0033
   local randomString18 = "doorlocked"
   uget_1.playSfx(randomString18)
   for randomOut82, randomOut83 in (var0s calling function) do --if (var0s calling function) isn't on the line before then remove this line
   --jump to 0042 (if previous if statement is false)
   --location 0038
   local randomString19 = "doornormal"
   uget_1.playSfx(randomString19)
   --location 0042
   var0.refresh(var0)
   return
end

Now this looks painful, but if we follow the instructions we can simplify this quite a bit to get

function randomOut85.cycleDoor(var0)
   local randomOut67, randomOut68 = uget_0.getObjType(var0.currentDoor)
   if var0.currentDoor then
   --jump to 0042 (if previous if statement is false)
   if randomOut68 ==  "Airlock" then
   --jump to 0016 (if previous if statement is false)
   if not var0.currentDoor:getScriptController() then
   --jump to 0042 (if previous if statement is false)
   --location 0016
   --location 0019
   var0.currentDoor:cycle()
   if var0.currentDoor.operation == 0 then
   --jump to 0029 (if previous if statement is false)
   uget_1.playSfx("doorforcedopen")
   --jump to 0042 (if previous if statement is false)
   --location 0029
   if var0.currentDoor.operation == 1 then
   --jump to 0038 (if previous if statement is false)
   --location 0033
   uget_1.playSfx("doorlocked")
   --jump to 0042 (if previous if statement is false)
   --location 0038
   uget_1.playSfx("doornormal")
   --location 0042
   var0:refresh()
   return
end

We now have to deal with if statements (I'll admit they're not so easy). To do this we look at where we are being told to jump to.

Here are some examples (with the locations being examples):

Standard if:

if statement then
   doStuff()
end

would decompile to:

if statement then
--jump to 0010 (if previous if statement is false)
doStuff()
--location 0010

else:

if statement then
   a()
else
   b()
end

would decompile to:

if statement then
--jump to 0010 (if previous if statement is false)
a()
--jump to 0020 (if previous if statement is false)
--location 0010
b()
--location 0020

elseif:

if statement then
   a()
elseif maybe then
   b()
end

would decompile to:

if statement then
--jump to 0010 (if previous if statement is false)
a()
--jump to 0020 (if previous if statement is false)
--location 0010
if maybe then
--jump to 0020 (if previous if statement is false)
b()
--location 0020

All if statements are made up of a combination of these, it's just looking for the pattern that matches that can be difficult.

Back to our function and you can see that this fits the bill:

function randomOut85.cycleDoor(var0)
   local randomOut67, randomOut68 = uget_0.getObjType(var0.currentDoor)
   if var0.currentDoor then
      --jump to 0042 (if previous if statement is false)
      if randomOut68 ==  "Airlock" then
         --jump to 0016 (if previous if statement is false)
         if not var0.currentDoor:getScriptController() then
         end
         --jump to 0042 (if previous if statement is false)
      else
         --location 0016
         var0.currentDoor:cycle()
         if var0.currentDoor.operation == 0 then
            --jump to 0029 (if previous if statement is false)
            uget_1.playSfx("doorforcedopen")
            --jump to 0042 (if previous if statement is false)
            --location 0029
         elseif var0.currentDoor.operation == 1 then
            --jump to 0038 (if previous if statement is false)
            --location 0033
            uget_1.playSfx("doorlocked")
            --jump to 0042 (if previous if statement is false)
         else
            --location 0038
            uget_1.playSfx("doornormal")
         end
      end
   end
   --location 0042
   var0:refresh()
   return
end

You can tell from this function that this script is outdated since the block

if not var0.currentDoor:getScriptController() then
end

has nothing in it.

This should be enough information to start decompiling LuaJIT scripts.

If you ever find something similar to CODE unhandled at 0000 then look for the relevant bytecode on the LuaJIT wiki and see what the parameters are in the readable bytecode.

To view readable bytecode run the asm batch file (don't run the decoder again or it will overwrite "out.lua" and remove your current progress - best to move everything but the decoder to another location and run the batches there instead), then compare the contents of "out.asm" (your code) to "test.asm" (the actual code) - you have to edit "out.lua" to change the contents of "out.asm" not the other way around!

To view the contents of tables you must use the obj batch file - good luck!

Clone this wiki locally