Fan made ages and vault chronicles

Help bring our custom Ages to life! Share tips and tricks, as well as code samples with other developers.

Fan made ages and vault chronicles

Postby D'Lanor » Sat Nov 24, 2007 11:41 am

While it is uncertain whether Cyan will let us use the personal chronicles in the vault I think we stand a better chance of approval if we agree to use chronicles in an orderly manner. Below is a model I submitted to Cyan as a proposal.


1. The User Age Chronicle Model

A vault chronicle model for fan made ages.

Sometimes a puzzle requires the storage of personal player progress which is not tied to an age state. Each avatar already has its own location in the vault to store personal variables: the ChronicleFolder.

Since we want to keep the ChronicleFolder in an orderly state I propose ONE base chronicle for all fan made ages called (for example) 'UserAges'. This chronicle should be used by ALL fan made ages. Within that chronicle age creators can add unique subchronicles for their ages.

Each user age subchronicle in turn contains 3rd layer subchronicles to store player variables. Note that in this model we will write one layer deeper than any of the Cyan subchronicles:

Code: Select all
  ChronicleFolder
    |
    |
    |__Cyan Chronicle-1
    |
    |__Cyan Chronicle-2
    |     |
    |     |__Cyan Subchronicle-1
    |     |
    |     |__Cyan Subchronicle-2
    |
    |__Cyan Chronicle-3
    |
    |
    |__UserAges
    |     |
          |
          |__Age-1
          |     |
          |     |__Variable-1
          |     |
          |     |__Variable-2
          |
          |
          |__Age-2
          |     |
                |__Variable-1
                |

    L1    L2    L3


This model has already been successfully used in UU for the Walker game (limited release).

UU experience has shown that multiple chronicle layers cannot be created in a single step. But more importantly, it is not desirable to let the fan made age create the base chronicle. Therefore the 3 layers are created one by one at an appropriate time.


1.1. Layer 1 - Base Chronicle
A separate function should take care of creating the base chronicle 'UserAges', preferably a function implemented by Cyan. If there is going to be a hub age for accessing user ages the best moment to create the base chronicle would be upon entering the hub. If this hub is Relto or Nexus it should be done there.


1.2. Layer 2 - Age Chronicle
In order to avoid confusion as to which user age added a chronicle, the age chronicle should have the same name as the user age (unless the puzzle spans multiple ages). The best time of creation for the age chronicle is when the player first enters the starting age of the puzzle.


1.3. Layer 3 - Player Variables
Creation and modification is determined by game events.



2. Chronicle Layers in Detail

This section contains the Python code needed to read and write chronicles. Since navigating through 3 layers of chronicle vaultnodes can get pretty elaborate the functions provided here are ready to go and do not need any alteration (at least for TPOTS or UU).


2.1. Layer 1 - Base Chronicle
Setting up the base chronicle through Python is rather straightforward. As mentioned previously I would prefer this to be implemented by Cyan. The base chronicle should be written to the vault upon entering the hub where fan made ages are accessed (if any).

Code: Select all
# Python code by D'Lanor
from Plasma import *
from PlasmaTypes import *
from PlasmaKITypes import *

# Although the use of chronicle types seems to be obsolete we
# can define a unique type for this just in case.
# This definition should be added to the PlasmaKITypes module.
kChronicleUserAgeType = 4


    def ISetBaseChron(self):
        vault = ptVault()
        entry = vault.findChronicleEntry('UserAges')
        if (type(entry) == type(None)):
            PtDebugPrint('ISetBaseChron: Creating base chronicle for all user ages')
            vault.addChronicleEntry('UserAges', kChronicleUserAgeType, '')



2.2. Layer 2 - Age Chronicle
Requirement: The base chronicle 'UserAges' must exist.

The age creator implements the function which creates the chronicle for his/her age. Creation takes place when the player first enters the starting age of the puzzle.

Code: Select all
# Python code by D'Lanor
from Plasma import *
from PlasmaTypes import *


    def ISetAgeChron(self, ageName):
        if (not ageName):
            return
        ourChild = None
        vault = ptVault()
        entry = vault.findChronicleEntry('UserAges')
        if (type(entry) == type(None)):
            PtDebugPrint('DEBUG: No UserAges chronicle')
            return
        chronRefList = entry.getChildNodeRefList()
        for ourChron in chronRefList:
            chronChild = ourChron.getChild()
            chronChild = chronChild.upcastToChronicleNode()
            if (chronChild.chronicleGetName() == ageName):
                ourChild = chronChild
                break
        if (type(ourChild) == type(None)):
            PtDebugPrint('ISetAgeChron: Creating subchronicle for age %s' % ageName)
            newNode = ptVaultChronicleNode(0)
            newNode.chronicleSetName(ageName)
            newNode.chronicleSetValue('')
            entry.addNode(newNode)


