Autocall

Status
Not open for further replies.
Level 13
Joined
Nov 7, 2014
Messages
571
Autocall - automatic function calling from player chat commands

I think this library can be useful while developing a map. Exploring native functions or testing jass functions for edge cases (although manually) without having to do any hooking (parsing, registering, etc.) should save time in my opinion.

JASS:
//! novjass

Autocall - automatic function calling from player chat commands

Description:
    Autocall automates the process of calling functions from chat commands.
    Both natives and jass functions are supported. Jass functions have the advantage
    of being type checkable (using "bytecde introspection", finally a good use for BJ functions).


Syntax:

    Only chat commands that begin with '&' or '$' are processed:

    Variable declaration:
        $my_integer : integer
        $my_real : real
        $my_string : string
        $my_handle : handle
        $my_boolean : boolean

        variable declaration with initialization:
            $my_integer : integer = 1
            $my_real : real = 6.28
            $my_string : string = "stuff inside double quotes"
            $my_handle : handle = Player 0
            $my_boolean : boolean = true

    Predefined variables:
            $i: integer
            $r: real
            $s: string
            $h: handle
            $b: boolean


    Types:
        integer:
            can be specified in base 10, 16 and 256:
            $i = 126 -- base 10
            $i = 0x7E -- base 16 (hex)
            $i = 'Hpal' -- base 256

        real:
            real arguments have a '.':
            $r = 1.0
            $r = 6.28

        string:
            $s = "stuff inside double quotes"

        handle:
            $h = P(0) -- Player(0)
            $h = {6.28,2.71} -- Location(6.28, 2.71)
            $h = CreateUnit P(0) 'hpea' 0.0 0.0 270.0

        boolean:
            the following values denote boolean true:
            T, t, true

            the following vlaues denote boolean false:
            F, f, false


    Calling functions:
        with no arguments and no return value:
            &DoNothing

        with no arguments and a return value:
            $r = GetRandomDirectionDeg

        with an argument and no return value:
            &BJDebugMsg "foo"

        with arguments and a return value:
            $r = RMinBJ 1.5 2.5

        note:
            because we dont know the arguments and their types of native functions (without having a big list)
            calling native functions with incorrect arguments can result in a game crash
            (be careful with typos when calling natives)

            jass functions have the advantage of being type checkable and thus safe


Examples:
    $my_hero : handle = CreateUnit P(0) 'Hpal' -512.0 1024.0 270.0
    &SetHeroLevelBJ $my_hero 5 F
    creates a Paladin hero at location x:-512.0, y: 1024.0 facing 270.0 degrees
    and sets the hero level to 5 without showing eyecandy

    $g : handle = CreateNUnitsAtLoc 4 'hpea' P(1) {512.0,1024.0} 45.0
    $random_unit : handle = GroupPickRandomUnit $g
    &KillUnit $random_unit
    creates 4 blue peasants at location x: 512.0, y: 1024.0 facing 45.0 degrees and adds them to a unit group
    selectes a random unit from the unit group
    and kills the randomly selected peasant

    &Autocall_list_vars
    calls the public function list_vars defined in the Autocall library

//! endnovjass


library Autocall initializer init uses BytecodeBoilerplate

private function dd takes string s returns nothing
    call BJDebugMsg(s)
endfunction

globals
    public boolean error
endglobals
private function exit takes nothing returns nothing
    set error = true
    if 1 / 0 == 1 then
    endif
endfunction

private function unreachable takes string s returns nothing
    call dd("unreachable: " + s)
    call exit()
endfunction

private function unimplemented takes string s returns nothing
    call dd("unimplemented: " + s)
    call exit()
endfunction


//# +nosemanticerror
private function r2i takes real r returns integer
    return r
    return r // prevent jasshelper from inlining in non debug mode
endfunction
private function i2i takes integer i returns integer
    return i
    return i
endfunction
public function real_as_int takes real r returns integer
    return i2i(r2i(r))
endfunction

//# +nosemanticerror
private function i2r takes integer i returns real
    return i
    return i
endfunction
private function r2r takes real r returns real
    return r
    return r
endfunction
public function int_as_real takes integer i returns real
    return r2r(i2r(i))
endfunction

public function from_hex takes string s returns integer
    local string hex_digits = "0123456789ABCDEF"
    local integer result = 0
    local string ch
    local integer i
    local integer j

    set s = StringCase(s, /*upper: */ true)

    set i = 0
    loop
        set ch = SubString(s, i, i + 1)
        exitwhen ch == ""

        set j = 0
        loop
            exitwhen j > 15

            if ch == SubString(hex_digits, j, j + 1) then
                set result = result * 16 + j
                exitwhen true
            endif

            set j = j + 1
        endloop

        set i = i + 1
    endloop

    return result
endfunction

public function to_hex takes integer i returns string
    local string hex_digits = "0123456789ABCDEF"
    local string result = ""
    local integer n
    local integer d
    local integer t
    local boolean is_neg

    set is_neg = i < 0
    if is_neg then
        set i = i + 0x80000000
    endif

    set n = 1
    loop
        exitwhen n > 7

        set t = i / 16
        set d = i - t * 16
        set i = t

        set result = SubString(hex_digits, d, d + 1) + result

        set n = n + 1
    endloop

    if is_neg then
        set i = i + 0x8
    endif

    set result = SubString(hex_digits, i, i + 1) + result

    return result
