1. Updated Resource Submission Rules: All model & skin resource submissions must now include an in-game screenshot. This is to help speed up the moderation process and to show how the model and/or texture looks like from the in-game camera.
    Dismiss Notice
  2. Hey guys, we've posted the Results for the 30th Modeling Contest. Check them out!
    Dismiss Notice
  3. The 15th Mini-Mapping Contest came to an end. The Secrets of Warcraft 3 are soon to be revealed! Come and vote in the public poll for your favorite maps.
    Dismiss Notice
  4. The 12th incarnation of the Music Contest is LIVE! The theme is Synthwave. Knight Rider needs a song to listen to on his journey. You should definitely have some fun with this theme!
    Dismiss Notice
  5. Join other hivers in a friendly concept-art contest. The contestants have to create a genie coming out of its container. We wish you the best of luck!
    Dismiss Notice
  6. Check out the Staff job openings thread.
    Dismiss Notice
Dismiss Notice
60,000 passwords have been reset on July 8, 2019. If you cannot login, read this.

[vJASS] FileIO

Discussion in 'Submissions' started by TriggerHappy, Aug 1, 2018.

  1. TriggerHappy

    TriggerHappy

    Code Moderator

    Joined:
    Jun 23, 2007
    Messages:
    3,633
    Resources:
    22
    Spells:
    11
    Tutorials:
    2
    JASS:
    9
    Resources:
    22
    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:

    Code (vJASS):
    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:

    Code (vJASS):

    // 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!
     
     

    Attached Files:

    Last edited: Jun 2, 2019
  2. TriggerHappy

    TriggerHappy

    Code Moderator

    Joined:
    Jun 23, 2007
    Messages:
    3,633
    Resources:
    22
    Spells:
    11
    Tutorials:
    2
    JASS:
    9
    Resources:
    22
    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: Aug 3, 2018
  3. TriggerHappy

    TriggerHappy

    Code Moderator

    Joined:
    Jun 23, 2007
    Messages:
    3,633
    Resources:
    22
    Spells:
    11
    Tutorials:
    2
    JASS:
    9
    Resources:
    22
    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.
     
  4. Zwiebelchen

    Zwiebelchen

    Joined:
    Sep 17, 2009
    Messages:
    6,786
    Resources:
    11
    Models:
    4
    Maps:
    1
    Spells:
    1
    Tutorials:
    1
    JASS:
    4
    Resources:
    11
    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: Sep 20, 2018
  5. TriggerHappy

    TriggerHappy

    Code Moderator

    Joined:
    Jun 23, 2007
    Messages:
    3,633
    Resources:
    22
    Spells:
    11
    Tutorials:
    2
    JASS:
    9
    Resources:
    22
    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).
     
  6. BizzaroFukuro

    BizzaroFukuro

    Joined:
    Dec 11, 2009
    Messages:
    93
    Resources:
    2
    Maps:
    2
    Resources:
    2
    Does this still work in 1.30.4?
     
  7. IcemanBo

    IcemanBo

    Joined:
    Sep 6, 2013
    Messages:
    6,111
    Resources:
    22
    Maps:
    3
    Spells:
    11
    Template:
    1
    Tutorials:
    4
    JASS:
    3
    Resources:
    22
  8. BizzaroFukuro

    BizzaroFukuro

    Joined:
    Dec 11, 2009
    Messages:
    93
    Resources:
    2
    Maps:
    2
    Resources:
    2
    Ooooh, the files are created in a different special directory now. Probably should be mentioned in OP.
    Code (vJASS):
    "%USERNAME%\\Documents\\Warcraft III\\CustomMapData"

    Totally wasted several hours, because I am so S-M-R-T.
     
    Last edited: May 10, 2019
  9. Jampion

    Jampion

    JASS Reviewer

    Joined:
    Mar 25, 2016
    Messages:
    1,285
    Resources:
    0
    Resources:
    0
    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:
    Code (vJASS):

    // 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:
    Code (vJASS):

    // Make sure the output is valid
                if (output == original) then
                    set output = null
                    return output
                endif
     
     
  10. Av3n

    Av3n

    Joined:
    Jul 18, 2005
    Messages:
    296
    Resources:
    2
    Tutorials:
    2
    Resources:
    2
    I'm wondering if this method isn't functioning as intended on patch 1.31?

    This is because the toy example provided on the thread when executed on 1.31 produces an empty string instead of "hello".
     
    Last edited: May 31, 2019
  11. TriggerHappy

    TriggerHappy

    Code Moderator

    Joined:
    Jun 23, 2007
    Messages:
    3,633
    Resources:
    22
    Spells:
    11
    Tutorials:
    2
    JASS:
    9
    Resources:
    22
    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.