• 🏆 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!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

[vJASS] FileIO

With Patch 1.30 removing the "Allow Local Files" requirement for reading files, I decided to write a FileIO library designed around the latest patches. This is lightweight compared to other implementations as it does not require setting the player's name over and over again until you are finished reading the buffer.

System Code:

JASS:
library FileIO
/***************************************************************
*
*   v1.1.0, by TriggerHappy
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*
*   Provides functionality to read and write files.
*   _________________________________________________________________________
*   1. Requirements
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*       - Patch 1.29 or higher.
*       - JassHelper (vJASS)
*   _________________________________________________________________________
*   2. Installation
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*   Copy the script to your map and save it.
*   _________________________________________________________________________
*   3. API
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*       struct File extends array
*
*           static constant integer AbilityCount
*           static constant integer PreloadLimit
*
*           readonly static boolean ReadEnabled
*           readonly static integer Counter
*           readonly static integer array List
*
*           static method open takes string filename returns File
*           static method create takes string filename returns File
*
*           ---------
*
*           method write takes string value returns File
*           method read takes nothing returns string
*           method clear takes nothing returns File
*
*           method readEx takes boolean close returns string
*           method readAndClose takes nothing returns string
*           method readBuffer takes nothing returns string
*           method writeBuffer takes string contents returns nothing
*           method appendBuffer takes string contents returns nothing
*
*           method close takes nothing returns nothing
*
*           public function Write takes string filename, string contents returns nothing
*           public function Read takes string filename returns string
*
***************************************************************/
   
    globals
        // Enable this if you want to allow the system to read files generated in patch 1.30 or below.
        // NOTE: For this to work properly you must edit the 'Amls' ability and change the levels to 2
        // as well as typing something in "Level 2 - Text - Tooltip - Normal" text field.
        //
        // Enabling this will also cause the system to treat files written with .write("") as empty files.
        //
        // This setting is really only intended for those who were already using the system in their map
        // prior to patch 1.31 and want to keep old files created with this system to still work.
        private constant boolean BACKWARDS_COMPATABILITY = false
    endglobals
   
    private keyword FileInit
    struct File extends array
        static constant integer AbilityCount = 10
        static constant integer PreloadLimit = 200
       
        readonly static integer Counter = 0
        readonly static integer array List
        readonly static integer array AbilityList
       
        readonly static boolean ReadEnabled
 
        readonly string filename
        private string buffer
       
        static method open takes string filename returns thistype
            local thistype this = .List[0]
       
            if (this == 0) then
                set this = Counter + 1
                set Counter = this
            else
                set .List[0] = .List[this]
            endif
           
            set this.filename = filename
            set this.buffer = null
           
            debug if (this >= JASS_MAX_ARRAY_SIZE) then
            debug   call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0, 120, "FileIO(" + filename + ") WARNING: Maximum instance limit " + I2S(JASS_MAX_ARRAY_SIZE) + " reached.")
            debug endif
           
            return this
        endmethod
       
        // This is used to detect invalid characters which aren't supported in preload files.
        static if (DEBUG_MODE) then
            private static method validateInput takes string contents returns string
                local integer i = 0
                local integer l = StringLength(contents)
                local string ch = ""
                loop
                    exitwhen i >= l
                    set ch = SubString(contents, i, i + 1)
                    if (ch == "\\") then
                        return ch
                    elseif (ch == "\"") then
                        return ch
                    endif
                    set i = i + 1
                endloop
                return null
            endmethod
        endif
        method write takes string contents returns thistype
            local integer i = 0
            local integer c = 0
            local integer len = StringLength(contents)
            local integer lev = 0
            local string prefix = "-" // this is used to signify an empty string vs a null one
            local string chunk
            debug if (.validateInput(contents) != null) then
            debug   call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0, 120, "FileIO(" + filename + ") ERROR: Invalid character |cffffcc00" + .validateInput(contents) + "|r")
            debug   return this
            debug endif
           
            set this.buffer = null
           
            // Check if the string is empty. If null, the contents will be cleared.
            if (contents == "") then
                set len = len + 1
            endif
           
            // Begin to generate the file
            call PreloadGenClear()
            call PreloadGenStart()
            loop
                exitwhen i >= len
               
                debug if (c >= .AbilityCount) then
                debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0, 120, "FileIO(" + filename + ") ERROR: String exceeds max length (" + I2S(.AbilityCount * .PreloadLimit) + ").|r")
                debug endif
               
                set lev = 0
                static if (BACKWARDS_COMPATABILITY) then
                    if (c == 0) then
                        set lev = 1
                        set prefix = ""
                    else
                        set prefix = "-"
                    endif
                endif
               
                set chunk = SubString(contents, i, i + .PreloadLimit)
                call Preload("\" )\ncall BlzSetAbilityTooltip(" + I2S(.AbilityList[c]) + ", \"" + prefix + chunk + "\", " + I2S(lev) + ")\n//")
                set i = i + .PreloadLimit
                set c = c + 1
            endloop
            call Preload("\" )\nendfunction\nfunction a takes nothing returns nothing\n //")
            call PreloadGenEnd(this.filename)
       
            return this
        endmethod
       
        method clear takes nothing returns thistype
            return this.write(null)
        endmethod
       
        private method readPreload takes nothing returns string
            local integer i = 0
            local integer lev = 0
            local string array original
            local string chunk = ""
            local string output = ""
           
            loop
                exitwhen i == .AbilityCount
                set original[i] = BlzGetAbilityTooltip(.AbilityList[i], 0)
                set i = i + 1
            endloop
           
            // Execute the preload file
            call Preloader(this.filename)
           
            // Read the output
            set i = 0
            loop
                exitwhen i == .AbilityCount
               
                set lev = 0
               
                // Read from ability index 1 instead of 0 if 
                // backwards compatability is enabled
                static if (BACKWARDS_COMPATABILITY) then
                    if (i == 0) then
                        set lev = 1
                    endif
                endif
               
                // Make sure the tooltip has changed
                set chunk = BlzGetAbilityTooltip(.AbilityList[i], lev)
               
                if (chunk == original[i]) then
                    if (i == 0 and output == "") then
                        return null // empty file
                    endif
                    return output
                endif
               
                // Check if the file is an empty string or null
                static if not (BACKWARDS_COMPATABILITY) then
                    if (i == 0) then
                        if (SubString(chunk, 0, 1) != "-") then
                            return null // empty file
                        endif
                        set chunk = SubString(chunk, 1, StringLength(chunk))
                    endif
                endif
               
                // Remove the prefix
                if (i > 0) then
                    set chunk = SubString(chunk, 1, StringLength(chunk))
                endif
               
                // Restore the tooltip and append the chunk
                call BlzSetAbilityTooltip(.AbilityList[i], original[i], lev)
               
                set output = output + chunk
               
                set i = i + 1
            endloop
           
            return output
        endmethod
       
        method close takes nothing returns nothing
            if (this.buffer != null) then
                call .write(.readPreload() + this.buffer)
                set this.buffer = null
            endif
            set .List[this] = .List[0]
            set .List[0] = this
        endmethod
       
        method readEx takes boolean close returns string
            local string output = .readPreload()
            local string buf = this.buffer
       
            if (close) then
                call this.close()
            endif
           
            if (output == null) then
                return buf
            endif
           
            if (buf != null) then
                set output = output + buf
            endif
           
            return output
        endmethod
       
        method read takes nothing returns string
            return .readEx(false)
        endmethod
       
        method readAndClose takes nothing returns string
            return .readEx(true)
        endmethod
       
        method appendBuffer takes string contents returns thistype
            set .buffer = .buffer + contents
            return this
        endmethod
 
        method readBuffer takes nothing returns string
            return .buffer
        endmethod
       
        method writeBuffer takes string contents returns nothing
            set .buffer = contents
        endmethod
       
        static method create takes string filename returns thistype
            return .open(filename).write("")
        endmethod
   
        implement FileInit
    endstruct
    private module FileInit
        private static method onInit takes nothing returns nothing
            local string originalTooltip
           
            // We can't use a single ability with multiple levels because
            // tooltips return the first level's value if the value hasn't
            // been set. This way we don't need to edit any object editor data.
            set File.AbilityList[0] = 'Amls'
            set File.AbilityList[1] = 'Aroc'
            set File.AbilityList[2] = 'Amic'
            set File.AbilityList[3] = 'Amil'
            set File.AbilityList[4] = 'Aclf'
            set File.AbilityList[5] = 'Acmg'
            set File.AbilityList[6] = 'Adef'
            set File.AbilityList[7] = 'Adis'
            set File.AbilityList[8] = 'Afbt'
            set File.AbilityList[9] = 'Afbk'
           
            // Backwards compatability check
            static if (BACKWARDS_COMPATABILITY) then
                static if (DEBUG_MODE) then
                    set originalTooltip = BlzGetAbilityTooltip(File.AbilityList[0], 1)
                    call BlzSetAbilityTooltip(File.AbilityList[0], SCOPE_PREFIX, 1)
                    if (BlzGetAbilityTooltip(File.AbilityList[0], 1) == originalTooltip) then
                        call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0, 120, "FileIO WARNING: Backwards compatability enabled but \"" + GetObjectName(File.AbilityList[0]) + "\" isn't setup properly.|r")
                    endif
                endif
            endif
           
            // Read check
            set File.ReadEnabled = File.open("FileTester.pld").write(SCOPE_PREFIX).readAndClose() == SCOPE_PREFIX
        endmethod
    endmodule
    public function Write takes string filename, string contents returns nothing
        call File.open(filename).write(contents).close()
    endfunction
    public function Read takes string filename returns string
        return File.open(filename).readEx(true)
    endfunction
