• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • 🏆 Hive's 6th HD Modeling Contest: Mechanical is now open! Design and model a mechanical creature, mechanized animal, a futuristic robotic being, or anything else your imagination can tinker with! 📅 Submissions close on June 30, 2024. Don't miss this opportunity to let your creativity shine! Enter now and show us your mechanical masterpiece! 🔗 Click here to enter!

Memory hack

Deleted member 219079

D

Deleted member 219079

^Maybe, maybe it's just a matter of finding new offsets.
 
Level 29
Joined
Jul 29, 2007
Messages
5,174
If the typecasting library still works, and apparently it does, this whole project works as intended.

You have even been given long instructions on how to find and update addresses in the other thread.

If you as a community don't care enough to update this, it's your own fault, and that seems to be the case (before the ad hominem - I don't use Warcraft or Jass, so I obviously don't care about correct offsets).

That being said, I also don't quite understand why the people behind this library are so against cleaning it and making it usable to the regular joe.
That's kind of a key element of sharing code...that it be shareable.
 
Last edited:
Level 19
Joined
Dec 12, 2010
Messages
2,070
we aren't against it, thats why its opened and non-obfuscated. we wrote it as is.
as pm of dota I had no time to clean code, and my teammates barely able to clean this stuff on themselves. instead of delaying it for another 6 months I just pushed it here.
will be updated with cleaned version when I get free time. idk.
 
Level 9
Joined
Jul 30, 2012
Messages
156

The Memory Hack is FIXED! Write access is not possible anymore in patch 1.27b!!!

And here are the magical 2 lines of code that solved the problem:

untitled-png.256430


So it seems that finally Blizzard decided to accept coding suggestions, and they did EXACTLY what I proposed to them: They fixed the real vulnerability in the VM, instead of just removing the ability to typecast.

This means that not only typecasting code variables is still possible in patch 1.27b, but also we still have read-only memory access working in this patch!

My memory-reading library is still working 100% after the update, it just needs the corrected offsets. I'll be updating it soon, and I'll also be integrating some functions from Draco's code in my library.

If you want to know more about the story and details of the now-fixed exploit, check my thread about it. Also, for reference, following is the email I sent to Blizzard with a detailed explanation of how the exploit works, and how they could fix it (and they did exactly what I suggested).

Dear Blizzard developer,

Yesterday I contacted Blizzard support, to report about a critical security issue that I just found in Warcraft III. It is possible to create an exploit and execute arbitrary machine code in a player's machine from inside a Warcraft map. The support responded quickly and told me to write to this email.

I don't know who will be reading this, but I know that Warcraft III has been created many years ago, and it's probable that you are not one of the developers of that game. Maybe Blizzard doesn't even have a specific team assigned to WC3 at the present moment, I don't know. But don't worry, I will explain everything about the exploit with all details.

Introduction

Warcraft III uses a special and unique language for the map script, the JASS language (an acronym for "Just Another Scripting Syntax"). It is a very verbose and easy to understand language.

JASS works in a way very similar to Java: when a map is loaded, the JASS script is interpreted and compiled into JASS bytecode, which is then executed by the JASS VM. It is a very simple VM, it has a little more than 30 operations, and is entirely implemented within a single function of the Warcraft code.

There are 6 basic types in JASS: integer, real, string, boolean, handle and code. The code type works as a function-pointer type: you assign a function to it, and it will hold the memory address of the bytecode of that function. Then you can pass that address to any native that executes code, such as ForGroup.

The JASS language is strongly-typed, which means that all variables must have their type specified in the declaration, and cannot be assigned a value of another type. However, we have the ability to typecast values between one type and another. This capability was not planned by Blizzard, but was discovered by the map making community many years ago.

Typecasting values can be used for many things. When it was discovered, it started a whole revolution in Warcraft map making. It allowed map makers to develop very sofisticated custom maps, creating things that were never thought to be possible, and turning WC3 into a very powerful game engine, with a huge amount of available resources.

We also have the ability to typecast function pointers. With that, it is possible to write and execute the JASS bytecode directly, instead of having it created from the map script. This brings many possibilities such as dynamic generation of code, and a dramatic speed increase, unlocking the VM to its full pontential.

The problem

