Page 1 of 1

Example for defining a custom value type in CE

Posted: Mon Jul 10, 2023 10:45 am
by bbfox

Some games may utilize custom data types that are not included in the standard definitions. (In this context, standard types include integer, float, double, word, byte, etc. which are built-in types in CE.)

This is a brief article demonstrating how to define a custom variable type. The article showcases the usage of the registerCustomTypeAutoAssembler() function to register Auto Assembler routines.

Definition of custom type labels (Note: this is not an exhaustive list of definitions, these are read by CE):

TypeName: The custom type name displayed in CE
ByteSize: Specifies the size of the custom type in bytes (32-bit)
UsesFloat: Indicates whether it should be treated as a float in CE (0 = No, 1 = Yes)
ConvertRoutine: A subroutine defining how to convert it (from CE to memory)
ConvertBackRoutine: A subroutine defining how to convert it back (from memory to CE)

registerCustomTypeAutoAssembler: This Lua script contains the main block of AA code for conversion.

Section:
[64-bit] [/64-bit]: Indicates a 64-bit conversion routine
[32-bit] [/32-bit]: Indicates a 32-bit conversion routine

I've only tested this in 64-bit games and haven't used the [64-bit][/64-bit] sections.

For 64-bit:
For ConvertRoutine: At this point, ecx/rcx contains the address where the bytes are stored. The return value is stored in eax/rax.
For ConvertBackRoutine: At this point, edx/rdx contains the address to write the value to (you need to write the value to this address), and ecx/rdx contains the source value.

For 32-bit routines: (Again, I haven't used them)

[32-bit]
ConvertRoutine:
push ebp
mov ebp,esp
//[ebp+8]=input
.
.
.
pop ebp
ret 4
[/32-bit]

ConvertBackRoutine:
[32-bit]
push ebp
mov ebp,esp
//[ebp+8]=input
//[ebp+c]=address of output

pop ebp
ret 8
[/32-bit]

Example: Civilization VI
In Civ 6, most values are stored as real values * 256 in memory. When the value is needed for display on the screen, the game will divide it by 256 (maybe; I have not traced it):

  • Values stored in memory: display value * 256

  • Values displayed on screen: memory value / 256

The following script utilizes XMM registers to perform the conversion:

Code: Select all

[ENABLE]
{$lua}

if _civ6_customFloat == nil then

registerCustomTypeAutoAssembler([[
alloc(TypeName,256)
alloc(ByteSize,8)
alloc(ConvertRoutine,1024)
alloc(ConvertBackRoutine,1024)
alloc(UsesFloat,1)

TypeName:
db 'Civ6 Float',0

ByteSize:
dd 4

UsesFloat:
db 1

ConvertRoutine:
//at this point rcx contains the address where the bytes are stored
xor rax,rax

mov eax, dword ptr [rcx]
cvtsi2ss xmm15, eax
mov eax, (float)256
movd xmm14, eax
vdivss xmm15, xmm15, xmm14
movd eax, xmm15

ret

ConvertBackRoutine:
//at this point rdx contains the address to write the value to
//and rcx contains the value

push rax
mov eax, ecx
movd xmm15, eax
mov eax, (float)256
movd xmm14, eax
vmulss xmm15, xmm15, xmm14
vcvtss2si eax, xmm15

mov dword ptr [rdx], eax
pop rax
ret

]])

_civ6_customFloat = true
end
[DISABLE]
{$asm}

Notice:
In CE 7.6, there is a bug: when registerCustomTypeAutoAssembler executed, process & $process global variable will changed to CE itself.
Workaround example:
Record pid and restore it:

_G.processID = getOpenedProcessID()

registerCustomTypeAutoAssembler([[.....
.
.

if getOpenedProcessID() ~= G.processID then
  openProcess(G.processID)
end

of force rewrite

_G.process2 = process

registerCustomTypeAutoAssembler([[.....
.
.

process = _G.process2
autoAssemble(string.format([[
unregistersymbol($process)
define($process, "%s")
registersymbol($process)
]], process))

Spoiler

Image