endlibrary
Examples:

JASS:
// Normal
local File file = File.open("test.txt")
call file.write("Hello World!")
call file.close()

// Inline
call BJDebugMsg(File.open("test.txt").write("hello ").appendBuffer("world").readAndClose()) // hello world
call File.open("test.txt").write("hello").close()
call BJDebugMsg(File.open("test.txt").appendBuffer(" world").appendBuffer("!").readAndClose()) // hello world!
 

Attachments

  • FileIO v1.0.3.w3m
    21.5 KB · Views: 276
  • FileIO v1.1.0.w3x
    47.3 KB · Views: 318
Last edited:
Updated, 1.0.1.
  • Replaced the extended tooltip natives with the normal ones. This prevents a bug where the "<" character could terminate the string.
  • In debug mode, contents being written to a file are checked for invalid characters before writing. Generated files do not like quotes and backslashes so you will be warned when using them.
  • Improved code readability and updated documentation.
EDIT:

Updated, 1.0.2.
  • Fixed a bug where reading would return null when not in debug mode.
 
Last edited:
Updated, 1.0.3.
  • Added appending functionality through the appendBuffer method. The buffer is written once the file is closed.
  • readBuffer and writeBuffer methods added.
  • File does it's own lightweight allocation and can now handle more than 8192 instances.
  • Removed file modes as it had issues with writing multiple files at once.
  • Removed destroy method.
 