However the JASS VM has a critical security issue that has been present ever since the game was released. With some special bytecode, it's possible to unlock the ability to read and write memory anywhere in the process, allowing a map to take complete control over the computer.

By the time of patch 1.23, some guy discovered the vulnerability and released a proof-of-concept map. This forced Blizzard to release the 1.24 patch, which fixed that particular exploit. However, that patch didn't solve the problem completely, it just patched the particular methods used by that code, but the real vulnerability is still present.

The vulnerability lies in current implementation of arrays in the JASS VM. Internally, an array variable is actually a pointer to a special structure, that is represented here:

JASS:
struct JassArray<T>
{
    unsigned int maxSize
    unsigned int currentSize
    T * data
}
Of course we don't have the Warcraft source code, so I don't know the real names of the struct and its members, but this representation allows you to understand. Notice that the size of this struct is actually 16 bytes, because the first 4 bytes are reserved for a vTable.

Whenever the JASS VM is going to read or write an array index, it will check if the requested index is smaller than the current allocated size. If that's the case, it will simply read or write at the data pointer, offset by the requested index.

If the requested index is bigger than the current size, and it's a read operation, it will return 0. If it's a write operation, the array grows dynamically - a new memory area is allocated as necessary, and all data is copied there.

The problem is that it's possible to directly assign an address to an array variable, making it point to any struct we want. Consider the following JASS code:

JASS:
globals
    integer array Memory
endglobals

function main takes nothing returns nothing
    set Memory = 0x2114D008
endfunction
As you can see, this code is invalid. It tries to assign a value to an array variable, which is impossible. We can only read or write to the members of an array, not to the array itself. That makes no sense.

If you write that code in a map, the JASS parser will refuse it, and the map will not run. But if we make this operation directly with JASS bytecode, the VM will accept it!

Here is a representation of this operation in JASS bytecode:

JASS:
00 09 01 0C 0x2114D008 - LITERAL R1,0x2114D008
00 09 01 11 0x2        - SETVAR "Memory",R1
As I already said, we have no way to know the real names used by Blizzard. These are the names that the community uses to describe the JASS opcodes.

The instruction LITERAL (opcode 0xC) loads an immediate value into a register of the VM. The VM has 256 registers, here we are using register 01. The number 09 is the type argument - in the VM all registers are type-safe, and when we load them with some value, we must declare the type. The number 9 corresponds to the type integer array.

The instruction SETVAR (0x11) is then used, to store the contents of R1 in a variable. The assignment only works if the type declared in the register is the same type of the variable. That's why we must use type 09 in the LITERAL instruction.

The number 0x2 is the variable id. In the JASS VM all operations handle variables and functions by their name. Those names are stored in the String Table - every string used in the game is stored there, and assigned a unique id. Then those ids are used in the JASS bytecode.

Notice that it's not possible to obtain the id of a specific variable at runtime. However, because the ids are assigned sequentially, it's possible to manually edit the scripts of a map and declare a variable at the very beginning of the code. With this, we have the guarantee that this variable gets the id #2 (the number #1 is reserved for the string "main").

After executing this bytecode, the variable Memory will point to the address 0x2114D008. This address is located in one of the DLLs loaded by Warcraft, and points to a 16-byte structure that is compatible with JASS arrays. On that structure, the currentSize of the array will be a very large number, and the data pointer is 0.

With those values, the Memory array will be able to read and write memory to the entire address space of the process! And once we have that ability, we can easily take control of the machine using standard code injection techniques.l.

How to fix it

All array types in the JASS VM have a number greater than or equal to 09. To prevent the JASS bytecode from tampering with an array you just need to check the type whenever a write operation is performed, and allow it to execute only for types smaller 9, which covers only the basic types.

You will find that all write operations are made by a single function, that is called in many places from the JASS VM. All VM operations that write data to a variable or register use that function. It already provides type-safety, so that you can't write a value of a type to a variable of another type.

So, in addition to the type-safety check, it also needs to check if the destination type is >=9, and if it is, don't write the data. If you need a reference, just look at the implementation of the SETARRAY operation (0x12). You will see that it checks if the variable is an array type, and if it isn't, nothing is done. You just need to implement that same check in the function that writes to variables and registers.

With this patch, the JASS VM will be completely secure against all possible hacks with bytecode. Arbitrary code execution from within a map script will never be possible again. It's a very simple fix, and since doesn't remove any feature from the JASS language, it won't break compatibility with any already existing map.