endfunction

public function from_base_256 takes string oid returns integer
    local string map = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
    local integer result = 0
    local integer i
    local integer j
    local string c

    set i = 0
    loop
        set c = SubString(oid, i, i + 1)
        exitwhen c == ""

        set j = 0
        loop
            if c == SubString(map, j, j + 1) then
                set result = result*256 + (j + 33)
                exitwhen true
            else
                set j = j + 1
            endif
        endloop

        set i = i + 1
    endloop

    return result
endfunction

public function bool_to_str takes boolean b returns string
    if b then
        return "true"
    else
        return "false"
    endif
endfunction


globals
    private string array T
    private integer T_count = 0
endglobals
private function tokenize takes nothing returns nothing
    local string s = GetEventPlayerChatString()
    local integer si
    local integer i
    local string ch

    set T_count = 0
    set i = 0
    loop
        loop
            set ch = SubString(s, i, i + 1)
            exitwhen ch != " "
            set i = i + 1
        endloop

        exitwhen ch == ""
        set si = i

        if SubString(s, i, i + 1) == "\"" then
            set i = i + 1
            loop
                set ch = SubString(s, i, i + 1)

                if ch == "\\" then
                    set i = i + 1
                    if SubString(s, i, i + 1) == "\"" then
                        set i = i + 1
                    endif

                elseif ch == "\"" then
                    set i = i + 1
                    exitwhen true
                else
                    exitwhen ch == ""
                    set i = i + 1
                endif
            endloop
        else
            loop
                set i = i + 1
                set ch = SubString(s, i, i + 1)
                exitwhen ch == " " or ch == ""
            endloop
        endif

        set T_count = T_count + 1
        set T[T_count] = SubString(s, si, i)
    endloop
endfunction


globals
    private integer array bc
    private code bc_code
    private integer bc_offset
endglobals

globals
    private integer result_integer = 0xFF
    private integer result_integer_stid

    private real result_real = 0.0
    private integer result_real_stid
    private real result_real2 = 0.0
    private integer result_real2_stid

    private string result_string = ""
    private integer result_string_stid

    private handle result_handle = null
    private integer result_handle_stid

    private boolean result_boolean = false
    private integer result_boolean_stid

    // used for finding the address of a jass function's bytecode
    private code var_code = null
    private integer var_code_stid

    private integer empty_string_stid

    private integer Value_h_stid // the h member of struct Value

    private integer first_blizzardj_func_stid
    private integer second_string_handle_table_entry_stid // first is ""

    private integer Player_stid
endglobals

private function stid takes string s returns integer
    return stid_from_handle(s)
endfunction

private function result_vars_and_funcs_stid_init takes nothing returns nothing
    set bc[8190] = 0
    set bc_code = I2C(Memory[get_array_struct_from_name(SCOPE_PRIVATE + "bc")/4 + 12/4])

    set result_integer_stid = stid(SCOPE_PRIVATE + "result_integer")
    set result_real_stid = stid(SCOPE_PRIVATE + "result_real")
    set result_real2_stid = stid(SCOPE_PRIVATE + "result_real2")
    set result_string_stid = stid(SCOPE_PRIVATE + "result_string")
    set result_handle_stid = stid(SCOPE_PRIVATE + "result_handle")
    set result_boolean_stid = stid(SCOPE_PRIVATE + "result_boolean")

    set var_code_stid = stid(SCOPE_PRIVATE + "var_code")

    set empty_string_stid = stid("")

    // doesn't work because vJass randomises the name (different number of underscores)
    // set Value_h_stid = stid("s__Autocall__Value_h")
    // see Value_h_stid_init

    set first_blizzardj_func_stid = stid("BJDebugMsg")
    set second_string_handle_table_entry_stid = stid(I2SH(2))

    set Player_stid = stid("Player")
endfunction

private function X takes nothing returns integer
    set bc_offset = bc_offset + 1
    return bc_offset
endfunction
private function call_bc_begin takes nothing returns nothing
    set bc_offset = -1
endfunction
private function call_bc_end takes nothing returns nothing
    call ForForce(bj_FORCE_PLAYER[0], bc_code)
endfunction


private /*enum*/ struct Value_Type extends array
    static constant integer Whoknows = 0x00
    static constant integer Integer = 0x04 // digits: 0 .. 9 (base 10) or 'XXXX' (base 256) or 0x89ABCDEF (base 16/hex)
    static constant integer Real = 0x05 // only digits and a single dot: 6.28
    static constant integer String = 0x06 // "stuff inside double quotes"
    static constant integer Handle = 0x07 // point/location literals: {6.28,2.71}, player literals: P(0 .. 15) and stuff returned from native functions different from integers, reals, etc.
    static constant integer Boolean = 0x08 // true = T, t, true, false = F, f, false
endstruct

globals
    private string array value_type_to_string