Zwiebelchen

Hosted Project GR
Level 35
Joined
Sep 17, 2009
Messages
7,236
So amazing. Can't wait to finally implement this for a codeless save/load now that the AllowLocalFiles thing is the default for every player with patch 1.30.1.

Have you thought about natively adding an encryption feature into this lib? I guess most people will use this for codeless save/loads anyway.
 
Last edited:
Have you thought about natively adding an encryption feature into this lib? I guess most people will use this for codeless save/loads anyway.

I want to keep it fairly lightweight, something the old FileIO library wasn't. I do have to update to 1.0.4 soon though. I am still working out how appending should work (without hanging the game for a second).
 

Jampion

Code Reviewer
Level 15
Joined
Mar 25, 2016
Messages
1,327
The append function as provided in the test map did not work for me, if the file has an empty string. You could manually add one letter in the file and you could append something.

There is something weird going on the close method:
call .write(.readPreload() + this.buffer)

If the file is empty (...call BlzSetAbilityTooltip('Amls', "", 1)...), the tooltip is not changed, as the tooltip cannot be changed to "". So .readPreload() will return null:
JASS:
// Make sure the output is valid
            if (output == original) then
                return null
            endif
This is a problem, because the returned null value is not a null string, but just null. As a result it cannot be concatenated with another string. This means .readPreload() + this.buffer in total becomes null, even though this.buffer is not empty.

Instead of directly returning null, you can return a string that is null:
JASS:
// Make sure the output is valid
            if (output == original) then
                set output = null
                return output
            endif
 
Updated, 1.1.0.
  • Patch 1.31 support.
  • Increased file content max length from ~200 characters to 2,000.
  • Can now differentiate between files with empty strings ("") over null or non existent files. This doesn't work with BACKWARDS_COMPATABILITY enabled as it will use the old behavior where "" == null.
  • BACKWARDS_COMPATABILITY constant added for those who were using this system in their maps before patch 1.31 went live. More information in the code documentation.
 

NEL

NEL

Level 6
Joined
Mar 6, 2017
Messages
113
Does my code works without desync?

JASS:
struct main extends array


    private static method read takes player whichPlayer returns string
        local File Notebook
        local string Output

        if GetLocalPlayer() == whichPlayer then
            set Notebook = Notebook.open("myNotebook-autoSave.pld")
            set Output   = Notebook.readEx(true)
        endif

        return Output
    endmethod


    private static method write takes player whichPlayer, string message, boolean saveDisk returns nothing
        local File Notebook

        if GetLocalPlayer() == whichPlayer then
            if saveDisk then
                set  Notebook = Notebook.open("myNotebook-autoSave.pld").write(message)
                call Notebook.close()
            else
                call PreloadGenClear()
                call PreloadGenStart()
                call Preload(message)
                call PreloadGenEnd("myNotebook.pld")
            endif
        endif

    endmethod


endstruct
 
