• Welcome to Freedom Reborn Archive.
 

Persistant Campaign Data

Started by GogglesPizanno, November 10, 2007, 11:30:16 PM

Previous topic - Next topic

GogglesPizanno

Having come up with some new ideas for a possible mod, I've been testing some of the more complex functions added to FFX, specifically:
Campaign_RewriteCharacterAttributes & Campaign_RewriteCharacterPowers

I've pretty much got it working, however I was wondering if there was a similar function for rewriting the stats of a character to the DAT.
I know that character stats can be manipulated via scripting during a mission, but I need any changes to remain persistant throughout the campaign, and I cant remember if there is an easy way to do this (campaign variables or something like that).

I started looking at the new Mission Variable Functions, but if I read it correctly its mostly for maintaining values within a specific mission save, not a campaign save. Is this correct? Or is it just late and my brain has decided to quit.

stumpy

It sounds like you are about right on the mission and object variables. They are there so that the information stored during a mission is still there if the player saves the mission and then loads it later on. In an earlier version of the missionobjvar module, I had functions to write data to a file (for instance when the mission was won), which could then be read at the beginning of the next mission. But, I took that out because a) no one was really using it and b) it wasn't really reliable if a player finished a mission but then played the next mission from a different saved game.

But, I am not quite sure what you mean when you say that stats can be manipulated during a mission and you want them to stay persistent from mission to mission. Any stats changed via FFX will have the same changes made at the start of the next mission, so that shouldn't be a problem. If you want to change a character's attribute via Object_SetAttr() outside the FFX system and have it keep track of the value it had at the end of that mission so that it can set to that again via Object_SetAttr() at the start of the next mission, we don't currently do that. But, there is probably a saved-game-safe way I hashed out a while back to write a script at the end of a mission and then run it when FFX initializes at the start of the next mission. I don't know if that's what you are talking about...

If you want to actually to make changes to a character's natural stats (strength, endurance, etc.) in the saved game file at the end of a mission, I don't think we know how to do that. The game doesn't tell us when the player has saved a game and the automatic saves don't happen before OnMissionWon(), so we can't be sure when to hack the saved game file.

I should note that either approach (if they were feasible) can result in other problems. For example, various FFX attributes and scripted in-game attributes (like FAST FLIER and NIMBLE) as well as some state swaps will change a character's stats. But, if those altered stats are stored as the character's natural stats at the end of a mission, then the next mission will start with the wrong stats. There are ways around that, I suppose, but it adds up to a fair amount of bookkeeping.

GogglesPizanno

QuoteIf you want to change a character's attribute via Object_SetAttr() outside the FFX system and have it keep track of the value it had at the end of that mission so that it can set to that again via Object_SetAttr() at the start of the next mission, we don't currently do that. But, there is probably a saved-game-safe way I hashed out a while back to write a script at the end of a mission and then run it when FFX initializes at the start of the next mission. I don't know if that's what you are talking about...

Thats pretty much what I'm talking about.

In essence if a character is exposed to a certain thing in the mission, it alters their stats. I want these new stats to be used for the character throughout the rest of the campaign. The trick is that I want the stat changes to be random each time it occurs, otherwise Id just make different versions of the character and swap them out of the recruitment screen.

stumpy

Well, I have the impression that you are comfortable doing some coding, which is good. There are a lot of potential landmines in this sort of project. Just some of them:

  • What I mentioned earlier holds true about a character's stats being in some very temporary state because of attributes or swaps. You'll want to undo that before saving his stats.
  • A character called 'alchemiss' in one mission may be called 'hero_3' in another. 
  • The same characters won't necessarily be on every mission, so it's not as simple as looping over the squad list. You also have to track all the recruits in each mission, whether or not they are in it, otherwise their stat changes will be lost.
  • Characters haven't always spawned by OnPostInit(). You have to check that a character who shows up in the middle of a mission (because of mission scripting, attributes like shapeshifter, russian doll, etc.) has the changes applied.
Anyway, I don't want to discourage you, but it's worth knowing what you are getting into. (Some of those issues diminish if you know ahead of time which characters you will be dealing with and what their attributes and so on will be. The toughest case is handling custom characters with arbitrary attributes.)

