Open TAEMBO opened 2 weeks ago
An importent information is that the l64 files differ from each other starting at the third byte, compared to the old files where the first 16 bytes always are the same.
@VidhosticeSDK Would your algorithm also work if the bytes now change from row to row? Since in fs22 the files all had the same first 16 bytes.
If someone has a solution, it would be nice if he would share it :). How did you open the .gar file?
FS25 does not use LuaJIT anymore, they now use Luau. The first 2 bytes of the .l64 is the version (0x02 0xEF = luau bytecode version 3 when interpreted by the game .exe). So the old lookup tables won't work at all for these :-) Other than that I don't have much information on it.
0x02, 0x13, 0x0a, 0x08, 0x01, 0x07, 0x02, 0x02
Hi, I found a way to decode the .l64 files. https://github.com/Rockstar94FS/Farming-Simulator-L64-Decoder
Unfortunately, they are still unreadable. Does anyone know of any tool to decompile them?
@Rockstar94FS What does this attempt decode them to? LuaJIT or Luau?
@TAEMBO Luau bytecode
That script will only decode FS19/FS22 .l64 files, not FS25
It works with FS25, the algorithm is similar but different from that of FS19/FS22.
Yeah my bad, it actually worked :-) I didn't notice the change of the formula, just that it used the same lookup table from previous versions. The lookup table I posted here works with the old formula
@CoderKane https://github.com/xgladius/luauDec seems to be the most prevalent Luau decompiler out there based on my research that isn't pay-walled, however I'm unable to comprehend how to fully build it w/ make
or with VS, have only gotten past the cmake
part.
If anyone has any insight into that or knows a better solution, do let me know. I'm looking for something I can ship as a binary as opposed to a GUI or online web kind.
luauDec only supports bytecode version 2 and unfortunately seems to fail randomly every time it's executed.
It seems that the bytecode format in FS25 is a bit modified from the "standard" format. I can deserialize v4 bytecode generated by luau compiler without any problems, but not these files.
Edit: Seemingly the bytecode version they use is 3
@scfmod How can you be sure FS25 uses luau format ? What tip did you use to find this out? Any magic numbers somewhere ?
@scfmod How can you be sure FS25 uses luau format ? What tip did you use to find this out? Any magic numbers somewhere ?
You can check this in game using print(_G._VERSION) function
@scfmod How can you be sure FS25 uses luau format ? What tip did you use to find this out? Any magic numbers somewhere ?
Found it when inspecting the executable. Luau files doesn't have a magic number like LuaJIT. It starts with version byte where as LuaJIT has a 3 byte magic number 0x1B 0x4C 0x4A.
As I understand it, there is no working solution to get scripts?
I was able to get the l64 to readable hex using this, but the "lua jit deompiler" can't finish the job. I prepended the luajit header to try and fool it to work.
This is no longer LuaJIT, the old method will not work, valid files start with 0x01 0x03, and such files work normally in the game, so they are certainly valid.
At the moment there is a half-working tool to get readable scripts https://github.com/atrexus/unluau/tree/v2.0.0-beta unfortunately we will not get a ready solution from it, only instructions in this form:
function <Player.lua:63> (19 instructions, 96 bytes)
1 params, 8 slots, 0 upvalues, 16 constants
function registerXMLPaths(v1) -- line 63 through 72
1 GETIMPORT 3 2
2 2147484672
3 LOADK 4 3
4 LOADK 5 4
5 LOADNIL
6 LOADB 7 1 0
7 NAMECALL 1 0 163
8 5
9 CALL 1 7 1
10 GETIMPORT 1 8
11 2153782272
12 MOVE 2 0
13 LOADK 3 9
14 CALL 1 3 1
15 GETIMPORT 1 11
16 2157976576
17 MOVE 2 0
18 CALL 1 2 1
19 GETIMPORT 1 14
20 2160079872
21 MOVE 2 0
22 LOADK 3 15
23 CALL 1 3 1
24 RETURN 0 1
constants (16)
index type value
0 string "XMLValueType"
1 string "STRING"
2 import ["XMLValueType","STRING"]
3 string "player.filename"
4 string "The file path of the player\'s i3d file"
5 string "register"
6 string "HumanModel"
7 string "registerXMLPaths"
8 import ["HumanModel","registerXMLPaths"]
9 string "player"
10 string "PlayerStyle"
11 import ["PlayerStyle","registerXMLPaths"]
12 string "IKUtil"
13 string "registerIKChainXMLPaths"
14 import ["IKUtil","registerIKChainXMLPaths"]
15 string "player.ikChains.ikChain(?)"
locals (1)
index name startpc endpc type
0 xmlSchema 0 24
end
You can try to create something readable from it, but there is currently no tool to do it for over 1700 files
function Player.registerXMLPaths(xmlSchema)
xmlSchema:register(XMLValueType.STRING, "player.filename", "The file path of the player's i3d file")
HumanModel:registerXMLPaths(xmlSchema, "player")
PlayerStyle:registerXMLPaths(xmlSchema)
IKUtil:registerIKChainXMLPaths(xmlSchema, "player.ikChains.ikChain(?)")
end
I already understood that this is not luajit, is it really possible to write code to return the original form of lua after l64decode.py, knowing the instructions?
@Rockstar94FS Do you use this tool after your l64Decoder.py
?
Because i got this error : Unluau.DecompilerException: Bytecode version mismatch, expected version 3...6, got 1
when I use Unlau on the output files from your script.
And if manually change the version (first byte), between 0x03 and 0x06, the decompilation can't finished due to various exceptions
@Rockstar94FS Do you use this tool after your
l64Decoder.py
? Because i got this error :Unluau.DecompilerException: Bytecode version mismatch, expected version 3...6, got 1
when I use Unlau on the output files from your script. And if manually change the version (first byte), between 0x03 and 0x06, the decompilation can't finished due to various exceptions
You need to remove the first byte 0x01 from the file, I haven't verified this but it is very possible that it is used by the game to check what form the file is in, encrypted (0x02), decrypted (0x01) or normal.
@Rockstar94FS Do you use this tool after your
l64Decoder.py
? Because i got this error :Unluau.DecompilerException: Bytecode version mismatch, expected version 3...6, got 1
when I use Unlau on the output files from your script. And if manually change the version (first byte), between 0x03 and 0x06, the decompilation can't finished due to various exceptionsYou need to remove the first byte 0x01 from the file, I haven't verified this but it is very possible that it is used by the game to check what form the file is in, encrypted (0x02), decrypted (0x01) or normal.
Thanks for the tips π
You said on your previous message that we only get instructions. I tested with some files and i get readable lua code (eg: debug/MemoryLeaks.lua
or A_staticAnalyzer.lua
) but with main.lua
, decompilation failed with error Unluau.DecompilerException: unhandled operation code (91)
, same with Player.lua
with error : Unexpected error; please report here: https://github.com/atrexus/unluau/issues System.NullReferenceException: Object reference not set to an instance of an object.
But I have the impression that you were able to open it.
It seems strange that Giants use different format for compiling to bytecode
@ThibBer This is a problem with the tool itself, it is in very early alpha version and cannot convert many files correctly. I tried it on my own files compiled with luau-compiler and got similar errors. Version 2.0 works much better, but does not generate ready script.
I have tried main
& v2.0.0-beta
branch but it's seems to be the same result with Player.lua
, NullPointerException
. It would be interesting to explore this idea further, as I'm sure it's not difficult to convert the instructions into readable code.
This code from the example above was compiled by some lua according to the instructions, as I understand it, if you study all the instructions you can translate them en masse, but my brain is not enough for this, bad
def parse_instructions_to_lua(instructions, constants, locals_info): """ Converts raw bytecode instructions into a Lua-like code structure.
Parameters:
- instructions: List of raw bytecode instructions.
- constants: Dictionary of constants used in the bytecode.
- locals_info: Information about local variables (e.g., names, start/end scope).
Returns:
- A list of strings representing Lua-like code.
"""
lua_code = ["function registerXMLPaths(xmlSchema)"] # Start of the Lua function
# Add comments for local variables if provided
for local in locals_info:
lua_code.append(f" local {local['name']} = nil -- local variable")
# Parse each instruction and convert it to Lua syntax
for instr in instructions:
parts = instr.split()
opcode = parts[0]
# Map bytecode opcodes to Lua code
if opcode == "GETIMPORT":
_, reg, const_idx = parts
const_value = constants[int(const_idx)]
lua_code.append(f" local var{reg} = {const_value}")
elif opcode == "LOADK":
_, reg, const_idx = parts
const_value = constants[int(const_idx)]
lua_code.append(f" local var{reg} = '{const_value}'")
elif opcode == "LOADNIL":
_, reg = parts
lua_code.append(f" local var{reg} = nil")
elif opcode == "MOVE":
_, dest, src = parts
lua_code.append(f" var{dest} = var{src}")
elif opcode == "CALL":
_, func_reg, *args = parts
args_list = ", ".join([f"var{arg}" for arg in args])
lua_code.append(f" var{func_reg}({args_list})")
elif opcode == "RETURN":
lua_code.append(" return")
lua_code.append("end") # Close the function
return lua_code
def transform_to_readable_lua(): """ Converts parsed Lua code into a final readable script.
This function takes the logic of the bytecode instructions and maps it to
meaningful Lua constructs.
Returns:
- A string containing the complete Lua script.
"""
lua_code = [
"function registerXMLPaths(xmlSchema)",
" -- Import values",
" local XMLValueType_STRING = XMLValueType.STRING",
" xmlSchema:register(XMLValueType_STRING, \"player.filename\", \"The file path of the player's i3d file\")",
"",
" -- Register PlayerStyle",
" xmlSchema:register(PlayerStyle.registerXMLPaths, \"player\")",
"",
" -- Register IKChains",
" xmlSchema:register(IKUtil.registerIKChainXMLPaths, \"player.ikChains.ikChain(?)\")",
"end"
]
return "\n".join(lua_code)
def main(): """ Main function that handles parsing and transformation of bytecode instructions. It demonstrates the entire process, from raw instructions to a readable Lua script. """
instructions = [
"GETIMPORT 3 2",
"LOADK 4 3",
"LOADK 5 4",
"LOADNIL 6",
"LOADB 7 1",
"NAMECALL 1 0 163",
"CALL 1 7 1",
"MOVE 2 0",
"LOADK 3 9",
"CALL 1 3 1",
"RETURN 0 1"
]
# Constants that map indices to strings or values in the bytecode
constants = {
0: "XMLValueType",
1: "STRING",
2: ["XMLValueType", "STRING"],
3: "player.filename",
4: "The file path of the player's i3d file",
5: "register",
6: "HumanModel",
7: "registerXMLPaths",
8: ["HumanModel", "registerXMLPaths"],
9: "player",
10: "PlayerStyle",
11: ["PlayerStyle", "registerXMLPaths"],
12: "IKUtil",
13: "registerIKChainXMLPaths",
14: ["IKUtil", "registerIKChainXMLPaths"],
15: "player.ikChains.ikChain(?)",
}
# Local variable definitions (if available)
locals_info = [
{"name": "xmlSchema", "startpc": 0, "endpc": 24, "type": "local"}
]
# Generate Lua-like code from bytecode
lua_code = parse_instructions_to_lua(instructions, constants, locals_info)
# Transform into the final readable Lua script
readable_lua_code = transform_to_readable_lua()
# Save the result to a file
output_file = "decompiled_script.lua"
with open(output_file, "w", encoding="utf-8") as f:
f.write(readable_lua_code)
print(f"Lua script successfully generated in file: {output_file}")
if name == "main": main()
I have tried
main
&v2.0.0-beta
branch but it's seems to be the same result withPlayer.lua
,NullPointerException
. It would be interesting to explore this idea further, as I'm sure it's not difficult to convert the instructions into readable code.
If you decode the file correctly, the 2.0.0-beta branch will disassemble the Player.l64 file without any issues. Shift all the bytes in the file, remove the first byte and then save it. Voila.
@Rockstar94FS Do you use this tool after your
l64Decoder.py
? Because i got this error :Unluau.DecompilerException: Bytecode version mismatch, expected version 3...6, got 1
when I use Unlau on the output files from your script. And if manually change the version (first byte), between 0x03 and 0x06, the decompilation can't finished due to various exceptionsYou need to remove the first byte 0x01 from the file, I haven't verified this but it is very possible that it is used by the game to check what form the file is in, encrypted (0x02), decrypted (0x01) or normal.
You remove the first byte because the interpreter in the game .exe reads both bytes to determine the bytecode version. That's basically it :-) Where as the "standard" Luau only uses the first byte.
I'm not entirely sure. If we change the second byte from 0xEF
to eg. 0xFF
we get a message about incompatible bytecode '256'. The second byte is swapped by the game from 0xEF
to 0x03
if it detects that the first byte is 0x02
. So if it is swapped, the bytocede version matches, meaning it is 0x03
.
Now the most important thing is how to present bytecode instructions in a readable form, what each byte is responsible for is less important.
Yeah creating a Lua(u) writer from instructions is doable, but a lot of work. I'd suggest looking at https://github.com/marsinator358/luajit-decompiler-v2 to get an overview and maybe use as a basis :-)
Or contribute to https://github.com/atrexus/unluau ofc.
AI is a great tool to process LUA instructions into readable code, especially if you train it on the FS22 lua files for coding style. It happily generates functions based on the example above. When you use the v2 branch of unluau and a bit of OpenAI, it will happily process all the files for you...
AI is a great tool to process LUA instructions into readable code, especially if you train it on the FS22 lua files for coding style. It happily generates functions based on the example above. When you use the v2 branch of unluau and a bit of OpenAI, it will happily process all the files for you...
or maybe it can automatically process 1k files and give us ready-made scripts?)
I'm not very optimistic about this solution, how can I be sure that the code generated by the AI ββis 1:1 the same as in the game? I can already see that it doesn't look completely correct.
Do we know what the correct Lookup table is yet or is that still under investigation?
Do we know what the correct Lookup table is yet or is that still under investigation?
https://github.com/chill1Penguin/l64decode/issues/11#issuecomment-2468969751
Do we know what the correct Lookup table is yet or is that still under investigation?
I checked this against a few l64s and that didn't seem to match up to the hex. However, one thing I came across messing with a few luau decompilers is that they all say the bytecode is version 2, which unluau is v3-v4 (v2.0.0 beta is v3-v5) and Luau-Decompiler supports v5. I haven't been able to find a v2 support just yet
For FS25 Luau it does match up, you just have to shift all bytes and then remove the first.
Still no way to decompile the files tho?
For FS25 Luau it does match up, you just have to shift all bytes and then remove the first.
Please link us to the tool being used and let us know how many bytes to shift by.
I personally would appreciate it greatly.
Run this script on a directory with .l64 files and your bytes should be fine
import os
import argparse
def read_and_modify_files(directory):
for root, _, files in os.walk(directory):
for file in files:
file_path = os.path.join(root, file)
try:
with open(file_path, 'rb') as f:
first_byte = f.read(1)
rest_of_file = f.read()
if first_byte:
first_byte_value = int.from_bytes(first_byte, 'big')
if first_byte_value == 1:
with open(file_path, 'wb') as f_mod:
f_mod.write(rest_of_file)
print(f"Removed first byte with value {first_byte_value} from {file_path}")
else:
print(f"{file_path}: Empty file")
except Exception as e:
print(f"Could not process {file_path}: {e}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Read and modify files in a directory.")
parser.add_argument("-dir", "--directory", required=True, help="Path to the directory to process")
args = parser.parse_args()
directory_path = args.directory
read_and_modify_files(directory_path)
import { readFileSync, writeFileSync } from "node:fs"
const BYTECODE_SHIFT_TABLE_LUAU_V3 = [0x02, 0x13, 0x0a, 0x08, 0x01, 0x07, 0x02, 0x02]
function shiftBytesUsingTable(buffer: Buffer, table: Array<number>, offset = 0): void {
for (let i = offset; i < buffer.length; i++) {
const value = buffer[i]
// Mask i with 0x07 to get the bytecode for this index
const bytecode = table[i & 0x07]
// Apply the shifting formula and ensure the result stays in the 0-255 range
buffer[i] = (value + bytecode + i) & 0xFF
}
}
const inputBuffer = readFileSync("C:/path/file.l64")
shiftBytesUsingTable(inputBuffer, BYTECODE_SHIFT_TABLE_LUAU_V3)
// Omit the first byte
const outputBuffer = inputBuffer.subarray(1)
writeFileSync("C:/path/file_decoded.l64", outputBuffer)
@4c65736975 Not sure if this script works, since the .l64 files report first bite as \x02, and the decoded ones (using @scfmod script for example) report first bite as \x03 (but you expect a 1).
But this still only gives bytecode, and not decompiled code right? I've tried all the decompilers here (LuauDec, Unluau, ...) with not much success apart from some instructions. I even tried the Lua 5.1 decompiler since luau supposedly has support for lua5.1. Anyone managed to get somewhat decent code out yet or is there a Discord or something for passing ideas around?
@Bizyak13 This script removes the first byte from .l64 files decompiled by L64Decoder, allowing decompilers like Unluau to recognize the bytecode version. It works for me, and using this method, I was able to successfully decompile 1,228 files out of 1,703 to plain Lua.
@4c65736975 This is still confusing, since using the L64Decoder, the files are already correct for Unluau v2.0.0 unless you are using some other decompiler?
If I run your script on a decoded file, I get zero output, since the first byte is not "1". But I do get the files properly decompiled if I use the Unluau, they just contain this:
; unluau disassembler version 2.0.0.0, elapsed: 0.1578657s
testing.lua: Luau bytecode executable, version 3, hash: 0x99892b7b730184275aedac347b9161c6
function <testing.lua:13> (31 instructions, 180 bytes)
0 params, 5 slots, 1 upvalues, 20 constants
function initTesting() -- line 13 through 31
1 GETIMPORT 0 2
2 2147484672
3 LOADK 1 3
4 CALL 0 2 2
5 JUMPXEQKNIL 0 38
6 0
7 GETUPVAL 2 0
8 GETTABLE 1 2 0
9 JUMPXEQKNIL 1 34
10 0
11 GETIMPORT 2 5
12 1077936128
13 GETTABLEKS 3 1 138
14 6
15 CALL 2 2 1
16 GETIMPORT 2 9
17 2154831872
18 GETTABLEKS 3 1 187
19 10
20 CALL 2 2 2
21 SETGLOBAL 2 0 43
22 11
23 GETGLOBAL 2 0 43
24 11
25 JUMPXEQKNIL 2 13
26 0
27 GETGLOBAL 3 0 43
28 11
29 GETTABLEKS 2 3 19
30 12
31 CALL 2 1 1
32 GETIMPORT 2 15
33 2161129472
34 LOADK 3 16
35 MOVE 4 0
36 CALL 2 3 1
37 LOADB 2 1 0
38 RETURN 2 2
39 GETIMPORT 2 18
40 2161132544
41 LOADK 3 19
42 MOVE 4 0
43 CALL 2 3 1
44 LOADB 1 0 0
45 RETURN 1 2
constants (20)
index type value
0 string "StartParams"
1 string "getValue"
2 import ["StartParams","getValue"]
3 string "test"
4 string "source"
5 import ["source"]
6 string "filename"
7 string "ClassUtil"
8 string "getClassObject"
9 import ["ClassUtil","getClassObject"]
10 string "className"
11 string "g_currentTest"
12 string "init"
13 string "Logging"
14 string "info"
15 import ["Logging","info"]
16 string "Started test \'%s\'"
17 string "error"
18 import ["Logging","error"]
19 string "Test \'%s\' not defined"
locals (2)
index name startpc endpc type
0 data 8 43
1 testName 4 45
upvalues (1)
index name type
0 tests
end
main <testing.lua:1> (44 instructions, 260 bytes)
0+ params, 3 slots, 0 upvalues, 18 constants
function main(...) -- line 1 through 33
1 PREPVARARGS
2 LOADNIL
3 SETGLOBAL 0 0 43
4 0
5 NEWTABLE 0 0
6 0
7 DUPTABLE 1 3
8 LOADK 2 4
9 SETTABLEKS 2 1 187
10 1
11 LOADK 2 5
12 SETTABLEKS 2 1 138
13 2
14 SETTABLEKS 1 0 109
15 4
16 DUPTABLE 1 3
17 LOADK 2 6
18 SETTABLEKS 2 1 187
19 1
20 LOADK 2 7
21 SETTABLEKS 2 1 138
22 2
23 SETTABLEKS 1 0 37
24 6
25 DUPTABLE 1 3
26 LOADK 2 8
27 SETTABLEKS 2 1 187
28 1
29 LOADK 2 9
30 SETTABLEKS 2 1 138
31 2
32 SETTABLEKS 1 0 197
33 8
34 DUPTABLE 1 3
35 LOADK 2 10
36 SETTABLEKS 2 1 187
37 1
38 LOADK 2 11
39 SETTABLEKS 2 1 138
40 2
41 SETTABLEKS 1 0 142
42 10
43 DUPTABLE 1 3
44 LOADK 2 12
45 SETTABLEKS 2 1 187
46 1
47 LOADK 2 13
48 SETTABLEKS 2 1 138
49 2
50 SETTABLEKS 1 0 164
51 12
52 DUPTABLE 1 3
53 LOADK 2 14
54 SETTABLEKS 2 1 187
55 1
56 LOADK 2 15
57 SETTABLEKS 2 1 138
58 2
59 SETTABLEKS 1 0 220
60 14
61 DUPCLOSURE 1 16
62 CAPTURE 0 0 0
63 SETGLOBAL 1 0 115
64 17
65 RETURN 0 1
constants (18)
index type value
0 string "g_currentTest"
1 string "className"
2 string "filename"
3 import {"className", "filename"}
4 string "TestAnimalCluster"
5 string "dataS/scripts/animals/husbandry/cluster/TestAnimalCluster.lua"
6 string "TestI3DManager"
7 string "dataS/scripts/i3d/TestI3DManager.lua"
8 string "TestDebugElements"
9 string "dataS/scripts/debug/TestDebugElements.lua"
10 string "TestXML"
11 string "dataS/scripts/xml/TestXML.lua"
12 string "TestPolygon"
13 string "dataS/scripts/collections/TestPolygon.lua"
14 string "TestMathUtil"
15 string "dataS/scripts/utils/TestMathUtil.lua"
16 closure 0
17 string "initTesting"
locals (1)
index name startpc endpc type
0 tests 6 65
end
So l64Decoder > script > luau decompiler
Is the working method?
Yes, but I had to adjust the decompiler a bit to decompile more scripts, because in the github version it throws errors for many files
I recommend a simple batch script that will automatically run unluau on all files in a folder and delete the ones that failed to decompile (empty files)
@Bizyak13 This script removes the first byte from .l64 files decompiled by L64Decoder, allowing decompilers like Unluau to recognize the bytecode version. It works for me, and using this method, I was able to successfully decompile 1,228 files out of 1,703 to plain Lua.
What version of Unluau are you using? I'm only getting 873 successfully converted files. any idea to why the other ones are failing?
With Unluau.CLI v1.09.alpha, some bytecode instructions are not recognized (e.g. missionManager.l64):
[10:13:49 WRN] Encountered unhandled code JUMP, skipping
[10:13:49 WRN] Encountered unhandled code FASTCALL2, skipping
[10:13:49 WRN] Encountered unhandled code FASTCALL1, skipping
[10:13:49 WRN] Encountered unhandled code FASTCALL1, skipping
[10:13:49 WRN] Encountered unhandled code FASTCALL2K, skipping
[10:13:49 FTL] Unexpected error; please report here: https://github.com/atrexus/unluau/issues
System.NullReferenceException: Object reference not set to an instance of an object.
at Unluau.Lifter.LiftBlock(Function function, Registers registers, Int32 pcStart, Int32 pcStop)
And, as has been said already, Unluau2.0.0.beta does not (yet?) generate lua source output. @4c65736975 , when you "had to adjust the decompiler a bit to decompile more scripts", did you work from v1.0.9alpha or v2.0.0beta?
This script works fine to decode Farming Simulator 22 files, however Farming Simulator 25 seems to be using a different format - an error
'File "{filePath}" contains an invalid .l64 header.'
is returned for any and all l64 files retrieved from FS25.Here's a download link to one of the .l64 files in question: https://cdn.taembo.net/main.l64