Level 3
Joined
Oct 13, 2012
Messages
28
Is there any specific reason why backslash and double quote is not escaped instead of rejecting whole string?

The same question I'd like to ask Blizzard, though. it works just because they didn't escape the characters.
 
Level 19
Joined
Jan 3, 2022
Messages
320
@Superz_Ze_Man Yes it works in multiplayer but because each player can have different data locally, you must synchronize it between players yourself.

FileIO v1.1.0-lua1.0.1​

Ported to Lua. Not thoroughly tested, but the basic tests provided by Trigger Happy worked correctly. Report bugs and problems if you find any :)
Lua:
-- FileIO v1.1.0-lua1.0.1
FileIO = {
    BACKWARDS_COMPATABILITY = false,
    AbilityList = {'Amls', 'Aroc', 'Amic', 'Amil', 'Aclf', 'Acmg', 'Adef', 'Adis', 'Afbt', 'Afbk'},
    AbilityCount = nil, -- set below
    PreloadLimit = 200,

    -- readonly
    -- some vJass stuff to ensure read/writes are from within one scope (code section)
    -- unused ReadEnabled = true,
    -- readonly
    -- unused Counter =
    -- readonly
    List = {},
    cc2Int = function (str)
        local n = 0
        local len = #str
        for i = len, 1, -1 do
            n = n + (str:byte(i,i) << 8*(len-i))
        end
        return n
    end,
    int2cc = function (int)
        return string.char((int & 0xff000000)>>24, (int & 0x00ff0000)>>16, (int & 0x0000ff00)>>8, int & 0x000000ff):match("[^\0]+")
    end,
}
FileIO.AbilityCount = #FileIO.AbilityList

function FileIO.hasInvalidChars(str)
    -- Original Jass FileIO did not permit double-quotes and backslash
    -- return str:find("[\0\"\\]") and true or false
    -- Instead we'll escape them properly before writing to file
    return str:find("[\0]") and true or false
end

do
    local subst = {
        -- only relevant if we saved it as  pure Lua
        -- the Jass2Lua transpiler works correctly on multiline Jass strings
        --["\n"] = "\\n",
        ["\\"] = "\\\\",
        ['"'] = '\"'
    }
    function FileIO.escapeChars(str)
        -- \n removed, no functional difference to 1.0.0
        return (str:gsub('["\\]', subst))
    end
end
function FileIO:open(fileName)
    if self.file then
        error("FileIO: Cannot use :open() on an existing file")
    end
    local file = {}
    setmetatable(file, {__index = self})
    file.fileName = fileName
    file.buffer = {}
 
    return file
end

function FileIO:write(contents)
    -- this is used to signify an empty string vs a null one
    local prefix = "-"
    if self.hasInvalidChars(contents) then
        error("FileIO: Invalid character in input: ".. tostring(contents))
    end
    contents = FileIO.escapeChars(contents)
 
    self.buffer = {}
 
    -- Begin file generation
    PreloadGenClear()
    PreloadGenStart()
    -- loop start
    local abilCount = 0
    local bufOffset = 1
 
    local len = #contents
    while bufOffset < len do
        --print(string.format("bufOffset=\037d, len=\037d", bufOffset, len))
        local level = 0
        if self.BACKWARDS_COMPATABILITY then
            if abilCount == 0 then
                level = 1
                prefix = ""
            else
                prefix = "-"
            end
        end
        if abilCount >= self.AbilityCount then
            error("FileIO: String exceeds max length: ".. tostring(self.AbilityCount*self.PreloadLimit))
        end
     
        local chunk = contents:sub(bufOffset, bufOffset+self.PreloadLimit-1)
        Preload(string.format(
            '" )\ncall BlzSetAbilityTooltip(\037d, "\037s", \037d)\n//',
            self.cc2Int(self.AbilityList[abilCount+1]),
            prefix .. chunk,
            level
        ))
        bufOffset = bufOffset + self.PreloadLimit
        abilCount = abilCount + 1
    end
    -- loop end
    Preload('" )\nendfunction\nfunction a takes nothing returns nothing\n //')
    PreloadGenEnd(self.fileName)
    return self
end

function FileIO:clear()
    return self:write("")
end