That all said, the basic idea is that we create a tag that stays with the saved game that we can read from within the game to see what text file we should be reading. The tag that comes to mind is a character's XP and that's what I went with. What I wanted to do was come up with a general mechanism for storing data from mission to mission in a campaign that would still work if players jumped back and forth between saved games.

The code a threw together is below. It's proof of concept stuff and barely tested, so be wary.

I also tossed in a wrapper for the two main functions to do what you wanted with character stats. Note that the endurance stat isn't one where changing it in game makes any difference. You can simulate it by using 'health' and 'maxHealth' in the atts list, though.

#! /usr/bin/env python

# MissionWonScript.py by stumpy

# The idea is to have a saved-game-safe way to save a bunch of data at
# the end of a mission (in the OnMissionWon() function) and then load
# it from OnPostInit() in the next mission.

# This can be used for a variety of purposes, generally to store
# ongong data about a campaign or character that is consistent
# throughout the campaign. Of course, we can already just write a file
# at the end of a mission and read it at the next mission. There are
# problems with that approach, though. The player can finish a
# mission, save at that point (say, in file save1.dat), then replay
# this mission either immediately or from a later point in the
# campaign. But, when he wants to load save1.dat and play from that
# point on, the text file read will be from whatever mission was
# played last, not from the one saved when he finished mission just
# loaded. Even naming the text file with the mission name doesn't
# solve the problem, since he coul have played that mission more thann
# once.

# The function WriteMissionWonScript() here allows a scripter to write
# a python file during OnMissionWon() that will be run at the next
# mission's OnPostInit(). WriteMissionWonScript(ScriptText) takes the
# argument ScriptText, which is the exact text of the python code to
# be run at the next mission's OnPostInit(). LoadMissionWonScript()
# will import that code as *.

import os.path
import imp
import js
import cshelper
import datfiles

# Globals
_ModName = os.path.basename(datfiles.GetModPath())
# The location of the last tag index value
_MISSIONWONLASTTAGFILE = os.path.join(datfiles.GetWinTempPath(),'user','SaveGames',('MissionWonScript_%s_LastTag'%_ModName))
# The prefix for the files that will contained the saved scripts
_MISSIONWONSCRIPTBASENAME = 'MissionWonScript_' + _ModName + '_'
# just a campaign-only character with object type GAME_OBJ_HERO that won't be played in a campaign
_DUMMYCHAR = '------------' 

def WriteMissionWonScript(ScriptText):
    sgd = datfiles.Campaign_ReadCharactersFromSavedGame()
    # check if this is the first mission where WriteMissionWonScript has been called
    # CHECK the mission number for this!
    mission = int(js.Mission_GetAttr('_cs_mission_number'))
    if mission == 1:
        OldDummyXP = 0
    elif not sgd.has_key(_DUMMYCHAR):   # odd case, hopefully rare
        OldDummyXP = 0
    else:
        OldDummyXP = sgd[_DUMMYCHAR]['XP']
    # new XP will be 1000 times mission number plus random int between 0 and 999.
    try:
        f = open(_MISSIONWONLASTTAGFILE,'r')
        LastTagVal = int(f.readlines()[-1])
        f.close()
    except IOError:
        LastTagVal = 0
    extraXP = LastTagVal - OldDummyXP + 1
    dummyXP = OldDummyXP + extraXP  # same as LastTagVal+1
    js.Campaign_Recruit(_DUMMYCHAR)
    js.Campaign_AddCP(_DUMMYCHAR,extraXP)
    js.Campaign_UnRecruit(_DUMMYCHAR)
    SavePath = os.path.join(datfiles.GetWinTempPath(),'user','SaveGames',('%s%d.py' % (_MISSIONWONSCRIPTBASENAME,dummyXP)))
    f = open(SavePath,'w')
    f.write('# This file was written by %s to store persistent game data\n' % os.path.basename(__file__))
    f.write('MISSIONWONPREVIOUSTAG = %d\n\n' % OldDummyXP)
    f.write('%s' % ScriptText)
    f.close()
    f = open(_MISSIONWONLASTTAGFILE,'w')
    f.write('# This file was written by %s to store persistent game data for mod %s.\n' % (os.path.basename(__file__),_ModName))
    f.write('# Don\'t erase it unless you are deleting all saved games for the mod.\n')
    f.write('# The number below is the most recently set XP value of the dummy character %s\n' % repr(_DUMMYCHAR))
    f.write('%d\n' % dummyXP)
    f.close()