endglobals
private function value_type_to_string_init takes nothing returns nothing
    set value_type_to_string[0] = "nothing"
    set value_type_to_string[Value_Type.Integer] = "integer"
    set value_type_to_string[Value_Type.Real] = "real"
    set value_type_to_string[Value_Type.String] = "string"
    set value_type_to_string[Value_Type.Handle] = "handle"
    set value_type_to_string[Value_Type.Boolean] = "boolean"
endfunction
private function value_type_to_str takes integer value_type returns string
    return value_type_to_string[value_type]
endfunction
private function value_type_from_str takes string s returns integer
    if s == "integer" then
        return Value_Type.Integer
    elseif s == "real" then
        return Value_Type.Real
    elseif s == "string" then
        return Value_Type.String
    elseif s == "handle" then
        return Value_Type.Handle
    elseif s == "boolean" then
        return Value_Type.Boolean
    else
        return Value_Type.Whoknows
    endif
endfunction

private keyword Variable
private keyword Value

private function Value_h_stid_init takes nothing returns nothing
    set Value(0).h = null
    set Value_h_stid = Memory[C2I(function Value_h_stid_init)/4 + (8 + 8 + 8 + 8 + 4)/4]
endfunction

private function var_get_value takes Variable var returns nothing
    local integer ty

    if var == 0 then
        call dd("error: var_get_value var == 0")
        call exit()
    endif

    call call_bc_begin()

    set ty = var.ty
    if ty == Value_Type.Integer then
        set bc[X()] = 0x0E010400
        set bc[X()] = var.name_stid
        set bc[X()] = 0x11010000
        set bc[X()] = result_integer_stid

    elseif ty == Value_Type.Real then
        set bc[X()] = 0x0E010500
        set bc[X()] = var.name_stid
        set bc[X()] = 0x11010000
        set bc[X()] = result_real_stid

    elseif ty == Value_Type.String then
        set bc[X()] = 0x0E010600
        set bc[X()] = var.name_stid
        set bc[X()] = 0x11010000
        set bc[X()] = result_string_stid

    elseif ty == Value_Type.Handle then
        set bc[X()] = 0x0E010700
        set bc[X()] = var.name_stid
        set bc[X()] = 0x11010000
        set bc[X()] = result_handle_stid

    elseif ty == Value_Type.Boolean then
        set bc[X()] = 0x0E010800
        set bc[X()] = var.name_stid
        set bc[X()] = 0x11010000
        set bc[X()] = result_boolean_stid

    else
        call unimplemented("var_get_value Value_Type ty: " + I2S(ty))
    endif

    set bc[X()] = 0x27000000
    set bc[X()] = 0x00000000

    call call_bc_end()
endfunction

private function var_set_value takes Variable var, Value value returns nothing
    local integer ty

    if var.ty != value.ty then
        call dd("error: cannot assign variable " + var.name + " (" + value_type_to_str(var.ty) + ") to value " + value.to_str() + " (" + value_type_to_str(value.ty) + ")")
        call exit()
    endif

    call call_bc_begin()

    set ty = value.ty
    if ty == Value_Type.Integer then
        set bc[X()] = 0x0C010400
        set bc[X()] = value.i
        set bc[X()] = 0x11010000
        set bc[X()] = var.name_stid

    elseif ty == Value_Type.Real then
        set bc[X()] = 0x0C010500
        set bc[X()] = real_as_int(value.r)
        set bc[X()] = 0x11010000
        set bc[X()] = var.name_stid

    elseif ty == Value_Type.String then
        set bc[X()] = 0x0C010600
        set bc[X()] = stid(value.s)
        set bc[X()] = 0x11010000
        set bc[X()] = var.name_stid

    elseif ty == Value_Type.Handle then
        set bc[X()] = 0x0C020400
        set bc[X()] = integer(value)
        set bc[X()] = 0x10010207
        set bc[X()] = Value_h_stid
        set bc[X()] = 0x11010000
        set bc[X()] = var.name_stid

    elseif ty == Value_Type.Boolean then
        set bc[X()] = 0x0C010800
        if value.b then
            set bc[X()] = 0x00000001
        else
            set bc[X()] = 0x00000000
        endif
        set bc[X()] = 0x11010000
        set bc[X()] = var.name_stid

    else
        call unimplemented("var_set_value Value_Type ty: " + I2S(ty))
    endif

    set bc[X()] = 0x27000000
    set bc[X()] = 0x00000000

    call call_bc_end()
endfunction


globals
    private hashtable ht = InitHashtable()
endglobals
private function get_var_or_null takes string name returns Variable
    return Variable(LoadInteger(ht, 0, SH2I(name)))
endfunction
private function set_var_exist takes Variable var returns nothing
    call SaveInteger(ht, 0, SH2I(var.name), var)
endfunction