function FileIO:readPreload()
    local originalDesc = {}
    for n = 1, self.AbilityCount  do
        originalDesc[n] = BlzGetAbilityTooltip(self.cc2Int(self.AbilityList[n]), 0)
        --print("Saving orig ab desc: ".. n ..": "..  originalDesc[n])
    end
 
    -- Execute the preload file
    Preloader(self.fileName)
 
    local level = 0
    local chunk = ""
    local output = ""
    local i = 0
    while true do
        if i == self.AbilityCount then break end
        level = 0
        --print("readPreload i=".. i)
        if level == 0 and self.BACKWARDS_COMPATABILITY then
            level = 1
        end
     
        -- Make sure the tooltip has changed
        chunk = BlzGetAbilityTooltip(self.cc2Int(self.AbilityList[i+1]), level)
        --print("Loaded chunk=".. tostring(chunk))
        if chunk == originalDesc[i+1] then
            if i == 0 and output == "" then
                -- empty file
                return ""
            end
            return output
        end
     
        if not self.BACKWARDS_COMPATABILITY then
            if i == 0 then
                if chunk:sub(1,1) ~= "-" then
                    -- empty file
                    return ""
                end
                -- exclude first "-" symbol
                chunk = chunk:sub(2)
            end
        end
     
        -- remove prefix
        if i > 0 then
            chunk = chunk:sub(2)
        end
        -- restore original
        --print("Restoring original tooltip i=".. i)
        BlzSetAbilityTooltip(self.cc2Int(self.AbilityList[i+1]), originalDesc[i+1], level)
        output = output .. chunk
     
        i = i + 1
    end
 
    return output
end

function FileIO:create(fileName)
    return self:open(fileName):write("")
end

function FileIO:close()
    if #self.buffer > 0 then
        self:write(self:readPreload() .. table.concat(self.buffer))
    end
end

function FileIO:readEx(toClose)
    local output = self:readPreload()
    local buf = table.concat(self.buffer)
 
    if toClose then
        self:close()
    end
    if output == nil then
        return buf
    end
 
    if buf ~= nil then
        output = output .. buf
    end
 
    return output
end

function FileIO:read()
    return self:readEx(false)
end

function FileIO:readAndClose()
    return self:readEx(true)
end

function FileIO:appendBuffer(str)
    table.insert(self.buffer, str)
    return self
end

function FileIO:readBuffer()
    return table.concat(self.buffer)
end

function FileIO:writeBuffer(str)
    if type(str) == "table" then
        self.buffer = str
    elseif type(str) == "string" then
        self.buffer = {str}
    else
        error("Expected new buffer of type table/string, received: ".. type(str))
    end
    return nil
end

function FileIO:Write(fileName, text)
    self:open(fileName):write(text):close()
    return nil
end

function FileIO:Read(fileName)
    return self:open(fileName):readEx(true)
end

Lua:
function fiotest()
    FileIO:open("fiotest.txt"):write("hello"):close()
end

function fiotestOriginal()
    BJDebugMsg(FileIO:open("test1.txt"):write("hello "):appendBuffer("world"):readAndClose()) -- hello world
    FileIO:open("test2.txt"):write("hello"):close()
    BJDebugMsg(FileIO:open("test2.txt"):appendBuffer(" world"):appendBuffer("!"):readAndClose()) -- hello world!
    BJDebugMsg(FileIO:open("test3.txt"):write("weird world"):appendBuffer("\"\\!"):readAndClose()) -- weird world"\!
    BJDebugMsg(FileIO:open("test3.txt"):write("weird\nworld"):appendBuffer("\"\\!"):readAndClose()) -- weird world"\! (\n was rendered/read as a space)
end

Additional code snippet used for testing, pcall wrapper to catch errors properly
Lua:
-- safely call functions from other code and catch any errors, outputting them to all chat
-- Note: By default Reforged doesn't output any errors, they can only be found in memory dumps
function safeCall(...)
    return safeCallHandle(pcall(...))
end
-- This is needed due to how varargs work in Lua
function safeCallHandle(...)
    local arg = {...}
    local ok, err = arg[1], arg[2]
    if ok == true then
        return select(2, ...)
    else
        print("|cffee2222Error occurred: ")
        for line in tostring(err):gmatch("[^\n]+") do
            -- I think errors don't have stack traces here, so only single line :(
            print("|cffee2222".. line)
        end
        -- abort execution and point to above function
        error(tostring(err), 3)
    end
end
Usage notes:
Instead of FileIO.open(...) etc. Use FileIO:open(...) == FileIO.open(FileIO, ...), in Lua this is equivalent to passing the referenced FileIO table as the first argument ("self", similar to "this" in JS).

The API isn't the most intuitive to use, if you're looking for an IO library based on Preloads, create something else. Ask me if you need an explanation how it works.
Explanation how Preload-based IO works
 
Last edited:
Level 25
Joined
Feb 2, 2006
Messages
1,683
Does this still work with Reforged? I want to use it for my save/load system.