Example
Write the age chronicle called 'Prad' within the base chronicle 'UserAges':

Code: Select all
    self.ISetAgeChron('Prad')

Result:
Code: Select all
  ChronicleFolder
    |
    |
    |__UserAges
         |
         |
         |__Prad


Note that 'UserAges' must already exists for this function to work.


2.3. Layer 3 - Player Variables
Requirements: The base chronicle 'UserAges' must exist. The age chronicle must exist.

These are the chronicle core functions that age creators will be using for their variables. These Python functions are designed to work only with layer 3 chronicles!

Code: Select all
# Python code by D'Lanor
from Plasma import *
from PlasmaTypes import *


# User Age Chronicle Core Functions (Generic for all user ages)
#
# IWriteAgeChron: Writes a layer 3 subchronicle for the user age.
#                 Be sure to always pass the correct name to the ageName argument.
#
# IReadAgeChron:  Reads a layer 3 subchronicle and returns the value.
#                 Notes: The function returns an integer 0 if the subchronicle does not exist.
#                        Existing chronicle values are always returned as a strings.

    def IWriteAgeChron(self, ageName, chronName, chronVal):
        ourChild = None
        vault = ptVault()
        entry = vault.findChronicleEntry('UserAges')
        if (type(entry) == type(None)):
            PtDebugPrint('DEBUG: No UserAges chronicle')
            return
        chronRefList = entry.getChildNodeRefList()
        for xpChron in chronRefList:
            xpChild = xpChron.getChild()
            xpChild = xpChild.upcastToChronicleNode()
            if (xpChild.chronicleGetName() == ageName):
                PtDebugPrint('IWriteAgeChron: Age chronicle found')
                chronRefList2 = xpChild.getChildNodeRefList()
                for xpChron2 in chronRefList2:
                    xpChild2 = xpChron2.getChild()
                    xpChild2 = xpChild2.upcastToChronicleNode()
                    if (xpChild2.chronicleGetName() == chronName):
                        ourChild = xpChild2
                        break
                if (type(ourChild) == type(None)):
                    PtDebugPrint('IWriteAgeChron: Creating age subchronicle with specified value')
                    newNode = ptVaultChronicleNode(0)
                    newNode.chronicleSetName(chronName)
                    newNode.chronicleSetValue(chronVal)
                    xpChild.addNode(newNode)
                else:
                    if (ourChild.chronicleGetValue() == chronVal):
                        PtDebugPrint('IWriteAgeChron: Age subchronicle value already correct, do nothing')
                        return
                    PtDebugPrint('IWriteAgeChron: Changing value of age subchronicle')
                    ourChild.chronicleSetValue(chronVal)
                    ourChild.save()



    def IReadAgeChron(self, ageName, chronName):
        vault = ptVault()
        entry = vault.findChronicleEntry('UserAges')
        if (type(entry) == type(None)):
            PtDebugPrint('DEBUG: No UserAges chronicle')
            return 0
        chronRefList = entry.getChildNodeRefList()
        for xpChron in chronRefList:
            xpChild = xpChron.getChild()
            xpChild = xpChild.upcastToChronicleNode()
            if (xpChild.chronicleGetName() == ageName):
                PtDebugPrint('IReadAgeChron: Age chronicle found')
                chronRefList2 = xpChild.getChildNodeRefList()
                for xpChron2 in chronRefList2:
                    xpChild2 = xpChron2.getChild()
                    xpChild2 = xpChild2.upcastToChronicleNode()
                    if (xpChild2.chronicleGetName() == chronName):
                        PtDebugPrint('IReadAgeChron: Age subchronicle found, retrieve value')
                        chronVal = xpChild2.chronicleGetValue()
                        return chronVal
        PtDebugPrint('IReadAgeChron: Chronicle missing')
        return 0


Example 1
Write the age subchronicle called 'JournalSeen' within the 'Walker' age chronicle and give it value 1:

Code: Select all
    self.IWriteAgeChron('Walker', 'JournalSeen', '1')