private struct Value
    Value_Type ty

    integer i // set only when ty == Value_Type.Integer
    real r // when ty == Value_Type.Real
    string s // when ty == Value_Type.String
    handle h // when ty == Value_Type.Handle
    boolean b // when ty == Value_Type.Boolean

    static method public_allocate takes nothing returns thistype
        return allocate()
    endmethod

    static method from_str takes string s returns thistype
        local thistype this = allocate()
        local integer slen = StringLength(s)
        local Variable var
        local integer ty
        local integer i
        local string first_char
        local string last_char
        local string c
        local integer sign
        local real x
        local real y

        set first_char = SubString(s, 0, 1)

        if first_char == "$" then
            set var = get_var_or_null(s)
            if var == 0 then
                call dd("error: use of undeclared variable " + s)
                call exit()
            endif

            set this.ty = var.ty
            call var_get_value(var)

            set ty = var.ty
            if ty == Value_Type.Integer then
                set this.i = result_integer

            elseif ty == Value_Type.Real then
                set this.r = result_real

            elseif ty == Value_Type.String then
                set this.s = result_string

            elseif ty == Value_Type.Handle then
                set this.h = result_handle

            elseif ty == Value_Type.Boolean then
                set this.b = result_boolean

            else
                call unreachable("Value.from_str Value_Type ty: " + I2S(ty))
            endif

            return this
        endif

        set c = first_char
        if c == "-" then
            set sign = -1
            set s = SubString(s, 1, slen)
            set c = SubString(s, 0, 1)
        else
            set sign = 1
        endif

        if c == "0" or c == "1" or c == "2" or c == "3" or c == "4" or c == "5" or c == "6" or c == "7" or c == "8" or c == "9" then
            if c == "0" then
                set c = SubString(s, 1, 2)
                if c == "x" or c == "X" then
                    if slen > 10 then
                        call dd("error: invalid base 16 integer literal: " + s + ", can be at most 8 digits") // 8 = 10 - 2 (0x prefix)
                        call exit()
                    endif

                    set this.ty = Value_Type.Integer
                    set this.i = from_hex(SubString(s, 2, slen)) * sign
                    return this
                endif
            endif

            set i = 1
            loop
                exitwhen i == slen or "." == SubString(s, i, i + 1)
                set i = i + 1
            endloop

            if i == slen then
                set this.ty = Value_Type.Integer
                set this.i = S2I(s) * sign
            else
                set this.ty = Value_Type.Real
                set this.r = S2R(s) * sign
            endif
            return this
        endif

        set last_char = SubString(s, slen - 1, slen)

        if first_char == "'" and last_char == "'" then
            if slen > 6 then
                call dd("error: invalid base 256 integer literal: " + s + ", can be at most 4 digits") // 4 = 6 - 2 (surrounding single quotes)
                call exit()
            endif

            set this.ty = Value_Type.Integer
            set this.i = from_base_256(SubString(s, 1, slen - 1))
            return this
        endif

        if first_char == "\"" then
            set this.ty = Value_Type.String
            if last_char == "\"" then
                set this.s = SubString(s, 1, slen - 1)
            else
                set this.s = SubString(s, 1, slen)
            endif
            return this
        endif

        if first_char == "{" and last_char == "}" then
            set i = 1
            loop
                set c = SubString(s, i, i + 1)
                exitwhen c == "," or c == "}"
                set i = i + 1
            endloop

            if c == "}" then
                call dd("error: invalid point literal: " + s + ", missing ','")
                call exit()

            else // if c == "," then
                set this.ty = Value_Type.Handle
                set x = S2R(SubString(s, 1, i))
                set y = S2R(SubString(s, i + 1, slen - 1))
                set this.h = Location(x, y)
                return this
            endif
        endif

        if SubString(s, 0, 2) == "P(" and last_char == ")" then
            set i = S2I(SubString(s, 2, slen - 1))
            if not (0 <= i and i <= 15) then
                call dd("error: invalid player literal: " + s + ", expected P(0 .. 15)")
                call exit()
            else
                set this.ty = Value_Type.Handle
                set this.h = Player(i)
                return this
            endif
        endif

        if s == "T" or s == "t" or s == "true" then
            set this.ty = Value_Type.Boolean
            set this.b = true
            return this
        elseif s == "F" or s == "f" or s == "false" then
            set this.ty = Value_Type.Boolean
            set this.b = false
            return this
        endif

        return 0
    endmethod

    method to_str takes nothing returns string
        local string result = ""
        local integer ty

        set ty = this.ty
        if ty == Value_Type.Integer then
            set result = result + I2S(this.i)

        elseif ty == Value_Type.Real then
            set result = result + R2S(this.r)

        elseif ty == Value_Type.String then
            set result = result + "\"" + this.s + "\""

        elseif ty == Value_Type.Handle then
            set result = result + I2S(GetHandleId(this.h))

        elseif ty == Value_Type.Boolean then
            set result = result + bool_to_str(this.b)

        else
            call unimplemented("Value.to_str Value_Type ty: " + I2S(ty))
        endif

        return result
    endmethod
endstruct

private function value_from_str_strict takes string s returns Value
    local Value result = Value.from_str(s)
    if result == 0 then
        call dd("error: unexpected " + s)
        call exit()
    endif
    return result
endfunction
private function value_from_str_or_null takes string s returns Value
    return Value.from_str(s)
endfunction