edit:
You have to sync the file content for Multiplayer support, right? Could you add some code to do this automatically using the new natives:
JASS:
EVENT_PLAYER_SYNC_DATA                 
native BlzTriggerRegisterPlayerSyncEvent           takes trigger whichTrigger, player whichPlayer, string prefix, boolean fromServer returns event
native BlzSendSyncData                             takes string prefix, string data returns boolean
native BlzGetTriggerSyncPrefix                     takes nothing returns string
native BlzGetTriggerSyncData                       takes nothing returns string

This would help us to write our own code.
I guess you have to store the content in a global variable and sync it with all other players.
You could allow passing a parameter of the player who's file you want to read and sync.

If this system still works with Reforged and has sync multiplayer support it should be approved here since it would be the base for ALL autoload save code systems. You could create some file like "auto.txt" which is then loaded automatically without typing any savecode.
 
Last edited:
Level 4
Joined
Aug 28, 2022
Messages
32
With Patch 1.30 removing the "Allow Local Files" requirement for reading files, I decided to write a FileIO library designed around the latest patches. This is lightweight compared to other implementations as it does not require setting the player's name over and over again until you are finished reading the buffer.

System Code:

JASS:
library FileIO
/***************************************************************
*
*   v1.1.0, by TriggerHappy
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*
*   Provides functionality to read and write files.
*   _________________________________________________________________________
*   1. Requirements
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*       - Patch 1.29 or higher.
*       - JassHelper (vJASS)
*   _________________________________________________________________________
*   2. Installation
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*   Copy the script to your map and save it.
*   _________________________________________________________________________
*   3. API
*   ¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
*       struct File extends array
*
*           static constant integer AbilityCount
*           static constant integer PreloadLimit
*
*           readonly static boolean ReadEnabled
*           readonly static integer Counter
*           readonly static integer array List
*
*           static method open takes string filename returns File
*           static method create takes string filename returns File
*
*           ---------
*
*           method write takes string value returns File
*           method read takes nothing returns string
*           method clear takes nothing returns File
*
*           method readEx takes boolean close returns string
*           method readAndClose takes nothing returns string
*           method readBuffer takes nothing returns string
*           method writeBuffer takes string contents returns nothing
*           method appendBuffer takes string contents returns nothing
*
*           method close takes nothing returns nothing
*
*           public function Write takes string filename, string contents returns nothing
*           public function Read takes string filename returns string
*
***************************************************************/
  
    globals
        // Enable this if you want to allow the system to read files generated in patch 1.30 or below.
        // NOTE: For this to work properly you must edit the 'Amls' ability and change the levels to 2
        // as well as typing something in "Level 2 - Text - Tooltip - Normal" text field.
        //
        // Enabling this will also cause the system to treat files written with .write("") as empty files.
        //
        // This setting is really only intended for those who were already using the system in their map
        // prior to patch 1.31 and want to keep old files created with this system to still work.
        private constant boolean BACKWARDS_COMPATABILITY = false
    endglobals
  
    private keyword FileInit
    struct File extends array
        static constant integer AbilityCount = 10
        static constant integer PreloadLimit = 200
      
        readonly static integer Counter = 0
        readonly static integer array List
        readonly static integer array AbilityList
      
        readonly static boolean ReadEnabled
 
        readonly string filename
        private string buffer
      
        static method open takes string filename returns thistype
            local thistype this = .List[0]
      
            if (this == 0) then
                set this = Counter + 1
                set Counter = this
            else
                set .List[0] = .List[this]
            endif
          
            set this.filename = filename
            set this.buffer = null
          
            debug if (this >= JASS_MAX_ARRAY_SIZE) then
            debug   call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0, 120, "FileIO(" + filename + ") WARNING: Maximum instance limit " + I2S(JASS_MAX_ARRAY_SIZE) + " reached.")
            debug endif
          
            return this
        endmethod
      
        // This is used to detect invalid characters which aren't supported in preload files.
        static if (DEBUG_MODE) then
            private static method validateInput takes string contents returns string
                local integer i = 0
                local integer l = StringLength(contents)
                local string ch = ""
                loop
                    exitwhen i >= l
                    set ch = SubString(contents, i, i + 1)
                    if (ch == "\\") then
                        return ch
                    elseif (ch == "\"") then
                        return ch
                    endif
                    set i = i + 1
                endloop
                return null
            endmethod
        endif
        method write takes string contents returns thistype
            local integer i = 0
            local integer c = 0
            local integer len = StringLength(contents)
            local integer lev = 0
            local string prefix = "-" // this is used to signify an empty string vs a null one
            local string chunk
            debug if (.validateInput(contents) != null) then
            debug   call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0, 120, "FileIO(" + filename + ") ERROR: Invalid character |cffffcc00" + .validateInput(contents) + "|r")
            debug   return this
            debug endif
          
            set this.buffer = null
          
            // Check if the string is empty. If null, the contents will be cleared.
            if (contents == "") then
                set len = len + 1
            endif
          
            // Begin to generate the file
            call PreloadGenClear()
            call PreloadGenStart()
            loop
                exitwhen i >= len
              
                debug if (c >= .AbilityCount) then
                debug call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0, 120, "FileIO(" + filename + ") ERROR: String exceeds max length (" + I2S(.AbilityCount * .PreloadLimit) + ").|r")
                debug endif
              
                set lev = 0
                static if (BACKWARDS_COMPATABILITY) then
                    if (c == 0) then
                        set lev = 1
                        set prefix = ""
                    else
                        set prefix = "-"
                    endif
                endif
              
                set chunk = SubString(contents, i, i + .PreloadLimit)
                call Preload("\" )\ncall BlzSetAbilityTooltip(" + I2S(.AbilityList[c]) + ", \"" + prefix + chunk + "\", " + I2S(lev) + ")\n//")
                set i = i + .PreloadLimit
                set c = c + 1
            endloop
            call Preload("\" )\nendfunction\nfunction a takes nothing returns nothing\n //")
            call PreloadGenEnd(this.filename)
      
            return this
        endmethod
      
        method clear takes nothing returns thistype
            return this.write(null)
        endmethod
      
        private method readPreload takes nothing returns string
            local integer i = 0
            local integer lev = 0
            local string array original
            local string chunk = ""
            local string output = ""
          
            loop
                exitwhen i == .AbilityCount
                set original[i] = BlzGetAbilityTooltip(.AbilityList[i], 0)
                set i = i + 1
            endloop
          
            // Execute the preload file
            call Preloader(this.filename)
          
            // Read the output
            set i = 0
            loop
                exitwhen i == .AbilityCount
              
                set lev = 0
              
                // Read from ability index 1 instead of 0 if
                // backwards compatability is enabled
                static if (BACKWARDS_COMPATABILITY) then
                    if (i == 0) then
                        set lev = 1
                    endif
                endif
              
                // Make sure the tooltip has changed
                set chunk = BlzGetAbilityTooltip(.AbilityList[i], lev)
              
                if (chunk == original[i]) then
                    if (i == 0 and output == "") then
                        return null // empty file
                    endif
                    return output
                endif
              
                // Check if the file is an empty string or null
                static if not (BACKWARDS_COMPATABILITY) then
                    if (i == 0) then
                        if (SubString(chunk, 0, 1) != "-") then
                            return null // empty file
                        endif
                        set chunk = SubString(chunk, 1, StringLength(chunk))
                    endif
                endif
              
                // Remove the prefix
                if (i > 0) then
                    set chunk = SubString(chunk, 1, StringLength(chunk))
                endif
              
                // Restore the tooltip and append the chunk
                call BlzSetAbilityTooltip(.AbilityList[i], original[i], lev)
              
                set output = output + chunk
              
                set i = i + 1
            endloop
          
            return output
        endmethod
      
        method close takes nothing returns nothing
            if (this.buffer != null) then
                call .write(.readPreload() + this.buffer)
                set this.buffer = null
            endif
            set .List[this] = .List[0]
            set .List[0] = this
        endmethod
      
        method readEx takes boolean close returns string
            local string output = .readPreload()
            local string buf = this.buffer
      
            if (close) then
                call this.close()
            endif
          
            if (output == null) then
                return buf
            endif
          
            if (buf != null) then
                set output = output + buf
            endif
          
            return output
        endmethod
      
        method read takes nothing returns string
            return .readEx(false)
        endmethod
      
        method readAndClose takes nothing returns string
            return .readEx(true)
        endmethod
      
        method appendBuffer takes string contents returns thistype
            set .buffer = .buffer + contents
            return this
        endmethod
 
        method readBuffer takes nothing returns string
            return .buffer
        endmethod
      
        method writeBuffer takes string contents returns nothing
            set .buffer = contents
        endmethod
      
        static method create takes string filename returns thistype
            return .open(filename).write("")
        endmethod
  
        implement FileInit
    endstruct
    private module FileInit
        private static method onInit takes nothing returns nothing
            local string originalTooltip
          
            // We can't use a single ability with multiple levels because
            // tooltips return the first level's value if the value hasn't
            // been set. This way we don't need to edit any object editor data.
            set File.AbilityList[0] = 'Amls'
            set File.AbilityList[1] = 'Aroc'
            set File.AbilityList[2] = 'Amic'
            set File.AbilityList[3] = 'Amil'
            set File.AbilityList[4] = 'Aclf'
            set File.AbilityList[5] = 'Acmg'
            set File.AbilityList[6] = 'Adef'
            set File.AbilityList[7] = 'Adis'
            set File.AbilityList[8] = 'Afbt'
            set File.AbilityList[9] = 'Afbk'
          
            // Backwards compatability check
            static if (BACKWARDS_COMPATABILITY) then
                static if (DEBUG_MODE) then
                    set originalTooltip = BlzGetAbilityTooltip(File.AbilityList[0], 1)
                    call BlzSetAbilityTooltip(File.AbilityList[0], SCOPE_PREFIX, 1)
                    if (BlzGetAbilityTooltip(File.AbilityList[0], 1) == originalTooltip) then
                        call DisplayTimedTextToPlayer(GetLocalPlayer(),0,0, 120, "FileIO WARNING: Backwards compatability enabled but \"" + GetObjectName(File.AbilityList[0]) + "\" isn't setup properly.|r")
                    endif
                endif
            endif
          
            // Read check
            set File.ReadEnabled = File.open("FileTester.pld").write(SCOPE_PREFIX).readAndClose() == SCOPE_PREFIX
        endmethod
    endmodule
    public function Write takes string filename, string contents returns nothing
        call File.open(filename).write(contents).close()
    endfunction
    public function Read takes string filename returns string
        return File.open(filename).readEx(true)
    endfunction
