is it worth adding some code for handling of special characters in group names, or is this a bigger task than I'm imagining it might be...?
I'm playing with some code, which I think is a "good enough" solution. It's tricky, because the "proper" way to fix it would be to fix the code in Props.java, right at the source in getArray (actually there's a bunch of getArray functions, so you'd want to fix them all). The getArray functions are little helper functions that retrieve a value from an ini file, and then treat it like a CSV array (comma separated value). It splits the string into an array of smaller strings wherever it finds a comma, then trims any spaces at the beginning and end of each one.
(That trimming is important to note for later)So a more thorough fix would be to create a setArray function that accepts an array as a parameter and does proper character escaping when it encounters a comma (i.e. replace , with \, or something), then fix up the getArray to recognize those character escapings when reading it back in.
I didn't do that. I took a lazier approach:
System.out.println(" loading level group " + idx);
// first string is the level group key identifier
// second string is a BigInteger used as bitfield to store available levels
String[] s = props.getArray("group" + idx, null);
if (s == null || s.length < 2 || s[0] == null) {
System.out.println(" no valid group data found... breaking...");
break;
}
String s1 = s[s.length-1]; // get the last
String groupName = "";
// due to a bug in the ini property "Array" saving, commas are not escaped, and so they're treated as you would any column separator
// because the getArray function above trims out any spaces, knowledge if there was or was not a space after a comma is lost
// so let's just assume there's always a space after it, because that's better grammar.
for (int a = 0; a < s.length-1; a++) {
groupName += s[a];
if (a < s.length-2 ) {
groupName +=", "; //we're assuming there's always a space after any comma
}
}
System.out.println(" group name identified: " + groupName);
So previously the code would getArray to get the CSV from the Player.ini file. It would only proceed if there were exactly 2 values: 1) the group name, and 2) the unlockedLevels. More or less than 2 values, and it would skip onto the next group. So now, I read in all values, and only skip if there are less than 2. Instead of taking the 2nd value as the unlockedBits, I take the
last value as the bits.
For the group name, I concatenate all the other values together. And I put a ", " between them.
So, the only thing I've lost is that because the getArray trims any spaces, I have no way of knowing if there's supposed to be a space after the comma or not. So I just assume there is, and leave it at that.
I figured that removing the "else = blank record" script altogether might leave it so that records would be displayed for all levels, whether they were unlocked or not. But, it seems that the "up to a certain point" code is still being naughty.
I then tried removing this entire section of code altogether, and it wiped my records (luckily I had a backup!). Really not sure what to do about this, but willing to learn.
It's not surprising that commenting out that (or a part of it) didn't help. The
else put(BLANK_LEVEL_RECORD) part isn't replacing or hiding existing records.
For each level, the game saves "completed (true/false)", "lemmingsSaved (integer)", "skillsUsed (integer)", "timeElapsed (integer)", "score (integer)". The assumption is that if "completed" is false (or doesn't exist) then logically the other values shouldn't exist either... they're only stats for if the level is completed. So, if the level's not completed, don't even try to load non-existant stats. Just create a blank record. I think that assumption still holds water.
Could it be made so that any available stats are displayed, regardless of a level's locked/unlocked status?
Yes, but not without some modifications. That's exactly what the code we commented out in the first place did.
if ( j == 0 || unlockedLevels.testBit(j) ) That's saying, try and load the records if it's the 1st level in the group, OR the level is unlocked. Removing that just loads all the stats up to the last level the ini files knows about. Unfortunately
that is the problem. The INI file doesn't know how many levels total there are... it can't tell the code where to stop, and at this point, the INI file is all we're asking.
So, I see 2 possible solutions around that: 1) add the total level count to the ini file, or 2) query whatever
does know how many levels there are.
Stats for listed levels (whether locked or unlocked) should always be displayed...
That's also a valid 3rd option. But I don't know that I'd consider
any option trivial.
For option 3, the difficulty is that currently the code specifies what key to read by name, one after the other. i.e. group0_level0_completed, then group0_level1_completed, ... , group0_level99_completed, ... , group0_level100_completed... etc. Without knowing how many levels there are, what number should you decide to stop at? Instead you have to flip that on it's ear, and loop through all the properties that
do exist. I don't believe that code exists yet, so you'd have to write it. Probably best to put it in the Props.java. It might look something like:
public int getHighestLevel(int groupNum) {
//psuedo-code
for each key in Properties
if key.startsWith("group" + grounNum + "_level") then
get the digits up to the next _ and compare with the next key until you find the highest one.
return highestNum;
}
Hmm... yeah, actually I do like option 3 the best. It retains the most backwards compatibility with SuperLemmini ... and, if there's no levels saved, who cares if it's unlocked or not (as far as record showing goes).
The other options I was thinking of require making changes to how the INI is saved, or I don't even know yet where to read in the total level number otherwise.