def LoadMissionWonScript():
    sgd = datfiles.Campaign_ReadCharactersFromSavedGame()
    # check if this is the first mission where WriteMissionWonScript has been called
    if not sgd.has_key(_DUMMYCHAR):
        print 'MissionWonScript module: Warning LoadMissionWonScript() called before WriteMissionWonScript()'
        raise
    dummyXP = sgd[_DUMMYCHAR]['XP']
    LoadPath = os.path.join(datfiles.GetWinTempPath(),'user','SaveGames',('%s%d.py' % (_MISSIONWONSCRIPTBASENAME,dummyXP)))
    imp.load_source('missionwon',LoadPath)
    from missionwon import *
   

# Write a WonScript that will set the attribs for all recruits to their current values
# This is a little involved because all the recruits aren't necessarily in the current mission
def SaveCharacterAtts(atts=['strength','speed','agility','energy']):
    # Make a list of the recruited templates, including those just recruited in the current mission
    sgd = datfiles.Campaign_ReadCharactersFromSavedGame()
    recruits = filter(js.Campaign_IsRecruited,sgd.keys())
    for char in map(js.Object_GetTemplate,cshelper.getAllHeroes()): # NB: may miss KOed heroes...
        if ( char not in recruits ) and ( js.Campaign_IsRecruited(char) ):
            recruits.append(char)
    # Now create the source code that is run when LoadMissionWonScript() is called at the start of the next mission
    script = 'import js,ff\n\n'
    # send a function to grab the first obect with a template
    script = script + _GetCharacterWithTemplate + '\n'
    for temp in recruits:
        attdict = {}
        obj = GetCharacterWithTemplate(temp)
        if obj: # The character is present in this mission
            for att in atts:
                # see if these are different than the sgd values
                if ( not sgd.has_key(temp) ) or ( js.Object_GetAttr(obj,att) != sgd[temp].get(att) ):
                    attdict[att] = js.Object_GetAttr(obj,att)
            if attdict:
                script = script + MakeAttrSettingString(temp,attdict)
        else:   # save any atts saved in mission attributes
            for att in atts:
                if Mission_AttrExists('MISSIONWON_%s_%s'%(temp,att)):
                    if ( not sgd.has_key(temp) ) or ( js.Mission_GetAttr('MISSIONWON_%s_%s'%(temp,att)) != sgd[temp].get(att) ):
                        attdict[att] = js.Mission_GetAttr('MISSIONWON_%s_%s'%(temp,att))
            if attdict:
                script = script + MakeAttrSettingString(temp,attdict)
    WriteMissionWonScript(script)

def MakeAttrSettingString(template,attdict):
    s = "\n# setting atts for %s\n" % repr(template)
    s = s + "obj = GetCharacterWithTemplate(%s)\n" % repr(template)
    s = s + "if obj:\n"
    for att in attdict.keys():
        s = s + "    js.Object_SetAttr(obj,%s,%f)\n" % (repr(att),attdict[att])
    s = s + "else:\n"
    for att in attdict.keys():
        s = s + "    js.Mission_SetAttr('MISSIONWON_%s_%s',%f)\n" % (template,att,attdict[att])
    return s

# Handy Functions
def GetCharacterWithTemplate(template):
    for o in js.Mission_GetDynamicObjects():
        if js.Object_GetTemplate(o) == template:
            return o
    return ''

_GetCharacterWithTemplate = \
"""def GetCharacterWithTemplate(template):
    for o in js.Mission_GetDynamicObjects():
        if js.Object_GetTemplate(o) == template:
            return o
    return ''
"""

def Mission_AttrExists(attrName):
    try:
        dummy = js.Mission_GetAttr(attrName)
        return 1
    except:
        return 0


The main functions are WriteMissionWonScript(), to be run in OnMissionWon(), and LoadMissionWonScript(), to be run in OnPostInit().