endlibrary
Examples:

JASS:
// Normal
local File file = File.open("test.txt")
call file.write("Hello World!")
call file.close()

// Inline
call BJDebugMsg(File.open("test.txt").write("hello ").appendBuffer("world").readAndClose()) // hello world
call File.open("test.txt").write("hello").close()
call BJDebugMsg(File.open("test.txt").appendBuffer(" world").appendBuffer("!").readAndClose()) // hello world!
Hi!!!!!

Do you know if it is possible via some process within the game, during meele matches against the computer, to send and receive information, other than by file? I'm trying to use this to train an AI with deep learning.
 
Level 19
Joined
Jan 3, 2022
Messages
320
@paulo101977 The standard game API (common.j file) does not provide any way, see Logging library: How to exfiltrate game data?
I suggest you take an old version (patch the balance files if needed), inject custom code in the game to hook some game state. Versions 1.30 and above are hard to "work with" I heard. Alternatively you could use 1.26 and the memhack exploit to extend the game from within.
 
Level 4
Joined
Aug 28, 2022
Messages
32
@paulo101977 The standard game API (common.j file) does not provide any way, see Logging library: How to exfiltrate game data?
I suggest you take an old version (patch the balance files if needed), inject custom code in the game to hook some game state. Versions 1.30 and above are hard to "work with" I heard. Alternatively you could use 1.26 and the memhack exploit to extend the game from within.
Thank you very much! I will try the memhack exploit. Do you know if there is any project dealing with this?
Because if there is, maybe I'll work with them to be able to extend the functionality to newer versions of wc3.