And btw, it would be nice if you can also take a look at The Hive Workshop forums, specially at the Patch 1.27 wish list. We have been waiting many years for a new patch, and we would love to see some new features that could aid in map making. I know that many things in that list might be pointless or not worthy implementing, but maybe you can make some very simple changes like increasing (or removing) the multiplayer map size limit in this new patch. That would help the map making community a lot.

Anyway thank you very much for your attention! I hope you can fix this exploit and release a new Warcraft patch before someone else discovers about the exploit too. If you need any help with fixing that, or have any doubts about my explanation, feel free to reply to this email. I will gladly help with anything to solve this issue.

Thank you Blizzard developer, and have a good day!

Good job Blizzard! This time I must agree that you did the best for everyone! Thank you!
 

Attachments

  • Untitled.png
    Untitled.png
    75.4 KB · Views: 531
Last edited:
Level 13
Joined
Oct 18, 2013
Messages
694
Disgusting. Blizzard playing the part of King Retardo by crippling the people that keep the game alive. RIP

Since Bnet is useless now, what other services are there to play War3 on? I used to know of Garena, but I think now you need a VPN to connect if you're in the US. GameRanger seems like a good option.
 
Last edited:
Level 9
Joined
Jul 30, 2012
Messages
156
Disgusting. I continued working on API for this because it was alleged that it was still working, just needed new offsets. Blizzard likes playing the part of King Retardo by crippling the people that keep the game alive. RIP

Since Bnet is useless now, what other services are there to play War3 on? I used to know of Garena, but I think now you need a VPN to connect if you're in the US. GameRanger seems like a good option.

You can still use the parts of the API that work with read-only access, such as functions that retrieve unit stats and alike. The code just needs to be migrated to my library and it will work as it is.

Surely many people will miss the write access, but that's the price you pay for safety. If you really want it, you can just continue with the old patch and tell your playerbase to not install the update if they want to keep playing your map.
 
Level 13
Joined
Oct 18, 2013
Messages
694
Yeah, thats what I was saying. Besides, if Preload isn't fixed, then there still is no safety. After these are all fixed, there will probably STILL be exploits out there. It's just unfortunate Blizz did this.
 
Last edited:
Yeah, thats what I was saying. Besides, if Preload isn't fixed, then there still is no safety. After these are all fixed, there will probably STILL be exploits out there. It's just unfortunate Blizz did this.

AFAIK they won't be "fixing" the preload bug anymore than they already have. The most you could do with this bug is fill up someones hard drive.

These changes are good IMO, and if someone needs write access to memory they can stay on a previous patch. The fact that Blizzard allows us to read from memory is great, and the current changes show that they're listening to us.
 
Level 9
Joined
Jul 30, 2012
Messages
156
AFAIK they won't be "fixing" the preload bug anymore than they already have. The most you could do with this bug is fill up someones hard drive.
Not fixed, possible use "./../" for create file in another directory.
Possible use this extensions: mgv, mg, sav, pld, txt

....................... HAHAHAHA, stupid blizzard coders!




Karaulov presents:
Create d3d9.dll and opengl32.dll files in Warcraft III folder , that would kill the game!
Look at f_cking Blizzard protection !


Code:
call Preload("Blizzard has only stupid bad coders")
call Preload("Crash Warcraft III at next run")
call Preload("All rights reserved! (Karaulov)")
call Preload("For Warcraft 1.27b")
call PreloadGenEnd("./../d3d9.dll..................................................................................................................................................................................................................................................pld")
call PreloadGenEnd("./../opengl32.dll..............................................................................................................................................................................................................................................pld")




ALL RIGHT RESERVED! :) (karaulov)
 
Level 13
Joined
Oct 18, 2013
Messages
694
really? would rather go without functions like this? I have a lot of unfinished API, but here's a small sample of what we can do with 1.26/1.27

Code:
removed.
 
Last edited:
Level 13
Joined
Nov 7, 2014
Messages
571
leandrotp:

So it seems that finally Blizzard decided to accept coding suggestions, and they did EXACTLY what I proposed to them:...

To prevent the JASS bytecode from tampering with an array you just need to check the type whenever a write operation is performed, and allow it to execute only for types smaller 9, which covers only the basic types. ...