WriteMissionWonScript(ScriptText) takes an argument, ScriptText, which is the script to be run when the next mission loads. ScriptText can be very simple, like
import js
Object_SetAttr('alchemiss','speed',6)

or more complicated. The example above just sets Alchmiss' speed higher, which might be appropriate if you just went through the random stat changing part of your mission and changed her speed. More below on this.

LoadMissionWonScript() takes no arguments. It just figures out which file to load and loads it.

Since you don't always know what object name a character will have in a mission and you don't always even know if a character will be in every mission, I wrote up SaveCharacterAtts() as a wrapper for WriteMissionWonScript() to automatically create the source string to pass to it to reinstate the stat attributes for all of the recruited characters that are present when a mission starts. You would call it instead of WriteMissionWonScript() in OnMissionWon(). By default, the attributes it saves are strength, speed, agility, and energy. You can have it save other attributes by changing the atts list.

Basically, SaveCharacterAtts() should help with the second and third concerns I mentioned above. You'll need to deal with the first and fourth, too, if your characters have FFX attributes or if they might be spawned midway through a mission. But, you are on your own for that.  ;)  You might want to check into the removability option for some of the FFX attributes for the first issue, and possibly play with mlogreader's regTemplateSpawn() or regCharacterSpawn() for the fourth.

Anyway, I hope this is helpful as a starting point.

GogglesPizanno

So what your saying is that this is easy to do...  :P

Thanks for that example. It actually helps me a lot to try and get a handle on some of the problems that I would have been oblivious to...

Conceptually it seems easy, but as you pointed out...Snakes.
Why'd it have to be snakes.

But this gives me some ideas and actually focuses my thinking down a little in terms of trying to simplify some of my ideas so that your example code  could work with the least amount of headaches.

stumpy

I didn't mean to be too discouraging. Honestly, with the tools in FFX, mlogreader, etc., there isn't any part of what you are proposing that can't be done. I think what I posted would work straightaway with a one-line call in each of OnPostInit() and OnMissionWon(). At least try it out and see if it works for you. Aside from those niggling details, of course.  ;)

And even those are just a matter of being careful with the bookkeeping. There are only so many FFX attributes that change stats and I think almost all of those are removable. Using that and missionobjvar's Object_GetVar(object,'attrs_'), you can probably clean up the FFX attributes before saving. In addition, I think there are just three or four power swaps (like gravity increase) that affect attributes by actually changing the stats and most of those have a 'template_strength' or similar object attribute you can use for them. (And really, at some point we need to update cshelper.normalize() so that it works to remove swap states as well...)

Anyway, this issue of a saved-game-safe way to store data across missions has come up before. Your thread reminded me that I had kind of noodled out a solution to it a while back but never posted it. I'd be glad if it got some use. :)

GogglesPizanno

Played with the script a little last night, and got it partially working.

Basically I got the save portion to work ok.
It writes out a unique python file for each level with stat changing code.

The problem arises when looking at the Dummy Character ('------------'). It seems to be having a problem seeing this as a legitimately recruited character. As a result it is causing the OldDummyXP value to continually return a value of '0' (which in your comments is "# odd case, hopefully rare")

This same issue causes the LoadMissionWonScript() to throw the following error:

MissionWonScript module: Warning LoadMissionWonScript() called before WriteMissionWonScript()

In both instances this looks to happen when sgd.has_key(_DUMMYCHAR) doesn't exist (at least as far as the script is concerned).

I've commented out the Unrecruit line and verified that the Dummy character is in fact being recruited...
So thats where that is. I'm gonna play with it a little more tonight.


stumpy

Aaargh! I meant tell you that you had to go into FFEdit and make that character Campaign Only on the Characters page and of Class GAME_OBJ_HERO on the Templates page. Change the code back to what it was and it should work. Sorry. I put the note in the code comments but forgot to mention it here. Oops.  :blush:

GogglesPizanno


GogglesPizanno

OK so now that I am at home and had a chance to make that change...PRESTO!
It works as promised...pretty damn spiffy.

There is the issue as you mentioned with Alchemiss suddenly being called 'hero_2' in the next mission (for example), and as a result its not firing off the saved python quite right, but these are things I can look at now that the basic framework you supplied seems to be doing its thing.

This could open up all kinds of wacky stuff....