globals
    private Variable array vars
    private integer vars_count = 0
endglobals

private struct Variable
    string name // all variable names begin with '$' so that they don't collide with user declared ones
    integer name_stid
    Value_Type ty
    // value is stored... wherever blizzard stores global variables' values (the globar variables hashtable probably)

    static method create takes string name, Value_Type ty returns thistype
        local thistype this = get_var_or_null(name)

        if this != 0 then
            return this
        endif

        set this = allocate()
        set this.name = name
        set this.name_stid = stid(name)
        set this.ty = ty

        call set_var_exist(this)

        set vars_count = vars_count + 1
        set vars[vars_count] = this

        call call_bc_begin()

        if ty == Value_Type.Integer then
            set bc[X()] = 0x06040000
            set bc[X()] = this.name_stid
            set bc[X()] = 0x0C010400
            set bc[X()] = 0x00000000
            set bc[X()] = 0x11010000
            set bc[X()] = this.name_stid

        elseif ty == Value_Type.Real then
            set bc[X()] = 0x06050000
            set bc[X()] = this.name_stid
            set bc[X()] = 0x0C010500
            set bc[X()] = 0x00000000
            set bc[X()] = 0x11010000
            set bc[X()] = this.name_stid

        elseif ty == Value_Type.String then
            set bc[X()] = 0x06060000
            set bc[X()] = this.name_stid
            set bc[X()] = 0x0C010600
            set bc[X()] = empty_string_stid
            set bc[X()] = 0x11010000
            set bc[X()] = this.name_stid

        elseif ty == Value_Type.Handle then
            set bc[X()] = 0x06070000
            set bc[X()] = this.name_stid
            set bc[X()] = 0x0C010700 // use type 02 instead?
            set bc[X()] = 0x00000000
            set bc[X()] = 0x11010000
            set bc[X()] = this.name_stid

        elseif ty == Value_Type.Boolean then
            set bc[X()] = 0x06080000
            set bc[X()] = this.name_stid
            set bc[X()] = 0x0C010800
            set bc[X()] = 0x00000000
            set bc[X()] = 0x11010000
            set bc[X()] = this.name_stid

        else
            call unimplemented("Variable.create, Value_Type ty: " + I2S(ty))
        endif

        set bc[X()] = 0x27000000
        set bc[X()] = 0x00000000

        call call_bc_end()

        return this
    endmethod

    method to_str takes nothing returns string
        local string result = this.name + " : " + value_type_to_str(ty) + " = "
        local integer ty
        local Value value

        set value = Value.public_allocate()
        set value.ty = this.ty

        call var_get_value(this)

        set ty = this.ty
        if ty == Value_Type.Integer then
            set value.i = result_integer

        elseif ty == Value_Type.Real then
            set value.r = result_real

        elseif ty == Value_Type.String then
            set value.s = result_string

        elseif ty == Value_Type.Handle then
            set value.h = result_handle

        elseif ty == Value_Type.Boolean then
            set value.b = result_boolean

        else
            call unimplemented("Variable.to_str Value_Type ty: " + I2S(ty))
        endif

        set result = result + value.to_str()
        call value.destroy()

        return result
    endmethod
endstruct


globals
    private Value array args
    private integer args_count = 0
endglobals

private function args_destroy takes nothing returns nothing
    local integer i = 0
    loop
        exitwhen i >= args_count
        call args[i].destroy()
        set i = i + 1
    endloop
    set args_count = 0
endfunction

private function fill_args takes nothing returns nothing
    local Value arg
    local integer i

    call args_destroy()
    set i = 2 // T[1] is the function name
    loop
        exitwhen i > T_count
        set args[args_count] = value_from_str_strict(T[i])
        set args_count = args_count + 1
        set i = i + 1
    endloop
endfunction

private function push_args takes nothing returns nothing
    local Value arg
    local integer ty
    local integer i

    set i = 0
    loop
        exitwhen i >=  args_count
        set arg = args[i]

        set ty = arg.ty
        if ty == Value_Type.Integer then
            set bc[X()] = 0x0C010400
            set bc[X()] = arg.i

        elseif ty == Value_Type.Real then
            set bc[X()] = 0x0C010500
            set bc[X()] = real_as_int(arg.r)

        elseif ty == Value_Type.String then
            set bc[X()] = 0x0C010600
            set bc[X()] = stid(arg.s)

        elseif ty == Value_Type.Handle then
            set bc[X()] = 0x0C020400
            set bc[X()] = integer(arg)
            set bc[X()] = 0x10010207
            set bc[X()] = Value_h_stid

        elseif ty == Value_Type.Boolean then
            set bc[X()] = 0x0C010800
            if arg.b then
                set bc[X()] = 0x00000001
            else
                set bc[X()] = 0x00000000
            endif

        else
            call unimplemented("push_args Value_Type.ty: " + I2S(ty))
        endif

        set bc[X()] = 0x13010000
        set bc[X()] = 0x00000000

        set i = i + 1
    endloop
endfunction


private /*enum*/ struct Func_Type extends array
    static constant integer Non_Function = 0
    static constant integer Jass_Function = 1
    static constant integer Native_Function = 2
