Split Nenya into proper Maven submodules (same treatment Narya got).

This commit is contained in:
Michael Bayne
2012-02-27 11:46:22 -08:00
parent 90146d517d
commit 5e2380eb24
645 changed files with 358 additions and 283 deletions
@@ -0,0 +1,170 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image {
import com.threerings.util.Log;
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.util.RandomUtil;
import com.threerings.util.StringUtil;
public class ClassRecord
{
private const log :Log = Log.getLog(ClassRecord);
/** An integer identifier for this class. */
public var classId :int;
/** The name of the color class. */
public var name :String;
/** The source color to use when recoloring colors in this class. */
public var source :uint;
/** Data identifying the range of colors around the source color
* that will be recolored when recoloring using this class. */
public var range :Array;
/** The default starting legality value for this color class. See
* {@link ColorRecord#starter}. */
public var starter :Boolean;
/** The default colorId to use for recoloration in this class, or
* 0 if there is no default defined. */
public var defaultId :int;
/** A table of target colors included in this class. */
public var colors :Map = Maps.newMapOf(int);
/** Used when parsing the color definitions. */
public function addColor (record :ColorRecord) :void
{
// validate the color id
if (record.colorId > 127) {
log.warning("Refusing to add color record; colorId > 127",
"class", this, "record", record);
} else if (colors.containsKey(record.colorId)) {
log.warning("Refusing to add duplicate colorId",
"class", this, "record", record, "existing", colors.get(record.colorId));
} else {
record.cclass = this;
colors.put(record.colorId, record);
}
}
/**
* Translates a color identified in string form into the id that should be used to look up
* its information. Throws an exception if no color could be found that associates with
* that name.
*/
public function getColorId (name :String) :int
{
// Check if the string is itself a number
var id :int = name as int;
if (colors.containsKey(id)) {
return id;
}
// Look for name matches among all colors
for each (var color :ColorRecord in colors.values()) {
if (StringUtil.compareIgnoreCase(color.name, name) == 0) {
return color.colorId;
}
}
// That input wasn't a color
throw new Error("No color named '" + name + "'", 0);
}
/** Returns a random starting id from the entries in this class. */
public function randomStartingColor () :ColorRecord
{
// figure out our starter ids if we haven't already
if (_starters == null) {
_starters = [];
for each (var color :ColorRecord in colors) {
if (color.starter) {
_starters.push(color);
}
}
}
// sanity check
if (_starters.length < 1) {
log.warning("Requested random starting color from colorless component class",
"class", this);
return null;
}
// return a random entry from the array
return RandomUtil.pickRandom(_starters);
}
/**
* Get the default ColorRecord defined for this color class, or
* null if none.
*/
public function getDefault () :ColorRecord
{
return colors.get(defaultId);
}
public function toString () :String
{
return "[id=" + classId + ", name=" + name + ", source=#" +
StringUtil.toHex(source & 0xFFFFFF, 6) +
", range=" + StringUtil.toString(range) +
", starter=" + starter + ", colors=" +
StringUtil.toString(colors.values()) + "]";
}
public static function fromXml (xml :XML) :ClassRecord
{
var rec :ClassRecord = new ClassRecord();
rec.classId = xml.@classId;
for each (var colorXml :XML in xml.color) {
rec.colors.put(int(colorXml.@colorId), ColorRecord.fromXml(colorXml, rec));
}
rec.name = xml.@name;
var srcStr :String = xml.@source;
rec.source = parseInt(srcStr.substr(1, srcStr.length - 1), 16);
rec.range = toNumArray(xml.@range);
rec.defaultId = xml.@defaultId;
return rec;
}
protected static function toNumArray (str :String) :Array
{
if (str == null) {
return null;
}
return str.split(",").map(function(element :*, index :int, arr :Array) :Number {
return Number(element);
});
}
protected var _starters :Array;
}
}
@@ -0,0 +1,215 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image {
import com.threerings.util.Log;
import com.threerings.util.Map;
import com.threerings.util.Maps;
import com.threerings.util.StringUtil;
public class ColorPository
{
private const log :Log = Log.getLog(ColorPository);
public function getClasses () :Array
{
return _classes.values();
}
/**
* Returns an array containing the records for the colors in the
* specified class.
*/
public function getColors (className :String) :Array
{
// make sure the class exists
var record :ClassRecord = getClassRecordByName(className);
if (record == null) {
return null;
}
// create the array
return record.colors.values();
}
/**
* Returns an array containing the ids of the colors in the specified
* class.
*/
public function getColorIds (className :String) :Array
{
// make sure the class exists
var record :ClassRecord = getClassRecordByName(className);
if (record == null) {
return null;
}
return record.colors.values().map(
function(element :ColorRecord, index :int, arr :Array) :int {
return element.colorId;
});
}
/**
* Returns true if the specified color is legal for use at character creation time. false is
* always returned for non-existent colors or classes.
*/
public function isLegalStartColor (classId :int, colorId :int) :Boolean
{
var color :ColorRecord = getColorRecord(classId, colorId);
return (color == null) ? false : color.starter;
}
/**
* Returns a random starting color from the specified color class.
*/
public function getRandomStartingColor (className :String) :ColorRecord
{
// make sure the class exists
var record :ClassRecord = getClassRecordByName(className);
return (record == null) ? null : record.randomStartingColor();
}
/**
* Looks up a colorization by id.
*/
public function getColorization (classId :int, colorId :int) :Colorization
{
var color :ColorRecord = getColorRecord(classId, colorId);
return (color == null) ? null : color.getColorization();
}
/**
* Looks up a colorization by color print.
*/
public function getColorizationByPrint (colorPrint :int) :Colorization
{
return getColorization(colorPrint >> 8, colorPrint & 0xFF);
}
/**
* Looks up a colorization by class and color names.
*/
public function getColorizationByName (className :String, colorName :String) :Colorization
{
var crec :ClassRecord = getClassRecordByName(className);
if (crec != null) {
var colorId :int = crec.getColorId(colorName);
var color :ColorRecord = crec.colors.get(colorId);
if (color != null) {
return color.getColorization();
}
}
return null;
}
/**
* Looks up a colorization by class and color Id.
*/
public function getColorizationByNameAndId (className :String, colorId :int) :Colorization
{
var crec :ClassRecord = getClassRecordByName(className);
if (crec != null) {
var color :ColorRecord = crec.colors.get(colorId);
if (color != null) {
return color.getColorization();
}
}
return null;
}
/**
* Loads up a colorization class by name and logs a warning if it doesn't exist.
*/
public function getClassRecordByName (className :String) :ClassRecord
{
for each (var crec :ClassRecord in _classes.values()) {
if (crec.name == className) {
return crec;
}
}
log.warning("No such color class", "class", className, new Error());
return null;
}
/**
* Looks up the requested color class record.
*/
public function getClassRecord (classId :int) :ClassRecord
{
return _classes.get(classId);
}
/**
* Looks up the requested color record.
*/
public function getColorRecord (classId :int, colorId :int) :ColorRecord
{
var record :ClassRecord = getClassRecord(classId);
if (record == null) {
// if they request color class zero, we assume they're just
// decoding a blank colorprint, otherwise we complain
if (classId != 0) {
log.warning("Requested unknown color class",
"classId", classId, "colorId", colorId, new Error());
}
return null;
}
return record.colors.get(colorId);
}
/**
* Looks up the requested color record by class & color names.
*/
public function getColorRecordByName (className :String, colorName :String) :ColorRecord
{
var record :ClassRecord = getClassRecordByName(className);
if (record == null) {
log.warning("Requested unknown color class",
"className", className, "colorName", colorName, new Error());
return null;
}
var colorId :int = record.getColorId(colorName);
return record.colors.get(colorId);
}
/**
* Loads up a serialized color pository from the supplied resource
* manager.
*/
public static function fromXml (xml :XML) :ColorPository
{
var pos :ColorPository = new ColorPository();
for each (var classXml :XML in xml.elements("class")) {
pos._classes.put(int(classXml.@classId), ClassRecord.fromXml(classXml));
}
return pos;
}
/** Our mapping from class names to class records. */
protected var _classes :Map = Maps.newMapOf(int);
}
}
@@ -0,0 +1,103 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image {
import com.threerings.util.Log;
import com.threerings.util.StringUtil;
public class ColorRecord
{
private const log :Log = Log.getLog(ColorRecord);
/** The colorization class to which we belong. */
public var cclass :ClassRecord;
/** A unique colorization identifier (used in fingerprints). */
public var colorId :int;
/** The name of the target color. */
public var name :String;
/** Data indicating the offset (in HSV color space) from the
* source color to recolor to this color. */
public var offsets :Array;
/** Tags this color as a legal starting color or not. This is a
* shameful copout, placing application-specific functionality
* into a general purpose library class. */
public var starter :Boolean;
/**
* Returns a value that is the composite of our class id and color
* id which can be used to identify a colorization record. This
* value will always be a positive integer that fits into 16 bits.
*/
public function getColorPrint () :int
{
return ((cclass.classId << 8) | colorId);
}
/**
* Returns the data in this record configured as a colorization
* instance.
*/
public function getColorization () :Colorization
{
return new Colorization(getColorPrint(), cclass.source,
cclass.range, offsets);
}
public function toString () :String
{
return "[id=" + colorId + ", name=" + name +
", offsets=" + StringUtil.toString(offsets) +
", starter=" + starter + "]";
}
public static function fromXml (xml :XML, cclass :ClassRecord) :ColorRecord
{
var rec :ColorRecord = new ColorRecord();
rec.colorId = xml.@colorId;
rec.name = xml.@name;
rec.offsets = toNumArray(xml.@offsets);
rec.starter = xml.@starter;
rec.cclass = cclass;
return rec;
}
protected static function toNumArray (str :String) :Array
{
if (str == null) {
return null;
}
return str.split(",").map(function(element :String, index :int, arr :Array) :Number {
return Number(StringUtil.trim(element));
});
}
/** Our data represented as a colorization. */
protected var _zation :Colorization;
}
}
@@ -0,0 +1,185 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image {
import com.threerings.display.ColorUtil;
import com.threerings.util.Hashable;
import com.threerings.util.Short;
import com.threerings.util.StringUtil;
public class Colorization
implements Hashable
{
/** Every colorization must have a unique id that can be used to
* compare a particular colorization record with another. */
public var colorizationId :int;
/** The root color for the colorization. */
public var rootColor :uint;
/** The range around the root color that will be colorized (in
* delta-hue, delta-saturation, delta-value. Note that this range is inclusive. */
public var range :Array;
/** The adjustments to make to hue, saturation and value. */
public var offsets :Array;
/**
* Constructs a colorization record with the specified identifier.
*/
public function Colorization (colorizationId :int, rootColor :uint,
range :Array, offsets :Array)
{
this.colorizationId = colorizationId;
this.rootColor = rootColor;
this.range = range;
this.offsets = offsets;
// compute our HSV and fixed HSV
_hsv = ColorUtil.RGBtoHSB(ColorUtil.getRed(rootColor), ColorUtil.getGreen(rootColor),
ColorUtil.getBlue(rootColor));
_fhsv = toFixedHSV(_hsv);
}
/**
* Returns the root color adjusted by the colorization.
*/
public function getColorizedRoot () :uint
{
return recolorColor(_hsv);
}
/**
* Adjusts the supplied color by the offests in this colorization,
* taking the appropriate measures for hue (wrapping it around) and
* saturation and value (clipping).
*
* @return the RGB value of the recolored color.
*/
public function recolorColor (hsv :Array) :uint
{
// for hue, we wrap around
var hue :Number = hsv[0] + offsets[0];
if (hue > 1.0) {
hue -= 1.0;
}
// otherwise we clip
var sat :Number = Math.min(Math.max(hsv[1] + offsets[1], 0), 1);
var val :Number = Math.min(Math.max(hsv[2] + offsets[2], 0), 1);
// convert back to RGB space
return ColorUtil.HSBtoRGB(hue, sat, val);
}
/**
* Returns true if this colorization matches the supplied color, false
* otherwise.
*
* @param hsv the HSV values for the color in question.
* @param fhsv the HSV values converted to fixed point via {@link
* #toFixedHSV} for the color in question.
*/
public function matches (hsv :Array, fhsv :Array) :Boolean
{
// check to see that this color is sufficiently "close" to the
// root color based on the supplied distance parameters
if (distance(fhsv[0], _fhsv[0], Short.MAX_VALUE) >
range[0] * Short.MAX_VALUE) {
return false;
}
// saturation and value don't wrap around like hue
if (Math.abs(_hsv[1] - hsv[1]) > range[1] ||
Math.abs(_hsv[2] - hsv[2]) > range[2]) {
return false;
}
return true;
}
public function hashCode () :int
{
return colorizationId ^ rootColor;
}
public function equals (other :Object) :Boolean
{
if (other is Colorization) {
return (Colorization(other)).colorizationId == colorizationId;
} else {
return false;
}
}
public function toString () :String
{
return String(colorizationId);
}
/**
* Returns a long string representation of this colorization.
*/
public function toVerboseString () :String
{
return StringUtil.toString(this);
}
/**
* Converts floating point HSV values to a fixed point integer
* representation.
*
* @param hsv the HSV values to be converted.
* @param fhsv the destination array into which the fixed values will
* be stored. If this is null, a new array will be created of the
* appropriate length.
*
* @return the <code>fhsv</code> parameter if it was non-null or the
* newly created target array.
*/
public static function toFixedHSV (hsv :Array) :Array
{
var fhsv :Array = new Array(hsv.length);
for (var ii :int = 0; ii < hsv.length; ii++) {
fhsv[ii] = int(hsv[ii] * Short.MAX_VALUE);
}
return fhsv;
}
/**
* Returns the distance between the supplied to numbers modulo N.
*/
public static function distance (a :int, b :int, N :int) :int
{
return (a > b) ? Math.min(a - b, b + N - a) : Math.min(b - a, a + N - b);
}
/** Fixed HSV values for our root color; used when calculating
* recolorizations using this colorization. */
protected var _fhsv :Array;
/** HSV values for our root color; used when calculating
* recolorizations using this colorization. */
protected var _hsv :Array;
}
}
@@ -0,0 +1,212 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image {
import flash.utils.ByteArray;
import com.threerings.display.ColorUtil;
import com.threerings.util.Log;
import com.threerings.util.StringUtil;
import com.threerings.util.F;
public class PngRecolorUtil
{
private static const log :Log = Log.getLog(PngRecolorUtil);
protected static function isPng (bytes :ByteArray) :Boolean
{
if (bytes.length < PNG_HEADER.length) {
return false;
}
for (var ii :int = 0; ii < PNG_HEADER.length; ii++) {
if (PNG_HEADER[ii] != bytes[ii]) {
return false;
}
}
return true;
}
/**
* Find the required chunks in the bytes that represent a PNG image. All of the chunks that
* are succesfully found are pushed into the returned array in the order in which they were
* found in the image.
*/
protected static function findChunks (pngBytes :ByteArray, chunkTypes :Array) :Array
{
var chunks :Array = [];
if (chunkTypes.length < 1) {
return chunks;
}
for (var ii :int = PNG_HEADER.length; ii < pngBytes.length;) {
pngBytes.position = ii;
var chunk :PngChunk =
new PngChunk(ii + 8, pngBytes.readInt(), pngBytes.readUTFBytes(4));
var idx :int = chunkTypes.indexOf(chunk.type);
if (idx >= 0) {
chunks.push(chunk);
if (chunkTypes.length == 1) {
return chunks;
} else {
chunkTypes.splice(idx, 1);
}
}
// 4 bytes each for length, chunk type and CRC
ii += 12 + chunk.length;
}
return chunks;
}
public static function recolorPNG (pngBytes :ByteArray,
colors :Array/* of Colorization */) :ByteArray
{
if (colors == null || colors.length == 0) {
return pngBytes;
}
// first, lets make sure we really have a PNG
if (!isPng(pngBytes)) {
log.warning("recolorPNG received invalid pngBytes", new Error());
return pngBytes;
}
var chunks :Array = findChunks(pngBytes, [HEADER_CHUNK, PALETTE_CHUNK]);
if (chunks.length != 2 || (chunks[0] as PngChunk).type != HEADER_CHUNK) {
log.warning("recolorPNG received an unexected PNG format", "requiredChunksFound",
chunks.length, new Error());
return pngBytes;
}
var header :PngChunk = chunks[0] as PngChunk;
var palette :PngChunk = chunks[1] as PngChunk;
// Check to make sure the header declares this to be an indexed-color PNG
var colorType :int = pngBytes[header.idx + COLOR_TYPE_IDX];
if (colorType != INDEXED_COLOR_TYPE) {
log.warning(
"Color Type in PNG header is not indexed-color", "type", colorType, new Error());
return pngBytes;
}
initializeCRCTable();
// CRC starts with chunk type
var crc :uint = 0xFFFFFFFF;
for (var ii :int = 0; ii < 4; ii++) {
crc = crcCalc(crc, PALETTE_CHUNK.charCodeAt(ii));
}
pngBytes.position = palette.idx;
for (ii = palette.idx; ii < palette.idx + palette.length; ii += 3) {
var red :uint = pngBytes.readUnsignedByte();
var green :uint = pngBytes.readUnsignedByte();
var blue :uint = pngBytes.readUnsignedByte();
var hsv :Array = ColorUtil.RGBtoHSB(red, green, blue);
var fhsv :Array = Colorization.toFixedHSV(hsv);
for each (var color :Colorization in colors) {
if (color != null && color.matches(hsv, fhsv)) {
var newRgb :uint = color.recolorColor(hsv);
pngBytes[ii] = red = ColorUtil.getRed(newRgb);
pngBytes[ii + 1] = green = ColorUtil.getGreen(newRgb);
pngBytes[ii + 2] = blue = ColorUtil.getBlue(newRgb);
break;
}
}
crc = F.foldl([red, green, blue], crc, crcCalc);
}
crc = uint(crc ^ uint(0xFFFFFFFF));
// write out the CRC for the modified color table
pngBytes.position = palette.idx + palette.length;
pngBytes.writeUnsignedInt(crc);
return pngBytes;
}
protected static function initializeCRCTable () :void
{
if (_crcTable != null) {
return;
}
_crcTable = [];
for (var n :uint = 0; n < 256; n++)
{
var c :uint = n;
for (var k :uint = 0; k < 8; k++)
{
if (c & 1) {
c = uint(uint(0xedb88320) ^ uint(c >>> 1));
} else {
c = uint(c >>> 1);
}
}
_crcTable[n] = c;
}
}
protected static function crcCalc (crc :uint, byte :uint) :uint
{
return uint(_crcTable[(crc ^ byte) & uint(0xFF)] ^ uint(crc >>> 8));
}
/** The amount to pad error messages by. */
private static const ERROR_PADDING :int = 5;
/** Various Png-recolor related constants. */
protected static const PNG_HEADER :Array =
[ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A ];
protected static const PALETTE_CHUNK :String = "PLTE";
protected static const HEADER_CHUNK :String = "IHDR";
protected static const COLOR_TYPE_IDX :int = 9;
protected static const INDEXED_COLOR_TYPE :int = 3;
protected static var _crcTable :Array;
}
}
import com.threerings.util.StringUtil;
/** Used for recoloring pngs. */
class PngChunk
{
/** The index of the beginning of the data section of the chunk. */
public var idx :int;
/** The length of the data section of the chunk. */
public var length :int;
/** The type of this chunk. */
public var type :String;
public function PngChunk (idx :int, length :int, type :String)
{
this.idx = idx;
this.length = length;
this.type = type;
}
public function toString () :String
{
return StringUtil.simpleToString(this);
}
}