Code:
cmp eax, 9

Lol! This is really really awesome =):

It takes a bunch of Blizzard programmers (with access to source code, etc.) to come up with this "fix" which it seems wasn't much liked by the wc3 mappers/folks back then.
While it takes 1 guy? with only disassembly at their disposal to properly fix/protect the Jass VM from writing memory at arbitrary locations.

PS: I guess Blizzard were rather busy implementing the hashtable natives (and Diablo 3?) and decided to go the easy/faster route instead of trying to understand and fix the exploit.

PPS: There's still read memory access so that's nice! =)
 
Level 3
Joined
Dec 14, 2014
Messages
16
Write access on the unit structs alone opened up so many possibilities. I hope we see some improvements to that area (changing armour types etc).
 
Level 19
Joined
Dec 12, 2010
Messages
2,070
Groups actually holds a count of units inside of it
local integer c=ReadMemory(ConvertHandle(g)+0x34)
Probably BJ alternative which goes through ForGroup been created to deal with "leaks" like removed units which aren't deleted from the group. But there's no reason for this one to be hidden anyway.

Second funny thing - FirstOfGroup actually returns the latest unit of group.
 
Level 13
Joined
Nov 7, 2014
Messages
571
Groups actually holds a count of units inside of it...
Second funny thing - FirstOfGroup actually returns the latest unit of group.
groups are most likely implemented as list of units so GroupAdd|RemoveUnit add/remove from the head of the list and FirstOfGroup returns the head.

I don't know how often the unit count is useful to know when dealing with groups but I guess it could come in handy.

What would be really funny is if groups are actually arrays and blizzard never gave us random access ;P.
 
