Extracted the Bang stats system into reusable land. Too late for Yohoho, but
Whirled will benefit. git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@251 c613c5cb-e716-0410-b11b-feb51c14d237
This commit is contained in:
@@ -204,6 +204,10 @@
|
|||||||
<fileset dir="${classes.dir}" includes="com/threerings/stage/**"/>
|
<fileset dir="${classes.dir}" includes="com/threerings/stage/**"/>
|
||||||
<fileset dir="${classes.dir}" includes="rsrc/**/stage/**"/>
|
<fileset dir="${classes.dir}" includes="rsrc/**/stage/**"/>
|
||||||
</jar>
|
</jar>
|
||||||
|
<jar destfile="${deploy.dir}/${app.name}-stats.jar">
|
||||||
|
<fileset dir="${classes.dir}" includes="com/threerings/stats/**"/>
|
||||||
|
<fileset dir="${classes.dir}" includes="rsrc/**/stats/**"/>
|
||||||
|
</jar>
|
||||||
<jar destfile="${deploy.dir}/${app.name}-whirled.jar">
|
<jar destfile="${deploy.dir}/${app.name}-whirled.jar">
|
||||||
<fileset dir="${classes.dir}" includes="com/threerings/whirled/**"/>
|
<fileset dir="${classes.dir}" includes="com/threerings/whirled/**"/>
|
||||||
<fileset dir="${classes.dir}" includes="rsrc/**/whirled/**"/>
|
<fileset dir="${classes.dir}" includes="rsrc/**/whirled/**"/>
|
||||||
@@ -238,6 +242,7 @@
|
|||||||
<weave inputjar="${inpre}-parlor.jar" outputjar="${outpre}-parlor.jar"/>
|
<weave inputjar="${inpre}-parlor.jar" outputjar="${outpre}-parlor.jar"/>
|
||||||
<weave inputjar="${inpre}-puzzle.jar" outputjar="${outpre}-puzzle.jar"/>
|
<weave inputjar="${inpre}-puzzle.jar" outputjar="${outpre}-puzzle.jar"/>
|
||||||
<weave inputjar="${inpre}-stage.jar" outputjar="${outpre}-stage.jar"/>
|
<weave inputjar="${inpre}-stage.jar" outputjar="${outpre}-stage.jar"/>
|
||||||
|
<weave inputjar="${inpre}-stats.jar" outputjar="${outpre}-stats.jar"/>
|
||||||
<weave inputjar="${inpre}-whirled.jar" outputjar="${outpre}-whirled.jar"/>
|
<weave inputjar="${inpre}-whirled.jar" outputjar="${outpre}-whirled.jar"/>
|
||||||
|
|
||||||
<!-- now weave again with the verifier to check for unweavable 1.5isms -->
|
<!-- now weave again with the verifier to check for unweavable 1.5isms -->
|
||||||
@@ -245,6 +250,7 @@
|
|||||||
<antcall target="vweave"><param name="which" value="parlor"/></antcall>
|
<antcall target="vweave"><param name="which" value="parlor"/></antcall>
|
||||||
<antcall target="vweave"><param name="which" value="puzzle"/></antcall>
|
<antcall target="vweave"><param name="which" value="puzzle"/></antcall>
|
||||||
<antcall target="vweave"><param name="which" value="stage"/></antcall>
|
<antcall target="vweave"><param name="which" value="stage"/></antcall>
|
||||||
|
<antcall target="vweave"><param name="which" value="stats"/></antcall>
|
||||||
<antcall target="vweave"><param name="which" value="whirled"/></antcall>
|
<antcall target="vweave"><param name="which" value="whirled"/></antcall>
|
||||||
</target>
|
</target>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.threerings.stats;
|
||||||
|
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A placeholder class that contains a reference to the log object used by this project.
|
||||||
|
*/
|
||||||
|
public class Log
|
||||||
|
{
|
||||||
|
/** We dispatch our log messages through this logger. */
|
||||||
|
public static Logger log = Logger.getLogger("com.threerings.stats");
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.threerings.stats.data;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.threerings.io.ObjectInputStream;
|
||||||
|
import com.threerings.io.ObjectOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps up to 127 string values to integers in the range 0 - 127.
|
||||||
|
*/
|
||||||
|
public class ByteByteStringMapStat extends StringMapStat
|
||||||
|
{
|
||||||
|
@Override // documentation inherited
|
||||||
|
public void persistTo (ObjectOutputStream out, AuxDataSource aux)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
out.writeByte(_keys.length);
|
||||||
|
for (int ii = 0; ii < _keys.length; ii++) {
|
||||||
|
out.writeByte((byte)aux.getStringCode(_type, _keys[ii]));
|
||||||
|
out.writeByte((byte)_values[ii]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // documentation inherited
|
||||||
|
public void unpersistFrom (ObjectInputStream in, AuxDataSource aux)
|
||||||
|
throws IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
_keys = new String[in.readByte()];
|
||||||
|
_values = new int[_keys.length];
|
||||||
|
for (int ii = 0; ii < _keys.length; ii++) {
|
||||||
|
_keys[ii] = aux.getCodeString(_type, in.readByte());
|
||||||
|
_values[ii] = in.readByte();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // from StringMapStat
|
||||||
|
protected int getMaxValue ()
|
||||||
|
{
|
||||||
|
return Byte.MAX_VALUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.threerings.stats.data;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.threerings.io.ObjectInputStream;
|
||||||
|
import com.threerings.io.ObjectOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string set that maps its values to bytes.
|
||||||
|
*/
|
||||||
|
public class ByteStringSetStat extends StringSetStat
|
||||||
|
{
|
||||||
|
@Override // documentation inherited
|
||||||
|
public void persistTo (ObjectOutputStream out, AuxDataSource aux)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
out.writeByte(_values.length);
|
||||||
|
for (int ii = 0; ii < _values.length; ii++) {
|
||||||
|
out.writeByte((byte)aux.getStringCode(_type, _values[ii]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // documentation inherited
|
||||||
|
public void unpersistFrom (ObjectInputStream in, AuxDataSource aux)
|
||||||
|
throws IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
_values = new String[in.readByte()];
|
||||||
|
for (int ii = 0; ii < _values.length; ii++) {
|
||||||
|
_values[ii] = aux.getCodeString(_type, in.readByte());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.threerings.stats.data;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import com.samskivert.util.ArrayUtil;
|
||||||
|
import com.samskivert.util.StringUtil;
|
||||||
|
|
||||||
|
import com.threerings.io.ObjectInputStream;
|
||||||
|
import com.threerings.io.ObjectOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to track an integer array statistic.
|
||||||
|
*/
|
||||||
|
public class IntArrayStat extends Stat
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the value of this statistic.
|
||||||
|
*/
|
||||||
|
public int[] getValue ()
|
||||||
|
{
|
||||||
|
return _value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets this statistic's value to the specified value.
|
||||||
|
*
|
||||||
|
* @return true if the stat was modified, false if not.
|
||||||
|
*/
|
||||||
|
public boolean setValue (int[] value)
|
||||||
|
{
|
||||||
|
if (!Arrays.equals(_value, value)) {
|
||||||
|
_value = value;
|
||||||
|
_modified = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a value to this statistic.
|
||||||
|
*/
|
||||||
|
public void appendValue (int value)
|
||||||
|
{
|
||||||
|
_value = ArrayUtil.append(_value, value);
|
||||||
|
_modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // documentation inherited
|
||||||
|
public String valueToString ()
|
||||||
|
{
|
||||||
|
return StringUtil.toString(_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // documentation inherited
|
||||||
|
public void persistTo (ObjectOutputStream out, AuxDataSource aux)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
out.writeObject(_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // documentation inherited
|
||||||
|
public void unpersistFrom (ObjectInputStream in, AuxDataSource aux)
|
||||||
|
throws IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
_value = (int[])in.readObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Contains the integer list value of this statistic. */
|
||||||
|
protected int[] _value = new int[0];
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.threerings.stats.data;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.threerings.io.ObjectInputStream;
|
||||||
|
import com.threerings.io.ObjectOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to track a single integer statistic.
|
||||||
|
*/
|
||||||
|
public class IntStat extends Stat
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the value of this integer statistic.
|
||||||
|
*/
|
||||||
|
public int getValue ()
|
||||||
|
{
|
||||||
|
return _value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets this statistic's value to the specified value.
|
||||||
|
*
|
||||||
|
* @return true if the stat was modified, false if not.
|
||||||
|
*/
|
||||||
|
public boolean setValue (int value)
|
||||||
|
{
|
||||||
|
if (value != _value) {
|
||||||
|
_value = value;
|
||||||
|
_modified = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments this statistic by the specified delta value.
|
||||||
|
*
|
||||||
|
* @return true if the stat was modified, false if not.
|
||||||
|
*/
|
||||||
|
public boolean increment (int delta)
|
||||||
|
{
|
||||||
|
return setValue(_value + delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // documentation inherited
|
||||||
|
public String valueToString ()
|
||||||
|
{
|
||||||
|
return String.valueOf(_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // documentation inherited
|
||||||
|
public void persistTo (ObjectOutputStream out, AuxDataSource aux)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
out.writeInt(_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // documentation inherited
|
||||||
|
public void unpersistFrom (ObjectInputStream in, AuxDataSource aux)
|
||||||
|
throws IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
_value = in.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Contains the integer value of this statistic. */
|
||||||
|
protected int _value;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.threerings.stats.data;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.threerings.io.ObjectInputStream;
|
||||||
|
import com.threerings.io.ObjectOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string set that maps its values to integers.
|
||||||
|
*/
|
||||||
|
public class IntStringSetStat extends StringSetStat
|
||||||
|
{
|
||||||
|
@Override // documentation inherited
|
||||||
|
public void persistTo (ObjectOutputStream out, AuxDataSource aux)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
out.writeInt(_values.length);
|
||||||
|
for (int ii = 0; ii < _values.length; ii++) {
|
||||||
|
out.writeInt(aux.getStringCode(_type, _values[ii]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // documentation inherited
|
||||||
|
public void unpersistFrom (ObjectInputStream in, AuxDataSource aux)
|
||||||
|
throws IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
_values = new String[in.readInt()];
|
||||||
|
for (int ii = 0; ii < _values.length; ii++) {
|
||||||
|
_values[ii] = aux.getCodeString(_type, in.readInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.threerings.stats.data;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.EOFException;
|
||||||
|
|
||||||
|
import com.threerings.io.ObjectInputStream;
|
||||||
|
import com.threerings.io.ObjectOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extends the {@link IntStat} by maintaining a maximum value which is updated every time a value
|
||||||
|
* is accumulated to the stat. Thus we track an accumulating value as well as the largest amount by
|
||||||
|
* which it has ever accumulated.
|
||||||
|
*/
|
||||||
|
public class MaxIntStat extends IntStat
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the maximum value every accumulated to this integer statistic.
|
||||||
|
*/
|
||||||
|
public int getMaxValue ()
|
||||||
|
{
|
||||||
|
return _maxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // from IntStat
|
||||||
|
public boolean increment (int delta)
|
||||||
|
{
|
||||||
|
if (super.increment(delta)) {
|
||||||
|
_maxValue = Math.max(delta, _maxValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // from IntStat
|
||||||
|
public String valueToString ()
|
||||||
|
{
|
||||||
|
return super.valueToString() + " " + String.valueOf(_maxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // from IntStat
|
||||||
|
public void persistTo (ObjectOutputStream out, AuxDataSource aux)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
super.persistTo(out, aux);
|
||||||
|
out.writeInt(_maxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // from IntStat
|
||||||
|
public void unpersistFrom (ObjectInputStream in, AuxDataSource aux)
|
||||||
|
throws IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
super.unpersistFrom(in, aux);
|
||||||
|
_maxValue = in.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The largest value ever accumulated to this stat. */
|
||||||
|
protected int _maxValue;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.threerings.stats.data;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import com.threerings.io.ObjectInputStream;
|
||||||
|
import com.threerings.io.ObjectOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string set that maps its values to shorts.
|
||||||
|
*/
|
||||||
|
public class ShortStringSetStat extends StringSetStat
|
||||||
|
{
|
||||||
|
@Override // documentation inherited
|
||||||
|
public void persistTo (ObjectOutputStream out, AuxDataSource aux)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
out.writeShort(_values.length);
|
||||||
|
for (int ii = 0; ii < _values.length; ii++) {
|
||||||
|
out.writeShort((short)aux.getStringCode(_type, _values[ii]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // documentation inherited
|
||||||
|
public void unpersistFrom (ObjectInputStream in, AuxDataSource aux)
|
||||||
|
throws IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
_values = new String[in.readShort()];
|
||||||
|
for (int ii = 0; ii < _values.length; ii++) {
|
||||||
|
_values[ii] = aux.getCodeString(_type, in.readShort());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.threerings.stats.data;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
|
||||||
|
import com.samskivert.util.HashIntMap;
|
||||||
|
import com.samskivert.util.StringUtil;
|
||||||
|
import com.threerings.util.MessageBundle;
|
||||||
|
|
||||||
|
import com.threerings.io.ObjectInputStream;
|
||||||
|
import com.threerings.io.ObjectOutputStream;
|
||||||
|
|
||||||
|
import com.threerings.presents.dobj.DSet;
|
||||||
|
|
||||||
|
import static com.threerings.stats.Log.log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A base class for persistent statistics tracked on a per-player basis (some for a single game,
|
||||||
|
* others for all time).
|
||||||
|
*/
|
||||||
|
public abstract class Stat
|
||||||
|
implements DSet.Entry, Cloneable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Defines the various per-player tracked statistics.
|
||||||
|
*/
|
||||||
|
public interface Type
|
||||||
|
{
|
||||||
|
/** Returns a new blank stat instance of the specified type. */
|
||||||
|
public Stat newStat ();
|
||||||
|
|
||||||
|
/** Returns the enum name of stat. */
|
||||||
|
public String name ();
|
||||||
|
|
||||||
|
/** Returns the unique code for this stat which is a function of its name. */
|
||||||
|
public int code ();
|
||||||
|
|
||||||
|
/** Returns true if this stat is persisted between sessions. */
|
||||||
|
public boolean isPersistent ();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Provides auxilliary information to statistics during the persisting process. */
|
||||||
|
public static interface AuxDataSource
|
||||||
|
{
|
||||||
|
/** Maps the specified string to a unique integer value. */
|
||||||
|
public int getStringCode (Type type, String value);
|
||||||
|
|
||||||
|
/** Maps the specified unique code back to its string value. */
|
||||||
|
public String getCodeString (Type type, int code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a {@link Type}'s code code back to a {@link Type} instance.
|
||||||
|
*/
|
||||||
|
public static Type getType (int code)
|
||||||
|
{
|
||||||
|
return _codeToType.get(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by the {@link Type} implementation to map itself to an integer code.
|
||||||
|
*
|
||||||
|
* @return the code to which the supplied type was mapped.
|
||||||
|
*/
|
||||||
|
public static int initType (Type type, Stat prototype)
|
||||||
|
{
|
||||||
|
// compute a code for this stat using the CRC32 hash of its name
|
||||||
|
_crc.reset();
|
||||||
|
_crc.update(type.name().getBytes());
|
||||||
|
int code = (int)_crc.getValue();
|
||||||
|
|
||||||
|
// make sure the code does not collide
|
||||||
|
if (_codeToType.containsKey(code)) {
|
||||||
|
log.warning("Stat type collision! " + type.name() + " and " +
|
||||||
|
_codeToType.get(code).name() + " both map to '" + code + "'.");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize the prototype
|
||||||
|
prototype._type = type;
|
||||||
|
|
||||||
|
// map the stat by its code
|
||||||
|
_codeToType.put(code, type);
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of this statistic.
|
||||||
|
*/
|
||||||
|
public Type getType ()
|
||||||
|
{
|
||||||
|
return _type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the integer code to which this statistic's name maps.
|
||||||
|
*/
|
||||||
|
public int getCode ()
|
||||||
|
{
|
||||||
|
return _type.code();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the supplied statistic has been modified since it
|
||||||
|
* was loaded from the repository.
|
||||||
|
*/
|
||||||
|
public boolean isModified ()
|
||||||
|
{
|
||||||
|
return _modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces this stat to consider itself modified. Generally this is not
|
||||||
|
* called but rather the derived class will update its modified state when
|
||||||
|
* it is actually modified.
|
||||||
|
*/
|
||||||
|
public void setModified (boolean modified)
|
||||||
|
{
|
||||||
|
_modified = modified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes our custom streamable fields. */
|
||||||
|
public void writeObject (ObjectOutputStream out)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
out.writeInt(_type.code());
|
||||||
|
out.defaultWriteObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads our custom streamable fields. */
|
||||||
|
public void readObject (ObjectInputStream in)
|
||||||
|
throws IOException, ClassNotFoundException
|
||||||
|
{
|
||||||
|
_type = getType(in.readInt());
|
||||||
|
in.defaultReadObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes this instance for storage in the item database. Derived classes must override
|
||||||
|
* this method to implement persistence.
|
||||||
|
*/
|
||||||
|
public abstract void persistTo (ObjectOutputStream out, AuxDataSource aux)
|
||||||
|
throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unserializes this item from data obtained from the item database. Derived classes must
|
||||||
|
* override this method to implement persistence.
|
||||||
|
*/
|
||||||
|
public abstract void unpersistFrom (ObjectInputStream in, AuxDataSource aux)
|
||||||
|
throws IOException, ClassNotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a string representation of this instance.
|
||||||
|
*/
|
||||||
|
public String toString ()
|
||||||
|
{
|
||||||
|
StringBuffer buf = new StringBuffer(StringUtil.toUSLowerCase(_type.name()));
|
||||||
|
buf.append("=");
|
||||||
|
buf.append(valueToString());
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derived statistics must override this method and render their value to a string. Used by
|
||||||
|
* {@link #toString} and to display the value in game.
|
||||||
|
*/
|
||||||
|
public abstract String valueToString ();
|
||||||
|
|
||||||
|
// documentation inherited from DSet.Entry
|
||||||
|
public String getKey ()
|
||||||
|
{
|
||||||
|
return _type.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
// documentation inherited from Cloneable
|
||||||
|
public Object clone ()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return super.clone();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Clone failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The type of the statistic in question. */
|
||||||
|
protected transient Type _type;
|
||||||
|
|
||||||
|
/** Indicates whether or not this statistic has been modified since it
|
||||||
|
* was loaded from the database. */
|
||||||
|
protected transient boolean _modified;
|
||||||
|
|
||||||
|
/** Used for computing our stat codes. */
|
||||||
|
protected static CRC32 _crc = new CRC32();
|
||||||
|
|
||||||
|
/** The table mapping stat codes to enumerated types. */
|
||||||
|
protected static HashIntMap<Type> _codeToType = new HashIntMap<Type>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.threerings.stats.data;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
import com.threerings.presents.dobj.DSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A distributed set containing {@link Stat} objects.
|
||||||
|
*/
|
||||||
|
public final class StatSet extends DSet<Stat>
|
||||||
|
{
|
||||||
|
/** An interface to be implemented by an entity that wishes to be notified when the contents of
|
||||||
|
* a stat set change. */
|
||||||
|
public interface Container
|
||||||
|
{
|
||||||
|
public void addToStats (Stat stat);
|
||||||
|
public void updateStats (Stat stat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a stat set with the specified contents. */
|
||||||
|
public StatSet (Iterator<Stat> contents)
|
||||||
|
{
|
||||||
|
super(contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a blank stat set. */
|
||||||
|
public StatSet ()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wires this stat set up to a containing user object. All subsequent modifications will be
|
||||||
|
* published to the container.
|
||||||
|
*/
|
||||||
|
public void setContainer (Container container)
|
||||||
|
{
|
||||||
|
_container = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets an integer statistic in this set.
|
||||||
|
*
|
||||||
|
* @exception ClassCastException thrown if the registered type of the specified stat is not an
|
||||||
|
* {@link IntStat}.
|
||||||
|
*/
|
||||||
|
public void setStat (Stat.Type type, int value)
|
||||||
|
{
|
||||||
|
IntStat stat = (IntStat)get(type.name());
|
||||||
|
if (stat == null) {
|
||||||
|
stat = (IntStat)type.newStat();
|
||||||
|
stat.setValue(value);
|
||||||
|
addStat(stat);
|
||||||
|
} else if (stat.setValue(value)) {
|
||||||
|
updateStat(stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets an integer stat to the specified value, if it exceeds our existing recorded value.
|
||||||
|
*
|
||||||
|
* @exception ClassCastException thrown if the registered type of the specified stat is not an
|
||||||
|
* {@link IntStat}.
|
||||||
|
*/
|
||||||
|
public void maxStat (Stat.Type type, int value)
|
||||||
|
{
|
||||||
|
int ovalue = getIntStat(type);
|
||||||
|
if (value > ovalue) {
|
||||||
|
setStat(type, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments an integer statistic in this set.
|
||||||
|
*
|
||||||
|
* @exception ClassCastException thrown if the registered type of the specified stat is not an
|
||||||
|
* {@link IntStat}.
|
||||||
|
*/
|
||||||
|
public void incrementStat (Stat.Type type, int delta)
|
||||||
|
{
|
||||||
|
IntStat stat = (IntStat)get(type.name());
|
||||||
|
if (stat == null) {
|
||||||
|
stat = (IntStat)type.newStat();
|
||||||
|
stat.increment(delta);
|
||||||
|
addStat(stat);
|
||||||
|
} else if (stat.increment(delta)) {
|
||||||
|
updateStat(stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends an integer value to an {@link IntArrayStat}.
|
||||||
|
*
|
||||||
|
* @exception ClassCastException thrown if the registered type of the specified stat is not an
|
||||||
|
* {@link IntArrayStat}.
|
||||||
|
*/
|
||||||
|
public void appendStat (Stat.Type type, int value)
|
||||||
|
{
|
||||||
|
IntArrayStat stat = (IntArrayStat)get(type.name());
|
||||||
|
if (stat == null) {
|
||||||
|
stat = (IntArrayStat)type.newStat();
|
||||||
|
stat.appendValue(value);
|
||||||
|
addStat(stat);
|
||||||
|
} else {
|
||||||
|
stat.appendValue(value);
|
||||||
|
updateStat(stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a string value to a {@link StringSetStat}.
|
||||||
|
*
|
||||||
|
* @exception ClassCastException thrown if the registered type of the specified stat is not an
|
||||||
|
* {@link StringSetStat}.
|
||||||
|
*/
|
||||||
|
public void addToSetStat (Stat.Type type, String value)
|
||||||
|
{
|
||||||
|
StringSetStat stat = (StringSetStat)get(type.name());
|
||||||
|
if (stat == null) {
|
||||||
|
stat = (StringSetStat)type.newStat();
|
||||||
|
stat.add(value);
|
||||||
|
addStat(stat);
|
||||||
|
} else if (stat.add(value)) {
|
||||||
|
updateStat(stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments a string value in a {@link StringMapStat}.
|
||||||
|
*
|
||||||
|
* @exception ClassCastException thrown if the registered type of the specified stat is not an
|
||||||
|
* {@link StringMapStat}.
|
||||||
|
*/
|
||||||
|
public void incrementMapStat (Stat.Type type, String value, int amount)
|
||||||
|
{
|
||||||
|
StringMapStat stat = (StringMapStat)get(type.name());
|
||||||
|
if (stat == null) {
|
||||||
|
stat = (StringMapStat)type.newStat();
|
||||||
|
stat.increment(value, amount);
|
||||||
|
addStat(stat);
|
||||||
|
} else if (stat.increment(value, amount)) {
|
||||||
|
updateStat(stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current value of the specified integer statistic.
|
||||||
|
*/
|
||||||
|
public int getIntStat (Stat.Type type)
|
||||||
|
{
|
||||||
|
IntStat stat = (IntStat)get(type.name());
|
||||||
|
return (stat == null) ? 0 : stat.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum value by which the specified integer statistic has ever been
|
||||||
|
* incremented.
|
||||||
|
*/
|
||||||
|
public int getMaxIntStat (Stat.Type type)
|
||||||
|
{
|
||||||
|
MaxIntStat stat = (MaxIntStat)get(type.name());
|
||||||
|
return (stat == null) ? 0 : stat.getMaxValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current value of the specified integer array statistic.
|
||||||
|
*/
|
||||||
|
public int[] getIntArrayStat (Stat.Type type)
|
||||||
|
{
|
||||||
|
IntArrayStat stat = (IntArrayStat)get(type.name());
|
||||||
|
return (stat == null) ? new int[0] : stat.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the specified {@link StringSetStat} contains the specified value, false
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
|
public boolean containsValue (Stat.Type type, String value)
|
||||||
|
{
|
||||||
|
StringSetStat stat = (StringSetStat)get(type.name());
|
||||||
|
return (stat == null) ? false : stat.contains(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value to which the specified string is mapped in a {@link StringMapStat} or zero
|
||||||
|
* if the value has not been mapped.
|
||||||
|
*/
|
||||||
|
public int getMapValue (Stat.Type type, String value)
|
||||||
|
{
|
||||||
|
StringMapStat stat = (StringMapStat)get(type.name());
|
||||||
|
return (stat == null) ? 0 : stat.get(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't call this method, it's only needed by some tricky business we do when preventing
|
||||||
|
* distributed object event generation for all but the bounty criteria related stats.
|
||||||
|
*/
|
||||||
|
public void addQuietly (Stat stat)
|
||||||
|
{
|
||||||
|
add(stat);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void addStat (Stat stat)
|
||||||
|
{
|
||||||
|
if (_container != null) {
|
||||||
|
_container.addToStats(stat);
|
||||||
|
} else {
|
||||||
|
add(stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void updateStat (Stat stat)
|
||||||
|
{
|
||||||
|
if (_container != null) {
|
||||||
|
_container.updateStats(stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected transient Container _container;
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.threerings.stats.data;
|
||||||
|
|
||||||
|
import com.samskivert.util.ArrayUtil;
|
||||||
|
import com.samskivert.util.StringUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to track a statistic comprised of a set of strings that map to numeric counts.
|
||||||
|
*/
|
||||||
|
public abstract class StringMapStat extends Stat
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns true if the specified key is contained in this map.
|
||||||
|
*/
|
||||||
|
public boolean containsKey (String key)
|
||||||
|
{
|
||||||
|
return (key == null) ? false :
|
||||||
|
ArrayUtil.binarySearch(_keys, 0, _keys.length, key) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value to which the specified key maps or zero if it is not contained in this
|
||||||
|
* map.
|
||||||
|
*/
|
||||||
|
public int get (String key)
|
||||||
|
{
|
||||||
|
if (key == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int iidx = ArrayUtil.binarySearch(_keys, 0, _keys.length, key);
|
||||||
|
return (iidx < 0) ? 0 : _values[iidx];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the specified key value pair to this set, overwriting any existing mapping.
|
||||||
|
*
|
||||||
|
* @return true if the stat's value was changed, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean put (String key, int value)
|
||||||
|
{
|
||||||
|
if (key == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int iidx = getOrCreateEntry(key);
|
||||||
|
int ovalue = _values[iidx];
|
||||||
|
_values[iidx] = Math.min(value, getMaxValue());
|
||||||
|
if (_values[iidx] != ovalue) {
|
||||||
|
_modified = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the specified key value pair to this set, overwriting any existing mapping.
|
||||||
|
*
|
||||||
|
* @return true if the stat's value was changed, false otherwise.
|
||||||
|
*/
|
||||||
|
public boolean increment (String key, int amount)
|
||||||
|
{
|
||||||
|
if (key == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int iidx = getOrCreateEntry(key);
|
||||||
|
int ovalue = _values[iidx];
|
||||||
|
_values[iidx] = Math.min(_values[iidx] + amount, getMaxValue());
|
||||||
|
if (_values[iidx] != ovalue) {
|
||||||
|
_modified = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // documentation inherited
|
||||||
|
public String valueToString ()
|
||||||
|
{
|
||||||
|
StringBuffer buf = new StringBuffer("[");
|
||||||
|
for (int ii = 0; ii < _keys.length; ii++) {
|
||||||
|
if (ii > 0) {
|
||||||
|
buf.append(", ");
|
||||||
|
}
|
||||||
|
buf.append(_keys[ii]).append("->").append(_values[ii]);
|
||||||
|
}
|
||||||
|
return buf.append("]").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the index of the specified key, creating an entry if necessary.
|
||||||
|
*/
|
||||||
|
protected int getOrCreateEntry (String key)
|
||||||
|
{
|
||||||
|
int iidx = ArrayUtil.binarySearch(_keys, 0, _keys.length, key);
|
||||||
|
if (iidx < 0) {
|
||||||
|
iidx = -iidx - 1;
|
||||||
|
String[] keys = new String[_keys.length+1];
|
||||||
|
System.arraycopy(_keys, 0, keys, 0, iidx);
|
||||||
|
System.arraycopy(_keys, iidx, keys, iidx+1, (_keys.length-iidx));
|
||||||
|
keys[iidx] = key;
|
||||||
|
_keys = keys;
|
||||||
|
int[] values = new int[_values.length+1];
|
||||||
|
System.arraycopy(_values, 0, values, 0, iidx);
|
||||||
|
System.arraycopy(
|
||||||
|
_values, iidx, values, iidx+1, (_values.length-iidx));
|
||||||
|
_values = values;
|
||||||
|
}
|
||||||
|
return iidx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum value to which we'll allow our strings to map (to avoid overflowing our
|
||||||
|
* underlying data types and to avoid accumulating forever when we don't really care).
|
||||||
|
*/
|
||||||
|
protected abstract int getMaxValue ();
|
||||||
|
|
||||||
|
/** Contains the string keys in this set. */
|
||||||
|
protected String[] _keys = {};
|
||||||
|
|
||||||
|
/** Contains the values in this set. */
|
||||||
|
protected int[] _values = {};
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.threerings.stats.data;
|
||||||
|
|
||||||
|
import com.samskivert.util.ArrayUtil;
|
||||||
|
import com.samskivert.util.StringUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to track a statistic comprised of a set of strings.
|
||||||
|
*/
|
||||||
|
public abstract class StringSetStat extends Stat
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns true if the specified string is contained in this set.
|
||||||
|
*/
|
||||||
|
public boolean contains (String key)
|
||||||
|
{
|
||||||
|
if (key == null) {
|
||||||
|
throw new IllegalArgumentException("StringSetStat cannot contain null");
|
||||||
|
}
|
||||||
|
return ArrayUtil.binarySearch(_values, 0, _values.length, key) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the specified string to this set.
|
||||||
|
*
|
||||||
|
* @return true if the string was newly added, false if it was already
|
||||||
|
* contained in the set.
|
||||||
|
*/
|
||||||
|
public boolean add (String key)
|
||||||
|
{
|
||||||
|
if (key == null) {
|
||||||
|
throw new IllegalArgumentException("Cannot add null to StringSetStat");
|
||||||
|
}
|
||||||
|
|
||||||
|
int iidx = ArrayUtil.binarySearch(_values, 0, _values.length, key);
|
||||||
|
if (iidx >= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
iidx = -iidx - 1;
|
||||||
|
String[] values = new String[_values.length+1];
|
||||||
|
System.arraycopy(_values, 0, values, 0, iidx);
|
||||||
|
System.arraycopy(_values, iidx, values, iidx+1, (_values.length-iidx));
|
||||||
|
values[iidx] = key;
|
||||||
|
_values = values;
|
||||||
|
_modified = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the values in this set. <em>Do not</em> modify the returned array.
|
||||||
|
*/
|
||||||
|
public String[] values ()
|
||||||
|
{
|
||||||
|
return _values;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // documentation inherited
|
||||||
|
public String valueToString ()
|
||||||
|
{
|
||||||
|
return StringUtil.toString(_values);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Contains the strings in this set. */
|
||||||
|
protected String[] _values = new String[0];
|
||||||
|
}
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
//
|
||||||
|
// $Id$
|
||||||
|
|
||||||
|
package com.threerings.stats.server.persist;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.Date;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
import com.samskivert.io.ByteArrayOutInputStream;
|
||||||
|
import com.samskivert.io.PersistenceException;
|
||||||
|
import com.samskivert.jdbc.ConnectionProvider;
|
||||||
|
import com.samskivert.jdbc.DatabaseLiaison;
|
||||||
|
import com.samskivert.jdbc.JDBCUtil;
|
||||||
|
import com.samskivert.jdbc.SimpleRepository;
|
||||||
|
import com.samskivert.util.HashIntMap;
|
||||||
|
import com.samskivert.util.StringUtil;
|
||||||
|
|
||||||
|
import com.threerings.io.ObjectInputStream;
|
||||||
|
import com.threerings.io.ObjectOutputStream;
|
||||||
|
|
||||||
|
import com.threerings.stats.data.Stat;
|
||||||
|
|
||||||
|
import static com.threerings.stats.Log.log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for the persistent storage of per-player statistics.
|
||||||
|
*/
|
||||||
|
public class StatRepository extends SimpleRepository
|
||||||
|
implements Stat.AuxDataSource
|
||||||
|
{
|
||||||
|
/** Used by {@link #processStats}. */
|
||||||
|
public interface Processor
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Called on every stat matching the criterion supplied to {@link #processStats}.
|
||||||
|
* <em>Note:</em> do not retain a reference to the supplied {@link Stat} instance as its
|
||||||
|
* contents will be overwritten prior to each call to process.
|
||||||
|
*/
|
||||||
|
public void process (int playerId, String accountName, String handle,
|
||||||
|
Date created, int sessionMinutes, Stat stat);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The database identifier used when establishing a database connection. This value being
|
||||||
|
* <code>statdb</code>.
|
||||||
|
*/
|
||||||
|
public static final String STAT_DB_IDENT = "statdb";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new statistics repository with the specified connection provider.
|
||||||
|
*
|
||||||
|
* @param conprov the connection provider via which we will obtain our
|
||||||
|
* database connection.
|
||||||
|
*/
|
||||||
|
public StatRepository (ConnectionProvider conprov)
|
||||||
|
throws PersistenceException
|
||||||
|
{
|
||||||
|
super(conprov, STAT_DB_IDENT);
|
||||||
|
|
||||||
|
// load up our string set mappings
|
||||||
|
loadStringCodes(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the stats associated with the specified player.
|
||||||
|
*/
|
||||||
|
public ArrayList<Stat> loadStats (final int playerId)
|
||||||
|
throws PersistenceException
|
||||||
|
{
|
||||||
|
final ArrayList<Stat> stats = new ArrayList<Stat>();
|
||||||
|
final String query = "select STAT_CODE, STAT_DATA from STATS where PLAYER_ID = " + playerId;
|
||||||
|
execute(new Operation<Object>() {
|
||||||
|
public Object invoke (Connection conn, DatabaseLiaison liaison)
|
||||||
|
throws SQLException, PersistenceException
|
||||||
|
{
|
||||||
|
Statement stmt = conn.createStatement();
|
||||||
|
try {
|
||||||
|
ResultSet rs = stmt.executeQuery(query);
|
||||||
|
while (rs.next()) {
|
||||||
|
Stat stat = decodeStat(
|
||||||
|
rs.getInt(1), (byte[])rs.getObject(2));
|
||||||
|
if (stat != null) {
|
||||||
|
stats.add(stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
JDBCUtil.close(stmt);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes out any of the stats in the supplied array that have been modified since they were
|
||||||
|
* first loaded. Exceptions that occur while writing the stats will be caught and logged.
|
||||||
|
*/
|
||||||
|
public void writeModified (int playerId, Stat[] stats)
|
||||||
|
{
|
||||||
|
for (int ii = 0; ii < stats.length; ii++) {
|
||||||
|
try {
|
||||||
|
if (stats[ii].getType().isPersistent() &&
|
||||||
|
stats[ii].isModified()) {
|
||||||
|
updateStat(playerId, stats[ii]);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.log(Level.WARNING, "Error flushing modified stat [stat=" + stats[ii] + "].", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// documentation inherited from interface Stat.AuxDataSource
|
||||||
|
public int getStringCode (Stat.Type type, String value)
|
||||||
|
{
|
||||||
|
HashMap<String,Integer> map = _stringToCode.get(type);
|
||||||
|
if (map == null) {
|
||||||
|
_stringToCode.put(type, map = new HashMap<String,Integer>());
|
||||||
|
}
|
||||||
|
Integer code = map.get(value);
|
||||||
|
if (code == null) {
|
||||||
|
try {
|
||||||
|
code = assignStringCode(type, value);
|
||||||
|
} catch (PersistenceException pe) {
|
||||||
|
log.log(Level.WARNING, "Failed to assign code [type=" + type +
|
||||||
|
", value=" + value + "].", pe);
|
||||||
|
// at this point the database is probably totally hosed, so we can just punt here,
|
||||||
|
// and assume that this value will never be persisted
|
||||||
|
code = -1;
|
||||||
|
}
|
||||||
|
mapStringCode(type, value, code);
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// documentation inherited from interface Stat.AuxDataSource
|
||||||
|
public String getCodeString (Stat.Type type, int code)
|
||||||
|
{
|
||||||
|
HashIntMap<String> map = _codeToString.get(type);
|
||||||
|
String value = (map == null) ? null : map.get(code);
|
||||||
|
if (value == null) {
|
||||||
|
// our value may have been mapped on a different server, so refresh
|
||||||
|
// this mapping table from the database; then try again
|
||||||
|
try {
|
||||||
|
loadStringCodes(type);
|
||||||
|
} catch (PersistenceException pe) {
|
||||||
|
log.log(Level.WARNING, "Failed to reload string codes " +
|
||||||
|
"[type=" + type + ", code=" + code + "].", pe);
|
||||||
|
}
|
||||||
|
map = _codeToString.get(type);
|
||||||
|
value = (map == null) ? null : map.get(code);
|
||||||
|
if (value == null) {
|
||||||
|
log.warning("Missing reverse maping [type=" + type +
|
||||||
|
", code=" + code + "].");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is only used for testing. Do not call this method.
|
||||||
|
*/
|
||||||
|
public void clearMapping (Stat.Type type, String value)
|
||||||
|
{
|
||||||
|
int ocode = _stringToCode.get(type).remove(value);
|
||||||
|
_codeToString.get(type).remove(ocode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the supplied processor on every stat in the database of the specified type.
|
||||||
|
*
|
||||||
|
* <p><em>Note:</em> the stats database will inevitable be extremely large (one row for every
|
||||||
|
* paying player and one for non-payers less than six months old; millions of rows if the game
|
||||||
|
* is at all successful). Don't call this method willy nilly and the summarized results should
|
||||||
|
* be cached for at least 12 hours. (Stats don't change that frequently in the aggregate.)
|
||||||
|
*/
|
||||||
|
public void processStats (final Processor processor, Stat.Type type)
|
||||||
|
throws PersistenceException
|
||||||
|
{
|
||||||
|
final Stat stat = type.newStat();
|
||||||
|
final String query = "select STATS.PLAYER_ID, ACCOUNT_NAME, HANDLE, CREATED, " +
|
||||||
|
"SESSION_MINUTES, STAT_DATA from STATS, PLAYERS " +
|
||||||
|
"where PLAYERS.PLAYER_ID = STATS.PLAYER_ID and STAT_CODE = " + type.code();
|
||||||
|
execute(new Operation<Object>() {
|
||||||
|
public Object invoke (Connection conn, DatabaseLiaison liaison)
|
||||||
|
throws SQLException, PersistenceException
|
||||||
|
{
|
||||||
|
Statement stmt = conn.createStatement();
|
||||||
|
try {
|
||||||
|
ResultSet rs = stmt.executeQuery(query);
|
||||||
|
while (rs.next()) {
|
||||||
|
if (decodeStat(stat, (byte[])rs.getObject(6)) != null) {
|
||||||
|
processor.process(rs.getInt(1), rs.getString(2), rs.getString(3),
|
||||||
|
rs.getDate(4), rs.getInt(5), stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
JDBCUtil.close(stmt);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates the appropriate stat class and decodes the stat from the data.
|
||||||
|
*/
|
||||||
|
protected Stat decodeStat (int statCode, byte[] data)
|
||||||
|
{
|
||||||
|
Stat.Type type = Stat.getType(statCode);
|
||||||
|
if (type == null) {
|
||||||
|
log.warning("Unable to decode stat, unknown type [code=" + statCode + "].");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return decodeStat(type.newStat(), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates the appropriate stat class and decodes the stat from the data.
|
||||||
|
*/
|
||||||
|
protected Stat decodeStat (Stat stat, byte[] data)
|
||||||
|
{
|
||||||
|
String errmsg = null;
|
||||||
|
Exception error = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// decode its contents from the serialized data
|
||||||
|
ByteArrayInputStream bin = new ByteArrayInputStream(data);
|
||||||
|
stat.unpersistFrom(new ObjectInputStream(bin), this);
|
||||||
|
return stat;
|
||||||
|
|
||||||
|
} catch (ClassNotFoundException cnfe) {
|
||||||
|
error = cnfe;
|
||||||
|
errmsg = "Unable to instantiate stat";
|
||||||
|
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
error = ioe;
|
||||||
|
errmsg = "Unable to decode stat";
|
||||||
|
}
|
||||||
|
|
||||||
|
log.log(Level.WARNING, errmsg + " [type=" + stat.getType() + "]", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the specified stat in the database, inserting it if necessary.
|
||||||
|
*/
|
||||||
|
protected void updateStat (int playerId, final Stat stat)
|
||||||
|
throws PersistenceException
|
||||||
|
{
|
||||||
|
final String uquery = "update STATS set STAT_DATA = ?" +
|
||||||
|
" where PLAYER_ID = " + playerId + " and STAT_CODE = " + stat.getCode();
|
||||||
|
final String iquery = "insert into STATS (PLAYER_ID, STAT_CODE, " +
|
||||||
|
"STAT_DATA) values (" + playerId + ", " + stat.getCode() + ", ?)";
|
||||||
|
final ByteArrayOutInputStream out = new ByteArrayOutInputStream();
|
||||||
|
try {
|
||||||
|
stat.persistTo(new ObjectOutputStream(out), this);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
String errmsg = "Error serializing stat " + stat;
|
||||||
|
throw new PersistenceException(errmsg, ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now update (or insert) the flattened data into the database
|
||||||
|
executeUpdate(new Operation<Object>() {
|
||||||
|
public Object invoke (Connection conn, DatabaseLiaison liaison)
|
||||||
|
throws SQLException, PersistenceException
|
||||||
|
{
|
||||||
|
PreparedStatement stmt = conn.prepareStatement(uquery);
|
||||||
|
try {
|
||||||
|
stmt.setBinaryStream(1, out.getInputStream(), out.size());
|
||||||
|
if (stmt.executeUpdate() == 0) {
|
||||||
|
JDBCUtil.close(stmt);
|
||||||
|
stmt = conn.prepareStatement(iquery);
|
||||||
|
stmt.setBinaryStream(1, out.getInputStream(), out.size());
|
||||||
|
JDBCUtil.checkedUpdate(stmt, 1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
JDBCUtil.close(stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper function for {@link #getStringCode}. */
|
||||||
|
protected Integer assignStringCode (final Stat.Type type, final String value)
|
||||||
|
throws PersistenceException
|
||||||
|
{
|
||||||
|
return executeUpdate(new Operation<Integer>() {
|
||||||
|
public Integer invoke (Connection conn, DatabaseLiaison liaison)
|
||||||
|
throws SQLException, PersistenceException
|
||||||
|
{
|
||||||
|
for (int ii = 0; ii < 10; ii++) {
|
||||||
|
try {
|
||||||
|
int code = getNextCode(conn, type);
|
||||||
|
// DEBUG: uncomment this to test code collision
|
||||||
|
// if (ii == 0 && code > 0) {
|
||||||
|
// code = code-1;
|
||||||
|
// }
|
||||||
|
insertStringCode(conn, type, value, code);
|
||||||
|
return code;
|
||||||
|
|
||||||
|
} catch (SQLException sqe) {
|
||||||
|
// if this is not a duplicate row exception, something is booched and we
|
||||||
|
// just fail
|
||||||
|
if (!liaison.isDuplicateRowException(sqe)) {
|
||||||
|
throw sqe;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if it is a duplicate row exception, possibly someone inserted our value
|
||||||
|
// before we could, in which case we can just look up the new mapping
|
||||||
|
int code = getCurrentCode(conn, type, value);
|
||||||
|
if (code != -1) {
|
||||||
|
log.info("Value collision assigning string code [type=" + type +
|
||||||
|
", value=" + value + "].");
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise someone used the code we were trying to use and we just need
|
||||||
|
// to loop around and get the next highest code
|
||||||
|
log.info("Code collision assigning string code [type=" + type +
|
||||||
|
", value=" + value + "].");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new SQLException("Unable to assign code after 10 attempts " +
|
||||||
|
"[type=" + type + ", value=" + value + "]");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper function for {@link #assignStringCode}. */
|
||||||
|
protected int getNextCode (Connection conn, Stat.Type type)
|
||||||
|
throws SQLException
|
||||||
|
{
|
||||||
|
return getCode(conn, "select MAX(CODE)+1 from STRING_CODES where STAT_CODE = " +
|
||||||
|
type.code());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper function for {@link #assignStringCode}. */
|
||||||
|
protected int getCurrentCode (Connection conn, Stat.Type type, String value)
|
||||||
|
throws SQLException
|
||||||
|
{
|
||||||
|
return getCode(conn, "select CODE from STRING_CODES where STAT_CODE = " + type.code() +
|
||||||
|
" and VALUE = " + JDBCUtil.escape(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper function for {@link #getNextCode} and {@link #getCurrentCode}. */
|
||||||
|
protected int getCode (Connection conn, String query)
|
||||||
|
throws SQLException
|
||||||
|
{
|
||||||
|
int code = -1;
|
||||||
|
Statement stmt = conn.createStatement();
|
||||||
|
try {
|
||||||
|
ResultSet rs = stmt.executeQuery(query);
|
||||||
|
while (rs.next()) {
|
||||||
|
code = rs.getInt(1);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
JDBCUtil.close(stmt);
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper function for {@link #assignStringCode}. */
|
||||||
|
protected void insertStringCode (Connection conn, Stat.Type type, String value, int code)
|
||||||
|
throws SQLException
|
||||||
|
{
|
||||||
|
String query = "insert into STRING_CODES (STAT_CODE, VALUE, CODE) values(" + type.code() +
|
||||||
|
", " + JDBCUtil.escape(value) + ", " + code + ")";
|
||||||
|
Statement stmt = conn.createStatement();
|
||||||
|
try {
|
||||||
|
int mods = stmt.executeUpdate(query);
|
||||||
|
if (mods != 1) {
|
||||||
|
throw new SQLException("Insertion failed to modify one row [type=" + type +
|
||||||
|
", value=" + value + ", code=" + code +
|
||||||
|
", mods=" + mods + "]");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
JDBCUtil.close(stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper function used at repository startup. */
|
||||||
|
protected void loadStringCodes (Stat.Type type)
|
||||||
|
throws PersistenceException
|
||||||
|
{
|
||||||
|
final String query = "select STAT_CODE, VALUE, CODE from STRING_CODES " +
|
||||||
|
((type == null) ? "" : (" where STAT_CODE = " + type.code()));
|
||||||
|
execute(new Operation<Object>() {
|
||||||
|
public Object invoke (Connection conn, DatabaseLiaison liaison)
|
||||||
|
throws SQLException, PersistenceException
|
||||||
|
{
|
||||||
|
Statement stmt = conn.createStatement();
|
||||||
|
try {
|
||||||
|
ResultSet rs = stmt.executeQuery(query);
|
||||||
|
while (rs.next()) {
|
||||||
|
mapStringCode(Stat.getType(rs.getInt(1)), rs.getString(2), rs.getInt(3));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
JDBCUtil.close(stmt);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Helper function used at repository startup. */
|
||||||
|
protected void mapStringCode (Stat.Type type, String value, int code)
|
||||||
|
{
|
||||||
|
HashMap<String,Integer> fmap = _stringToCode.get(type);
|
||||||
|
if (fmap == null) {
|
||||||
|
_stringToCode.put(type, fmap = new HashMap<String,Integer>());
|
||||||
|
}
|
||||||
|
fmap.put(value, code);
|
||||||
|
HashIntMap<String> rmap = _codeToString.get(type);
|
||||||
|
if (rmap == null) {
|
||||||
|
_codeToString.put(type, rmap = new HashIntMap<String>());
|
||||||
|
}
|
||||||
|
rmap.put(code, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override // documentation inherited
|
||||||
|
protected void migrateSchema (Connection conn, DatabaseLiaison liaison)
|
||||||
|
throws SQLException, PersistenceException
|
||||||
|
{
|
||||||
|
JDBCUtil.createTableIfMissing(conn, "STATS", new String[] {
|
||||||
|
"PLAYER_ID INTEGER NOT NULL",
|
||||||
|
"STAT_CODE INTEGER NOT NULL",
|
||||||
|
"STAT_DATA BLOB NOT NULL",
|
||||||
|
"KEY (PLAYER_ID)",
|
||||||
|
"KEY (STAT_CODE)",
|
||||||
|
}, "");
|
||||||
|
|
||||||
|
JDBCUtil.createTableIfMissing(conn, "STRING_CODES", new String[] {
|
||||||
|
"STAT_CODE INTEGER NOT NULL",
|
||||||
|
"VALUE VARCHAR(255) NOT NULL",
|
||||||
|
"CODE INTEGER NOT NULL",
|
||||||
|
"UNIQUE (STAT_CODE, VALUE)",
|
||||||
|
"UNIQUE (STAT_CODE, CODE)",
|
||||||
|
}, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected HashMap<Stat.Type,HashMap<String,Integer>> _stringToCode =
|
||||||
|
new HashMap<Stat.Type,HashMap<String,Integer>>();
|
||||||
|
protected HashMap<Stat.Type,HashIntMap<String>> _codeToString =
|
||||||
|
new HashMap<Stat.Type,HashIntMap<String>>();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user