Creating new PBS files

A tutorial on how to easily create new PBS files.

  1. Marin
    Pokémon Essentials Version:
    v17.2 ✅
    PBS files such as pokemon.txt, abilities.txt, items.txt, or moves.txt all follow a very simple layout. The script section called "Compiler" reads them and turns them into data files. When you make a change to a PBS file, the compiler will re-compile all PBS files and update the data files accordingly.

    PLEASE NOTE: THIS IS HOW I MAKE MY PBS FILES; NOT EVERYTHING YOU SEE IS HOW ESSENTIALS ITSELF DOES IT.

    Now, what if you want to make your own PBS file? It's important that you choose what layout your PBS file will have first. I'll cover two layouts; sections (pokemon.txt), and line-based with commas (moves.txt, items.txt)

    • It's a good idea to visualize what your PBS layout will look like first thing. For this tutorial, I'll be going with this:
      Code:
      [Joey]
      BaseMoney=32
      BattleMusic=Battle! Youngster
       
      [Zoey]
      BaseMoney=16
      BattleMusic=Battle! Lass
      
      For demonstration's sake, I'll call it my_new_pbs.txt.

      The first place we need to locate is def pbCompileAll. While we could probably get away with putting our method that compiles our new PBS file at the top of that list, I would put it at the bottom, just above yield(_INTL("Compiling messages")). We're gonna display that our new PBS file is being compiled in the game's name, so we need to yield (yields to Win32API.SetWindowText). I've also decided that the method that will compile our PBS file is going to be called pbCompileNewPBS. We've now got this:
      Code:
      yield(_INTL("Compiling my new PBS file"))
      pbCompileNewPBS
      Right above the last yield for messages.

      We now need to make the method and do our reading in there.

      I'm only going to show how to easily make these using Essentials methods, as they are small and easy to explain. An alternative would be just processing the string (f.read) yourself and processing it appropriately. Anyhow, this is one way to do it:
      Code:
      def pbCompileNewPBS
        sections = {}
        if File.exists?("PBS/my_new_pbs.txt")
      	File.open("PBS/my_new_pbs.txt", "r") do |f|
      	  pbEachFileSectionEx(f) do |section,name|
      		sections[name] = section
      	  end
      	end
        end
        save_data(sections,"Data/my_new_pbs.dat")
        $MyNewPBS = sections
        p $MyNewPBS
      end
      To see what the parsed PBS actually looks like, I added p $MyNewPBS. This shows me the following:
      [​IMG]

      We now have our data stored in $MyNewPBS. To access data in one of our sections, we simply do the following:
      Code:
      $MyNewPBS["Joey"]["BaseMoney"]
      or
      Code:
      $MyNewPBS["Zoey"]["BattleMusic"]
      or
      Code:
      $MyNewPBS["Joey"]
      This last one will simply give me all data of that section.

      As you'll notice, all keys and values in there are strings. We expect BaseMoney to be a number, so we'll need to add a bit of processing.
      Code:
        for key in sections.keys
      	sections[key]["BaseMoney"] = sections[key]["BaseMoney"].to_i
        end
        save_data(sections,"Data/my_new_pbs.dat")
        $MyNewPBS = sections
      
      What we're doing here, is looping through every section in $MyNewPBS and turning the BaseMoney field into a number.

      In full:
      Code:
      def pbCompileNewPBS
        sections = {}
        if File.exists?("PBS/my_new_pbs.txt")
      	File.open("PBS/my_new_pbs.txt", "r") do |f|
      	  pbEachFileSectionEx(f) do |section,name|
      		sections[name] = section
      	  end
      	end
        end
        for key in sections.keys
      	sections[key]["BaseMoney"] = sections[key]["BaseMoney"].to_i
        end
        save_data(sections,"Data/my_new_pbs.dat")
        $MyNewPBS = sections
      end
      We now have a fine hash, $MyNewPBS, if we compiled the game. If the game has not compiled, though, it will be nil. We need to change that. With knowledge from my tutorial about user-friendliness and Plug and Play, we can overwrite the method that initializes the start menu (which is always called no matter what), as follows (can be in any section underneath PScreen_Load):
      Code:
      class PokemonLoadScreen
        def initialize(scene)
      	@scene = scene
      	$MyNewPBS = load_data("Data/my_new_pbs.dat") if !$MyNewPBS
        end
      end
      
      What we're doing here:
      Check if $MyNewPBS already exists, and if it doesn't, don't do anything and call the start menu.
      If it does not exist yet, it'll do load_data("Data/my_new_pbs.dat"). Where earlier we did save_data(sections, "Data/my_new_pbs.dat"), we're now loading it from there. This gives us the same hash as we would get when compiling.


      Incorporating this into a class
      In this example, we've got "trainers". So, we'll make a class for those "trainers":
      Code:
      class TrainerClass
        def initialize(name)
      	@name = name
        end
      end
      
      You can now create an instance of this class by doing TrainerClass.new("Marin") or any other name. There is no connection with the PBS data yet.

      ...Which is obviously what we don't want. This TrainerClass class is meant for entries in the PBS, which means that we'll have to check if the passed name exists as a key in $MyNewPBS. If it does, we can continue, if it doesn't, it should throw an error (I'd do an ArgumentError, but it doesn't matter). Here's how I achieved that:
      Code:
      class TrainerClass
        def initialize(name)
      	if !$MyNewPBS.include?(name)
      	  raise ArgumentError.new(_INTL("There is no entry with this name!"))
      	end
      	# Since "raise" will give an error, the method is discontinued (that's why we don't need to use "else" here.
      	@name = name
        end
      end
      
      We've now got something that checks if a section with that name exists. We now want to be able to get properties of that section. To do so, we simply need to reference the hash that stores our data, $MyNewPBS, give it the key of @name (which we ensured exists), and then our value:
      Code:
        def basemoney
      	return $MyNewPBS[@name]["BaseMoney"]
        end
       
        def battlemusic
      	return $MyNewPBS[@name]["BattleMusic"]
        end
      
      We add this to our class, and voilà! We can now create an instance of this class if the name given exists in the PBS file, and we can easily get properties from it.


      Finishing touch
      We currently always have to manually recompile if we make a change in the PBS file. Luckily, we can change that.
      Find the following:
      Code:
      	 datafiles = [
      	   "attacksRS.dat",
      	   "berryplants.dat",
      	   "connections.dat",
      		.....
      	   "trainertypes.dat",
      	   "types.dat",
      	   "Constants.rxdata"
      	]
      	textfiles = [
      	   "abilities.txt",
      	   "berryplants.txt",
      	   "connections.txt",
      	   ....
      	   "trainers.txt",
      	   "trainertypes.txt",
      	   "types.txt"
      	]
      
      ...And simply add the names to those! Congratulations, you've now got a fully working PBS file!

    • Now, as for line-based PBS files such as moves.txt, items.txt, and abilities.txt. We need to once again figure out the basic layout of this thing. I think I've got a pretty good idea; a PBS files for Pokémon that each have an ID, and a method that will give the Pokémon based on ID. That sounds confusing, but what matters is the PBS file itself.
      The basic layout will be id,species,level,[item], where [item] is optional.
      Code:
      1,BULBASAUR,100,POKEDOLL
      2,PICHU,50
      3,GROVYLE,66,
      Before we continue, be sure to give this page a read!


      Add a new method and yield message as we did for the section-based PBS file by going in def pbCompileAll and putting it at the bottom, but just above the yield for messages.

      For people who have also followed the section-based PBS file guide, This PBS file will be called my_new_pbs2.
      Code:
      yield(_INTL("Compiling my new PBS file 2"))
      pbCompileNewPBS2
      We now need to make the method that actually reads the file:
      Code:
       def pbCompileNewPBS2
        sections = []
        pbCompilerEachPreppedLine("PBS/my_new_pbs2.txt") do |line, lineno|
      	pbGetCsvRecord(line, lineno, [0, "vevN",
      		nil,PBSpecies,nil,nil])
        end
        save_data(sections, "Data/my_new_pbs2.dat")
      end
      
      We're telling the method pbCompilerEachPreppedLine to read from PBS/my_new_pbs2.txt and it gives us the line and line number.

      We then call pbGetCsvRecord which checks the line for errors and determines the format, as well as format it for us. The "vevN" you see is the actual format of the line (which field is what). There are a couple values you should know:
      • * = Repeats the character that comes after it until the very end
      • u = Positive number
      • v = Any whole number greater than 0 (often used for IDs)
      • i = Any whole number (including negative)
      • U, I = Optional, Positive number
      • x = Hexadecimal number
      • s = String (a lap of text; often a description)
      • S = Optional string
      • n = Name (will become the internal name; can only consists of letters, numbers, and underscores, but can't start with a number).
      • N = Optional name
      • b = Boolean (true or false)
      • e = Enum (a list of values that it can have. This can be an array: ["Physical","Special","Status"] or a module: PBSpecies, PBItems, PBAbilities, etc.
      Now that we know what these characters stand for, we can apply them.
      If your format has an e in there, you'll have to add a list of values per character you got.

      Say, you have [0, "vee"], you'll have to change it to:

      Code:
      [0, "vee",nil,PBSpecies,PBSpecies]
      Where PBSpecies can be any other enum as described after e in the list above.

      ...

      In our case, vevN. Here's a short explanation as to why:
      • v = It's an ID, so we want it to be a positive number greater than 0.
      • e = We want this field to be a valid species (internal name), such as BULBASAUR.
      • v = This is the level the Pokémon will get. This has to be a positive number greater than 0, too.
      • N = Because there's nothing for optional enums, we'll have to go with an optional string. This is an item, so we'll need to parse that manually ourselves.

      So now that we know how pbGetCsvRecord works, we can continue. The method returns a formatted array with all values. We'll need to store what it returns to a variable, so let's do that. We also need to turn our N field into a valid item, so we'll include that too.
      Code:
      def pbCompileNewPBS2
        sections = []
        pbCompilerEachPreppedLine("PBS/my_new_pbs2.txt") do |line, lineno|
      	line = pbGetCsvRecord(line, lineno, [0, "vevN",
      		nil,PBSpecies,nil,nil])
      	item = 0
      	if line[3] && line[3] != ""
      	  item = getConst(PBItems,line[3])
      	end
        end
        save_data(sections, "Data/my_new_pbs2.dat")
      end
      
      line is set to whatever pbGetCsvRecord returns. In our case, that would be:
      Code:
      [[1,1,100,500],
      [2,172,50,0],
      [3,253,66,0]]
      
      A short explanation of line 1 (the rest is all quite similar):
      • Slot 1 (index 0): The ID we defined.
      • Slot 2 (index 1): The species we gave it -- but it has been formatted. This means that we have the ID of that species.
      • Slot 3 (index 2): The level we gave it.
      • Slot 4 (index 3): The item field, which we said will default to 0. As you can see, where we entered POKEDOLL, it's 500. This too was parsed into the ID of an item.
      I've decided that I want to store all this data in a global variable called $MyNewPBS2. It'll be stored a bit like this:
      Code:
      [nil, [1, 100, 500], [172, 50, 0], [253, 66, 0]]
      
      It'll be an array. The slot each Pokémon is in depends on the ID we gave it (meaning that overlapping IDs will result in Pokémon being overwritten in this array; that's why you should make sure not to use an ID more than once)

      Now, to get it the way we want it, we need to do a bit of formatting:
      Code:
      def pbCompileNewPBS2
        sections = []
        pbCompilerEachPreppedLine("PBS/my_new_pbs2.txt") do |line, lineno|
      	line = pbGetCsvRecord(line, lineno, [0, "vevN",
      		nil,PBSpecies,nil,nil])
      	item = 0
      	if line[3] && line[3] != ""
      	  item = getConst(PBItems,line[3])
      	end
      	sections[line[0]] = [line[1],line[2],item]
        end
        save_data(sections, "Data/my_new_pbs2.dat")
        $MyNewPBS2 = sections
      end
      
      The line of interest is:
      Code:
      sections[line[0]] = [line[1],line[2],item]
      What this is doing is adding a new entry in the array called sections at line[0] (which is the first slot in our formatted array, which is the ID of the Pokémon we gave it).
      It sets that to an array of [species,level,item], [line[1],line[2],item]. We already formatted item ourselves, which is now either 0 or a number representing an item.


      Now that we've got our sections array just how we want it, we need to save it to a data file. To be able to access it in the game, we also set $MyNewPBS2 to sections.


      That global variable, $MyNewPBS2 is still empty if the game hasn't been compiled, though (because this method is never called). We need to do the same as in the tutorial for section-based PBS files, which is overwriting the method that initializes the start menu to load our data. If you've already done that in the other tutorial, you can just add this underneath:
      It will now look like this:
      Code:
      class PokemonLoadScreen
        def initialize(scene)
      	@scene = scene
      	$MyNewPBS2 = load_data("Data/my_new_pbs2.dat") if !$MyNewPBS2
        end
      end
      
      *Note: You may have this method and way of loading data already from the other tutorial. If so, you can just add this line to there.

      Again, what we're doing is this:
      Check to see if $MyNewPBS2 exists. If not, we load the data from Data/my_new_pbs2.dat and store it in $MyNewPBS2. If it does, we don't do anything.




      Using the stored data
      This varies for what you want to do, but this is what I want to do.
      • Create a method called pbGivePokemon(id)
      • id will be whatever ID we gave our Pokémon in the PBS file.
      • If our $MyNewPBS2 contains an entry for id, we'll go on. If not, the method ends.
      This is the code:
      Code:
      def pbGivePokemon(id)
        if $MyNewPBS2[id]
      	poke = PokeBattle_Pokemon.new($MyNewPBS2[id][0], $MyNewPBS2[id][1], $Trainer)
      	poke.item = $MyNewPBS2[id][2] if $MyNewPBS2[id][2] > 0
      	pbAddPokemon(poke)
        end
      end
      What we're doing is checking if there exists and entry at id (if $MyNewPBS2[id]).
      If it does exist, we create a new variable called poke and set it to a new PokeBattle_Pokemon object. We pass it a species and a level, which are $MyNewPBS2[id][0] for the species and $MyNewPBS2[id][1] for the level.
      We also had one for the item, and if you remember, we stated that that field defaults to 0 if no item was given. This means that we'll need to check if $MyNewPBS2[id][2] is greater than 0, and if it is, set poke's item to that value.
      Last, we add the Pokémon to our party with a nice message, and that's that!



      Finishing touch
      We currently always have to manually recompile if we make a change in the PBS file. Luckily, we can change that.
      Find the following:
      Code:
      	 datafiles = [
      	   "attacksRS.dat",
      	   "berryplants.dat",
      	   "connections.dat",
      		.....
      	   "trainertypes.dat",
      	   "types.dat",
      	   "Constants.rxdata"
      	]
      	textfiles = [
      	   "abilities.txt",
      	   "berryplants.txt",
      	   "connections.txt",
      	   ....
      	   "trainers.txt",
      	   "trainertypes.txt",
      	   "types.txt"
      	]
      
      ...And simply add the names to those! Congratulations, you've now got a fully working PBS file!



      Yes, this method and PBS file is completely useless. It's about the idea and working, though, not what it does.
    werty81180 and Mr. Gela like this.