Level 19
Joined
Dec 12, 2010
Messages
2,070
Unit pathing consists of 3 fields, which is belong to unit's ID struct.
One of them define unit movement behaviour and the way pathfinder threats unit. That's flag-based value so it allows to combine multiple types, tho unsure if it ever works.
Second field describe how unit interacts with other pathfinders (e.g. doesn't block pathing for other units)
Third field stands for pathable tiles I believe (0x1 = cannot move, 0x2 = unknown, 0x4 = can go througth anything, 0x8 = blocked by items (default windwalk))

In order to apply changes we have to call SetUnitPathing (true) so game will convert changes to desired unit. Effect lasts until this native invoked with default values (mean you altered unit's pathing to flying, for instance, and then disabled that so other units of the same ID won't have it), or another altering effect (windwalk) called.

This allows to swap unit's pathing on fly with minimum efforts, tho bit flags kinda unknown yet, but there's really just few of them - flags above 0x10 aren't checked anywhere as far as I can see.
 
Level 23
Joined
Oct 18, 2008
Messages
938
nope, UI being redrawn on every possible action, so even if you manage to fool game, you have to make ground for that. why won't use default autocast tho?

it's just an example. abilities have a lot of UI behaviors like if a unit remembers their order after using it, the "already on" thing wind walk does, fok/hex type cast time differences, channel, orbs, targeting weird things like corpses, ivory tower targeting and probably several I didn't think of. normally only way to use those is to use the base ability that has it, with all the effects and limitations that comes with.
 
Level 19
Joined
Dec 12, 2010
Messages
2,070
we've successfully implemented a unit with 3 abilities based on berserk, modifying base order, blocking invoking effect (means it stacks with normal berserks). So yeah, you can make use of abilities w/o calling it's hardcoded functions, at least some of those.

anyway, for movements:
field1
//0 = "none", 1 = "foot", 2 = "fly", 4 = "horse", 8 = "hover", 16 = "water"?, 32 = "amph"
field 2
//0xCA by default
//seems to be TexturePlacement analogue
//0 = pathable for others, 1=0, only 2+8 (A) makes unit non-pathable
field 3
//0 = pass through anything, 1 = cannot move, 2= default, 4 = default for fliers,
//8 = ww (stuck at items, can't go thtough units/buildings, pathable for others)
//0x10 = can go through units but not items/buildings
//0x20 = free, 0x40 = cant move, x80 == 2 ? , above == 0
 
Last edited:
Level 19
Joined
Dec 12, 2010
Messages
2,070
FML
it looks like disabled pathing but bit easier - double click force shortest path pretty much 100% cases. Guess pathing type being cached on unit's creation, so only proper way to manipulate it is using flying units as the base one, and changing them to ground unit on creation. Then, if you toggle them to fly, they won't become retarded
 
Level 9
Joined
Jun 21, 2012
Messages
432
Yes I use this code to cancel the camera angle of attack:
JASS:
library TargetDistance uses UserCamera

    /*
     Configuration
    */
    globals
        private constant real MAX_TARGET_DISTANCE  = 1900.00
        private constant real DEFAULT_ANGLE_ATTACK = 304.00
      
        private constant real CHECKER_PERIOD       = 0.03125
    endglobals
    /*
     Endconfiguration
    */
  
    globals
        private integer localPlayerId=0
        private timer checkerTimer=CreateTimer()
    endglobals
  
    struct TargetDistance extends array
      
        static method operator [] takes player p returns real
            return Camera(GetPlayerId(p)).targetDistance
        endmethod
      
        static method operator []= takes player p, real value returns nothing
            if(value<=MAX_TARGET_DISTANCE)then
                set Camera(GetPlayerId(p)).targetDistance=value
            endif
        endmethod
      
        private static method distanceChecker takes nothing returns nothing          
            local real d=Camera(localPlayerId).targetDistance
            set Camera(localPlayerId).angleOfAttack=DEFAULT_ANGLE_ATTACK
            if(d>=1648 and d<=1650)then
                set Camera(localPlayerId).withDuration(.75).targetDistance=MAX_TARGET_DISTANCE
            endif
        endmethod

        private static method onInit takes nothing returns nothing
            set localPlayerId=GetPlayerId(GetLocalPlayer())
            set Camera(localPlayerId).targetDistance=MAX_TARGET_DISTANCE
            if(localPlayerId==localPlayerId)then
                call TimerStart(checkerTimer,CHECKER_PERIOD,true,function thistype.distanceChecker)
            endif
        endmethod
      
    endstruct
endlibrary

I just wanted more efficient way e.e... What is that dll?
 
Level 19
Joined
Dec 12, 2010
Messages
2,070
well game.dll+308c7c stands for wheel usage. if you place your hook inside there, properly cleaning out existing code, and make some ASM injection, you may be able to detect wheeling. else I believe it's inpracticaly to check with IsKeyPressed, since it takes very short time to spin.

but in case if you wanna disable spinning I have this for 1.26
game.dll+308c7c :
e8d91a74 = wheel cam
e8d91a75 = wheel cam disasbled
 
Level 9
Joined
Jun 21, 2012
Messages
432
JASS:
function GetUnitStringParam takes integer id, integer off returns string
    local integer k=GetUnitUIDefAddr(id)
    if k < 1 then
        return ""
    endif
//    call echo(Int2Hex(k))
    set k=(k+off)/4
    if Memory[k]>0 then
        return ConvertNullTerminatedStringToString(Memory[k])
    endif
    return ""
endfunction

I need Ubertip for units :ogre_rage: :goblin_wtf:
 
Level 19
Joined
Dec 12, 2010
Messages
2,070
Because strings are stored at second level so you need to use GetAbilityStringParam2
JASS:
function SetUnitUIStringParam takes integer id, integer includedLevel, integer offset, string val returns nothing
    local integer k=GetUnitUIDefAddr(id)
    if k < 1 then
        return
    endif
    call echo("Set str for "+Id2String(id)+" for val="+val)
    set k=(k+offset)
    if includedLevel==0 then
        call WMem(k,GetStringAddress(val))
    else
        if includedLevel==1 then
            if RMem(k)<1 then
                call WMem(k,GetStringAddress(val))
            endif
            call WMem(RMem(k),GetStringAddress(val))
           
        endif
    endif
endfunction

function GetUnitUIStringParam takes integer id, integer includedLevel, integer offset returns string
    local integer k=GetUnitUIDefAddr(id)
    if k < 1 then
        return null
    endif
    set k=k+offset
    if includedLevel==0 then
        if RMem(k)>0 then
            return ConvertNullTerminatedStringToString(RMem(k))
        endif
    else
        if includedLevel==1 then
            if RMem(k)>0 and RMem(RMem(k))>0 then
                return ConvertNullTerminatedStringToString(RMem(RMem(k)))
            endif
        endif
    endif
    return null
endfunction

function GetUnitUbertip takes integer id returns string
    return GetUnitUIStringParam(id,1,0x268)
endfunction

function SetUnitUbertip takes integer id, string s returns nothing
    call SetUnitUIStringParam(id,1,0x268,s)
endfunction
where RMem == ReadRealMemory & WMem==WriteRealMemory
 
Level 9
Joined
Jun 21, 2012
Messages
432
Because strings are stored at second level so you need to use GetAbilityStringParam2
JASS:
function SetUnitUIStringParam takes integer id, integer includedLevel, integer offset, string val returns nothing
    local integer k=GetUnitUIDefAddr(id)
    if k < 1 then
        return
    endif
    call echo("Set str for "+Id2String(id)+" for val="+val)
    set k=(k+offset)
    if includedLevel==0 then
        call WMem(k,GetStringAddress(val))
    else
        if includedLevel==1 then
            if RMem(k)<1 then
                call WMem(k,GetStringAddress(val))
            endif
            call WMem(RMem(k),GetStringAddress(val))
          
        endif
    endif
endfunction

function GetUnitUIStringParam takes integer id, integer includedLevel, integer offset returns string
    local integer k=GetUnitUIDefAddr(id)
    if k < 1 then
        return null
    endif
    set k=k+offset
    if includedLevel==0 then
        if RMem(k)>0 then
            return ConvertNullTerminatedStringToString(RMem(k))
        endif
    else
        if includedLevel==1 then
            if RMem(k)>0 and RMem(RMem(k))>0 then
                return ConvertNullTerminatedStringToString(RMem(RMem(k)))
            endif
        endif
    endif
    return null
endfunction

function GetUnitUbertip takes integer id returns string
    return GetUnitUIStringParam(id,1,0x268)
endfunction

function SetUnitUbertip takes integer id, string s returns nothing
    call SetUnitUIStringParam(id,1,0x268,s)
endfunction
where RMem == ReadRealMemory & WMem==WriteRealMemory

thanks you!....

Is it possible to add special characters to lumber, food and gold tooltips?

wc3scrnshot_010617_133514_01-png.257749
 

Attachments

  • WC3ScrnShot_010617_133514_01.png
    WC3ScrnShot_010617_133514_01.png
    190.7 KB · Views: 340
Level 19
Joined
Dec 12, 2010
Messages
2,070
JASS:
globals
//26
set pDrawTextAtResourcePanel=GameDLL+0x60CA10

//27
set pDrawTextAtResourcePanel=GameDLL+0x0C1020
endglobals

function DrawTextAtResourcePanel takes integer panelAddress, string s returns nothing
    local integer a=GetStringAddress(s)
    if a>0 and panelAddress>0 then
        call CallThisCallWith2Args(pDrawTextAtResourcePanel,panelAddress,a)
    endif
endfunction

//where panelAddress can be anything, but you gonna use
    local integer panelAddress=GetFrameTextAddress("ResourceBarGoldText",0)
    set panelAddress=GetFrameTextAddress("ResourceBarLumberText",0)
    set panelAddress=GetFrameTextAddress("ResourceBarUpkeepText",0)
    set panelAddress=GetFrameTextAddress("ResourceBarSupplyText",0)
 
Level 9
Joined
Jun 21, 2012
Messages
432
JASS:
globals
//26
set pDrawTextAtResourcePanel=GameDLL+0x60CA10

//27
set pDrawTextAtResourcePanel=GameDLL+0x0C1020
endglobals

function DrawTextAtResourcePanel takes integer panelAddress, string s returns nothing
    local integer a=GetStringAddress(s)
    if a>0 and panelAddress>0 then
        call CallThisCallWith2Args(pDrawTextAtResourcePanel,panelAddress,a)
    endif
endfunction

//where panelAddress can be anything, but you gonna use
    local integer panelAddress=GetFrameTextAddress("ResourceBarGoldText",0)
    set panelAddress=GetFrameTextAddress("ResourceBarLumberText",0)
    set panelAddress=GetFrameTextAddress("ResourceBarUpkeepText",0)
    set panelAddress=GetFrameTextAddress("ResourceBarSupplyText",0)
Cool, is perfect to replace some multiboards fields for me (like kill/death/assist) you made my day :ogre_love: haha
 
Top