Profile location

The user's profile file is named "pers2.dat" and the location depends on the platform and version of World of Goo.

Windows Vista and Windows 7

Prior to version 1.10 it was stored in %ProgramData%, e.g. C:\ProgramData\2DBoy\WorldOfGoo

From version 1.10 it is stored in the user's profile under %LOCALAPPDATA%, e.g. C:\Users\david\AppData\Local\2DBoy\WorldOfGoo

Windows XP

Prior to version 1.10 it was stored in the All Users profile under %ALLUSERSPROFILE%, e.g. C:\Documents and Settings\All Users\Application Data\2DBoy\WorldOfGoo

From version 1.10 it is stored in the user's profile under %LOCALAPPDATA%, e.g. C:\Documents and Settings\david\Local Settings\Application Data\2DBoy\WorldOfGoo

Mac OS X

The profile is stored in the user's home directory under Library/Application Support/WorldOfGoo/

Linux

In the Linux native port, the profile is stored at ${HOME}/.WorldOfGoo/pers2.dat

Users playing under PlayOnLinux or wine may have their profile located in one of the following locations depending on version:

  • ${HOME}/.PlayOnLinux/wineprefix/WorldOfGoo/drive_c/windows/profiles/${USERNAME}/Application Data/2DBoy/WorldOfGoo
  • ${HOME}/.PlayOnLinux/wineprefix/WorldOfGoo/drive_c/windows/profiles/All Users/Application Data/2DBoy/WorldOfGoo
  • ${HOME}/.wine/drive_c/windows/profiles/${USERNAME}/Application Data/2DBoy/WorldOfGoo
  • ${HOME}/.wine/drive_c/windows/profiles/All Users/Application Data/2DBoy/WorldOfGoo

iPad

On the iPad, the profile is serialised into a string which is stored in a single preferences entry.

With wog.app installed in /Applications/, the property list file is found at /var/mobile/Library/Preferences/com.2dboy.worldofgoo.plist.

With wog.app installed normally through the App Store, it will presumably be located in the application's sandbox in /var/mobile/Applications/GUID/Library

The actual plist contains a single entry, "pers2.dat", with binary data.

davids-iPhone-2:/var/mobile/Library/Preferences root# plutil  com.2dboy.worldofgoo.plist
{
    "pers2.dat" = <342c6d72 7070312c 30392c70 726f6669 6c655f30 31323338 [...many bytes snipped...] 5f2c5f2c 302e>;
}

As with other iPad files, the pers2.dat contents are unencrypted.

Profile Encryption

The profile is encrypted using the platform's standard .bin file encryption, i.e. AES for Windows and Linux, and XOR for Mac OS X, except the plaintext is padded with zero bytes. The character encoding used is unknown; most likely it is the native character set of the operating system.

Profile Format

At the highest level, it contains a sequence of strings, terminated by the sequence ",0." (without quotes). Each string is encoded with a decimal integer signifying its length, followed by a comma, followed by the string contents.
For example, "foo" would be encoded as "3,foo" (quotes for clarity only).

Note that the number specifies the number of BYTES that follow, not characters, so be careful not to use Unicode functions at this stage. This will particularly show up in version 1.40+ where the Unicode representation of non-ASCII characters in profile names has been fixed. See also the notes at the bottom of this page regarding encoding.

The strings in the data file describe a dictionary, with alternating keys and values. Keys in use are:

  • countrycode: values are two-letter ISO 3166-1 country codes, or EU for the European Union, or XX for unknown.
  • fullscreen: indicates whether the game runs in full-screen or in windowed mode; value is either "true" or "false"
  • mrpp: most recent player profile; value is a decimal integer between 1 and 3.
  • profile_0/profile_1/profile_2: the three player profile slots.

countrycode and fullscreen are not present in the iPad version.

Obviously, the player profile strings are the most interesting. These strings consist of a number of fields, seperated by commas. The first four fields are:

  • player name
  • player flags (see below)
  • total play time (in seconds)
  • number of levels played/completed

The player flags field is an integer that combines several bits:

Flag Meaning
1 online play is enabled
2 World of Goo Corporation has been unlocked
4 World of Goo Corporation has been destroyed
8 the whistle has been found
16 the Terms & Conditions have been accepted (this unlocks Deliverance)

So a player that has finished the first two chapters, for example, would have flags set to 2|8 = 10 or 1|2|8 = 11 (here '|' indicates bitwise-or).

