dsrc isn't necessary for this repo
[client-tools.git] / tools / perllib / Customization.pm
blobf87b0e8f9ed831c609413779aced408f5e1ead83
1 package Customization;
3 use strict;
4 use warnings;
6 # Module boiler plating.
8 BEGIN {
9 use Exporter ();
10 our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
12 # set the version for version checking
13 $VERSION = 1.00;
14 @ISA = qw(Exporter);
15 # @EXPORT = qw(&func1 &func2 &func4);
16 @EXPORT = qw(&createNewDataFromVariableInfo &dumpStringInHex &dumpVariableInfo &escapeBinaryData &getVariableInfoFromNewString &getVariableInfoFromOldString &initializeCustomization &removeEscapesFromString);
17 %EXPORT_TAGS = ( ); # eg: TAG => [ qw!name1 name2! ],
19 # your exported package globals go here,
20 # as well as any optionally exported functions
21 # @EXPORT_OK = qw($Var1 %Hashit &func3);
22 @EXPORT_OK = qw(%IdsByVariableName %VariableNamesById);
24 our @EXPORT_OK;
26 # exported package globals go here
27 # e.g.
28 # our $Var1;
29 # our %Hashit;
31 our %IdsByVariableName;
32 our %VariableNamesById;
34 # non-exported package globals go here
35 # e.g.
36 # our @more;
37 # our $stuff;
38 our $Debug;
40 # initialize package globals, first exported ones
41 # e.g.
42 # $Var1 = '';
43 # %Hashit = ();
44 %IdsByVariableName = ();
45 %VariableNamesById = ();
47 # then the others (which are still accessible as $Some::Module::stuff)
48 # $stuff = '';
49 # @more = ();
50 $Debug = 0;
52 # all file-scoped lexicals must be created before
53 # the functions below that use them.
55 # file-private lexicals go here
56 # e.g.
57 # my $priv_var = '';
58 # my %secret_hash = ();
60 # here's a file-private function as a closure,
61 # callable as &$priv_func; it cannot be prototyped.
62 # e.g.
63 # my $priv_func = sub {
64 # # stuff goes here.
65 # };
67 # make all your functions, whether exported or not;
68 # remember to put something interesting in the {} stubs
69 # e.g.
70 # sub func1 {} # no prototype
71 # sub func2() {} # proto'd void
72 # sub func3($$) {} # proto'd to 2 scalars
74 # this one isn't exported, but could be called!
75 # e.g.
76 # sub func4(\%) {} # proto'd to 1 hash ref
78 # ======================================================================
80 sub dumpStringInHex($)
82 my $data = shift;
83 my $dataLength = length $data;
85 print STDERR "dumping string (length=$dataLength characters):\n";
86 for (my $i = 0; $i < $dataLength; ++$i)
88 my $character = substr($data, $i, 1);
89 if ($character =~ m/\w/)
91 printf STDERR "$i: 0x%x ($character)\n", ord($character);
93 else
95 printf STDERR "$i: 0x%x (<?>)\n", ord($character);
100 # ======================================================================
101 # input: reference to variable info hash,
102 # directory string from old-style customization data,
103 # string representing variable path up to this point.
105 # output: results returned in variable info hash.
107 # return: return code, 1 if successful, 0 if failed.
109 # private
110 # ======================================================================
112 # Extra prototype needed for recursive calling within function.
113 sub getVariableInfoFromOldStringDirectory(\%$$);
115 sub getVariableInfoFromOldStringDirectory(\%$$)
117 my $variableInfoRef = shift;
118 my $dirData = shift;
119 my $fullDirectory = shift;
121 # Grab # variables.
122 if (($dirData =~ s/^([0-9a-fA-F]+)\#//) == 0)
124 warn "getVariableInfoFromOldStringDirectory(): failed to parse variable count from dir data [$dirData].\n";
125 return 0;
128 # Process variables local to this directory.
129 my $varCount = hex($1);
130 print "<dir: [$fullDirectory] has $varCount variables.>\n" if $Debug;
132 for (my $varIndex = 0; $varIndex < $varCount; ++$varIndex)
134 #-- Variable format: <variable name>#<contentByteCount>#<contents>
135 if (($dirData =~ s/^(\w+)\#([0-9a-fA-F]+)\#//) == 0)
137 warn "getVariableInfoFromOldStringDirectory(): failed to parse variable name, content size and contents from dir data [$dirData].\n";
138 return;
141 my $varShortName = $1;
142 my $varDataLength = hex($2);
144 #-- Take the first $varDataLength bytes off the front of $dirData.
145 my $varData = hex(substr($dirData, 0, $varDataLength));
146 $dirData = substr($dirData, $varDataLength);
148 #-- Try to fix negative numbers (assume anything over 32637 is really a negative number.
149 if (($varData =~ m/^\d+$/) && ($varData > 32637))
151 # Assume this is a 32-bit hex encoded negative number. Fix it up. I think this code can only
152 # work if perl is using 32-bit integers.
154 # Do 2's complement formula in reverse to get the absolute value of the negative number.
155 my $negativeValue = ($varData - 1);
156 $negativeValue = ~$negativeValue;
158 $varData = -$negativeValue;
161 #-- Add variable to variable info hash.
162 my $varFullName = $fullDirectory . $varShortName;
163 $$variableInfoRef{$varFullName} = $varData;
164 print "<oldVar: [$varFullName] = [$varData]>\n" if $Debug;
167 # Process subdirs local to this directory.
169 # Grab # subdirs.
170 if (($dirData =~ s/^([0-9a-fA-F]+)\#//) == 0)
172 warn "getVariableInfoFromOldStringDirectory(): failed to parse subdirectory count from dir data [$dirData].\n";
173 return 0;
176 my $subdirCount = hex($1);
177 print "<dir: [$fullDirectory] has $subdirCount subdirectories.>\n" if $Debug;
179 for (my $subdirIndex = 0; $subdirIndex < $subdirCount; ++$subdirIndex)
181 #-- Variable format: <variable name>#<contentByteCount>#<contents>
182 if (($dirData =~ s/^(\w+)\#([0-9a-fA-F]+)\#//) == 0)
184 warn "getVariableInfoFromOldStringDirectory(): failed to parse dir name, content size and contents from dir data [$dirData].\n";
185 return;
188 my $dirShortName = $1;
189 my $dirDataLength = hex($2);
191 #-- Take the first $varDataLength bytes off the front of $dirData.
192 my $subdirData = substr($dirData, 0, $dirDataLength);
193 $dirData = substr($dirData, $dirDataLength);
195 #-- Recursively call this function to process the local directory's data.
196 my $dirFullName = $fullDirectory . $dirShortName . '/';
197 print "<oldDir: [$dirFullName]>\n" if $Debug;
199 getVariableInfoFromOldStringDirectory(%$variableInfoRef, $subdirData, $dirFullName);
202 # Indicate success.
203 return 1;
206 # ======================================================================
207 # input: reference to variable info hash,
208 # directory string from old-style customization data,
210 # output: results returned in variable info hash.
212 # return: return code, 1 if successful, 0 if failed.
213 # ======================================================================
215 sub getVariableInfoFromOldString(\%$)
217 # Get parameters.
218 my $variableInfoRef = shift;
219 my $oldString = shift;
221 # Get the version number.
222 if (($oldString =~ s/^([0-9a-fA-F]+)\#//) == 0)
224 warn "getVariableInfoFromOldString:could not parse out version number:string [$oldString]\n";
225 return "";
228 if (hex($1) != 2)
230 warn "getVariableInfoFromOldString:version number [$1] unsupported:string [$oldString]\n";
231 return "";
234 # Fill up variable info from the root directory '/'.
235 return getVariableInfoFromOldStringDirectory(%$variableInfoRef, $oldString, '/');
238 # ======================================================================
239 # input: reference to variable info hash
240 # output: variables encoded with binary encoding scheme version 1.
242 # A variable info hash is a simple hash mapping a full customization
243 # variable path (e.g. /shared_owner/index_color_skin) to its value
244 # (e.g. 3).
245 # ======================================================================
247 sub createNewDataFromVariableInfo(\%)
249 # Build output string.
250 my $newData = "";
252 # Write version number.
253 $newData .= chr(1);
255 # Process each variable in the hash mapping variable name to value.
256 my $varInfoRef = shift;
257 my @variableNames = keys %$varInfoRef;
259 # Pass 1: count how many of these vars exist in the id table. Any vars not
260 # in the id table will be lost.
261 my $existingVariableCount = 0;
263 foreach my $variableName (@variableNames)
265 # Get value for variable.
266 if (defined($IdsByVariableName{$variableName}))
268 ++$existingVariableCount;
270 else
272 print "<varNotMapped: variable [$variableName] length [" . (length $variableName) . "] referenced but is not mapped, dropping value.>\n" if $Debug;
276 # Write variable count.
277 $newData .= chr($existingVariableCount);
279 # Setup minimum expected data size.
280 my $minExpectedDataLength = 2 + 2 * $existingVariableCount;
282 foreach my $variableName (keys %$varInfoRef)
284 # Process only if variable exists.
285 next if !defined($IdsByVariableName{$variableName});
287 # Get id for variable name.
288 my $variableId = $IdsByVariableName{$variableName};
289 die "variableId $variableId out of valid range 1..127" if ($variableId < 1) || ($variableId > 127);
291 my $byteCount;
293 # Build combined id: most significant bit is turned on if value is signed short; otherwise value is unsigned char.
294 # NOTE: this code assumes only the variable named <something>/blend_flat_chest can contain signed short values since
295 # I have no range info at this stage of the game. True at time of upgrade.
296 if ($variableName =~ m!/blend_flat_chest$!i)
298 $variableId |= 0x80;
299 $byteCount = 2;
301 else
303 $byteCount = 1;
306 # Get value for variable.
307 my $value = $$varInfoRef{$variableName};
309 # Write variable id and data.
310 $newData .= chr($variableId);
311 if ($byteCount == 1)
313 $newData .= chr($value & 0xff);
315 else
317 # Store 16-bit 2's complement number, within range -32768 .. 32767. As long as this machine uses
318 # 2's complement for signed integers, and assuming Perl guarantees at least 32 bits of precision on integer math,
319 # then it doesn't matter how many bits the value is stored in so long as the value is within the specified range.
320 if ($value > 32767)
322 $value = 32767;
324 elsif ($value < -32768)
326 $value = -32768;
329 $newData .= chr($value & 0xff);
330 $newData .= chr(($value & 0xff00) >> 8);
334 # Check min size.
335 my $newDataLength = length $newData;
336 if ($newDataLength < $minExpectedDataLength)
338 warn "wrote fewer bytes than expected; min expected=$newDataLength, written=$newDataLength.";
339 dumpVariableInfo(%$varInfoRef);
342 return $newData;
345 # ======================================================================
346 # input: variable info hash, mapping full-path variable info name to value.
347 # output: dumps variable names and values, sorted by variable name.
348 # ======================================================================
350 sub dumpVariableInfo(\%)
352 my $variableInfoRef = shift;
354 foreach my $variableName (sort keys %$variableInfoRef)
356 my $value = $$variableInfoRef{$variableName};
357 print "[$variableName]=[$value]\n";
361 # ======================================================================
362 # input: string, possibly with embedded nulls, to be escaped so it doesn't contain embedded nulls.
363 # output: string, possibly with added escape characters, does not contain any NULLs.
364 # ======================================================================
366 sub escapeBinaryData($)
368 my $rawString = shift;
369 my $escapedString = "";
371 my $rawLength = length $rawString;
372 for (my $i = 0; $i < $rawLength; ++$i)
374 my $rawChar = substr($rawString, $i, 1);
375 if (ord($rawChar) == 0)
377 # replace 0x00 with 0xff 0x01
378 $escapedString .= chr(0xff) . chr(0x01);
380 elsif (ord($rawChar) == 0xff)
382 # replace 0xff with 0xff 0x02
383 $escapedString .= chr(0xff) . chr(0x02);
385 else
387 $escapedString .= $rawChar;
391 # Add end-of-data marker.
392 $escapedString .= chr(0xff);
393 $escapedString .= chr(0x03);
395 return $escapedString;
398 # ======================================================================
399 # input: escaped string.
400 # output: de-escaped binary data with possible embedded nulls.
401 # ======================================================================
403 sub removeEscapesFromString($)
405 my $escapedString = shift;
406 my $rawString = "";
408 my $escapedLength = length $escapedString;
409 for (my $i = 0; $i < $escapedLength; ++$i)
411 my $escapedChar = substr($escapedString, $i, 1);
412 if (ord($escapedChar) == 0xff)
414 # Look at next escaped character to find out what the
415 # real binary value looks like, consume it.
416 my $nextChar = substr($escapedString, $i+1, 1);
417 ++$i;
419 if (ord($nextChar) == 0x01)
421 # this is the escape sequence 0xff 0x01 => 0x00
422 $rawString .= chr(0x00);
424 elsif (ord($nextChar) == 0x02)
426 # this is the escape sequence 0xff 0x02 => 0xff
427 $rawString .= chr(0xff);
429 elsif (ord($nextChar) == 0x03)
431 # ignore, end of data marker.
432 if (($i + 1) != $escapedLength)
434 warn "found escape for end of data, but at index " . ($i+1) . " of total length $escapedLength.";
437 else
439 my $warningString;
440 $warningString = sprintf("removeEscapesFromString: found invalid escape sequence 0xff 0x%x in escaped string [$escapedString], escape failure.\n", ord($nextChar));
441 warn $warningString;
442 return "";
445 else
447 $rawString .= $escapedChar;
451 return $rawString;
454 # ======================================================================
455 # input: pass-by-reference variable info hash. Will be filled upon return.
456 # return: 1 if successful, 0 if false.
457 # ======================================================================
459 sub getVariableInfoFromNewString(\%$)
461 # Get parameters.
462 my $variableInfoRef = shift;
463 my $encodedDataString = shift;
465 if ($Debug)
467 print "<getVariableInfoFromNewString: dumping escaped string>\n";
468 dumpStringInHex($encodedDataString);
471 my $data = removeEscapesFromString($encodedDataString);
473 if ($Debug)
475 print "<getVariableInfoFromNewString: dumping unescaped string>\n";
476 dumpStringInHex($data);
479 # Get version.
480 my $dataLength = length $data;
481 my $dataIndex = 0;
483 if (ord(substr($data, $dataIndex, 1)) != 1)
485 warn "getVariableInfoFromNewString: unsupported version " . ord(substr($data, $dataIndex, 1));
486 return 0;
488 ++$dataIndex;
490 # Get variable count.
491 my $variableCount = ord(substr($data, $dataIndex, 1));
492 ++$dataIndex;
494 # Do a quick string size sanity check.
495 my $minExpectedLength = 2 + $variableCount * 2;
496 if ($dataLength < $minExpectedLength)
498 warn "getVariableInfoFromNewString: encoded data string is too small for encoded variable count [$variableCount], expecting at least [$minExpectedLength], has [$dataLength] bytes, padding with 0x20.";
499 dumpStringInHex($encodedDataString);
500 dumpStringInHex($data);
502 # Try padding with spaces.
503 $data .= chr(0x20) x ($minExpectedLength + 1 - $dataLength);
504 $dataLength = $minExpectedLength + 1;
507 # Handle each variable.
508 for (my $variableIndex = 0; $variableIndex < $variableCount; ++$variableIndex)
510 if ($dataIndex + 1 >= $dataLength)
512 warn "Truncating variable unpacking due to unexpected termination of packed data.\n";
513 last;
516 # Get combined variable id.
517 my $combinedId = ord(substr($data, $dataIndex, 1));
518 ++$dataIndex;
520 # Break combined Id into data length and variable id.
521 my $variableId = $combinedId & 0x7f;
522 my $variableDataLength = (($combinedId & 0x80) != 0) ? 2 : 1;
524 # Get variable's value.
525 my $variableValue;
527 if ($variableDataLength == 2)
529 # Read & interpret as signed 16-bit value.
530 my $lowByte = ord(substr($data, $dataIndex, 1));
531 ++$dataIndex;
533 my $hiByte = ord(substr($data, $dataIndex, 1));
534 ++$dataIndex;
535 $hiByte = 0x20 if !defined($hiByte);
537 if (($hiByte & 0x80) != 0)
539 # represents a negative number.
541 # get binary representation right: assumes 32-bit perl representation, not guaranteed on all platforms.
542 my $negativeValue = 0xffff0000 | ($hiByte << 8) | $lowByte;
544 # interpret as signed integer.
545 $variableValue = sprintf("%d", $negativeValue);
547 else
549 $variableValue = ($hiByte << 8) | $lowByte;
552 else
554 # Read & interpret as single unisgned byte.
555 $variableValue = ord(substr($data, $dataIndex, 1));
556 ++$dataIndex;
559 # Lookup variable name by value.
560 my $variableName = $VariableNamesById{$variableId};
561 if (!defined($variableName))
563 print "newData: found variable id [$variableId] with no name mapping.\n";
564 return 0;
567 # Map variable name to value in returned parameter
568 print "<getVariableInfoFromNewString: variable [$variableName] appears multiple times in customization data>\n" if ($Debug && defined($$variableInfoRef{$variableName}));
569 $$variableInfoRef{$variableName} = $variableValue;
572 # Indicate success.
573 return 1;
576 # ======================================================================
577 # Read variable id assignment info from customization id manager's MIF file.
579 # input:
580 # string: CustomizationIdManager's initialization MIF filename.
581 # ======================================================================
583 sub initializeCustomization($)
585 my $filename = shift;
586 open(MIF_FILE, $filename) or die "failed to open specified CustomizationIdManager mif file [$filename]: $!";
588 my $nextAssignmentId = 0;
589 my $expectingId = 1;
591 while (<MIF_FILE>)
593 chomp();
594 if (m/int16\s+(\d+)\s*$/)
596 # Ensure we're expecting a new id.
597 die "error: file [$filename] appears malformed, out of order int16/cstring declarations.\n" if !$expectingId;
598 $expectingId = 0;
600 $nextAssignmentId = $1;
602 elsif (m/cstring\s+\"([^\"]+)\"\s*$/)
604 # Ensure we're expecting a variable name.
605 die "error: file [$filename] appears malformed, out of order int16/cstring declarations.\n" if $expectingId;
606 $expectingId = 1;
608 # Add new variable name. It is associated with $nextAssignmentId collected previously.
609 $IdsByVariableName{$1} = $nextAssignmentId;
610 $VariableNamesById{$nextAssignmentId} = $1;
611 print "<existing: mapping variable name [$1] to [$nextAssignmentId]>\n" if $Debug;
615 close(MIF_FILE);
617 return 1;
620 # ======================================================================
622 END { } # module clean-up code here (global destructor)
624 # Return true from the file.