EDIT: From what I read here quickly, the memory exploit allows me to inject running code, but it might not be possible to do a realtime listener. Maybe I have to work on this exploit and create something for what I need.
 
Level 7
Joined
Sep 27, 2016
Messages
68
Yes it works in multiplayer but because each player can have different data locally, you must synchronize it between players yourself.
How i syncronize the data between players.. i tried the fileIO but it always crash when i playing multiplayer.. but if i play alone, its no proble.. please help me.. what should i do?
 
Level 25
Joined
Feb 2, 2006
Messages
1,683
I think you can synchronize it with the Blizzard natives I posted. You can sync any strings with BlzSendSyncData and the event. I guess you could read and write data with GetLocalPlayer for one player only:

JASS:
globals
       string loadedData = ""
endglobals

function Read takes nothing returns nothing
local string data = ""

if (GetLocalPlayer() == Player(0)) then
     set data = File.open("test.txt").appendBuffer(" world").appendBuffer("!").readAndClose() // hello world!
     call BlzSendSyncData("loaded", data)
endif

endfunction

...

function TriggerConditionLoad takes nothing returns boolean
set loadedData = BlzGetTriggerSyncData()
return false
endfunction

....

call BlzTriggerRegisterPlayerSyncEvent(whichTrigger, Player(0), "loaded", false)
call TriggerAddCondition(whichTrigger, Condition(function TriggerConditionLoad))

This is just pseudo code from the basic idea to use the sync natives. There should be a vJass library which requires FileIO and allows reading files from specific players only and writing them for specific players only (forces maybe).
 
Level 19
Joined
Jan 3, 2022
Messages
320
@Savantic Yes, Jass scripts are run in a separate limited environment for Preload files. Some functions will not work, but I don't think anybody has tested it. Due to buggy, breaking caching I urge you to test in old classic versions or 1.32.10.
Lua scripts have no such limitation but must be converted to be used among Preload files: Blizzard's hidden jass2lua transpiler
 
Top