endstruct

private struct Func_Info extends array
    static Func_Type func_type
    static string func_name
    static integer func_name_stid
    static integer func_decl_addr // set only when func_type == Func_Type.Jass_Function
endstruct

private function func_info_from_name takes string func_name returns integer
    local integer func_name_stid = stid(func_name)
    local Func_Info result = 69105

    set result.func_name = func_name

    if func_name_stid < first_blizzardj_func_stid then
        set result.func_type = Func_Type.Native_Function
        set result.func_name_stid = func_name_stid
        set result.func_decl_addr = 0

        // Note: result.func_name_stid might not really be a native function,
        // if it isn't, then when we call it we are going to crash!

    elseif func_name_stid < second_string_handle_table_entry_stid then
        set result.func_type = Func_Type.Jass_Function
        set result.func_name_stid = func_name_stid

        // Note: result.func_name_stid might not really be a jass function,
        // if it isn't, then when we try to find its addr (with the 0x0F) we
        // are going to crash!

        call call_bc_begin()

        set bc[X()] = 0x0F010000
        set bc[X()] = func_name_stid
        set bc[X()] = 0x11010000
        set bc[X()] = var_code_stid
        set bc[X()] = 0x0E010400
        set bc[X()] = var_code_stid
        set bc[X()] = 0x11010000
        set bc[X()] = result_integer_stid
        set bc[X()] = 0x27000000
        set bc[X()] = 0x00000000

        call call_bc_end()
        set result.func_decl_addr = result_integer - 8

    else
        set result.func_type = Func_Type.Non_Function
        set result.func_name_stid = func_name_stid
        set result.func_decl_addr = 0
    endif

    return result
endfunction


private function call_function takes Func_Info fi, Variable result returns nothing
    local integer ty
    local Value result_value
    local boolean type_error
    local integer bc_arg_ty
    local Value arg
    local integer ret_type
    local string func_signature
    local string input_signature
    local integer offset
    local integer instr
    local integer opcode
    local integer i

    // T looks like:
    //     FunctionName arg1 arg2 ... argn
    // or
    //    &FunctionName arg1 arg2 ... argn
    //
    // i.e T[1] stores the function's name, T[2] the first argument, etc.

    if fi.func_type == Func_Type.Non_Function then
        call dd("error: function " + fi.func_name + " does not exist")
        call exit()
    endif

    if result != 0 then
        set result_value = Value.public_allocate()
        set result_value.ty = result.ty
    endif
    call fill_args()

    if fi.func_type == Func_Type.Native_Function then
        call call_bc_begin()

        call push_args()
        set bc[X()] = 0x15000000
        set bc[X()] = fi.func_name_stid

        if result != 0 then
            set bc[X()] = 0x11000000

            set ty = result.ty
            if ty == Value_Type.Integer then
                set bc[X()] = result_integer_stid

            elseif ty == Value_Type.Real then
                set bc[X()] = result_real_stid

            elseif ty == Value_Type.String then
                set bc[X()] = result_string_stid

            elseif ty == Value_Type.Handle then
                set bc[X()] = result_handle_stid

            elseif ty == Value_Type.Boolean then
                set bc[X()] = result_boolean_stid

            else
                call unimplemented("call_function(Func_Type.Jass_Function) Value_Type ty: " + I2S(ty))
            endif
        endif

        set bc[X()] = 0x27000000
        set bc[X()] = 0x00000000

        call call_bc_end()

    else // if fi.func_type == Func_Type.Jass_Function then
        set offset = fi.func_decl_addr

        if result != 0 then
            set ret_type = (Memory[offset/4] * 0x100) / 0x01000000

            if result.ty != ret_type then
                call dd("error: cannot assign the result (" + value_type_to_str(ret_type) + ") of function " + fi.func_name + " to variable " + result.name + " (" + value_type_to_str(result.ty) + ")")
                call exit()
            endif
        endif
        set offset = offset + 8

        set type_error = false
        set i = 0
        loop
            set instr = Memory[offset/4]
            set opcode = instr / 0x01000000
            exitwhen opcode != 0x08

            set bc_arg_ty = (instr * 0x100) / 0x01000000
            if value_type_to_str(bc_arg_ty) == null then
                call unimplemented("bc_arg_ty: " + I2S(bc_arg_ty))
                call exit()
            endif

            if i >= args_count then
                set type_error = true
                exitwhen true
            endif
            set arg = args[i]

            if bc_arg_ty != arg.ty then
                set type_error = true
                exitwhen true
            endif

            set offset = offset + 8
            set i = i + 1
        endloop

        if type_error or i != args_count then
            set func_signature = "takes"

            set offset = fi.func_decl_addr + 8
            loop
                set instr = Memory[offset/4]
                set opcode = instr / 0x01000000
                exitwhen opcode != 0x08

                set bc_arg_ty = (instr * 0x100) / 0x01000000
                set func_signature = func_signature + " " + value_type_to_str(bc_arg_ty)

                set offset = offset + 8
            endloop

            set input_signature = "given"
            set i = 0
            loop
                exitwhen i >= args_count
                set arg = args[i]
                set input_signature = input_signature + " " + value_type_to_str(arg.ty)
                set i = i + 1
            endloop

            call dd("error: " + fi.func_name + " mismatched arguments:")
            call dd(func_signature)
            call dd(input_signature)
            call exit()
        endif

        call call_bc_begin()

        call push_args()
        set bc[X()] = 0x16000000
        set bc[X()] = fi.func_name_stid
        set bc[X()] = 0x0B000000 + (args_count * 0x00010000)
        set bc[X()] = 0x00000000

        if result != 0 then
            set bc[X()] = 0x11000000

            set ty = result.ty
            if ty == Value_Type.Integer then
                set bc[X()] = result_integer_stid

            elseif ty == Value_Type.Real then
                set bc[X()] = result_real_stid

            elseif ty == Value_Type.String then
                set bc[X()] = result_string_stid

            elseif ty == Value_Type.Handle then
                set bc[X()] = result_handle_stid

            elseif ty == Value_Type.Boolean then
                set bc[X()] = result_boolean_stid

            else
                call unimplemented("call_function(Func_Type.Jass_Function) Value_Type ty: " + I2S(ty))
            endif
        endif

        set bc[X()] = 0x27000000
        set bc[X()] = 0x00000000

        call call_bc_end()
    endif

    if result != 0 then
        set ty = result.ty
        if ty == Value_Type.Integer then
            set result_value.i = result_integer

        elseif ty == Value_Type.Real then
            set result_value.r = result_real

        elseif ty == Value_Type.String then
            set result_value.s = result_string

        elseif ty == Value_Type.Handle then
            set result_value.h = result_handle

        else // if ty == Value_Type.Boolean then
            set result_value.b = result_boolean
        endif

        call var_set_value(result, result_value)
        call result_value.destroy()
    endif