Result:
Code: Select all
  ChronicleFolder
    |
    |
    |__UserAges
         |
         |
         |__Walker
              |
              |__JournalSeen (value = 1)


The function 'IWriteAgeChron' handles all situations. It verifies if the chronicle to be modified exists. If not, it creates the chronicle with the specified value.
If the chronicle is already there the function changes its value.
If the chronicle is there and already has the correct value the function does nothing.

Note that 'UserAges' and 'Walker' must already exists for this function to work.


Example 2
Read the value of the age subchronicle 'JournalSeen' and then increase its value by 1:

Code: Select all
    jrnlLevel = self.IReadAgeChron('Walker', 'JournalSeen')
    if (not jrnlLevel):
        self.IWriteAgeChron('Walker', 'JournalSeen', '1')
        return
    nextLevel = int(jrnlLevel) + 1
    self.IWriteAgeChron('Walker', 'JournalSeen', str(nextLevel))

Note 1: In order to do calculations with chronicles we have to convert the datatype to integer. In order to write the calculated value to a chronicle we convert it back to a string.

Note 2: The function IReadAgeChron returns integer 0 when it does not find the chronicle value. So we can use an "if not" statement to check for that condition and abort (return). Optionally we can do other stuff such as creating the chronicle / giving it a value (see example).
Last edited by D'Lanor on Sun Nov 25, 2007 3:48 am, edited 1 time in total.
"It is in self-limitation that a master first shows himself." - Goethe
User avatar
D'Lanor
 
Posts: 1980
Joined: Sat Sep 29, 2007 4:24 am

Re: Fan made ages and vault chronicles

Postby belford » Sat Nov 24, 2007 12:12 pm

Looks reasonable.

Jumping in from a totally-unfamiliar-with-Age-scripting perspective: you have the idiom "if (type(entry) == type(None))" in several places. Is there a specific reason for that? In reasonably current Python (certainly works in 2.3 and later), you'd say "if (entry is None)", which does the same thing faster and more concisely. (And it still protects you from the type errors risked by "if (entry == None)".)

Similarly, as a stylistic thing, it's better to return None as a whoopsie value from IReadAgeChron(). This still permits the caller to say "if (not jrnlLevel)", but also allows "if (jrnlLevel is None)", which has the added bonus of letting the caller distinguish the whoopsie from a valid empty string.
belford
 
Posts: 344
Joined: Sat Sep 29, 2007 7:18 pm

Re: Fan made ages and vault chronicles

Postby Paradox » Sat Nov 24, 2007 12:14 pm

Uru PotS uses Python 2.2 and has to follow some old coding conventions.

If I'm correct, it appears that MOUL is using Python 2.3; but they might look at upgrading over the duration of the hiatus.
Paradox
 
Posts: 1292
Joined: Fri Sep 28, 2007 6:48 pm
Location: Canada

Re: Fan made ages and vault chronicles

Postby belford » Sat Nov 24, 2007 2:54 pm

Ok, thanks.
belford
 
Posts: 344
Joined: Sat Sep 29, 2007 7:18 pm

Re: Fan made ages and vault chronicles

Postby D'Lanor » Sun Nov 25, 2007 4:05 am

belford wrote:Similarly, as a stylistic thing, it's better to return None as a whoopsie value from IReadAgeChron(). This still permits the caller to say "if (not jrnlLevel)", but also allows "if (jrnlLevel is None)", which has the added bonus of letting the caller distinguish the whoopsie from a valid empty string.

Empty strings are not valid in chronicle layer 3. If we are reading the chronicle we want it to return a value. So an empty string should be treated the same as a chronicle that does not exist. I changed my post above to make that clear.

btw, the None object is returned by default if "return" is used without argument.
"It is in self-limitation that a master first shows himself." - Goethe
User avatar
D'Lanor
 
Posts: 1980
Joined: Sat Sep 29, 2007 4:24 am

Re: Fan made ages and vault chronicles

Postby Trylon » Sun Nov 25, 2007 8:27 am

How about we write some sort of Fan-Age SDK?

I vote for D'Lanor to start with it :P
One day I ran through the cleft for the fiftieth time, and found that uru held no peace for me anymore.
User avatar
Trylon
 
Posts: 1446
Joined: Fri Sep 28, 2007 11:08 pm
Location: Gone from Uru


Return to Scripting

Who is online

Users browsing this forum: No registered users and 2 guests