[RGSS] (REQUEST) COUNTING GAME OVERS IN AN EXTERNAL FILE

Posts

Pages: 1
Okay, so I admit, I'm a complete noob when it comes to scripting in RGSS, so I have no idea how the syntax works, but I do have some programming experience here and there so I kinda have an idea of how to do this, sorta.

Basically, I want to be able to count the amount of times a particular savefile gets a gameover. I figure the best way would be by outputting something to a text file with some data that can possibly be read back later.

so far I guess it would sorta look like this:

(writing)
-game over codeblock here
-open gameovercounter(savefilenumber).txt
-write a special line start indicator, map location(x,y,ID) and a newline to gameovercounter(savefilenumber).txt
-close gameovercounter.txt
-end game over codeblock

(reading)
-open gameovercounter(savefilenumber).txt
-count the number of lines that start with the special line start indicator and put that in an in-game variable to be used

or (don't really need to have this one, but it might be nice)
(called in certain rooms)
-check for an entry in this room in gameovercounter(savefilenumber).txt

I'd imagine this would be something fairly doable, since there are ways to read and write a file, I just don't know how to do it yet. Thank you.
(yes, I realize this is a double-post, but there is a status update here)

Okay, so I've set out on trying to learn this scripting language and solve this thing.
So far, I've figured out a couple of things here and there to help with editing small stuff. Also, more importantly, I found a handy looking script that will output a file and allow me to add stuff to it.
The issue now though, is getting to call the things I want. I can get the functions to work within a script event, but sticking the stuff in a package that can be called later is something I haven't figured out yet. Here's my code:
class Gameover_Counter


def initialize #let's start this baby up

#FileTest.exist?("content/log.ath")
#sticking this here for when I figure out how to conditional branch properly
#currently I have a separate thing that generates the base file that works
#but checking right here would probably be better

appTxt("log", "\nPlayer died")
#appTxt("log", " on the date"
#appTxt("log", strftime(%x))
#got an error here, not too important to include though
appTxt("log", " at this location: Map#" )
appTxt("log", $game_map.map_id)
appTxt("log", " x=" )
appTxt("log", $game_player.x)
appTxt("log", ", y=")
appTxt("log", $game_player.y)
end

I have no idea which thing in the textfile manager script to reference to let the functions work in my script.
As mentioned before, I'm new to this scripting language so I'm not familiar with everything yet.
Don't make it a text file. Ruby/RGSS has a built in method for serializing and deserializing data objects via Marshal calls. Open a file for reading/writing and pass the file handle to the appropriate Marshal.dump or Marshal.load methods if you are writing or reading. Pass it your container class and it'll handle the rest. This is how RPG Maker handles save game data. I'm not sure which RPG Maker you're using but all of the RGSS ones should do this and a find-all (Ctrl+Shift+F iirc) in the script editor for 'Marshal' should give an example in RGSS itself.



e: To expand on this, make a new class called GameOverManager or something. It'll hold all of the game over instances the game recorded. We also need something to hold an individual record of a game over, I'll call it GameOverRecord. Here's how it might look:

class GameOverManager

  def initialize
    @game_over_records = []
  end

  def game_over_records
    return @game_over_records
  end

  def record_game_over
    @game_over_records.push(GameOverRecord.new($game_player.x, $game_player.y, $game_player.$game_map.map_id))
  end
end

class GameOverRecord
  attr_reader :x, :y, :mapid
  def initialize(x, y, mapid)
    this.x = x
    this.y = y
    this.mapid = mapid
  end
end

Load it with the rest of the database loading, update it when the player dies, and save it to its external file. It'll be easier to use inside of your game as an object than trying to handle a text file as you'll have to write a parser than can read it too.

I can help with the Marshal stuff later if needed but I need to know the maker.


e2: this is also assuming you want the data to use in-game in some capacity and not for dev/user only records. In which case yeah, a text file is fine and I can help with that too. It's easy.
I'm using rpg maker xp.
I'm planning on using the script for in-game stuff, though I'd also like for a readable file to be generated, in case players want to read back through it for the heck of it.
Ahh, then there will have to be a quick and dirty parser then. For the sake of simplicity I'll mark the file spec as each line being a record of death, each item in a death record separated by a comma, and each item being a key value pair separated by a colon. It's not as effective as a regex parsing each line but I'm too tired to deal with one right now.

So an example line might look like:

Death:1,Time:2015-09-22 12:00:00,MapID:10,X:4,Y:6
Death:2,Time:2015-09-22 12:20:00,MapID:11,X:7,Y:16

We want to be able to puke our GameOverRecords in this format, so it might look like this:
class GameOverRecord
  # The base of the object hasn't changed much, I just added more fields for the # and time
  attr_reader :death_number, :time, :mapid, :x, :y
  def initialize(death_number, time, mapid, x, y)
    this.death_number = death_number
    this.time = time
    this.x = x
    this.y = y
    this.mapid = mapid
  end

  # This is new: It'll convert our properties into a string that we can dump into a file. We need this line to be parsable when we read the file
  def to_string
    return "Death:#{@death_number},Time:#{@time},MapID:#{@mapid},X:#{@x},Y:#{@y}"
  end
end

Now we need our manager to convert each record into a string. We also want to dump that into our GameOverFile
class GameOverManager

  def initialize
    @game_over_records = []
  end

  def game_over_records
    return @game_over_records
  end

  # This got expanded to include the death # and the time of death to match the updated GameOverRecord above
  def record_game_over
    @game_over_records.push(GameOverRecord.new(@game_over_records.length + 1, Time.now, $game_player.$game_map.map_id, $game_player.x, $game_player.y))
  end
  
  # Call this when you want to write the game overs to file
  def save_records
    # First we'll open our file for writing. The 'w' flag means we're going to truncate any existing file and create it as necessary. 
    # The manager should always have all the game over records so we don't need to worry about appending data
    # gameovers.txt can be changed to be your text file you want to dump your game overs to
    File.open("gameovers.txt", "w")  { |file|
      # We want to write every record to the file
      @game_over_records.each |record| do
        # This will write the record's to_string value to the file and a \n character which denotes a new line character
        file.write(record.to_string + "\n")
      end
    }
  end
end

If you want to save the game over records each time you record one just add 'save_records' after the '@game_over_records.push(...' line in record_game_over like so:
def record_game_over
  @game_over_records.push(GameOverRecord.new(@game_over_records.length + 1, Time.now, $game_player.$game_map.map_id, $game_player.x, $game_player.y))
  save_records
end


Now we need to be able to read the file back. We need to be able to do something with this game over data in game, right? We'll need to add a new method to the GameOverManager that will open the file for reading and parse each line and create a GameOverRecord out of it.
def load_records
  # First we need to clear our existing game over records so we don't have any cross contamination
  @game_over_records.clear

  # This is like when we're saving files but we changed the flag from "w" to "r" which means it opens the file in read mode and starts at the beginning of the file
  File.open("gameovers.txt", "r") { |file|
    # Now process each line separately
    file.each_line do |line|
      # For easy quickness I'm just going to use split to break up a string based on our dividers: , then :
      # There's better ways to do this (as is with a lot of this) but it'll do for now
      contents = line.split(',')
      # We'll assume each line is in the same order every time
      death = contents[0].split(':')[1]
      time = Time.parse(contents[1].split(':')[1]) # We expect a time object here so we need to tell the Time class to parse what we wrote
      mapid = contents[2].split(':')[1]
      x = contents[3].split(':')[1]
      y = contents[4].split(':')[1]
      # We have our record values now we just need to make the record and add it to our collection of GameOverRecords
      @game_over_records.push(GameOverRecord.new(death, time, mapid, x, y)
    end
  }
end


That should handle the saving and loading of the data. Now we need an instance of the GameOverManager so you can actually use it! I don't have a copy of RMXP on this machine so I can't give specifics but it's probably best to load it with the other database load operations. If you look for references of the data files like Actors.rxdata file or where stuff like $data_actors is set it should be around there. You can create a new global GameOverManager you can access anywhere there like so:

$game_over_manager = GameOverManager.new
$game_over_manager.load_records

That'll make an instance of it and load it when the database is loaded. Now whenever you get a game over you can just do
$game_over_manager.record_game_over
and it'll make a new record and save the file if record_game_over is set up to do so.


I can't actually test this code due to the lack of RMXP. I can try to dig it up over the weekend or so though if needed. Let me know how/if it works (read what syntax I fucked up on and now it won't build)


e: uh the code bits looked way better in notepad++. Copy/paste them into RMXP or a notepad so you can read my comments.
Whoa, you got a lot of stuff going on there.
I put the stuff in and on line 33 (the game_records push thing)it looks like you accidentally wrote $game_player.$game_map.map_id instead of $game_map.map_id (the former gave a syntax error). Once I changed that it stopped complaining about that line. (there was also a chunk where the same thing was copy-pasted elsewhere and that was fixed the same way too)
on line 44 though, where it says @game_over_records.each |record| do, I have no idea how to fix that one. I'm assuming it has something to do with not knowing what to do with record, which never shows up exactly like that anywhere else. Then again, maybe it's the weird line break between stuff that it's sticking into the text file.

Temporarily leaving out save_records to check the later stuff, it came across an error at the second to last end in load_records. I'm guessing because you put death as a variable name there instead of death_number, but changing that still made it run into the same syntax error.
taff it all that's what I get

I fucked up the syntax of ruby's each code (execute this block of code for every element found), it's @array.each { |element| }, I forget the proper syntax with do but it's not necessary anyways. The syntax error at the end is a mismatch of parenthesis. Here's the syntax correct code:

class GameOverRecord
  # The base of the object hasn't changed much, I just added more fields for the # and time
  attr_reader :death_number, :time, :mapid, :x, :y
  def initialize(death_number, time, mapid, x, y)
    this.death_number = death_number
    this.time = time
    this.x = x
    this.y = y
    this.mapid = mapid
  end

  # This is new: It'll convert our properties into a string that we can dump into a file. We need this line to be parsable when we read the file
  def to_string
    return "Death:#{@death_number},Time:#{@time},MapID:#{@mapid},X:#{@x},Y:#{@y}"
  end
end


class GameOverManager

  def initialize
    @game_over_records = []
  end

  def game_over_records
    return @game_over_records
  end

  # This got expanded to include the death # and the time of death to match the updated GameOverRecord above
  def record_game_over
    @game_over_records.push(GameOverRecord.new(@game_over_records.length + 1, Time.now, $game_map.map_id, $game_player.x, $game_player.y))
    save_records
  end
  
  # Call this when you want to write the game overs to file
  def save_records
    # First we'll open our file for writing. The 'w' flag means we're going to truncate any existing file and create it as necessary. 
    # The manager should always have all the game over records so we don't need to worry about appending data
    # gameovers.txt can be changed to be your text file you want to dump your game overs to
    File.open("gameovers.txt", "w")  { |file|
      # We want to write every record to the file
      @game_over_records.each { |record|
        # This will write the record's to_string value to the file and a \n character which denotes a new line character
        file.write(record.to_string + "\n")
      }
    }
  end
  
  def load_records
      # First we need to clear our existing game over records so we don't have any cross contamination
      @game_over_records.clear

      # This is like when we're saving files but we changed the flag from "w" to "r" which means it opens the file in read mode and starts at the beginning of the file
      File.open("gameovers.txt", "r") { |file|
        # Now process each line separately
        file.each { |line|
          # For easy quickness I'm just going to use split to break up a string based on our dividers: , then :
          # There's better ways to do this (as is with a lot of this) but it'll do for now
          contents = line.split(',')
          # We'll assume each line is in the same order every time
          death = contents[0].split(':')[1]
          time = Time.parse(contents[1].split(':')[1]) # We expect a time object here so we need to tell the Time class to parse what we wrote
          mapid = contents[2].split(':')[1]
          x = contents[3].split(':')[1]
          y = contents[4].split(':')[1]
          # We have our record values now we just need to make the record and add it to our collection of GameOverRecords
          @game_over_records.push(GameOverRecord.new(death, time, mapid, x, y))
        }
      }
    end
end

And really it's generic enough that I can make sure that this chunk of code actually works in other RMs besides RMXP, I've got those collecting dust somewhere. Gimme a bit and I'll give it a shakedown.


e: yeah, found a bug already. Forgot that reading a file that doesn't exist throws an error instead of an empty file.

e2: also forgot there's no 'this' keyword in ruby, it's self! (and should be using @ anyways for instance variables)
I forgot a lot of ruby quirks and my own failings (whoops I was having strings and ints playing together) and that RGSS does not have the Time.parse method. Here's a horrible hackjob that works in Ace at least:

class GameOverRecord
  # The base of the object hasn't changed much, I just added more fields for the # and time
  attr_reader :death_number, :time, :mapid, :x, :y
  def initialize(death_number, time, mapid, x, y)
    @death_number = death_number
    @time = time
    @x = x
    @y = y
    @mapid = mapid
  end

  # This is new: It'll convert our properties into a string that we can dump into a file. We need this line to be parsable when we read the file
  def to_string
    return "Death;#{@death_number},Time;#{@time.strftime("%Y-%m-%d %H:%M:%S")},MapID;#{@mapid},X;#{@x},Y;#{@y}"
  end
end


class GameOverManager

  def initialize
    @game_over_records = []
  end

  def game_over_records
    return @game_over_records
  end

  # This got expanded to include the death # and the time of death to match the updated GameOverRecord above
  def record_game_over
    @game_over_records.push(GameOverRecord.new(
      @game_over_records.length + 1, 
      Time.now,
      $game_map.map_id, 
      $game_player.x, 
      $game_player.y))
    save_records
  end
  
  # Call this when you want to write the game overs to file
  def save_records
    # First we'll open our file for writing. The 'w' flag means we're going to truncate any existing file and create it as necessary. 
    # The manager should always have all the game over records so we don't need to worry about appending data
    # gameovers.txt can be changed to be your text file you want to dump your game overs to
    File.open("gameovers.txt", "w")  { |file|
      # We want to write every record to the file
      @game_over_records.each { |record|
        # So ruby write is by default a line and doesn't need the extra \n character!
        file.puts record.to_string
      }
    }
  end
  
  TIME_REGEX = /([0-9]{4})-([0-9]{2})-([0-9]{2})\s([0-9]{2}):([0-9]{2}):([0-9]{2})/i
  
  def load_records
      # First we need to clear our existing game over records so we don't have any cross contamination
      @game_over_records.clear

      return unless File.exists? "gameovers.txt"
      # This is like when we're saving files but we changed the flag from "w" to "r" which means it opens the file in read mode and starts at the beginning of the file
      File.open("gameovers.txt", "r") { |file|
        # Now process each line separately
        file.each { |line|
          next if line.empty?
          
          begin
          
            # For easy quickness I'm just going to use split to break up a string based on our dividers: , then :
            # There's better ways to do this (as is with a lot of this) but it'll do for now
            contents = line.split(',')
            # We'll assume each line is in the same order every time
            death = contents[0].split(';')[1].to_i
            # ofc rgss doesn't have the time.parse method
            time_string = contents[1].split(';')[1]
            t = TIME_REGEX.match(time_string).captures
            time = Time.new(
              t[0].to_i, 
              t[1].to_i, 
              t[2].to_i, 
              t[3].to_i, 
              t[4].to_i, 
              t[5].to_i)
            
            mapid = contents[2].split(';')[1].to_i
            x = contents[3].split(';')[1].to_i
            y = contents[4].split(';')[1].to_i
            # We have our record values now we just need to make the record and add it to our collection of GameOverRecords
            @game_over_records.push(GameOverRecord.new(death, time, mapid, x, y))
            
          #rescue
           # raise
            #next
          end
        }
      }
    end
end

I might go and just make a proper regex for the file parser that isn't so rigid at this point.
It looks like the main codeblocks don't throw any errors this time :D
I really must thank you for all the stuff you've done so far. Now to see if I can figure out how to even use it.

I haven't quite worked out how to call a method properly or where to put stuff so that it notices what I'm trying to get it to use. (yes, I am seriously that new to this stuff that I haven't figured something like that out.)
I tried putting the
$game_over_manager = GameOverManager.new
$game_over_manager.load_records
stuff in Scene_Title, after the load database things, but trying to call $game_over_manager.record_game_over in Scene_Gameover or in a script event just confuses the thing and it errors.

Yes, I am using the current version of your code that is made to work with vx ace.
Damnit RMXP


I'll look into digging up xp somewhere and testing it on it and making a demo project this week.
Okay, I've tried a couple of things. First of all, I attempted to stick the old script I was working with in the 2nd post in a common event and call that from inside the main thing in Scene_Gameover, and that was simply skipped over. (I'm assuming common events just can't run on the game over screen in a relatively vanilla state, even if all the content is in a script event.)

Second thing I tried, going back to trying to activate your script, was to slap attr_accessor :record_game_over into a bunch of different places, but every single time, I still got the same exact error:

Script 'Scene_Gameover' line 25: NoMethodError occured.
Undefined method `record_game_over' for nil:NilClass

As a note, I placed $game_over_manager.record_game_over after the sound and picture stuff but before the Graphics.transition(20) loop.

I've looked up that kind of error, and apparently it's a common sort of error that can pop up when something it needs isn't in memory or whatever.
The attr_accessor tags are for properties of a class only. Think stuff like
class SomeClass
  def get_thing
    return @thing
  end

  def set_thing(newValue)
    @thing = newValue
  end

  attr_accessor: thing
end

Without the attr_accessor the only way for code outside of the SomeClass code to get and set the value of @thing is to use the get_thing and set_thing methods. The attr_accesor means you can access the @thing property directly from another class. record_game_over is a method and I'm not sure what Ruby does in a collision case, but basically attr_accessor won't do anything for it at best.


For an error like:
Script 'Scene_Gameover' line 25: NoMethodError occured.
Undefined method `record_game_over' for nil:NilClass

The 'Undefined method 'record_game_over' for nil:NilClass' error means that whatever instance you're trying to call the 'record_game_over' method from is nothing aka nil. In this case I'd make sure that the $game_over_manager is being initialized and your spelling is correct. It should be something like
$game_over_manager = GameOverManager.new
$game_over_manager.load_records

and
$game_over_manager.record_game_over

Creating it has to come before you use it otherwise it will just be a nil instance which you can't call 'record_game_over' on. Make sure you're doing so. The $ marks the instance as global which also means it shouldn't be garbage collected or otherwise removed until done explicitly.
YES! I GOT IT TO WORK!
Apparently I just had to put all three of those things in that spot in Scene_Gameover, right next to eachother in that order.

Thank you so much for helping me, I'll make sure your screen name makes it in the credits for my game.
That works! The problem there is if you want to use the game over records in game it won't create the GameOverManager until you game over. I'll still try to make a quick demo project this weekend to show it working all the time so you can use it as needed.
Okay, I don't know how, but somehow it broke in a weird place. This is the error it pulled up:
Script 'gameover manager' line 87: ArgumentError occured.
Wrong number of arguments (6 for 0)

It appears to be the part where the time array stuff is. If it helps, the time I encountered the error at was about 11:55pm. Not sure if it'll do any good, since it seems the output has it in 24-hour format and the last time it generated a successful report was at 18:25:37. I don't get how it just suddenly broke like that.

Edit: it appears to be an issue with the loading stuff when there's already something recorded in the file. When I cleared out the file, it worked just fine. Also, when I omitted loading the data in Scene_Gameover, it worked, but it overwrote everything previously in the file.
I think I know what the cause is (it's probably fucked up writing each record to a line, it worked before when the Y value was a string and had an EOL character at the end but that got dropped when I convert it to a proper number). I thought puts writes a line but ofc not. I'll fix it tonight, I dug up a copy of XP and I'll make a demo project in that for ya.
Hey there, just wondering if you've have any time or luck on working on it.
It's been a couple days so I figure you've been pretty busy with other things, which is alright, but I'm just sorta curious. If you need me to, I could try to take a crack at it a little, I guess.
Sorry, something came up over the weekend and I didn't have time to work on this. ANYWAYS I dug up a copy of RMXP, found out Windows 10 changed a security setting on my computer, and found out the Ruby version in RGSS1 doesn't support how I was initializing the Time object to track when the player died! Horray! I did some digging and found another way of creating the Time object I was using at least.

Demo project with script. It'll load the game over file when you hit the title screen (because that's where RMXP loads its data) and record a game over whenever a GameOverScene starts. Did some quick testing throwing myself on a battle mob.
It works perfectly! I got it to work in my game and everything!
Thank you once again, you're totally awesome.
Glad to hear it. Give a holler if it breaks or you need help with using it or something.
Pages: 1