endfunction

private function call_function_by_name takes string func_name, Variable result returns nothing
    local Func_Info fi = func_info_from_name(func_name) // func_name is without &
    call call_function(fi, result)
endfunction

globals
    public string chat_string
    public player trigger_player
endglobals

private function main takes nothing returns nothing
    local string first_char
    local string func_name
    local string var_name
    local Variable var
    local integer value_type
    local boolean is_var_assignment
    local Value value
    local Func_Info fi
    local integer ty
    local integer i

    set error = false
    set chat_string = GetEventPlayerChatString()
    set trigger_player = GetTriggerPlayer()

    call ForForce(bj_FORCE_PLAYER[0], function tokenize)

    set first_char = SubString(T[1], 0, 1)

    if first_char != "$" and first_char != "&" then
        return
    endif

    if first_char == "&" then
        set func_name = SubString(T[1], 1, StringLength(T[1]))
        call call_function_by_name(func_name, /*no result:*/ 0)
        return
    endif

    // first_char == "$"

    set var_name = T[1]

    if T_count == 1 then
        set var = get_var_or_null(var_name)
        if var == 0 then
            call dd("error: use of undeclared variable " + var_name)
            call exit()
        else
            call dd(var.to_str())
            return
        endif
    endif

    set is_var_assignment = false

    if T[2] == ":" then // variable declaration
        set value_type = value_type_from_str(T[3])

        if value_type == Value_Type.Whoknows then
            call dd("error: unknown type " + T[3] + ", expected: integer, real, string, handle, boolean, point or player")
            call exit()
        endif

        set var = get_var_or_null(var_name)
        if var != 0 then
            call dd("error: redeclaration of variable " + var.name + " : " + value_type_to_str(var.ty))
            call exit()
        endif

        set var = Variable.create(T[1], value_type)

        if T_count == 3 then
            return
        endif

        if T[4] == "=" then
            set is_var_assignment = true

            // T looks like:
            //     $var : <type> = ...
            // shift the tokens left so that it looks like:
            //    $var = ...
            //
            set i = 4
            loop
                exitwhen i > T_count
                set T[i - 2] = T[i]
                set i = i + 1
            endloop
            set T_count = T_count - 2
        endif

    elseif T[2] == "=" then
        set is_var_assignment = true

        set var = get_var_or_null(var_name)
        if var == 0 then
            call dd("error: use of undeclared variable " + var_name)
            call exit()
        endif
    endif

    if is_var_assignment then
        if T_count == 2 then
            call dd("error: expected a literal or function call after '='")
            call exit()
        endif

        set value = value_from_str_or_null(T[3])
        if value != 0 then
            call var_set_value(var, value)

        else
            set fi = func_info_from_name(T[3])
            if fi.func_type == Func_Type.Non_Function then
                call dd("error: unexpected " + T[3])
                call exit()
            endif

            // T looks like:
            //     $var = Function_Name ...
            // shift the tokens left so that it looks like:
            //     Function_Name ...
            //
            set i = 3
            loop
                exitwhen i > T_count
                set T[i - 2]  = T[i]
                set i = i + 1
            endloop
            set T_count = T_count - 2

            call call_function(fi, var)
            call dd(var.to_str())
        endif

        return
    endif

    call dd("error: syntax error: " + chat_string)
    call exit()
