Jump to content


Photo

BLU spell checklist script


    14 replies to this topic

    #1 shinpad

    shinpad

      Newbie

    • Members
    • Pip
    • 7 posts

      Posted 01 October 2015 - 01:00 AM

      I know it's probably an unimpressive script for you veterans but this is my very first and I'm proud of myself.

       

      I always found it difficult to keep track of which BLU spells I still needed to learn.  I made a simple script that tells you what spells you have and what spells you must still learn.

       

      I don't like that since I'm a newbie to lua I did this in three loops.  I'm sure there's a more efficient way so please tell me your feedback so I can learn.

       

      Cheers

       

      res = require('resources')
      
      spells = res.spells:type('BlueMagic') 
      spells_have = windower.ffxi.get_spells()
      
      windower.add_to_chat(24, "+----------------------------------------+")
      windower.add_to_chat(24, "    Simple BLU Spell Checklist by Jess")
      windower.add_to_chat(24, "+----------------------------------------+")
      
      count = 0
      total = 0
      for i,v in pairs(spells) do
      	if spells_have[v.id] then
      		count = count + 1
      	end
      	total = total + 1
      end
      num_left = total - count
      
      have_percent = string.format("%.1f",(count/total)*100)
      missing_percent = string.format("%.1f",(num_left/total)*100)
      
      windower.add_to_chat(2, "Blue Magic you have: "..count.."/"..total.." ("..have_percent.."%)")
      
      for i,v in pairs(spells) do
      	if spells_have[v.id] then
      		windower.add_to_chat(7, " - You have "..v.en)
      	end
      end
      
      windower.add_to_chat(2, "Blue Magic you still need to learn: "..num_left.."/"..total.." ("..missing_percent.."%)")
      
      for i,v in pairs(spells) do
      	if spells_have[v.id] then
      		-- do nothing
      	else
      		windower.add_to_chat(121, " - You DO NOT have "..v.en)
      	end
      end
      

       



      #2 Arcon

      Arcon

        Advanced Member

      • Windower Staff
      • 1189 posts
      • LocationMunich, Germany

      Posted 01 October 2015 - 11:48 AM

      This is a perfect application for sets. A set is an unordered collection of unique elements. It also has a few convenience functions which you could use there to make your code more concise.

       

      A quick sets usage example with our Lua library:

      require('sets')
       
      -- A capital S is used to initialize a set
      s1 = S{1,2,3}
      s2 = S{3,4,5}
       
      -- Can also initialize it from another table (notice the round parentheses this time, as opposed to the brackets before)
      s3 = S(windower.ffxi.get_spells())
       
      -- We can easily print out an entire set:
      print(s1)

       

      A set implements all common set operations:

      • Union: Combination of all elements from two sets, duplicates will be ignored (+)

         

         

      • Intersection: Collection of all elements that exist in both sets (*)

         

         

      • Set difference: Filtering one set off all elements from another set (-)

         

         

      • Counting: Returns the number of elements in a set (#)

       

       

      -- This will print {1, 2, 3, 4, 5} (maybe in a different order, since order is not preserved)
      print(s1 + s2)
       
      -- This will print {3}, since that is the only element both sets have in common
      print(s1 * s2)
       
      -- This will print {1, 2}, since 3 is part of the second set and will be removed
      print(s1 - s2)
       
      -- This will print 3, since the first set has three elements
      print(#s1)

       

      You can adjust your code in various places using that. First we can create a set of all BLU spells. To do that we query the resources the same way you did (to filter for type "BlueMagic"), but then we call the keyset function. That function returns a set of all keys of the provided table, which in our case means the spell IDs:

      spells = res.spells:type('BlueMagic'):keyset()

       

      Now we use the code from earlier to get a set of all spell keys the player posesses. One thing we need to adjust is that we are interested in the table keys, not the table values, so, like above, we need to call the keyset function:

      spells_have = T(windower.ffxi.get_spells()):keyset()

       

      The "T" in front of it is just there so we can call keyset. Without it we would have to write table.keyset(windower.ffxi.get_spells()). Why this is is a bit more complicated, and I can explain it another time.

       

      To get the set of all spells we are still missing we simply subtract the set we have from the set of all spells. Similarly we can obtain the set of BLU spells we do have using simple set operations:

      missing = spells - spells_have
      have = spells * spells_have

       

      To print them we can iterate the respective loops. We can use the iterator interface the sets provide to make it a bit more convenient:

      for spell in missing:it() do
          windower.add_to_chat(121, " - You DO NOT have " .. res[spell].name)
      end
       
      for spell in have:it() do
          windower.add_to_chat(7, " - You have " .. res[spell].name)
      end

       

      All in all we could replace your code with this (I also adjusted the formatting string a bit to show you how you can easily make longer strings work without concatenation):

      require('sets')
      res = require('resources')
       
      windower.add_to_chat(24, "+----------------------------------------+")
      windower.add_to_chat(24, "    Simple BLU Spell Checklist by Jess")
      windower.add_to_chat(24, "+----------------------------------------+")
       
      spells = res.spells:type('BlueMagic'):keyset()
      spells_have = T(windower.ffxi.get_spells()):keyset()
       
      missing = spells - spells_have
      have = spells * spells_have
       
      windower.add_to_chat(2, "Blue Magic you have: %i/%i (%.1f%%)":format(#have, #spells, 100*#have/#spells))
       
      for spell in have:it() do
          windower.add_to_chat(7, " - You have " .. res[spell].name)
      end
       
      windower.add_to_chat(2, "Blue Magic you still need to learn: %i/%i (%.1f%%)":format(#missing, #spells, 100*#missing/#spells))
       
      for spell in missing:it() do
          windower.add_to_chat(121, " - You DO NOT have " .. res[spell].name)
      end


      #3 shinpad

      shinpad

        Newbie

      • Members
      • Pip
      • 7 posts

        Posted 01 October 2015 - 12:52 PM

        Excellent.  Thanks for taking the time to teach me!

         

        I'll test it out tonight.



        #4 Kainsin

        Kainsin

          Member

        • Members
        • PipPip
        • 27 posts

          Posted 09 October 2015 - 08:21 PM

          I'm stealing this script as I'm working on learning BLU spells. :)



          #5 Kainsin

          Kainsin

            Member

          • Members
          • PipPip
          • 27 posts

            Posted 09 October 2015 - 08:51 PM

            Hmm, having a little issue with this.

             

            The # doesn't seem to be working right, it's returning 0 for a lot of the tests. #spells_have is returning a valid number but #spells is just 0, even though there's data there when I print it.

             

            The intersection operation seems to be working alright but the difference one is not. missing appears to be empty when iterating through it.

             

            Your print methods have a slight error there as well, should be res.spells[spell].name instead of res[spell].name.

             

            Otherwise it looks good. Working on a quick addon that plays a sound when it sees a BLU spell I don't have in the chatlog so that I can AFK much easier. :P



            #6 Kainsin

            Kainsin

              Member

            • Members
            • PipPip
            • 27 posts

              Posted 09 October 2015 - 09:32 PM

              Upon further debugging it seems as though set.length isn't being called at all with #set. I can manually call set:length() to get its length.

               

              - and * are working for set.diff and set.intersection, however rawget(s2, el) is always returning true... and I can assure you I don't have every BLU spell in there. So missing is empty and have contains everything.



              #7 trv

              trv

                Advanced Member

              • Members
              • PipPipPip
              • 34 posts

                Posted 09 October 2015 - 11:21 PM

                This checks out for me:

                 

                require('sets')
                res = require('resources')
                 
                windower.add_to_chat(24, "+----------------------------------------+")
                windower.add_to_chat(24, "    Simple BLU Spell Checklist by Jess")
                windower.add_to_chat(24, "+----------------------------------------+")
                 
                spells = res.spells:type('BlueMagic'):keyset()
                spells_have = windower.ffxi.get_spells()
                for k,v in pairs(spells_have) do
                    if not v then
                        spells_have[k] = nil
                    end
                end
                
                missing = spells - spells_have
                have = spells * spells_have
                
                spells_len = spells:length()
                have_len = have:length()
                missing_len = missing:length()
                
                windower.add_to_chat(2, "Blue Magic you have: %i/%i (%.1f%%)":format(have_len, spells_len, 100*have_len/spells_len))
                 
                for spell in have:it() do
                    windower.add_to_chat(7, " - You have " .. res.spells[spell].name)
                end
                 
                windower.add_to_chat(2, "Blue Magic you still need to learn: %i/%i (%.1f%%)":format(missing_len, spells_len, 100*missing_len/spells_len))
                 
                for spell in missing:it() do
                    windower.add_to_chat(121, " - You DO NOT have " .. res.spells[spell].name)
                end
                
                

                 

                 

                The __len metamethod was introduced in Lua 5.2, which windower initially used.



                #8 Kainsin

                Kainsin

                  Member

                • Members
                • PipPip
                • 27 posts

                  Posted 10 October 2015 - 09:28 PM

                  Yeah, I Googled a bit and found the same info, then printed out _VERSION to see that Windower's Lua version is 5.1.

                   

                  I tested out your changes and it worked for me. I'm not sure why set.intersection and set.diff weren't working for me before. :/



                  #9 Kainsin

                  Kainsin

                    Member

                  • Members
                  • PipPip
                  • 27 posts

                    Posted 10 October 2015 - 09:53 PM

                    Oh! I see now, you're nil'ing out the spells that the player doesn't have. I take it those spells are in there but just not set to nil?



                    #10 trv

                    trv

                      Advanced Member

                    • Members
                    • PipPipPip
                    • 34 posts

                      Posted 11 October 2015 - 01:10 AM

                      It's a result of some of the quirks of sets. The structure of windower.ffxi.get_spells() is [id] = boolean (true if you have the spell, false if you do not).

                       

                      function table.keyset(t)
                          local res = {}
                          if _libs.sets then
                              for key in pairs(t) do
                                  res[key] = true
                              end
                      
                              return setmetatable(res, _meta.S)
                          end
                      
                          local res = {}
                          local i = 0
                          for key in pairs(t) do
                              i = i + 1
                              res[i] = key
                          end
                      
                          if _libs.lists then
                              res.n = i
                          end
                      
                          return setmetatable(res, _libs.lists and _meta.L or _meta.T)
                      end
                      
                      
                      
                       

                       

                      Keyset returns a set containing all of the keys in a table, so it contains the ids in get_spells that correspond to false values as well.

                       

                      The more general problem is how to convert a table with a similar structure to get_spells into a set.

                       

                      function S(t)
                          t = t or {}
                          if class(t) == 'Set' then
                              return t
                          end
                      
                          local s = {}
                      
                          if class(t) == 'List' then
                              for _, val in ipairs(t) do
                                  s[val] = true
                              end
                          else
                              for _, val in pairs(t) do
                                  s[val] = true
                              end
                          end
                      
                          return setmetatable(s, _meta.S)
                      end
                      
                      

                      Trying S(windower.ffxi.get_spells()) won't work: you'll get a set equivalent to (probably) S{true, false}.

                       

                      T(windower.ffxi.get_spells()):keyset won't work (as stated).

                       

                      setmetatable(windower.ffxi.get_spells(), _meta.S) is usually sufficient. This skips the steps of converting the table to the format of a set, but tells Lua to look in the sets metatable for methods. The only problem with this is that sets can only contain one value (true), while the table returned by the previous code will contain two (true, false) assuming you don't know every spell in the game. Usually, that's not an issue. Unless I'm mistaken, all set operations will still be performed correctly, however the sets iterator behaves in an unintended way.

                       

                      function set.it(s)
                          local key = nil
                          return function()
                              key = next(s, key)
                              return key
                          end
                      end
                      
                      

                      Sets shouldn't contain false normally, but subverting the set converting functions let the values sneak through. The sets iterator doesn't know any better, so it iterates over the false values as well as the true values.

                       

                      To work around that, I removed the false values. A better solution might have been to iterate over them anyway, but check the value before printing anything.

                       

                      It also wasn't actually necessary to convert windower.ffxi.get_spells() to a set in this case. When Lua sees that you're using a non-table operator on a table, it checks to see if a table involved in the operation has a corresponding metakey. Since the other tables were already sets, Lua used the sets methods, and since windower.ffxi.get_spells()'s structure is pretty much a set, nothing went wrong.



                      #11 Arcon

                      Arcon

                        Advanced Member

                      • Windower Staff
                      • 1189 posts
                      • LocationMunich, Germany

                      Posted 11 October 2015 - 04:49 AM

                      A very detailed (and correct) analysis of both the functionality of the structures involved and the issue with my original approach to converting it to a set. There is, however, still a way to very simply get the desired set without an explicit for loop:

                       

                      spells_have = T(windower.ffxi.get_spells()):filter(boolean._true):keyset()

                       

                      boolean._true is a function that returns true if the provided value is true. table.filter filters a table based on a provided function. That way, of the original table that maps IDs to true or false, only the entries that map IDs to true remain in the table, which are precisely the spells you have. Of those, then, you can just make the key set as above.



                      #12 trv

                      trv

                        Advanced Member

                      • Members
                      • PipPipPip
                      • 34 posts

                        Posted 12 October 2015 - 03:42 AM

                        That's much more readable.

                        I'm fairly certain that Arcon has written a function for pretty much everything at this point.



                        #13 Kainsin

                        Kainsin

                          Member

                        • Members
                        • PipPip
                        • 27 posts

                          Posted 12 October 2015 - 12:27 PM

                          Gotta love filters. Playing around with Lua this weekend was pretty fun.



                          #14 Vaulout

                          Vaulout

                            Newbie

                          • Members
                          • Pip
                          • 1 posts

                            Posted 04 January 2017 - 02:20 PM

                            Total LUA noob, was wondering if there is a way to output the missing spells list to a file(xml, lua, txt whichever is easier) and if so what would i need to add to the LUA?



                            #15 trv

                            trv

                              Advanced Member

                            • Members
                            • PipPipPip
                            • 34 posts

                              Posted 04 January 2017 - 08:00 PM

                              Use the files library and write to a file anywhere there's an add_to_chat call. It would (probably?) look something like this:

                              files = require 'files'
                              
                              -- create a new file object
                              output_file = files.new('output.txt')
                              
                              -- make sure the file exists, and create it if it doesn't
                              if not output_file:exists() then output_file:create() end
                              
                              -- Write to the file:
                              -- Call files.write first so that any old output is overwritten
                              output_file:write('Spells you have:\n')
                              -- Now append the rest
                              output_file:append(have:concat('\n'))
                              output_file:append('\n\nSpells you do not have:\n')
                              output_file:append(missing:concat('\n'))
                              

                               

                              There are probably a few errors there. You'll have to play around with it to get it to work.






                              1 user(s) are reading this topic

                              0 members, 1 guests, 0 anonymous users