Then follows, for each level:

  • level id (same as the directory name in res/levels/)
  • greatest number of balls collected
  • least number of moves used
  • least time taken (in seconds)

Note that these three stats are "best ever" and are independent for each stat. i.e. they were NOT necessarily achieved during the same level attempt.

The number of levels in this list is not necessarily equal to the fourth field in the header (levels played).

A level id with an integer value indicates the end of level data. This is usually 0, meaning no fields follow. However if it is a positive integer, it is the number of levels that the user has skipped, and will be followed by a list of the level IDs.

Then, the World of Goo Corporation tower data follows, which is a string prefixed by an underscore character (_) which is how it can be distinguished from more level data.

It may be empty if WoGC has not been unlocked yet. Otherwise, it contains first the location of balls, and then the configuration of strands (connections between balls). All data is seperated by colon characters (:).

Each ball description consists of six fields; for example:
b:Drained:-61.88:211.98:-0.96:1.75. The fields are:

  • "b" indicating ball (fixed)
  • "Drained" indicating ball type (fixed)
  • x-coordinate as a decimal number
  • y-coordinate as a decimal number
  • x-velocity as a decimal number
  • y-velocity as a decimal number

Each strand description consists of seven fields; for example: s:Drained:288,20,9.000,140.0,0. The fields are:

  • "s" indicating strand (fixed)
  • "Drained" indicating ball type (fixed)
  • first endpoint (zero-based index of a ball in the list above)
  • second endpoint (same as above)
  • spring constant: read from the ball.xml and always "9.0"
  • "natural" length: length of strand when it was created
  • "0" or "1": indicates whether or not a ball was absorbed to create this strand, which is released if the strand is destroyed.

Next, the online player key follows, which is prefixed by an underscore; the player key is 32 characters long and consists of lower-case hexadecimal digits. If the player has never connected to the internet, the key is empty.

Since this key is used to authenticate players online, it should be kept private. For other purposes e.g. Soultaker's site, he proposes to take an MD5 hash code of the string and use that instead, so players can be identified without compromising their scoreboard standing. For example: "d41d8cd98f00b204e9800998ecf8427e" would be transformed to "74be16979710d4c4e7c6647856088456". If you do not want players to be globally identifiable, use an HMAC instead.

The final field is a decimal integer, representing the number of newly collected goo balls since the player last visited World of Goo Corporation. This number is displayed on the chapter and title screen overlay.

Credit to Soultaker for first documenting this file format.

In the iPad version, two further fields follow. If the user has never connected to the Game Centre, they are both "_". Otherwise, the first is an underscore followed by their Game Centre Player Key (corresponding to GKAuthenticatedPlayerKey in com.apple.gamed.plist), for example "_G:100002345". The second field is an underscore followed by their Game Centre name, for example "_davidc".

Name encoding

The name stored in the profile is technically UTF-8. In the case of version 1.40 and above, this is correct. However there is a bug in previous versions of World of Goo. It appears that the top bit of the continuation bytes is cleared when the profile is saved.

For example, the character ä, Unicode code point U+00E4, should be encoded in UTF-8 as C3 A4, but in pers2.dat you actually find C3 24.

Given that (a) the first byte of the UTF-8 representation allows you to determine the number of continuation bytes by looking in the high-order bits and (b) the continuation bits should ALL begin with the high bit set (binary 10xx xxxx), it is possible to write code to reconstruct the correct UTF-8 representation: use the first byte to determine the number of continuation bytes, and then for this number of subsequent bytes, set the top bit. Then use standard UTF-8 routines to decode it.

This has been confirmed working for 2-byte UTF-8 characters. It is presumed that it will work with 3- and 4-byte representations; at least it can do no harm since these should all have the top bit set in the continuation bytes anyway.

Sample code from GooTool 1.0.3:

    // Go through and restore high-order bits for UTF-8 sequences (#0000270)
 
    int nskip = 0;
 
    for (int i=0; i<buf.length; ++i) {
      if (nskip > 0) {
        buf[i] |= 0x80; // set top bit
        nskip--;
      }
      else if ((buf[i] & 0xE0) == 0xC0) { // 110yyyyy: U+0080 to U+07FF
        nskip = 1;
      }
      else if ((buf[i] & 0xF0) == 0xE0) { // 1110zzzz: U+0800 to U+FFFF
        nskip = 2;
      }
      else if ((buf[i] & 0xF8) == 0xF0) { // 11110www: U+010000 to U+10FFFF
        nskip = 3;
      }
    }
 
    return new String(buf, "UTF-8");