endfunction

private function init takes nothing returns nothing
    local trigger t

    call result_vars_and_funcs_stid_init()
    call Value_h_stid_init()
    call value_type_to_string_init()

    call Variable.create("$i", Value_Type.Integer)
    call Variable.create("$r", Value_Type.Real)
    call Variable.create("$s", Value_Type.String)
    call Variable.create("$h", Value_Type.Handle)
    call Variable.create("$b", Value_Type.Boolean)

    set t = CreateTrigger()

    call TriggerRegisterPlayerChatEvent(t, Player(0), "", false)
    // call TriggerRegisterPlayerChatEvent(t, Player(1), "", false)
    // call TriggerRegisterPlayerChatEvent(t, Player(2), "", false)
    // call TriggerRegisterPlayerChatEvent(t, Player(3), "", false)
    // call TriggerRegisterPlayerChatEvent(t, Player(4), "", false)
    // call TriggerRegisterPlayerChatEvent(t, Player(5), "", false)
    // call TriggerRegisterPlayerChatEvent(t, Player(6), "", false)
    // call TriggerRegisterPlayerChatEvent(t, Player(7), "", false)
    // call TriggerRegisterPlayerChatEvent(t, Player(8), "", false)
    // call TriggerRegisterPlayerChatEvent(t, Player(9), "", false)
    // call TriggerRegisterPlayerChatEvent(t, Player(10), "", false)
    // call TriggerRegisterPlayerChatEvent(t, Player(11), "", false)
    // call TriggerRegisterPlayerChatEvent(t, Player(12), "", false)

    call TriggerAddAction(t, function main)
endfunction

public function list_vars takes nothing returns nothing
    local Variable var
    local integer i

    set i = 1
    loop
        exitwhen i > vars_count
        set var = vars[i]
        call dd(var.to_str())
        set i = i + 1
    endloop

    if vars_count > 16 then
        call dd("hint: open the Log (F12)")
    endif
endfunction

endlibrary

Edit: Attached the "big list" of native wrappers. The safe version of a native starts with an 's':
CreateUnit => sCreateUnit
 

Attachments

  • bytecode-boilerplate.j
    10.3 KB · Views: 57
  • common-j-native-wrappers.j
    165.7 KB · Views: 48
  • common-ai-native-wrappers.j
    19.9 KB · Views: 50
Last edited:
Level 3
Joined
May 19, 2010
Messages
35
Pretty cool concept. Also it's good to see something done with all that bytecode hackery we did :thumbs_up:

But imho to really be usefull it would need to have many variables.
Something like $myhero : handle would define a new variable that could then be used $myhero = CreateUnit ....

Also nested evaluation would be immensly useful. So that I could write $KillUnit (&GetSelectedUnit). If we assume that every new call is between (...) then parsing shouldn't be too hard.

With that we are one big step closer to an ingame debugger.
 
Level 6
Joined
Jul 30, 2013
Messages
282
natives are listed in blizzard j or some place tho, and they are really simple to parse. (in fact i once tinkered on a project that needed parsing jass2 and it was basically trivial)

i cant see why not just have a hashtable to look up the return type of all natives.

now not to say that capturing values in variables is bad in anyway.. its rly useful, but in chat things usually need to be super user friendly or else people get too pissed to actually use it and if you just care about the side effect its useless cruff.

you could even .. if the memory reading is too fragile... add a compile step and generate a lookup table of ALL functions.. native AND user defined.
 
Level 3
Joined
May 19, 2010
Messages
35
I saw an interesting concept for a spell system by PitzerMike once.

There is a native (GetAbilityEffectyId to read the Art - Target field of an ability. Using this he wrote an interpreter to directly execute code that's written into an ability.

I used something simmilar for an TD I build. My towers all have upgrades that can be bought. These upgrades are based on the ability "Charge Gold and Lumber". The Art - Target field of the upgrade abilities looked something like this:
Code:
buy
1
400
#A00Z
$1
end
Meaning it's the first ("1") upgrade ("buy"), costs 400 gold and when it's bought then add ability 'A00Z' to the tower and execute the first ("$1") jass function.

Something like this could be much easier and much more powerfull with Autocall. Especially the "$1" part. I had to create an array with all possible callback functions (using vjass function interfaces) and register every function in it. Now we can just write the function name.
 
Level 13
Joined
Nov 7, 2014
Messages
571
But imho to really be usefull it would need to have many variables.
Yeah... good call! =)

Also nested evaluation would be immensly useful. So that I could write $KillUnit (&GetSelectedUnit)
I want to do as little parsing in jass as possible, because its not fun... that's why the syntax is restricted to only simple assignment ($my_handle = GetSlectedUnit, although GetSelectedUnit would probably return null in non EVENT_PLAYER_UNIT_SELECTED handler).
 
Level 6
Joined
Jul 30, 2013
Messages
282
if you did bother tho.. and to compile it to bitecode on the fly..
we could have a really nice jass REPL which would be rly awesome for debugging.

just being able to call any defined function is awesome tho.

nicely done!
 
Status
Not open for further replies.
Top