Checkpoint of "work as is".
authornotzed@gmail.com <notzed@gmail.com@b8b59bfb-1aa4-4687-8f88-a62eeb14c21e>
Sun, 15 Sep 2013 00:13:33 +0000 (00:13 +0000)
committernotzed@gmail.com <notzed@gmail.com@b8b59bfb-1aa4-4687-8f88-a62eeb14c21e>
Sun, 15 Sep 2013 00:13:33 +0000 (00:13 +0000)
git-svn-id: file:///home/notzed/svn/duskz/trunk@17 b8b59bfb-1aa4-4687-8f88-a62eeb14c21e

61 files changed:
DuskServer/CommandsWork.java [new file with mode: 0644]
DuskServer/README
DuskServer/TODO [new file with mode: 0644]
DuskServer/docs/dusk-script
DuskServer/docs/duskz-classes [new file with mode: 0644]
DuskServer/docs/duskz-javascript [new file with mode: 0644]
DuskServer/docs/duskz-script [new file with mode: 0644]
DuskServer/docs/duskz-state [new file with mode: 0644]
DuskServer/src/duskz/io/Convert.java [new file with mode: 0644]
DuskServer/src/duskz/io/Tiled.java
DuskServer/src/duskz/server/Commands.java
DuskServer/src/duskz/server/DuskEngine.java
DuskServer/src/duskz/server/Faction.java
DuskServer/src/duskz/server/entity/DuskObject.java
DuskServer/src/duskz/server/entity/Equipment.java
DuskServer/src/duskz/server/entity/LivingThing.java
DuskServer/src/duskz/server/entity/Mob.java
DuskServer/src/duskz/server/entity/TileMap.java
DuskServer/src/duskz/server/entityz/Ability.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Active.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Armour.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Battle.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Commands.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Condition.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/ConditionList.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Container.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Converter.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Drink.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Equipment.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Faction.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Food.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Game.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/GameServer.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/GameShop.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Holdable.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Inventory.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Item.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Location.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Log.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Mobile.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Pack.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Pet.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Player.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/PlayerCommands.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/PlayerConnection.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/PlayerShop.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/PlayerState.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Prop.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/PropertyLoader.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Race.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/ScriptManager.java [moved from DuskServer/src/duskz/server/ScriptManager.java with 95% similarity]
DuskServer/src/duskz/server/entityz/Shop.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Sign.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Thing.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/ThingTable.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/TileMap.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/TooManyTriesException.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Training.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/VariableList.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Weapon.java [new file with mode: 0644]
DuskServer/src/duskz/server/entityz/Wearable.java [new file with mode: 0644]

diff --git a/DuskServer/CommandsWork.java b/DuskServer/CommandsWork.java
new file mode 100644 (file)
index 0000000..8056dbd
--- /dev/null
@@ -0,0 +1,2393 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+/**
+ * Changes
+ * Feb-2013 Michael Zucchi - modernised java
+ * Mar-2013 Michael Zucchi - changed server protocol
+ */
+package duskz.server.entityz;
+
+import duskz.server.*;
+import duskz.protocol.DuskProtocol;
+import duskz.server.entity.Mob;
+import duskz.server.entity.Merchant;
+import duskz.server.entity.Sign;
+import duskz.server.entity.Item;
+import duskz.server.entity.Prop;
+import duskz.server.entity.DuskObject;
+import duskz.server.entity.Equipment;
+import duskz.server.entity.PlayerMerchant;
+import duskz.server.entity.LivingThing;
+import duskz.server.entity.TileMap;
+import java.io.*;
+import java.util.LinkedList;
+import java.util.StringTokenizer;
+
+public class CommandsWork implements DuskProtocol {
+
+       public static String parseCommand(LivingThing lt, DuskEngine game, String cmdline) throws Exception {
+               if (cmdline == null) {
+                       return null;
+               }
+               if (lt == null) {
+                       return null;
+               }
+               if (game == null) {
+                       return null;
+               }
+
+               String cmd = null;
+               String args = null;
+
+               int intIndex = cmdline.indexOf(" ");
+               if (intIndex == -1) {
+                       cmd = cmdline.toLowerCase();
+               } else {
+                       cmd = cmdline.substring(0, intIndex).toLowerCase();
+                       args = cmdline.substring(intIndex + 1).trim();
+               }
+
+               if (cmd.length() < 1) {
+                       return "huh?";
+               }
+
+               lt.isAlwaysCommands = true;
+               boolean blnFoundScriptedCommand = false;
+               /*
+                ** Don't try to find a scripted command if they are doing a tell
+                */
+               if (cmd.substring(0, 1) != "/") {
+                       try {
+                               Script script = new Script("commands/" + cmd, game, false);
+                               script.varVariables.addVariable("trigger", lt);
+                               if (args != null) {
+                                       script.runScript(args);
+                               } else {
+                                       script.runScript();
+                               }
+                               script.close();
+                               blnFoundScriptedCommand = true;
+                       } catch (Exception e) {
+                               blnFoundScriptedCommand = false;
+                       }
+                       if (!lt.isAlwaysCommands) {
+                               return null;
+                       }
+               }
+               if (lt.privs > 2) {
+                       if (cmdline.startsWith(">")) {
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               cmdline = cmdline.substring(1);
+                               if (cmdline.equalsIgnoreCase("s")) {
+                                       if (game.blnSavingGame) {
+                                               return "Game already being saved, please wait.";
+                                       }
+                                       game.saveMap();
+                                       return "Game settings saved";
+                               }
+
+                               String strMapLog = "shortmap_redraw";
+                               try (PrintStream psMap = new PrintStream(new FileOutputStream(strMapLog, true), true)) {
+                                       game.changeMap(lt, lt.x, lt.y, Short.parseShort(cmdline));
+                                       psMap.println("changetile " + lt.x + " " + lt.y + " " + Short.parseShort(cmdline));
+                                       return null;
+                               } catch (Exception e) {
+                                       game.log.printError("parseCommand():While " + lt.name + " tried to >" + cmdline, e);
+                                       return null;
+                               }
+                       }
+                       if (cmdline.startsWith("<")) {
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               cmdline = cmdline.substring(1);
+                               if (cmdline.equals("s")) {
+                                       if (game.blnSavingGame) {
+                                               return "Game already being saved, please wait.";
+                                       }
+                                       game.saveMap();
+                                       return "Game settings saved";
+                               } else if (cmdline.equalsIgnoreCase("merchant")) {
+                                       if (lt.overMerchant() != null) {
+                                               return "There's already a merchant there.";
+                                       }
+                                       if (lt.overPlayerMerchant() != null) {
+                                               return "There's already a merchant there.";
+                                       }
+                                       Merchant mrcStore = new Merchant(game);
+                                       mrcStore.x = lt.x;
+                                       mrcStore.y = lt.y;
+                                       //game.vctMerchants.add(mrcStore);
+                                       //game.blnMerchantListChanged = true;
+                                       game.addDuskObject(lt.map, mrcStore);
+                                       return null;
+                               } else if (cmdline.toLowerCase().startsWith("prop ")) {
+                                       if (cmdline.length() == 5) {
+                                               return "Add what prop?";
+                                       }
+                                       cmdline = cmdline.substring(5);
+                                       Prop prpStore = game.getProp(cmdline);
+                                       if (prpStore != null) {
+                                               prpStore.x = lt.x;
+                                               prpStore.y = lt.y;
+                                               //game.vctProps.addElement(prpStore);
+                                               //game.blnPropListChanged = true;
+                                               game.addDuskObject(lt.map, prpStore);
+                                       }
+                                       return null;
+                               } else if (cmdline.startsWith("sign ")) {
+                                       if (cmdline.length() == 5) {
+                                               return "What should the sign say?";
+                                       }
+                                       Sign sgnStore = new Sign(game, "sign", cmdline.substring(5), lt.x, lt.y, game.getID());
+                                       //game.vctSigns.add(sgnStore);
+                                       //game.blnSignListChanged = true;
+                                       game.addDuskObject(lt.map, sgnStore);
+                                       return null;
+                               }
+                               Item itmStore = game.getItem(cmdline);
+                               if (itmStore != null) {
+                                       itmStore.x = lt.x;
+                                       itmStore.y = lt.y;
+                                       //game.vctItems.add(itmStore);
+                                       game.addDuskObject(lt.map, itmStore);
+                                       return null;
+                               }
+                               try {
+                                       Mob mob = new Mob(cmdline, lt.x, lt.y, game);
+                                       // TODO: this previously didn't call addDuskObject - bug or intentional?
+                                       //game.vctMobs.addElement(mob);
+                                       //game.blnMobListChanged = true;
+                                       game.addDuskObject(lt.map, mob);
+
+                                       mob.changeLocBypass(lt.map, lt.x, lt.y);
+                               } catch (Exception e) {
+                                       game.log.printError("parseCommand():While creating mob \"" + cmdline + "\"", e);
+                               }
+                               return null;
+                       }
+               }
+
+               // Remap shortcuts
+               if (cmd.startsWith(";")) {
+                       args = cmdline.substring(1).trim();
+                       cmd = "gossip";
+               }
+               if (cmd.startsWith(":")) {
+                       args = cmdline.substring(1).trim();
+                       cmd = "clan";
+               }
+               if (cmd.startsWith("'")) {
+                       args = cmdline.substring(1).trim();
+                       cmd = "say";
+               }
+               if (cmd.startsWith(".")) {
+                       args = cmdline.substring(1).trim();
+                       cmd = "emote";
+               }
+               if (cmd.startsWith("/")) {
+                       args = cmdline.substring(1).trim();
+                       cmd = "tell";
+               }
+
+               switch (cmd) {
+
+                       case "addmember": {
+                               if (lt.privs != 1)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "Add who?";
+                               }
+                               LivingThing thnStore = game.getPlayer(args);
+                               if (thnStore == null) {
+                                       return "They're not in this world";
+                               }
+                               if (lt.battle != null) {
+                                       return "Not while you're fighting!";
+                               }
+                               if (thnStore.battle != null) {
+                                       thnStore.chatMessage(lt.name + " has invited you to join their clan, but you are in the middle of a battle");
+                                       return "They're in the middle of a battle. They have been notified that you tried to clan them.";
+                               }
+                               lt.chatMessage("You have invited " + thnStore.name + " to join the clan " + lt.clan + ".");
+
+                               if (true)
+                                       // FIXME: protocol implementation
+                                       throw new RuntimeException("cannot ask questions yet");
+                               // FIXME: this looks dodgy
+                               // FIXME: move to livingthing
+
+                               /*
+                                //             thnStore.halt();
+                                thnStore.stillThere();  // This puts something in the buffer
+                                thnStore.stillThere();  // Have to do this twice to ensure that thnStore is out of
+                                // its read loop
+                                lt.connectionThread.sleep(500);  // wait for the "notdead" response to get back from client.
+                                try {
+                                // Empty out the BufferedReader for the answer
+                                //     while (thnStore.instream.ready()) {
+                                //             thnStore.instream.read();
+                                //     }
+                                } catch (Exception e) {
+                                game.log.printError("parseCommand():While " + lt.name + " was trying to addmember " + thnStore.name, e);
+                                }
+                                thnStore.chatMessage(lt.name + " has invited you to join the clan " + lt.clan + ". If you accept, type yes.");
+                                try {
+                                if (thnStore.instream.readLine().equalsIgnoreCase("yes")) {
+                                thnStore.clan = lt.clan;
+                                if (thnStore.privs == 1) {
+                                thnStore.privs = 0;
+                                }
+                                thnStore.chatMessage("You have been added to the clan, " + lt.clan + "");
+                                thnStore.proceed();
+                                game.removeDuskObject(thnStore);
+                                game.addDuskObject(thnStore);
+                                return thnStore.name + " has accepted your invitation.";
+                                }
+                                } catch (Exception e) {
+                                game.log.printError("parseCommand():While reading the answer to " + lt.name + "'s attempt to addmember " + thnStore.name, e);
+                                }
+                                thnStore.proceed();
+                                */
+                               return thnStore.name + " has declined your invitation.";
+                       }
+                       case "kick": {
+                               if (lt.privs != 1)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "Kick who?";
+                               }
+                               LivingThing thnStore = game.getPlayer(args);
+                               if (thnStore == null) {
+                                       return "They're not in this world.";
+                               }
+                               if (!thnStore.clan.equalsIgnoreCase(lt.clan)) {
+                                       return "They're not in your clan.";
+                               }
+                               thnStore.chatMessage("You have been kicked out of " + lt.clan + ".");
+                               thnStore.clan = "none";
+                               game.removeDuskObject(thnStore);
+                               game.addDuskObject(thnStore.map, thnStore);
+                               return thnStore.name + " has been kicked out of your clan.";
+                       }
+//             if (lt.privs > 2) {
+//                     if (lt.privs > 4) {
+                       case "makegod": {
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               if (args == null) {
+                                       return "Make who a god?";
+                               }
+                               int iSPloc = args.indexOf(" ");
+                               if (iSPloc < 0) {
+                                       return "Make them what level of a god?";
+                               }
+                               String sName = args.substring(0, iSPloc).trim();
+                               int level = Integer.parseInt(args.substring(iSPloc).trim());
+
+                               LivingThing thnStore = game.getPlayer(sName);
+                               if (thnStore == null) {
+                                       return "They're not in this world.";
+                               }
+                               int oldLevel = thnStore.privs;
+                               thnStore.privs = level;
+                               thnStore.isSaveNeeded = true;
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":Changed " + thnStore.name + "'s priveledges from " + oldLevel + " to " + level + ".");
+                               return thnStore.name + "'s priveledges have been set to " + level + ".";
+                       }
+                       case "reloadprefs": {
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               game.loadPrefs();
+                               return "Preferences reloaded";
+                       }
+                       case "resizemap":
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               game.resizeMap(lt.map, lt.x + 1, lt.y + 1);
+                               return "Map resized";
+                       case "shutdown":
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+                               game.log.printMessage(Log.ALWAYS, lt.name + " has shut down the server.");
+                               game.chatMessage("The server is going down.", "default");
+                               game.blnShuttingDown = true;
+                               for (LivingThing thnStore : game.playersByName.values()) {
+                                       try {
+                                               if (thnStore != lt) {
+                                                       thnStore.close();
+                                               }
+                                       } catch (Exception e) {
+                                               if (thnStore != null) {
+                                                       game.log.printError("parseCommand():While trying to close " + thnStore.name + " for " + lt.name + "'s shutdown request", e);
+                                               } else {
+                                                       game.log.printError("parseCommand():While trying to close a null player for " + lt.name + "'s shutdown request", e);
+                                               }
+                                       }
+                               }
+                               lt.isSaveNeeded = true;
+                               lt.savePlayer();
+                               System.gc();
+                               System.exit(0);
+                               return null;
+                       case "stop":
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               game.blnShuttingDown = true;
+                               return "Stopped accepting incoming socket connections.";
+                       case "start":
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               game.blnShuttingDown = false;
+                               return "Started accepting incoming connections";
+                       case "filteron":
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               game.blnIPF = true;
+                               return "Started filtering duplicate IP addressess of socket connections.";
+                       case "filteroff":
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               game.blnIPF = false;
+                               return "Stopped filtering duplicate IP addressess of socket connections.";
+                       case "floodlimit":
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               if (args == null) {
+                                       return "What value do you want the floodlimit to have?";
+                               }
+                               try {
+                                       game.floodLimit = (long) Integer.parseInt(args);
+                                       return "Set floodlimit to " + args + " milliseconds.";
+                               } catch (Exception e) {
+                                       game.log.printError("parseCommand():Invalid value \"" + args + "\" for floodlimit.", e);
+                                       return "Invalid value \"" + args + "\" for floodlimit.";
+                               }
+                       case "ipban": {
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+                               String strBlockedIP;
+                               if (args == null) {
+                                       return "Whos IP address do you wish to ban?";
+                               }
+                               LivingThing thnStore = game.getPlayer(args);
+                               if (thnStore == null) {
+                                       return "They're not in this world.";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               String strIP = thnStore.getAddress();
+                               int i = strIP.indexOf("/");
+                               strIP = strIP.substring(i + 1, strIP.length());
+                               // FIXME: better i/o
+                               try (RandomAccessFile rafBannedIP = new RandomAccessFile("conf/blockedIP", "rw")) {
+                                       strBlockedIP = rafBannedIP.readLine();
+                                       while (strBlockedIP != null) {
+                                               if (strIP.indexOf(strBlockedIP) != -1) {
+                                                       //rafBannedIP.close();
+                                                       return "Already blocked.";
+                                               }
+                                               strBlockedIP = rafBannedIP.readLine();
+                                       }
+                                       rafBannedIP.seek(rafBannedIP.length());
+                                       rafBannedIP.writeBytes(strIP + "\n");
+                               } catch (IOException ex) {
+                                       game.log.printError("parseCommand():When " + lt.name + " tried to ban " + thnStore + "'s IP address", ex);
+                               }
+                               return thnStore.name + "'s IP address has been blocked.";
+                       }
+                       case "loglevel":
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               if (args == null) {
+                                       return "Logging level is currently " + game.log.getLogLevel();
+                               }
+                               try {
+                                       int level = Integer.parseInt(args);
+                                       game.log.setLogLevel(level);
+                                       return "Logging level is now " + game.log.getLogLevel();
+                               } catch (Exception e) {
+                                       game.log.printError("parseCommand():Invalid value \"" + args + "\" for loglevel.", e);
+                                       return "Invalid value \"" + args + "\" for loglevel.";
+                               }
+                       case "gc":
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+                               game.log.printMessage(Log.INFO, "Starting garbage collection.");
+                               System.gc();
+                               game.log.printMessage(Log.INFO, "Finished garbage collection.");
+                               return "Finished garbage collection.";
+                       case "finalize":
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+                               game.log.printMessage(Log.INFO, "Starting finalization.");
+                               System.runFinalization();
+                               game.log.printMessage(Log.INFO, "Finished finalization.");
+                               return "Finished finalization.";
+                       case "cleanup":
+                               if (lt.privs <= 4)
+                                       return "Huh?";
+                               game.cleanup();
+                               return "Finished cleanup.";
+                       case "save":
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (game.blnSavingGame) {
+                                       return "Game already being saved, please wait.";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               game.saveMap();
+                               return "Game settings saved";
+                       case "backup":
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               game.backupMap();
+                               return "Game settings backed up";
+                       case "list": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "What do you want to list?";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               cmdline = cmdline.substring(5);
+                               String filename = null;
+                               String title = null;
+                               if (args.equals("items")) {
+                                       filename = "defItems";
+                                       title = "Items:\n";
+                               } else if (args.equals("conf")) {
+                                       filename = "conf";
+                                       title = "Conf files:\n";
+                               } else if (args.equals("mobs")) {
+                                       filename = "defMobs";
+                                       title = "Mobiles:\n";
+                               } else if (args.equals("commands")) {
+                                       filename = "commands";
+                                       title = "Custom commands:\n";
+                               } else if (args.equals("races")) {
+                                       filename = "defRaces";
+                                       title = "Races:\n";
+                               } else if (args.equals("pets")) {
+                                       filename = "defPets";
+                                       title = "Pets:\n";
+                               } else if (args.equals("factions")) {
+                                       filename = "factions";
+                                       title = "Factions:\n";
+                               } else if (args.equals("conditions")) {
+                                       filename = "defConditions";
+                                       title = "Conditions:\n";
+                               } else if (args.equals("help")) {
+                                       filename = "helpFiles";
+                                       title = "Help Files:\n";
+                               } else if (args.equals("scripts")) {
+                                       filename = "scripts";
+                                       title = "Scripts:\n";
+                               } else if (args.equals("spell groups")) {
+                                       filename = "defSpellGroups";
+                                       title = "Spell Groups:\n";
+                               } else if (args.equals("spells")) {
+                                       filename = "defSpells";
+                                       title = "Spells:\n";
+                               } else if (args.equals("props")) {
+                                       filename = "defProps";
+                                       title = "Props:\n";
+                               } else if (args.equals("move actions")) {
+                                       filename = "defMoveActions";
+                                       title = "Move Action Scripts:\n";
+                               } else if (args.equals("can move")) {
+                                       filename = "defCanMoveScripts";
+                                       title = "Can Move Scripts:\n";
+                               } else if (args.equals("can see")) {
+                                       filename = "defCanSeeScripts";
+                                       title = "Can See Scripts:\n";
+                               } else if (args.equals("tile actions")) {
+                                       filename = "defTileActions";
+                                       title = "Tile Action Scripts:\n";
+                               } else if (args.equals("tile move")) {
+                                       filename = "defTileScripts";
+                                       title = "Can Move Tile Scripts:\n";
+                               } else if (args.equals("tile see")) {
+                                       filename = "defTileSeeScripts";
+                                       title = "Tile See Scripts:\n";
+                               }
+                               if (filename != null) {
+                                       File filList = new File(filename);
+                                       String strResult[] = filList.list();
+                                       StringBuilder sb = new StringBuilder();
+                                       //strBuff.append("").append((char) 20).append(strTitle).append("\n");
+                                       for (int i = 0; i < strResult.length; i++) {
+                                               // Only output files that do not end in .dsko
+                                               if (strResult[i].indexOf(".dsko") == -1) {
+                                                       sb.append(strResult[i]).append("\n");
+                                               }
+                                       }
+                                       //strBuff.append("--EOF--\n");
+                                       //lt.send(strBuff.toString());
+                                       lt.viewText(title, false, sb.toString());
+                                       return null;
+                               }
+                               return "You can't list that.";
+                       }
+                       case "view": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+
+                               if (args == null) {
+                                       return "What do you want to view?";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               if (args.indexOf("..") != -1) {
+                                       return "You don't have permission to access that file.";
+                               }
+                               String filename = null;
+                               boolean blnUser = false;
+                               boolean blnPet = false;
+                               if (args.startsWith("item ")) {
+                                       args = args.toLowerCase();
+                                       filename = "defItems/" + args.substring(5);
+                               } else if (args.startsWith("conf ")) {
+                                       filename = "conf/" + args.substring(5);
+                               } else if (args.startsWith("mob ")) {
+                                       filename = "defMobs/" + args.substring(4);
+                               } else if (args.startsWith("command ")) {
+                                       filename = "commands/" + args.substring(8);
+                               } else if (args.startsWith("race ")) {
+                                       filename = "defRaces/" + args.substring(5);
+                               } else if (args.startsWith("pet ")) {
+                                       filename = "defPets/" + args.substring(5);
+                               } else if (args.startsWith("faction")) {
+                                       return "You cannot view faction files.";
+                               } else if (args.startsWith("condition ")) {
+                                       filename = "defConditions/" + args.substring(10);
+                               } else if (args.startsWith("help ")) {
+                                       filename = "helpFiles/" + args.substring(5);
+                               } else if (args.startsWith("script ")) {
+                                       filename = "scripts/" + args.substring(7);
+                               } else if (args.startsWith("spell group ")) {
+                                       filename = "defSpellGroups/" + args.substring(12);
+                               } else if (args.startsWith("spell ")) {
+                                       filename = "defSpells/" + args.substring(6);
+                               } else if (args.startsWith("prop ")) {
+                                       filename = "defProps/" + args.substring(5);
+                               } else if (args.startsWith("move action ")) {
+                                       filename = "defMoveActions/" + args.substring(12);
+                               } else if (args.startsWith("can move ")) {
+                                       filename = "defCanMoveScripts/" + args.substring(9);
+                               } else if (args.startsWith("can see ")) {
+                                       filename = "defCanSeeScripts/" + args.substring(8);
+                               } else if (args.startsWith("tile action ")) {
+                                       filename = "defTileActions/" + args.substring(12);
+                               } else if (args.startsWith("tile move ")) {
+                                       filename = "defTileScripts/" + args.substring(10);
+                               } else if (args.startsWith("tile see ")) {
+                                       filename = "defTileSeeScripts/" + args.substring(9);
+                               } else if (args.startsWith("user ")) {
+                                       if (lt.privs < 5) {
+                                               return "You don't have enough privelages to edit a user's file.";
+                                       }
+                                       blnUser = true;
+                                       filename = "users/" + args.substring(5);
+                               } else if (args.startsWith("pet ")) {
+                                       if (lt.privs < 5) {
+                                               return "You don't have enough privelages to edit a user's pet file.";
+                                       }
+                                       blnPet = true;
+                                       filename = "pets/" + args.substring(4);
+                               }
+                               File filView = new File(filename);
+                               if (!filView.exists()) {
+                                       if (blnUser) {
+                                               return "There is no player named \"" + filView.getName() + "\".";
+                                       }
+                                       if (blnPet) {
+                                               return "The player named \"" + filView.getName() + "\" does not have a pet.";
+                                       }
+                                       //lt.send((char) 18 + args + "\n--EOF--\n");
+                                       lt.viewText(args, true, null);
+                                       return null;
+                               }
+                               RandomAccessFile rafView = null;
+                               StringBuilder sb = new StringBuilder();
+                               try {
+                                       rafView = new RandomAccessFile(filView, "rw");
+                                       if (blnUser) {
+                                               rafView.readLine();  //Skip over users' password
+                                       }
+                                       String strStore2 = rafView.readLine();
+                                       //sb.append((char) 18 + args + "\n");
+                                       while (strStore2 != null) {
+                                               sb.append(strStore2 + "\n");
+                                               strStore2 = rafView.readLine();
+                                       }
+                                       //sb.append("--EOF--\n");
+                                       lt.viewText(args, true, sb.toString());
+                               } catch (Exception e) {
+                                       game.log.printError("parseCommand():Reading file for " + filView.getName(), e);
+                               }
+                               try {
+                                       rafView.close();
+                               } catch (Exception e) {
+                                       game.log.printError("parseCommand():Closing file after " + filView.getName(), e);
+                               }
+                               return null;
+                       }
+                       case "submit": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "What do you want to submit?";
+                               }
+                               if ((lt.privs < 4) && (!args.startsWith("mob "))) {
+                                       return "You are not allowed to submit files.";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               if (args.indexOf("..") != -1) {
+                                       return "You don't have permission to access that file.";
+                               }
+
+                               if (true)
+                                       return "Developer hasn't implemented submit yet";
+
+                               // FIXME: implement submit, just use some submit message protocol
+
+                               boolean compile = false;
+                               boolean blnUser = false;
+                               boolean blnPet = false;
+                               String strFileName = null;
+                               if (args.startsWith("item ") && (lt.privs > 3)) {
+                                       args = args.toLowerCase();
+                                       strFileName = "defItems/" + args.substring(5);
+                               } else if (args.startsWith("conf ") && (lt.privs > 3)) {
+                                       strFileName = "conf/" + args.substring(5);
+                               } else if (args.startsWith("mob ")) {
+                                       strFileName = "defMobs/" + args.substring(4);
+                               } else if (args.startsWith("command ") && (lt.privs > 3)) {
+                                       strFileName = "commands/" + args.substring(8);
+                                       compile = true;
+                               } else if (args.startsWith("race ") && (lt.privs > 3)) {
+                                       strFileName = "defRaces/" + args.substring(5);
+                               } else if (args.startsWith("pet ") && (lt.privs > 3)) {
+                                       strFileName = "defPets/" + args.substring(4);
+                               } else if (args.startsWith("faction") && (lt.privs > 3)) {
+                                       return "You cannot submit faction files.";
+                               } else if (args.startsWith("condition ") && (lt.privs > 3)) {
+                                       strFileName = "defConditions/" + args.substring(10);
+                               } else if (args.startsWith("help ") && (lt.privs > 3)) {
+                                       strFileName = "helpFiles/" + args.substring(5);
+                               } else if (args.startsWith("script ") && (lt.privs > 3)) {
+                                       strFileName = "scripts/" + args.substring(7);
+                                       compile = true;
+                               } else if (args.startsWith("spell group ") && (lt.privs > 3)) {
+                                       strFileName = "defSpellGroups/" + args.substring(12);
+                                       compile = true;
+                               } else if (args.startsWith("spell ") && (lt.privs > 3)) {
+                                       strFileName = "defSpells/" + args.substring(6);
+                               } else if (args.startsWith("prop ")) {
+                                       strFileName = "defProps/" + args.substring(5);
+                               } else if (args.startsWith("move action ") && (lt.privs > 3)) {
+                                       strFileName = "defMoveActions/" + args.substring(12);
+                                       compile = true;
+                               } else if (args.startsWith("can move ") && (lt.privs > 3)) {
+                                       strFileName = "defCanMoveScripts/" + args.substring(9);
+                                       compile = true;
+                               } else if (args.startsWith("can see ") && (lt.privs > 3)) {
+                                       strFileName = "defCanSeeScripts/" + args.substring(8);
+                                       compile = true;
+                               } else if (args.startsWith("tile action ") && (lt.privs > 3)) {
+                                       strFileName = "defTileActions/" + args.substring(12);
+                                       compile = true;
+                               } else if (args.startsWith("tile move ") && (lt.privs > 3)) {
+                                       strFileName = "defTileScripts/" + args.substring(10);
+                                       compile = true;
+                               } else if (args.startsWith("tile see ") && (lt.privs > 3)) {
+                                       strFileName = "defTileSeeScripts/" + args.substring(9);
+                                       compile = true;
+                               } else if (args.startsWith("user ")) {
+                                       if (lt.privs < 5) {
+                                               return "You don't have enough privelages to submit a user's file.";
+                                       }
+                                       if (game.getPlayer(args.substring(5)) != null) {
+                                               return "You cannot submit a file for an active user.";
+                                       }
+                                       blnUser = true;
+                                       strFileName = "users/" + args.substring(5);
+                               } else if (args.startsWith("pet ")) {
+                                       if (lt.privs < 5) {
+                                               return "You don't have enough privelages to submit a user's pet file.";
+                                       }
+                                       if (game.getPlayer(args.substring(4)) != null) {
+                                               return "You cannot submit a pet file for an active user.";
+                                       }
+                                       blnPet = true;
+                                       strFileName = "pets/" + args.substring(4);
+                               }
+                               if (strFileName == null) {
+                                       return "Cannot submit " + args;
+                               }
+                               File filView = null;
+                               try {
+                                       filView = new File(strFileName);
+                               } catch (Exception e) {
+                                       return "Cannot submit " + args + " (" + strFileName + ")";
+                               }
+                               RandomAccessFile rafView = null;
+                               try {
+                                       if (blnUser) {
+                                               /*
+                                                Read in the user's password before deleting the file
+                                                */
+                                               rafView = new RandomAccessFile(filView, "r");
+                                               cmdline = rafView.readLine();
+                                       }
+                                       if (filView.exists()) {
+                                               filView.delete();
+                                       }
+                                       rafView = new RandomAccessFile(filView, "rw");
+                                       if (blnUser) {
+                                               /*
+                                                Write out the password for user files
+                                                */
+                                               rafView.writeBytes(cmdline + "\n");
+                                       }
+                                       /**
+                                        * FIXME: from message
+                                        */
+                                       /*
+                                        cmdline = lt.instream.readLine();
+                                        while (!cmdline.equals("--EOF--")) {
+                                        rafView.writeBytes(cmdline + "\n");
+                                        cmdline = lt.instream.readLine();
+                                        }*/
+                                       rafView.close();
+                                       if (compile) {
+                                               Script scrStore = new Script(filView.getPath(), game, true);
+                                               scrStore.close();
+                                       }
+                                       if (blnUser || blnPet) {
+                                               /*
+                                                Delete the .backup file for users and pets
+                                                */
+                                               filView = new File(strFileName + ".backup");
+                                               if (filView.exists()) {
+                                                       filView.delete();
+                                               }
+                                       }
+                               } catch (Exception e) {
+                                       game.log.printError("parseCommand():While submitting file " + args + " (" + filView.getName() + ")", e);
+                                       try {
+                                               rafView.close();
+                                       } catch (Exception e2) {
+                                               game.log.printError("parseCommand():While closing file " + args + " (" + filView.getName() + ") after failed submit", e);
+                                       }
+                                       return "Error while trying to submit " + args + " (" + filView.getName() + ").";
+                               }
+                               return "Submit of " + args + " was successful.";
+                       }
+                       case "delete": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "What do you want to delete?";
+                               }
+                               if (lt.privs < 4) {
+                                       return "You are not allowed to delete files.";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               if (args.indexOf("..") != -1) {
+                                       return "You don't have permission to access that file.";
+                               }
+                               String filename = null;
+                               String strReturn = null;
+                               if (args.startsWith("item ")) {
+                                       filename = "defItems/" + args.substring(5);
+                                       strReturn = "item " + args.substring(5);
+                               } else if (args.startsWith("conf ")) {
+                                       filename = "conf/" + args.substring(5);
+                                       strReturn = "conf " + args.substring(5);
+                               } else if (args.startsWith("mob ")) {
+                                       filename = "defMobs/" + args.substring(4);
+                                       strReturn = "mob " + args.substring(4);
+                               } else if (args.startsWith("command ")) {
+                                       filename = "commands/" + args.substring(8);
+                                       strReturn = "command " + args.substring(8);
+                               } else if (args.startsWith("race ")) {
+                                       filename = "defRaces/" + args.substring(5);
+                                       strReturn = "race " + args.substring(5);
+                               } else if (args.startsWith("pet ")) {
+                                       filename = "defPets/" + args.substring(4);
+                                       strReturn = "pet " + args.substring(4);
+                               } else if (args.startsWith("faction")) {
+                                       return "You cannot delete faction files.";
+                               } else if (args.startsWith("condition ")) {
+                                       filename = "defConditions/" + args.substring(8);
+                                       strReturn = "condition " + args.substring(8);
+                               } else if (args.startsWith("help ")) {
+                                       filename = "helpFiles/" + args.substring(5);
+                                       strReturn = "help " + args.substring(5);
+                               } else if (args.startsWith("script ")) {
+                                       filename = "scripts/" + args.substring(7);
+                                       strReturn = "script " + args.substring(7);
+                               } else if (args.startsWith("spell group ")) {
+                                       filename = "defSpellGroups/" + args.substring(12);
+                                       strReturn = "spell group " + args.substring(12);
+                               } else if (args.startsWith("spell ")) {
+                                       filename = "defSpells/" + args.substring(6);
+                                       strReturn = "spell " + args.substring(6);
+                               } else if (args.startsWith("prop ")) {
+                                       filename = "defProps/" + args.substring(5);
+                                       strReturn = "prop " + args.substring(5);
+                               } else if (args.startsWith("move action ")) {
+                                       filename = "defMoveActions/" + args.substring(12);
+                                       strReturn = "move action " + args.substring(12);
+                               } else if (args.startsWith("can move ")) {
+                                       filename = "defCanMoveScripts/" + args.substring(9);
+                                       strReturn = "can move " + args.substring(9);
+                               } else if (args.startsWith("can see ")) {
+                                       filename = "defCanSeeScripts/" + args.substring(8);
+                                       strReturn = "can see " + args.substring(8);
+                               } else if (args.startsWith("tile action ")) {
+                                       filename = "defTileActions/" + args.substring(12);
+                                       strReturn = "tile action " + args.substring(12);
+                               } else if (args.startsWith("tile move ")) {
+                                       filename = "defTileScripts/" + args.substring(10);
+                                       strReturn = "tile move " + args.substring(10);
+                               } else if (args.startsWith("tile see ")) {
+                                       filename = "defTileSeeScripts/" + args.substring(9);
+                                       strReturn = "tile see " + args.substring(9);
+                               }
+                               File filDelete = null;
+                               if (filename != null) {
+                                       filDelete = new File(filename);
+                                       if (filDelete.exists()) {
+                                               filDelete.delete();
+                                               filDelete = new File(filename + ".dsko");
+                                               if (filDelete.exists()) {
+                                                       filDelete.delete();
+                                                       strReturn += " and the associated .dsko file.";
+                                               }
+                                       } else {
+                                               return strReturn + " does not exist.";
+                                       }
+                                       return "Deleted " + strReturn;
+                               }
+                               return "You cannot delete that.";
+                       }
+                       case "clanleader": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+
+                               if (args == null) {
+                                       return "Clanleader who?";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               LivingThing thnStore = game.getPlayer(args.substring(0, args.indexOf(' ')));
+                               if (thnStore == null) {
+                                       return "They're not in this world";
+                               }
+                               if (args.length() < thnStore.name.length() + 2) {
+                                       return "What clan?";
+                               }
+                               if (thnStore.privs > 1) {
+                                       return "You can't clanleader them.";
+                               }
+                               args = args.substring(thnStore.name.length() + 1);
+                               thnStore.clan = args;
+                               if (args.equals("none")) {
+                                       thnStore.privs = 0;
+                                       thnStore.chatMessage("You are now clanless.");
+                               } else {
+                                       thnStore.privs = 1;
+                                       thnStore.chatMessage("You are now a member of the " + args + " clan.");
+                               }
+                               game.removeDuskObject(thnStore);
+                               game.addDuskObject(thnStore.map, thnStore);
+                               return thnStore.name + " is now a leader of the " + args + " clan.";
+                       }
+                       case "boot": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "Boot who?";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               LivingThing thnStore = game.getPlayer(args);
+                               if (thnStore == null) {
+                                       return "They're not in this world.";
+                               }
+                               if (thnStore.privs >= lt.privs) {
+                                       thnStore.chatMessage(lt.name + " attempted to boot you.");
+                                       return "You do not have high enough privelages to boot them.";
+                               }
+                               thnStore.chatMessage("You have been booted.");
+                               thnStore.close();
+                               return null;
+                       }
+                       case "hardkill": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "HardKill who?";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               LivingThing thnStore = game.getPlayer(args);
+                               if (thnStore == null) {
+                                       return "They're not in this world.";
+                               }
+                               if (thnStore.privs >= lt.privs) {
+                                       thnStore.chatMessage(lt.name + " attempted to HardKill you.");
+                                       return "You do not have high enough privelages to HardKill them.";
+                               }
+                               thnStore.closeNoMsgPlayer();
+                               return null;
+                       }
+                       case "nochannel": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "nochannel who for how long?";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               LivingThing thnStore;
+                               int duration;
+                               try {
+                                       thnStore = game.getPlayer(args.substring(0, args.indexOf(" ")));
+                                       duration = Integer.parseInt(args.substring(args.indexOf(" ") + 1));
+                               } catch (Exception e) {
+                                       game.log.printError("parseCommand():When " + lt.name + " tried to nochannel " + args, e);
+                                       return "nochannel who for how long?";
+                               }
+                               if (thnStore == null) {
+                                       return "They're not in this world.";
+                               }
+                               if (thnStore.privs >= lt.privs) {
+                                       thnStore.chatMessage(lt.name + " attempted to nochannel you.");
+                                       return "You do not have high enough privelages to nochannel them.";
+                               }
+                               if (duration > game.noChannelMax) {
+                                       duration = game.noChannelMax;
+                               }
+                               thnStore.chatMessage("You have been nochanneled for " + duration + " seconds.");
+                               thnStore.noChannel = duration;
+                               return "You have nochanneled " + thnStore.name + " for " + duration + " seconds";
+                       }
+                       case "allowchannel": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "allowchannel who?";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               LivingThing thnStore = game.getPlayer(args);
+                               if (thnStore == null) {
+                                       return "They're not in this world.";
+                               }
+                               thnStore.chatMessage("Your nochannel status has been removed.");
+                               thnStore.noChannel = 0;
+                               return thnStore.name + "'s nochannel status has been removed.";
+                       }
+                       case "gecho": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "G-Echo what?";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               game.chatMessage(args, "default");
+                               return null;
+                       }
+                       case "teleport": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "Teleport to where?";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               int index = args.lastIndexOf('_');
+                               char charSep = ' ';
+                               if (index != -1) {
+                                       charSep = '_';
+                               }
+                               try {
+                                       String[] params = args.split(" ");
+                                       TileMap map;
+                                       int destX, destY;
+                                       if (params.length == 3) {
+                                               map = game.maps.get(params[0]);
+                                               destX = Integer.parseInt(params[1]);
+                                               destY = Integer.parseInt(params[2]);
+                                       } else {
+                                               map = lt.map;
+                                               destX = Integer.parseInt(params[0]);
+                                               destY = Integer.parseInt(params[1]);
+                                       }
+                                       if (lt.privs < 5 && destX >= map.getCols()) {
+                                               destX = map.getCols() - 1;
+                                       }
+                                       if (lt.privs < 5 && destY >= map.getRows()) {
+                                               destY = map.getRows() - 1;
+                                       }
+                                       if (destX < 0) {
+                                               destX = 0;
+                                       }
+                                       if (destY < 0) {
+                                               destY = 0;
+                                       }
+                                       lt.changeLocBypass(map, destX, destY);
+                               } catch (Exception e) {
+                                       LivingThing thnStore = game.getPlayer(args);
+                                       if (thnStore == null) {
+                                               return "Teleport to where?";
+                                       } else {
+                                               // Hmm, i'm not sure what this does, teleport to another player?
+                                               // Whats with the weird range checking?
+                                               int destX = thnStore.x;
+                                               int destY = thnStore.y;
+                                               if (lt.privs < 5 && destX > 349) {
+                                                       destX = 349;
+                                               }
+                                               if (lt.privs < 5 && destY > 349) {
+                                                       destY = 349;
+                                               }
+                                               if (destX < 0) {
+                                                       destX = 0;
+                                               }
+                                               if (destY < 0) {
+                                                       destY = 0;
+                                               }
+                                               lt.changeLocBypass(thnStore.map, destX, destY);
+                                       }
+                               }
+                               return null;
+                       }
+                       case "remove": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "remove what?";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               DuskObject objStore = lt.getLocalObject(args);
+                               if (objStore != null) {
+                                       if (objStore.isLivingThing()) {
+                                               LivingThing thnStore = (LivingThing) objStore;
+                                               if (thnStore.isMob()) {
+                                                       thnStore.close();
+                                                       game.blnMobListChanged = true;
+                                                       return "Object removed.";
+                                               } else {
+                                                       return "You can't remove players/pets.";
+                                               }
+                                       } else if (objStore.isItem()) {
+                                               //game.vctItems.remove(objStore);
+                                               game.removeDuskObject(objStore);
+                                               return "Object removed.";
+                                       } else if (objStore.isSign()) {
+                                               //game.vctSigns.remove(objStore);
+                                               //game.blnSignListChanged = true;
+                                               game.removeDuskObject(objStore);
+                                               return "Object removed.";
+                                       } else if (objStore.isProp()) {
+                                               //game.vctProps.removeElement(objStore);
+                                               //game.blnPropListChanged = true;
+                                               game.removeDuskObject(objStore);
+                                               return "Object removed.";
+                                       } else if (objStore.isMerchant()) {
+                                               //game.vctMerchants.remove(objStore);
+                                               //game.blnMerchantListChanged = true;
+                                               game.removeDuskObject(objStore);
+                                               return "Object removed.";
+                                       }
+                               }
+                               return "You don't see that here.";
+                       }
+                       case "changeclan": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "ChangeClan who?";
+                               }
+                               LivingThing thnStore = game.getPlayer(args.substring(0, args.indexOf(' ')));
+                               if (thnStore == null) {
+                                       return "They're not in this world";
+                               }
+                               if (cmdline.length() < thnStore.name.length() + 2) {
+                                       return "What clan?";
+                               }
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                               args = args.substring(thnStore.name.length() + 1);
+                               thnStore.clan = args;
+                               if (thnStore.privs == 1) {
+                                       thnStore.privs = 0;
+                               }
+                               if (args.equals("none")) {
+                                       thnStore.chatMessage("You are now a member of no clan.");
+                               } else {
+                                       thnStore.chatMessage("You are now a member of the " + args + " clan.");
+                               }
+                               return thnStore.name + " has been added to the " + args + " clan";
+                       }
+                       case "madd": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "Madd what?";
+                               }
+                               Merchant mrcStore = lt.overMerchant();
+                               if (mrcStore != null) {
+                                       game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                                       mrcStore.items.add(args);
+                                       game.refreshEntities(lt);
+                               } else {
+                                       if (lt.overPlayerMerchant() != null) {
+                                               return "You cannot add items to a player's merchant this way.";
+                                       }
+                                       return "You are not on a merchant.";
+                               }
+                               return null;
+                       }
+                       case "mremove": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               if (args == null) {
+                                       return "Mremove what?";
+                               }
+                               Merchant mrcStore = lt.overMerchant();
+                               if (mrcStore != null) {
+                                       game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+                                       mrcStore.items.remove(args);
+                                       game.refreshEntities(lt);
+                               } else {
+                                       if (lt.overPlayerMerchant() != null) {
+                                               return "You cannot remove items from a player's merchant this way.";
+                                       }
+                                       return "You are not on a merchant.";
+                               }
+                               return null;
+                       }
+                       case "whoip": {
+                               if (lt.privs <= 2)
+                                       return "Huh?";
+                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":" + cmdline + ":" + lt.x + "," + lt.y);
+
+                               int nPlayers = game.playersByName.size();
+                               StringBuilder sb = new StringBuilder();
+
+                               // TODO: this used to be atomic, does it need to be?
+
+                               for (LivingThing thnStore : game.playersByName.values()) {
+                                       boolean hidden = false;
+                                       if (thnStore.privs > 2) {
+                                               if (thnStore.hasCondition("invis")) {
+                                                       hidden = true;
+                                               }
+                                       }
+                                       if (hidden && (lt.privs < thnStore.privs)) {
+                                               nPlayers--;
+                                       }
+                               }
+
+                               lt.chatMessage("\tThere are " + nPlayers + " players online:");
+
+                               for (LivingThing thnStore : game.playersByName.values()) {
+                                       boolean hidden = false;
+                                       boolean skip = false;
+                                       if (thnStore.privs > 2) {
+                                               if (thnStore.hasCondition("invis")) {
+                                                       hidden = true;
+                                               }
+                                       }
+                                       if (hidden && (lt.privs < thnStore.privs)) {
+                                               skip = true;
+                                       }
+                                       if (!skip) {
+                                               String ip = thnStore.getAddress().toString();
+                                               while (ip.length() < 34) {
+                                                       ip += " ";
+                                               }
+                                               sb.setLength(0);
+                                               sb.append("   ").append(ip);
+                                               sb.append(thnStore.getCharacterPoints()).append("cp ");
+                                               if (thnStore.privs == 1) {
+                                                       sb.append("{Clan Leader}");
+                                               } else if (thnStore.privs == 3) {
+                                                       sb.append("{God}");
+                                               } else if (thnStore.privs == 4) {
+                                                       sb.append("{High God}");
+                                               } else if (thnStore.privs > 4) {
+                                                       sb.append("{Master God}");
+                                               }
+                                               if (hidden) {
+                                                       sb.append("{hidden}");
+                                               }
+                                               if (thnStore.noChannel != 0) {
+                                                       sb.append("{nochanneled}");
+                                               }
+                                               if (!thnStore.clan.equals("none")) {
+                                                       sb.append("<" + thnStore.clan + "> ");
+                                               }
+                                               sb.append(thnStore.name + "\n");
+                                               lt.chatMessage(sb.toString());
+                                       }
+                               }
+
+                               return null;
+                       }
+                       case "change": {
+                               if (lt.battle != null) {
+                                       return "Not while you're fighting!";
+                               }
+                               if (args == null) {
+                                       return "Change what?";
+                               }
+
+                               if (true)
+                                       return "Race changing unimplemented";
+
+                               if (args.equals("race")) {
+                                       if (lt.getCharacterPoints() > game.changeRaceCpLimit) {
+                                               return "You can no longer change your race.";
+                                       }
+                                       lt.unloadRace();
+
+                                       // FIXME: I'm not sure why this needs to clear messages here.
+/*
+                                        if (lt.isPet()) {
+
+                                        lt.getMaster().halt();
+                                        //                                     lt.getMaster().stillThere();  // This puts something in the buffer
+                                        //                                     lt.getMaster().thrConnection.sleep(750);  // wait for it...
+                                        try {
+                                        // Empty out the BufferedReader for the answer
+                                        //     while (lt.getMaster().instream.ready()) {
+                                        //             lt.getMaster().instream.readLine();
+                                        //     }
+                                        } catch (Exception e) {
+                                        game.log.printError("parseCommand():Trying to empty ready buffer of pet's master for change race.", e);
+                                        }
+                                        } else {
+                                        lt.halt();
+                                        //                                     lt.stillThere();  // This puts something in the buffer
+                                        //                                     lt.thrConnection.sleep(750);  // wait for it...
+                                        try {
+                                        // Empty out the BufferedReader for the answer
+                                        //     while (lt.instream.ready()) {
+                                        //             lt.instream.readLine();
+                                        //     }
+                                        } catch (Exception e) {
+                                        game.log.printError("parseCommand():Trying to empty ready buffer of player for change race.", e);
+                                        }
+                                        }
+                                        lt.loadRace();
+                                        if (lt.isPet()) {
+                                        lt.getMaster().proceed();
+                                        lt.getMaster().updateStats();
+                                        } else {
+                                        lt.proceed();
+                                        }
+                                        */
+                                       game.removeDuskObject(lt);
+                                       game.addDuskObject(lt.map, lt);
+                                       lt.updateStats();
+                                       return "Your race has been changed.";
+                               }
+                       }
+                               
+                               I am here!
+                               
+                       case "clan": {
+                               if (!lt.isPlayer() && !lt.isMob()) {
+                                       return "Only players can use the gossip/clan/tell channels.";
+                               }
+                               if (lt.clan.equals("none")) {
+                                       return "You're not in a clan. Use gossip instead";
+                               }
+                               if (lt.noChannel != 0) {
+                                       return "You can't do that when nochanneled.";
+                               }
+                               if (args == null) {
+                                       return "Clan what?";
+                               }
+                               if (args.length() > game.messagecap) {
+                                       return "That message was too long.";
+                               }
+                               if (!args.equals("")) {
+                                       long lTemp = lt.lastMessageStamp;
+                                       lt.lastMessageStamp = System.currentTimeMillis();
+                                       if ((System.currentTimeMillis() - lTemp) < game.floodLimit) {
+                                               return "No flooding.";
+                                       }
+                                       game.chatMessage(lt.name + " clans: " + args, lt.clan, lt.name);
+                               }
+                               return null;
+                       }
+
+                       case "emote": {
+                               if (args == null) {
+                                       return "Emote what?";
+                               }
+                               if (lt.noChannel != 0) {
+                                       return "You can't do that when nochanneled.";
+                               }
+                               if (args.length() > game.messagecap) {
+                                       return "That message was too long.";
+                               }
+                               if (!args.equals("")) {
+                                       long lTemp = lt.lastMessageStamp;
+                                       lt.lastMessageStamp = System.currentTimeMillis();
+                                       if ((System.currentTimeMillis() - lTemp) < game.floodLimit) {
+                                               return "No flooding.";
+                                       }
+                                       String strPerson = lt.name;
+                                       if (lt.privs > 2
+                                                       && lt.hasCondition("invis")
+                                                       && lt.hasCondition("invis2")) {
+                                               strPerson = "A god";
+                                       }
+                                       game.chatMessage(lt.map, strPerson + " " + args, lt.x, lt.y, lt.name);
+                               }
+                               return null;
+                       }
+                       case "tell": {
+                               if (!lt.isPlayer()) {
+                                       return "Only players can use the gossip/clan/tell channels.";
+                               }
+                               if (args == null) {
+                                       return "Tell who what?";
+                               }
+                               if (lt.noChannel != 0) {
+                                       return "You can't do that when nochanneled.";
+                               }
+                               StringTokenizer tknStore = new StringTokenizer(args, " ");
+                               String strStore2 = null;
+                               try {
+                                       strStore2 = tknStore.nextToken();
+                               } catch (Exception e) {
+                                       return "Tell who?";
+                               }
+                               LivingThing thnStore = game.getPlayer(strStore2);
+                               if (thnStore == null) {
+                                       return "They're not in this world.";
+                               }
+                               if (thnStore.privs > 2
+                                               && thnStore.hasCondition("invis")
+                                               && thnStore.hasCondition("invis2")) {
+                                       return "They're not in this world.";
+                               }
+                               if (thnStore.name.equalsIgnoreCase(lt.name)) {
+                                       return "Talking to yourself is not a good sign.";
+                               }
+                               String strIgnoreName = thnStore.name.toLowerCase();
+                               if (lt.ignoreList.contains(strIgnoreName)) {
+                                       return "You can't do that while you are ignoring them.";
+                               }
+                               strIgnoreName = lt.name.toLowerCase();
+                               if (thnStore.ignoreList.contains(strIgnoreName)) {
+                                       return "They did not get the message, they are ignoring you.";
+                               }
+                               args = args.substring(strStore2.length(), args.length()).trim();
+                               if (args.length() > game.messagecap) {
+                                       return "That message was too long.";
+                               }
+                               if (args.length() == 0) {
+                                       return "Tell them what?";
+                               }
+                               long lTemp = lt.lastMessageStamp;
+                               lt.lastMessageStamp = System.currentTimeMillis();
+                               if ((System.currentTimeMillis() - lTemp) < game.floodLimit) {
+                                       return "No flooding.";
+                               }
+                               String strPerson = lt.name;
+                               if (lt.privs > 2
+                                               && lt.hasCondition("invis")
+                                               && lt.hasCondition("invis2")) {
+                                       strPerson = "A god";
+                               }
+                               game.log.printMessage(Log.ALWAYS, lt.name + " tells " + thnStore.name + " : " + args);
+                               thnStore.chatMessage(strPerson + " tells you: " + args);
+                               return "You tell " + strStore2 + ": " + args;
+                       }
+                       case "who": {
+                               int nPlayers = game.playersByName.size();
+                               StringBuilder sb = new StringBuilder();
+
+                               // TOOD: originally this was atomic on stream
+                               // although 'atomic' is wrong since nobody else was atomic on stream
+
+                               for (LivingThing thnStore : game.playersByName.values()) {
+                                       boolean hidden = false;
+                                       if (thnStore.privs > 2) {
+                                               if (thnStore.hasCondition("invis")) {
+                                                       hidden = true;
+                                               }
+                                       }
+                                       if (hidden && (lt.privs < thnStore.privs)) {
+                                               nPlayers--;
+                                       }
+                                       if (lt.privs < 3 && !thnStore.isWorking) {
+                                               nPlayers--;
+                                       }
+                                       if (lt.privs < 3 && !thnStore.isReady) {
+                                               nPlayers--;
+                                       }
+                               }
+
+                               lt.chatMessage("\tThere are " + nPlayers + " players online:");
+
+                               for (LivingThing thnStore : game.playersByName.values()) {
+                                       boolean hidden = false;
+                                       boolean skip = false;
+                                       if (thnStore.privs > 2) {
+                                               if (thnStore.hasCondition("invis")) {
+                                                       hidden = true;
+                                               }
+                                       }
+                                       if (hidden && (lt.privs < thnStore.privs)) {
+                                               skip = true;
+                                       }
+                                       if (lt.privs < 3 && !thnStore.isWorking) {
+                                               skip = true;
+                                       }
+                                       if (lt.privs < 3 && !thnStore.isReady) {
+                                               skip = true;
+                                       }
+                                       System.out.println(" user " + thnStore.name + " skip " + skip);
+                                       if (!skip) {
+                                               sb.setLength(0);
+                                               sb.append("\t");
+                                               sb.append(thnStore.getCharacterPoints());
+                                               sb.append("cp ");
+                                               if (lt.privs > 2 && !thnStore.isWorking) {
+                                                       sb.append("{* Not Working *}");
+                                               }
+                                               if (lt.privs > 2 && !thnStore.isReady) {
+                                                       sb.append("{Entering the world}");
+                                               }
+                                               if (lt.privs > 2 && !thnStore.isSaveable) {
+                                                       sb.append("{Loading/Saving}");
+                                               }
+                                               if (thnStore.privs == 1) {
+                                                       sb.append("{Clan Leader}");
+                                               } else if (thnStore.privs == 3) {
+                                                       sb.append("{God}");
+                                               } else if (thnStore.privs == 4) {
+                                                       sb.append("{High God}");
+                                               } else if (thnStore.privs > 4) {
+                                                       sb.append("{Master God}");
+                                               }
+                                               if (hidden) {
+                                                       sb.append("{hidden}");
+                                               }
+                                               if (thnStore.noChannel != 0) {
+                                                       sb.append("{nochanneled}");
+                                               }
+                                               if (thnStore.ignoreList.contains(lt.name.toLowerCase())) {
+                                                       sb.append("{Ignoring you}");
+                                               }
+                                               if (lt.ignoreList.contains(thnStore.name.toLowerCase())) {
+                                                       sb.append("{Ignored}");
+                                               }
+                                               if (!thnStore.clan.equals("none")) {
+                                                       sb.append("<");
+                                                       sb.append(thnStore.clan);
+                                                       sb.append("> ");
+                                               }
+                                               if (thnStore.title == null
+                                                               || thnStore.title.equals("none")) {
+                                                       sb.append(thnStore.name);
+                                                       //sb.append("\n");
+                                               } else {
+                                                       sb.append(thnStore.name);
+                                                       sb.append(" ");
+                                                       sb.append(thnStore.title);
+                                                       //sb.append("\n");
+                                               }
+                                               lt.chatMessage(sb.toString());
+                                       }
+                               }
+                               return "Who complete.";
+                       }
+                               //kill
+                       case "order": {
+                               if (args == null) {
+                                       return "Order who to do what?";
+                               }
+                               int intStore = args.indexOf(" ");
+                               if (intStore == -1) {
+                                       return "Order them to do what?";
+                               }
+                               DuskObject objStore = lt.getLocalObject(args.substring(0, intStore));
+                               if (objStore == null) {
+                                       return "You don't see that here.";
+                               }
+                               if (!objStore.isLivingThing()) {
+                                       return "You can't order that.";
+                               }
+                               LivingThing thnStore = (LivingThing) objStore;
+                               if (thnStore.getCharmer() != lt) {
+                                       return "They don't take orders from you.";
+                               }
+                               args = args.substring(intStore + 1);
+                               try {
+                                       thnStore.chatMessage(Commands.parseCommand(thnStore, game, args));
+//                             lt.chatMessage(Commands.parseCommand(thnStore, engGame, strArgs));
+                               } catch (Exception e) {
+                                       game.log.printError("parseCommand():" + thnStore.name + ", while trying to follow the following order: \"" + args + "\"", e);
+                               }
+                               return null;
+                       }
+
+                       case "inv":
+                       case "inventory": {
+                               final String[] formats = {
+                                       "Wielded: %s",
+                                       "Arms: %s",
+                                       "Legs: %s",
+                                       "Torso: %s",
+                                       "Waist: %s",
+                                       "Neck: %s",
+                                       "Skull: %s",
+                                       "Eyes: %s",
+                                       "Hands: %s"};
+                               lt.chatMessage("-Worn-");
+                               for (int i = 0; i < formats.length; i++) {
+                                       Item item = lt.wornItems.getWorn(i);
+                                       if (item != null)
+                                               lt.chatMessage(String.format(formats[i], item.description));
+                               }
+                               lt.chatMessage("-Inventory-:");
+                               for (LinkedList<Item> list : lt.itemList.values()) {
+                                       if (!list.isEmpty()) {
+                                               Item item = (Item) list.element();
+                                               lt.chatMessage(list.size() + " " + item.name);
+                                       }
+                               }
+                               return null;
+                       }
+
+                       case "help": {
+                               File file;
+                               String title;
+
+                               // FIXME: was atomic
+                               if (args == null) {
+                                       file = new File("help");
+                                       title = "Help";
+                               } else {
+                                       if (args.indexOf("..") != -1) {
+                                               return "There is no help on that subject";
+                                       }
+                                       file = new File("helpFiles/" + args);
+                                       title = "Help on " + args;
+                               }
+
+                               try (RandomAccessFile helpFile = new RandomAccessFile(file, "r")) {
+                                       lt.chatMessage(title);
+                                       while ((cmdline = helpFile.readLine()) != null) {
+                                               lt.chatMessage(cmdline);
+                                       }
+                               } catch (IOException e) {
+                                       game.log.printError("parseCommand():When " + lt.name + " tried to get help on " + args, e);
+                                       return "There is no help on that subject";
+                               }
+                               return null;
+                       }
+
+                       case "get": {
+                               if (args == null) {
+                                       return "Get what?";
+                               }
+                               DuskObject objStore = lt.getLocalObject(args);
+                               if (objStore == null) {
+                                       return "You don't see that here.";
+                               }
+                               if (!objStore.isItem()) {
+                                       return "You can't get that.";
+                               }
+                               Item itmStore = (Item) objStore;
+                               if (Math.abs(lt.x - itmStore.x) + Math.abs(lt.y - itmStore.y) < 2) {
+                                       if (lt.privs > 2) {
+                                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":gets " + args + ":" + lt.x + "," + lt.y);
+                                       }
+                                       lt.itemList.addElement(itmStore);
+                                       lt.updateItems();
+                                       game.removeDuskObject(itmStore);
+                               } else {
+                                       return "That's too far away.";
+                               }
+                               itmStore.onGetItem(game, lt);
+                               return null;
+                       }
+
+                       case "drop": {
+                               if (args == null) {
+                                       return "Drop what?";
+                               }
+                               int intDot = args.indexOf(".");
+                               int intNumToDrop = 1;
+                               if (intDot != -1) {
+                                       try {
+                                               intNumToDrop = Integer.parseInt(args.substring(0, intDot));
+                                       } catch (NumberFormatException e) {
+                                               intNumToDrop = 1;
+                                       }
+                               }
+                               Item itmStore = lt.getItem(args);
+                               if (itmStore != null) {
+                                       String strMessage = "You drop " + itmStore.name + ".";
+                                       if (intNumToDrop > 1) {
+                                               strMessage = "You drop " + intNumToDrop + " " + itmStore.name + ".";
+                                       }
+                                       if (itmStore.intCost == 0) {
+                                               strMessage = "A " + itmStore.name + " vanishes into thin air.";
+                                               if (intNumToDrop > 1) {
+                                                       strMessage = intNumToDrop + " " + itmStore.name + " vanish into thin air.";
+                                               }
+                                       }
+                                       if (lt.privs > 2) {
+                                               game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":drops " + args + ":" + lt.x + "," + lt.y);
+                                       }
+                                       for (int i = 0; i < intNumToDrop; i++) {
+                                               itmStore = lt.getItemAndRemove(itmStore.name);
+                                               itmStore.x = lt.x;
+                                               itmStore.y = lt.y;
+                                               if (itmStore.intCost != 0) {
+                                                       //game.vctItems.add(itmStore);
+                                                       game.addDuskObject(lt.map, itmStore);
+                                                       lt.updateItems();
+                                               }
+                                               itmStore.onDropItem(game, lt);
+                                       }
+                                       return strMessage;
+                               }
+                               return "You don't have that.";
+                       }
+
+                       case "use": {
+                               if (args == null) {
+                                       return "Use what?";
+                               }
+                               if (lt.battle == null) {
+                                       lt.useItem(args, -1);
+                               } else {
+                                       lt.battle.addCommand(lt, "use " + args);
+                               }
+                               return null;
+                       }
+                       case "eat": {
+                               if (args == null) {
+                                       return "Eat what?";
+                               }
+                               if (lt.battle == null) {
+                                       lt.useItem(args, Item.FOOD);
+                               } else {
+                                       lt.battle.addCommand(lt, "eat " + args);
+                               }
+                               return null;
+                       }
+                       case "drink": {
+                               if (args == null) {
+                                       return "Drink what?";
+                               }
+                               if (lt.battle == null) {
+                                       lt.useItem(args, Item.DRINK);
+                               } else {
+                                       lt.battle.addCommand(lt, "drink " + args);
+                               }
+                               return null;
+                       }
+
+                       case "give": {
+                               if (args == null) {
+                                       return "Give who what?";
+                               }
+                               StringTokenizer tknStore = new StringTokenizer(args, " ");
+                               String strStore2 = null;
+                               try {
+                                       strStore2 = tknStore.nextToken();
+                               } catch (Exception e) {
+                                       return "Give who what?";
+                               }
+                               DuskObject objStore = lt.getLocalObject(strStore2);
+                               if (objStore == null) {
+                                       return "You don't see them here.";
+                               }
+                               if (!objStore.isLivingThing()) {
+                                       return "You can't give to that.";
+                               }
+                               LivingThing thnStore = (LivingThing) objStore;
+                               if ((lt.privs < 3) && (Math.abs(thnStore.x - lt.x) + Math.abs(thnStore.y - lt.y) > 1)) {
+                                       return "They're too far away.";
+                               }
+                               args = args.substring(strStore2.length() + 1);
+                               if (lt.privs > 2) {
+                                       game.log.printMessage(Log.INFO, "godcommand:" + lt.name + ":gives " + args + " to " + strStore2 + ":" + lt.x + "," + lt.y);
+                               }
+                               if (args.startsWith("gp")) {
+                                       args = args.substring(3);
+                                       try {
+                                               int intStore = Integer.parseInt(args);
+                                               if (intStore < 0) {
+                                                       return "You can't give negative money!";
+                                               }
+                                               if (intStore <= lt.cash) {
+                                                       lt.cash -= intStore;
+                                                       thnStore.cash += intStore;
+                                                       lt.updateStats();
+                                                       thnStore.updateStats();
+                                                       thnStore.chatMessage(lt.name + " gives you " + intStore + "gp.");
+                                                       return "You give " + thnStore.name + " " + intStore + "gp.";
+                                               } else {
+                                                       lt.chatMessage("You don't have that much gp");
+                                               }
+                                       } catch (Exception e) {
+                                               return "That is not a valid amount of gp to give.";
+                                       }
+                               }
+                               int intDot = args.indexOf(".");
+                               int intNumToGive = 1;
+                               if (intDot != -1) {
+                                       try {
+                                               intNumToGive = Integer.parseInt(args.substring(0, intDot));
+                                       } catch (NumberFormatException e) {
+                                               intNumToGive = 1;
+                                       }
+                               }
+                               Item itmStore = lt.getItem(args);
+                               if (itmStore != null) {
+                                       String strMessage = lt.name + " gives you ";
+                                       String strMessage2 = "You give " + thnStore.name + " ";
+                                       if (intNumToGive > 1) {
+                                               strMessage += intNumToGive + " ";
+                                               strMessage2 += intNumToGive + " ";
+                                       }
+                                       strMessage += itmStore.name + ".";
+                                       strMessage2 += itmStore.name + ".";
+                                       cmdline = itmStore.name;
+
+                                       while (intNumToGive > 0) {
+                                               itmStore = lt.getItemAndRemove(cmdline);
+                                               thnStore.itemList.addElement(itmStore);
+                                               intNumToGive--;
+
+                                               itmStore.onDropItem(game, lt);
+                                               itmStore.onGetItem(game, thnStore);
+                                       }
+
+                                       thnStore.chatMessage(strMessage);
+                                       thnStore.updateItems();
+                                       lt.updateItems();
+                                       return strMessage2;
+                               }
+                               return "You don't have that.";
+                       }
+
+                       case "buy": {
+                               if (args == null) {
+                                       return "Buy what?";
+                               }
+                               int quantity;
+                               try {
+                                       int i = args.indexOf(" ");
+                                       quantity = Integer.parseInt(args.substring(0, i));
+                                       args = args.substring(i + 1);
+                               } catch (Exception e) {
+                                       return "How many of what do you want to buy?";
+                               }
+                               if (quantity > 100) {
+                                       quantity = 100;
+                               } else if (quantity < 1) {
+                                       return "You can't buy less than one of something.";
+                               }
+                               PlayerMerchant pmrStore = lt.overPlayerMerchant();
+                               if (pmrStore != null) {
+                                       long numItem = pmrStore.contains(args);
+                                       if (numItem > 0) {
+                                               Item itmStore = game.getItem(args);
+                                               if (itmStore != null) {
+                                                       if (quantity > numItem) {
+                                                               return "This merchant does not have that many.";
+                                                       }
+                                                       int intCost = (itmStore.intCost * 3) / 4;
+                                                       if (lt.name.equalsIgnoreCase(pmrStore.strOwner)) {
+                                                               intCost = 0;
+                                                       }
+                                                       if (intCost * quantity > lt.cash) {
+                                                               return "You can't afford that";
+                                                       } else {
+                                                               lt.cash -= intCost * quantity;
+                                                               pmrStore.cash += intCost * quantity;
+                                                               itmStore = pmrStore.remove(args);
+                                                               lt.itemList.addElement(itmStore);
+                                                               for (int i = 1; i < quantity; i++) {
+                                                                       itmStore = pmrStore.remove(args);
+                                                                       lt.itemList.addElement(itmStore);
+                                                               }
+                                                               lt.updateItems();
+                                                               lt.updateStats();
+                                                       }
+                                               }
+                                       }
+                               }
+
+                               Merchant mrcStore = lt.overMerchant();
+                               if (mrcStore == null) {
+                                       return "Buy from whom?";
+                               }
+                               if (lt.getFollowing() != null && lt.getFollowing().isPet()) {
+                                       if (args.startsWith(lt.getFollowing().name + ":")) {
+                                               args = args.substring(lt.getFollowing().name.length() + 1);
+                                               if (mrcStore.contains(args)) {
+                                                       if (args.startsWith("train:")) {
+                                                               args = args.substring(6);
+                                                               mrcStore.train(args, quantity, lt.getFollowing());
+                                                               lt.updateStats();
+                                                       }
+                                               }
+                                               return null;
+                                       }
+                               }
+                               if (mrcStore.contains(args)) {
+                                       if (args.startsWith("train:")) {
+                                               args = args.substring(6);
+                                               mrcStore.train(args, quantity, lt);
+                                               lt.updateStats();
+                                       } else {
+                                               if (args.startsWith("pet")) {
+                                                       mrcStore.pet(lt);
+                                                       lt.updateStats();
+                                               } else {
+                                                       Item itmStore = game.getItem(args);
+                                                       if (itmStore != null) {
+                                                               if (itmStore.intCost * quantity > lt.cash) {
+                                                                       return "You can't afford that";
+                                                               } else {
+                                                                       lt.cash -= itmStore.intCost * quantity;
+                                                                       lt.itemList.addElement(itmStore);
+                                                                       for (int i = 1; i < quantity; i++) {
+                                                                               lt.itemList.addElement(game.getItem(args));
+                                                                       }
+                                                                       lt.updateItems();
+                                                                       lt.updateStats();
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                               return null;
+                       }
+
+                       case "sell": {
+                               if (args == null) {
+                                       return "Sell what?";
+                               }
+
+                               PlayerMerchant pmrStore = lt.overPlayerMerchant();
+                               if (pmrStore != null) {
+                                       if (lt.name.equalsIgnoreCase(pmrStore.strOwner)) {
+                                               int quantity = 1;
+                                               try {
+                                                       int i = args.indexOf(" ");
+                                                       quantity = Integer.parseInt(args.substring(0, i));
+                                                       args = args.substring(i + 1);
+                                               } catch (Exception e) {
+                                                       return "How many of what do you want to sell?";
+                                               }
+                                               Item itmStore = lt.getItem(args);
+                                               for (int i = 0; i < quantity; i++) {
+                                                       itmStore = lt.getItemAndRemove(args);
+                                                       if (itmStore != null) {
+                                                               itmStore.onDropItem(game, lt);
+                                                               pmrStore.add(itmStore);
+                                                               lt.isSaveNeeded = true;
+                                                       } else {
+                                                               i = quantity;
+                                                       }
+                                               }
+                                               lt.updateItems();
+                                               lt.updateStats();
+                                               return null;
+                                       }
+                                       return "You cannot sell items to this merchant.";
+                               }
+
+                               Merchant mrcStore = lt.overMerchant();
+                               if (mrcStore == null) {
+                                       return "Sell that to whom?";
+                               }
+                               int quantity = 1;
+                               try {
+                                       int i = args.indexOf(" ");
+                                       quantity = Integer.parseInt(args.substring(0, i));
+                                       args = args.substring(i + 1);
+                               } catch (Exception e) {
+                                       return "How many of what do you want to sell?";
+                               }
+                               if (quantity > 100) {
+                                       quantity = 100;
+                               }
+                               Item itmStore = lt.getItem(args);
+                               for (int i = 0; i < quantity; i++) {
+                                       itmStore = lt.getItemAndRemove(args);
+                                       if (itmStore != null) {
+                                               itmStore.onDropItem(game, lt);
+                                               lt.cash += (itmStore.intCost / 2);
+                                               lt.isSaveNeeded = true;
+                                       } else {
+                                               i = quantity;
+                                       }
+                               }
+                               lt.updateItems();
+                               lt.updateStats();
+                               return null;
+                       }
+
+                       case "cast": {
+                               if (args == null) {
+                                       return "Cast what?";
+                               }
+                               if (lt.battle == null) {
+                                       lt.castSpell(args);
+                               } else {
+                                       lt.battle.addCommand(lt, "cast " + args);;
+                               }
+                               return null;
+                       }
+
+                       case "follow": {
+                               if (args == null) {
+                                       return "Follow who?";
+                               }
+                               if (lt.isSleeping) {
+                                       return "You can't do that while you're sleeping";
+                               }
+                               DuskObject objStore = lt.getLocalObject(args);
+                               if (objStore == null) {
+                                       return "You don't see that here.";
+                               }
+                               if (objStore.isLivingThing()) {
+                                       LivingThing thnStore = (LivingThing) objStore;
+                                       if (lt.getMaster() != null && thnStore != lt.getMaster()) {
+                                               if (lt.isPet()) {
+                                                       return "You can only follow your owner.";
+                                               }
+                                               return "You're already following someone. Leave them first.";
+                                       }
+                                       if (Math.abs(lt.x - thnStore.x) + Math.abs(lt.y - thnStore.y) > 1) {
+                                               return "They're too far away.";
+                                       }
+                                       if (thnStore == lt) {
+                                               return "You can't follow yourself.";
+                                       }
+                                       if (!thnStore.isPlayer() && !lt.isMob()) {
+                                               return "You can only follow players.";
+                                       }
+                                       if (thnStore.noFollow || (thnStore.isPet() && thnStore.getMaster().noFollow)) {
+                                               return "They won't let you follow them.";
+                                       }
+                                       if (lt.isPet()) {
+                                               thnStore.setFollowing(lt);
+                                               lt.setMaster(thnStore);
+                                               thnStore.updateStats();
+                                               lt.updateStats();
+                                               return "You are now following " + lt.getMaster().name + ".";
+                                       }
+                                       LivingThing thnStore2 = thnStore;
+                                       while (thnStore2 != null) {
+                                               if (lt == thnStore2) {
+                                                       return "You're already in that group.";
+                                               }
+                                               thnStore2 = thnStore2.getMaster();
+                                       }
+                                       thnStore.chatMessage("You are now being followed by " + lt.name + ".");
+                                       while (thnStore.getFollowing() != null) {
+                                               thnStore = thnStore.getFollowing();
+                                               if (thnStore.isPlayer()) {
+                                                       thnStore.chatMessage("You are now being followed by " + lt.name + ".");
+                                               }
+                                       }
+                                       thnStore.setFollowing(lt);
+                                       lt.setMaster(thnStore);
+                                       thnStore.updateStats();
+                                       lt.updateStats();
+                                       return "You are now following " + lt.getMaster().name + ".";
+                               }
+                               return "That's not something you can follow.";
+                       }
+
+                       case "unfollow": {
+                               if (args == null) {
+                                       return "Unfollow who?";
+                               }
+
+                               // FIXME: implemente unfollow
+                               if (true)
+                                       return "unfollow is not yet implemented";
+
+
+                               LivingThing thnStore = lt.getFollowing();
+                               if (thnStore != null && thnStore.isPet()) {
+                                       if (thnStore.name.equalsIgnoreCase(args)) {
+                                               /*
+                                                lt.halt();
+                                                lt.chatMessage("Do you really want to permanently erase your pet?");
+                                                try {
+                                                if (lt.instream.readLine().equalsIgnoreCase("yes")) {
+                                                lt.getFollowing().close();
+                                                File deleteme = new File("pets/" + lt.name.toLowerCase());
+                                                deleteme.delete();
+                                                deleteme = new File("pets/" + lt.name.toLowerCase() + ".backup");
+                                                deleteme.delete();
+                                                lt.getFollowing().close();
+                                                lt.setFollowing(lt.getFollowing().getFollowing());
+                                                lt.proceed();
+                                                return "Your pet has been erased.";
+                                                }
+                                                } catch (Exception e) {
+                                                game.log.printError("parseCommand():While unfollowing pet for " + lt.name, e);
+                                                }
+                                                lt.proceed();
+                                                * */
+                                               return null;
+                                       }
+                                       thnStore = thnStore.getFollowing();
+                               }
+                               while (thnStore != null) {
+                                       if (thnStore.name.equalsIgnoreCase(args)) {
+                                               if (thnStore.isPet()) {
+                                                       thnStore = thnStore.getMaster();
+                                               }
+                                               thnStore.removeFromGroup();
+                                               return null;
+                                       }
+                                       thnStore = thnStore.getFollowing();
+                               }
+                               return "They're not following you.";
+                       }
+
+                       case "stay": {
+                               if (lt.isPet()) {
+                                       lt.removeFromGroup();
+                                       return Commands.parseCommand(lt, game, "emote sits down to await " + lt.getMaster().name + "'s return.");
+                               }
+                               return (Commands.parseCommand(lt, game, "emote stays like a good little puppy."));
+                       }
+
+                       case "leave": {
+                               if (lt.isPet()) {
+                                       return "You cannot leave your master unless he unfollows you.";
+                               }
+                               lt.removeFromGroup();
+                               return "You are now on your own.";
+                       }
+
+                       case "unclan": {
+                               if (lt.clan.equals("none")) {
+                                       return "You're not in a clan.";
+                               }
+                               if (lt.battle != null) {
+                                       return "Wait until you're done battling.";
+                               }
+                               // FIXME: reimplement unclan
+                               if (true)
+                                       return "unclan not implemented yet";
+                               try {
+                                       /*
+                                        lt.halt();
+                                        lt.chatMessage("Are you sure you want to drop out of your clan? If so type yes.");
+                                        if (lt.instream.readLine().equalsIgnoreCase("yes")) {
+                                        lt.clan = "none";
+                                        if (lt.privs == 1) {
+                                        lt.privs = 0;
+                                        }
+                                        lt.proceed();
+                                        game.removeDuskObject(lt);
+                                        game.addDuskObject(lt);
+                                        return "You have been removed from your clan.";
+                                        }*/
+                               } catch (Exception e) {
+                                       game.log.printError("parseCommand():While " + lt.name + " was trying to dropout of their clan", e);
+                               }
+                               //lt.proceed();
+                               return null;
+                       }
+
+                       case "description": {
+                               if (args == null) {
+                                       lt.description = null;
+                                       return "Your description has been removed.";
+                               }
+                               lt.description = args;
+                               return "Your description has been set to:" + lt.description;
+                       }
+
+                       case "title": {
+                               if (!lt.isPlayer()) {
+                                       return "Only players may have titles.";
+                               }
+                               if (args == null) {
+                                       lt.title = null;
+                                       return "Your title has been removed.";
+                               }
+                               lt.title = args;
+                               if (lt.title.length() > game.titlecap) {
+                                       lt.title = lt.title.substring(0, game.titlecap);
+                               }
+                               return "Your title has been set to:" + lt.title;
+                       }
+
+                       case "password": {
+                               if (!lt.isPlayer()) {
+                                       return "Only players can change their password.";
+                               }
+                               return "password changing not re-implemented yet";
+                               /*
+                                try {
+                                lt.halt();
+                                lt.chatMessage("Enter your current password.");
+                                String strOldPass = lt.instream.readLine();
+                                if (!strOldPass.equals(lt.password)) {
+                                lt.proceed();
+                                return "Sorry, that is not your password.";
+                                }
+                                lt.chatMessage("Enter a new password.");
+                                String strNewPass = lt.instream.readLine();
+                                lt.chatMessage("Repeat that password.");
+                                String strNewPassRepeat = lt.instream.readLine();
+                                if (strNewPass == null) {
+                                lt.proceed();
+                                return "Not a valid password.";
+                                }
+                                if (!strNewPass.equals(strNewPassRepeat)) {
+                                lt.proceed();
+                                return "Sorry, those passwords do not match.";
+                                }
+                                lt.password = strNewPass;
+                                lt.proceed();
+                                return "Your password has now been changed.";
+                                } catch (Exception e) {
+                                game.log.printError("parseCommand():While " + lt.name + " was changing their password", e);
+                                }
+                                lt.proceed();
+                                */
+                       }
+
+                       case "wear": {
+                               if (args == null) {
+                                       return "Wear what?";
+                               }
+                               // FIXME: this should go on livingthing, but the interaction flow is messy
+                               LinkedList<Item> qStore = lt.itemList.get(args);
+                               if (qStore != null) {
+                                       Item itmStore = (Item) qStore.element();
+                                       int where = -1;
+
+                                       switch (itmStore.intType) {
+                                               case (1): {
+                                                       where = Equipment.WIELD;
+                                                       break;
+                                               }
+                                               case (2):
+                                                       where = itmStore.intKind + Equipment.ARMS;
+                                                       break;
+                                               default:
+                                                       return "You can't wear that";
+                                       }
+
+                                       Item old = lt.wornItems.wear(where, itmStore);
+                                       if (old != null) {
+                                               lt.itemList.addElement(old);
+                                               lt.onUnwear(old);
+                                       }
+                                       lt.onWear(itmStore);
+
+                                       lt.itemList.removeElement(itmStore.name);
+                                       if (lt.isPet()) {
+                                               lt.getMaster().updateStats();
+                                       }
+                                       if (lt.isPlayer()) {
+                                               lt.updateStats();
+                                       }
+                                       lt.updateEquipment();
+                                       lt.updateItems();
+                                       return null;
+                               }
+                               return "You don't have that";
+                       }
+                       case "unwear": {
+                               if (args == null) {
+                                       return "Unwear what?";
+                               }
+                               lt.unWear(args);
+                               return null;
+                       }
+                       case "rement": {
+                               if (args == null) {
+                                       return null;
+                               }
+                               long lngID = Long.parseLong(args);
+                               lt.removeEntity(lngID);
+                               return null;
+                       }
+                       case "audio": {
+                               if (args == null) {
+                                       if (lt.audioon) {
+                                               return "Your audio is turned on.";
+                                       }
+                                       return "Your audio is turned off.";
+                               } else if (args.equalsIgnoreCase("off")) {
+                                       lt.audioon = false;
+                                       return "Your audio has been turned off.";
+                               } else if (args.equalsIgnoreCase("on")) {
+                                       lt.audioon = true;
+                                       return "Your audio has been turned on.";
+                               }
+                       }
+                       case "color": {
+                               if (args == null) {
+                                       if (lt.coloron) {
+                                               return "Your color is turned on.";
+                                       }
+                                       return "Your color is turned off.";
+                               } else if (args.equalsIgnoreCase("off")) {
+                                       lt.coloron = false;
+                                       return "Your color has been turned off.";
+                               } else if (args.equalsIgnoreCase("on")) {
+                                       lt.coloron = true;
+                                       return "Your color has been turned on.";
+                               }
+                       }
+                       case "highlight": {
+                               if (args == null) {
+                                       if (lt.highlight) {
+                                               return "Highlighting of enemies in battle is turned on.";
+                                       }
+                                       return "Highlighting of enemies in battle is turned off.";
+                               } else if (args.equalsIgnoreCase("off")) {
+                                       lt.highlight = false;
+                                       lt.clearFlags();
+                                       return "Highlighting of enemies in battle has been turned off.";
+                               } else if (args.equalsIgnoreCase("on")) {
+                                       lt.highlight = true;
+                                       return "Highlighting of enemies in battle has been turned on.";
+                               }
+                       }
+                       case "popup": {
+                               // FIXME: TBD
+                               if (args == null) {
+                                       if (lt.popup) {
+                                               return "You have popup windows turned on.";
+                                       }
+                                       return "You have popup windows turned off.";
+                               } else if (args.equalsIgnoreCase("off")) {
+                                       lt.popup = false;
+                                       return "You have turned popup windows off.";
+                               } else if (args.equalsIgnoreCase("on")) {
+                                       lt.popup = true;
+                                       return "You have turned popup windows on.";
+                               }
+                       }
+                       case "nofollow": {
+                               if (args == null) {
+                                       if (lt.noFollow) {
+                                               return "You are not allowed to be followed.";
+                                       }
+                                       return "You can be followed.";
+                               } else if (args.equalsIgnoreCase("off")) {
+                                       lt.noFollow = false;
+                                       return "You can now be followed.";
+                               } else if (args.equalsIgnoreCase("on")) {
+                                       lt.noFollow = true;
+                                       return "You can no longer be followed.";
+                               }
+                       }
+                       case "ignore": {
+                               if (args == null) {
+                                       return "Ignore who?";
+                               }
+                               LivingThing thnStore = game.getPlayer(args);
+                               if (thnStore == null) {
+                                       return "They're not in this world.";
+                               }
+                               String strIgnoreName = thnStore.name.toLowerCase();
+                               if (lt.name.equalsIgnoreCase(strIgnoreName)) {
+                                       return "Trying to ignore yourself is not a good sign.";
+                               }
+                               if (thnStore.privs > 2) {
+                                       return "You cannot ignore a god.";
+                               }
+                               if (!lt.ignoreList.contains(strIgnoreName)) {
+                                       lt.ignoreList.add(strIgnoreName);
+                               } else {
+                                       return "You are already ignoring them.";
+                               }
+                               return "You are now ignoring " + strIgnoreName;
+                       }
+                       case "unignore": {
+                               if (args == null) {
+                                       return "UnIgnore who?";
+                               }
+                               String strIgnoreName = args.toLowerCase();
+                               if (strIgnoreName == "all") {
+                                       lt.ignoreList.clear();
+                                       return "You are no longer ignoring anyone.";
+                               }
+                               if (lt.ignoreList.contains(strIgnoreName)) {
+                                       lt.ignoreList.remove(strIgnoreName);
+                               } else {
+                                       return "You are not ignoring them.";
+                               }
+                               return "You are no longer ignoring " + strIgnoreName;
+                       }
+                       case "appletimages": {
+                               //lt.updateAppletImages();
+                               return "obsolete command";
+                       }
+                       case "applicationimages": {
+                               //lt.updateApplicationImages();
+                               return "obsolete command";
+                       }
+                       case "notdead": {
+                               return null;
+                       }
+                       case "quit":
+                       case "logout": {
+                               if (lt.battle == null) {
+                                       lt.close();
+                                       return null;
+                               }
+                               return "You cannot quit in the middle of a fight.";
+                       }
+               }
+               if (!blnFoundScriptedCommand) {
+                       return "huh?";
+               }
+               return null;
+       }
+}
index 658caf2..ad770de 100644 (file)
@@ -1,13 +1,40 @@
 
+CODE STATUS September 2013
+--------------------------
+
+After a very active development period the work simply stopped - so it
+is in a bit of a half-way state as you'd imagine.  Other time
+pressures and interests meant I haven't had time to work on this for
+months but I hope to again one day in some shape or form.
+
+Essentially the code in duskz/server and duskz/server/entity is the
+original Dusk code which has been refactored a bit (badly), updated to
+newer Java and a new protocol (I think), but more or less shares the
+original features and data format.  It's basically a dead branch.
+
+The code in dusk/server/entityz is mostly a complete re-write of the
+object class hierarchy, data i/o, map handling and a few other things.
+Unfortunately as it was in active development when it stopped it too
+isn't entirely in a good state either. 
+
+I think the object heirarchy is pretty good, and the script system was
+"upgraded" to javascript but there is a ton of work left to make it
+usable.  In hindsight i should've gone straight to a database
+(berkeleydb) backend as too much of the entity code is dealing with
+i/o.
+
+The original readme follows ...
+
 README
 ------
+
 This is the server implementation of DuskZ.  A client is required
 to access the game.
 
 It is a fork and major overhaul of the Dusk 2.7.3 source code, released
 circa 2000.
 
-This is currently in an alpha state and in very active development.
+This is currently in an alpha state.
 
  ... to be completed ...
 
diff --git a/DuskServer/TODO b/DuskServer/TODO
new file mode 100644 (file)
index 0000000..7d93ab8
--- /dev/null
@@ -0,0 +1,14 @@
+
+
+Well, write most of it ... but the big items are:
+
+commands
+Spells and Skills
+Shops
+
+save faction changes
+save player changes
+
+send player updates for changed things
+
+check all the bonus stats stuff, i think i did it a bit wrong
index 10570dc..858e647 100644 (file)
@@ -6,12 +6,6 @@ to impelement.  i.e. a requirements specification plus some notes.
 Thoughts
 ========
 
-Most of the global scripts are very simple and define global game
-policies.  Some need to run very often.
-
-It is quite possible they could be removed - either provide fixed game policies,
-or converted into Java and loaded at runtime.
-
 Certain game state is tracked by changing tiles!  I think this
 facility should be removed entirely and alternative mechanisms
 provided.
@@ -38,6 +32,13 @@ what it's for (for tests)
 Global per-game scripts
 -----------------------
 
+Most of the global scripts are very simple and define global game
+policies.  Some need to run very often.
+
+It is quite possible they could be removed - either provide fixed game policies,
+or converted into Java and loaded at runtime.
+
+
 OnBoot
 ------
 
@@ -161,8 +162,9 @@ Implements resetting of certain quests.
 
 OnBattle
 --------
-Only used for mobs.  The script to run is defined by the mob file
-(finally, some indirection!)
+
+Only used for mobs.  The script to run is defined by the mob file.
+These tweak the mob AI.
 
 scripts/name
  trigger=mob
@@ -616,3 +618,46 @@ Set a global varialbe of the type?  Doesn't appear used.
 
 "battlechat"
   Send a message to battle chat window.  unused.
+
+Values
+------
+
+Apart from constants and variables there are also a number of
+pre-defined values available to scripts.  These are also used to
+implement arithmetic expressions.
+
++ value value
+- value value
+* value value
+/ value value
+ Binary arithmetic operators.
+
+rand
+ Return a random number between zero and 1.0 (Math.random()).
+
+thing attribute
+ Read accessor for an attribute of thing, as below.
+
+  cp
+  tp
+  cash
+  exp
+  locx
+  locy
+  hp
+  maxhp
+  mp
+  maxmp
+  stre
+  dext
+  inte
+  wisd
+  cons
+  dammod
+  ac
+  privs
+  skill name
+   Retrieve skill level
+  count name
+   Count of items named name in thing's inventory
+
diff --git a/DuskServer/docs/duskz-classes b/DuskServer/docs/duskz-classes
new file mode 100644 (file)
index 0000000..3f0cafe
--- /dev/null
@@ -0,0 +1,163 @@
+
+Existing class structure
+
+DuskObject
+  LivingThing (type=0)
+    *Pet (type=2)
+    Mob (type=1)
+    
+  Item
+  Merchant
+  PlayerMerchant
+  Prop
+  Sign
+
+
+PlayerMerchant
+--------------
+
+Basically a cache that the owner can store things to for zero cost of transaction.
+
+
+State fields
+------------
+
+Fields and values that are saved and restored as part of the game state.
+
+Mob
+ name
+ originalX
+ originalY
+
+Sign
+ message
+ x
+ y
+
+Merchant
+ x
+ y
+ names of items
+ end (end marker)
+
+PlayerMerchant - is not saved.  This appears to be a bug because items
+are removed from the player when they put them in the store.
+
+Props
+ x
+ y
+ name
+
+
+Global variables
+ name
+ type code
+ value
+
+
+Players are saved separately.  TODO: document
+
+Engine Startup
+--------------
+
+Actions in the following order
+
+0. load preferences
+
+0.1 comple event scripts
+0.2 compile per-tile scripts
+
+1. load map(s)
+
+2. load map privs(*1)
+
+3. load map owner(*1)
+
+4. load merchants and the items each sells
+
+5. load mobs.  First load the mob prototype by name and set the
+   location from the state file.
+
+6. load sign.  State file includes full sign definition.
+
+7. load props.  First load prop prototype and set the prop location.
+
+8. load global variables.
+
+9. trigger the onBoot script.
+
+10. start the engine thread.
+
+1 - I commented that out so need to study it further, seems to be
+    ownership of rectangular regions of the map.  I think this can be
+    stored in memory more efficiently since it isn't needed very
+    often.
+
+Player Startup
+--------------
+
+0. init sockets and stream instances
+
+1. perform a bunch of global policy checks - maximum connections,
+   server shutting down, and blocked address checking.  Doesn't belong
+   here.
+
+2. Start the player thread, which does more initialisation.
+
+Player thread
++++++++++++++
+
+0. Perform login/create authentication/checks.  New code.
+
+1. Greet user
+
+2. Invoke onStart script for living thing.
+
+3. Initialise with prototype 'default' user.
+
+4. If it is an existing user, load user state.
+
+5. Load the pet for the user if not already set.  Pets are stored
+   indexed by the owner name.  This seems to be a cross-check of the
+   player state file because when that is loaded pets are also loaded.
+
+6. Load race which modifies base stats.
+
+7. Set pet location if set.
+
+8. If creating user, save now.
+
+9. Initialise client with:
+9.1 New map
+9.2 Set location (add player to game map)
+9.3 Updated player info
+9.4 Updated player stats
+9.5 Updated player items
+9.6 Updated player worn information
+9.7 Updated player actions
+
+10. Enter the command loop.
+
+Pet Startup
+-----------
+
+Pets are only added from the player load or by buying one.
+
+0. Load default pet as a prototype
+
+1. Load pet information
+
+2. If the pet has a race, load race modifiers
+
+3. Link to master.
+
+Mob Startup
+-----------
+
+Mobs are loaded at startup, scripts and gods can also create them.
+
+0. Load the mob prototype by name
+
+1. Override location
+
diff --git a/DuskServer/docs/duskz-javascript b/DuskServer/docs/duskz-javascript
new file mode 100644 (file)
index 0000000..b69818d
--- /dev/null
@@ -0,0 +1,136 @@
+
+This documents the JavaScript api and mechanisms.
+
+
+Public interfaces
++++++++++++++++++
+
+Game {
+  Item createItem(String type);
+  void removeItem(Item item);
+  Mobile createMob(String type);
+}
+
+Active {
+  // send dialogue to the player
+  chat(String v);
+  // emote an action
+  emote(String v);
+  setCondition(String name, int duration);
+  // get or change an attribute, XXX = INT, DEX, etc.
+  int getXXX();
+  int addXXX(int amount);
+  // Remove an item from inventory
+  removeItem(Item item);
+  // Add an item to inventory
+  addItem(Item item);
+
+  // Variable functions - persistent per-active state
+  int getInteger(String name, int def);
+  int getString(String name, String def);
+  int getDouble(String name, double def);
+
+  void setInteger(String name, int val);
+  void setString(String name, String val);
+  void setDouble(String name, double val);
+}
+
+Item {
+  // Name of item
+  String getName();
+  // How many uses left
+  int getUses();
+  // Alter uses
+  int addUses(int count);
+}
+
+// TODO: do i want conditions to have arbitrary persistent variables too?
+// possibly
+Condition {
+}
+
+Items
+=====
+
+onGet
+-----
+
+Scripts are stored at
+ onItem/itemname.drop
+
+Parameters
+  game - global game object
+  owner - current Active holding the object
+  item - item being dropped
+
+If provided, the script is executed after the object is taken from
+the map and added to the inventory.
+
+onDrop
+------
+
+Scripts are stored at
+ onItem/itemname.drop
+
+Parameters
+  game - global game object
+  owner - current Active holding the object
+  item - item being dropped
+
+If provided, the script OVERRIDES the default drop behaviour which is
+to leave the item at the current location.  This is different from
+Dusk where the script was run after the default drop behaviour.
+
+onUse
+-----
+
+Scripts are stored at
+ onItem/itemname.use
+
+Parameters
+  game - global game object
+  owner - current Active holding the object
+  item - item being used
+
+If provided the script provides the on-use behaviour for the item in
+question.  The script will only be called if Item.getUses() is
+infinite (-1) or greater than 0.
+
+Conditions
+==========
+
+onStart
+-------
+
+Scripts are stored at
+ onCondition/condition.start
+
+Parameters
+ game - global game object
+ owner - Active to which condition applies
+ condition - Condition object
+
+
+onOccurance
+-----------
+
+Scripts are stored at
+ onCondition/condition.tick
+
+Parameters
+ game - global game object
+ owner - Active to which condition applies
+ condition - Condition object
+
+
+onEnd
+-------
+
+Scripts are stored at
+ onCondition/condition.end
+
+Parameters
+ game - global game object
+ owner - Active to which condition applies
+ condition - Condition object
+
diff --git a/DuskServer/docs/duskz-script b/DuskServer/docs/duskz-script
new file mode 100644 (file)
index 0000000..6d72f58
--- /dev/null
@@ -0,0 +1,211 @@
+
+This is a planning document for the new 'duskz' scripting system.
+
+Overview
+========
+
+The new scripting system will be using the ScriptEngine framework
+included in Java 6+.
+
+The goal will be to provide a super-set of the features previously
+available, with a more modern language.
+
+Security
+========
+
+JavaScript has far more facilities available than one would expect
+from a scripting language such as the ability to reflect into the
+system and access any public state.
+
+Whilst this allows for powerful scripts it is also an issue of
+security.
+
+Unfortunately fully securing the sandbox will be difficult, but the
+initial basic approach will be to rely on the security manager for
+high level control (files, network), and scripts will only have access
+to wrapper interfaces rather than the live game objects.
+
+Dessign
+=======
+
+The design has to take into account the way the script system works.
+For example in javascript object definitions within scripts persist
+between script invocations.
+
+For efficiency reasons it may be desirable to preload all scripts in
+some manner.  Precompilation is also an option but has restircted use.
+
+Some obvious possibilities for the design:
+
+0. all behaviour of all objects loaded together
+1. all behaviour of one object in one file using toplevel functions
+2. behaviour in separate scripts using toplevel logic (as current
+script system)
+3. all behaviour of one object in one file using arguments (like a
+shell script)
+
+0. All behaviour global
+-----------------------
+
+Implemented by having a separate 'object' for each type of script.
+The object would follow some convention to implement class (no need
+for class inheritence), e.g. Mob's would have an onBattle function,
+Items would not (or perhaps they could?).
+
+Each item is loaded (on demand?) into the ScriptEngine, and methods
+invoked via an object of the same name as the script.
+
+Example
++++++++
+items/absinthe_brain
+var absinthe = {
+    onUse: function(thing) {
+          thing.emote('wails', 'oh, my head!');
+          if (thing.getInte() > 10)
+             thing.incrInte(-1);
+          thing.remove('absinthe');
+    },
+    onDrop: function(thing) {
+           thing.order("get absinthe");
+           thing.remove('absinthe');
+    }
+};
+
+Pros:
+o Everything is only compiled once
+o Can replace scripts when they are changed
+o Behaviour is all together in one place
+o Member variables can be used for session-persistent state.
+o Argument counts are checked at runtime.
+
+Cons:
+o Entire game logic is loaded into script engine at once - memory?
+o Can replace a different script when it is changed - security.
+o Member variables can be abused for session-persistent state.
+o Fair bit of scaffolding.
+o Arguments must be deined.
+
+Another possibility is to go even further and replace all object
+definitions with javascript objects instead:
+
+var absinthe = {
+    type: drink,
+    description: "a mug of green liquer flavoured with wormwood",
+    cost: 1,
+    image: 6,
+    ... etc
+};
+
+However, accessing field values is clumsy from Java.
+
+1. Behaviour in top-level functions
+-----------------------------------
+
+Example
++++++++
+items/absinthe_brain
+onUse = function(thing) {
+    thing.emote('wails: oh, my head!');
+    if (thing.getInte() > 10)
+        thing.incrInte(-1);
+    thing.remove('absinthe');
+};
+
+onDrop = function(thing) {
+    thing.order("get absinthe");
+    thing.remove('absinthe');
+};
+
+Pros:
+o Simpler syntax
+o Function names can be checked at runtime
+o Argument count is checked at runtime.
+
+Cons:
+o Requires running the script every time before use even if
+precompiled.  Or many engines.
+o Arguments must be defined
+
+2. Behaviour in separate scripts
+---------------------------------
+
+This is like the existing system, a master file indicates which script
+is executed for every event on a given type of object.
+
+Example
++++++++
+script/getabsinthe:
+thing.emote('wails: oh, my head!');
+if (thing.getInte() > 10)
+    thing.incrInte(-1);
+    thing.remove('absinthe');
+}
+
+script/dropabsinthe:
+thing.order("get absinthe");
+thing.remove('absinthe');
+
+Pros:
+o Apart from syntax, identical to existing system
+o Can be precompiled
+o Small scripts faster to parse if passing every time
+o Requires no scaffolding in script
+
+Cons:
+o Unwieldly number of files to manage and setting to correlate
+o Logic spread across multiple files
+
+3. Object behaviour in single top-level script
+----------------------------------------------
+
+Here global variables are used to pass which behaviour is desired.
+
+Example
++++++++
+items/absinthe_brain:
+switch (script) {
+case 'onUse':
+     thing.emote('wails: oh, my head!');
+     if (thing.getInte() > 10)
+       thing.incrInte(-1);
+        thing.remove('absinthe');
+    }
+    break;
+case 'onDrop':
+    thing.order("get absinthe");
+    thing.remove('absinthe');
+    break;
+}
+
+Pros:
+o Scripts can be precompiled
+o Script execution relatively isolated and cannot be overriden by another script
+
+Cons:
+o Typos in case strings wont be detected at runtime
+o Requires some scaffolding.
+o Very little automatic checking.
+
+Conclusions
+-----------
+
+Although the object and pre-compiled nature of proposal 0 has some
+appeal, it relies too much on the implementation of the
+ScriptingEngine, and is the most memory intensive solution.
+
+Proposal 1 is appealing from the player perspective but seems a little
+clumsy to use in the host.
+
+Proposal 2 benefits from being "how the game already does it", but
+that probably doesn't mean too much.  It's main drawback is the number
+of files and settings required.
+
+Proposal 3 seems a little too complicated and error prone although it
+tries to combine the benefits of a single-file behaviour and lower
+scaffolding of the individual script files.
+
+One thing I find odd is that the JavaScript script engine ... doesn't
+let you add new script functions!  You can only add objects, which
+makes their use clumsy - i.e. there is no way to remove 'thing.'
+inside the scripts.  It's probably worth supporting java language
+scripts too.
diff --git a/DuskServer/docs/duskz-state b/DuskServer/docs/duskz-state
new file mode 100644 (file)
index 0000000..fdaf838
--- /dev/null
@@ -0,0 +1,35 @@
+
+Online game state is stored in state files which are semi-serialised
+versions of things.
+
+Format
+------
+
+The format is the same as other object files - pseudo-properties format,
+but each object is wrapped in a container.
+
+type=Class,objectname
+properties
+=end
+
+Class is the name of the class (relative to duskz.server.entity) this
+object represents.
+
+objectname is the name of the object prototype, e.g. name of the mob,
+item, etc.
+
+Saving process
+--------------
+
+1. Write envelope header (class,name)
+2. Write volatile state variables, by calling Thing.writeState()
+3. Write envelope footer
+
+Loading process
+---------------
+
+1. Read envelope header.
+2. Instantiate object of type
+3. Load object prototype using Thing.load(name)
+4. Read volatile state variables and set them via setProperty()
+5. Until envelope footer is encountered.
diff --git a/DuskServer/src/duskz/io/Convert.java b/DuskServer/src/duskz/io/Convert.java
new file mode 100644 (file)
index 0000000..027ac01
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.io;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintStream;
+
+/**
+ * Convert a few basic files
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Convert {
+
+       public static void main(String[] args) throws IOException {
+               File src = new File("/home/notzed/src/DuskRPG/DuskFiles/Dusk2.7.3");
+               File dst = new File("/home/notzed/dusk/game");
+               
+               PrintStream o = new PrintStream(new File(dst, "mobs.new"));
+               
+               try (BufferedReader is = new BufferedReader(new FileReader(new File(src, "mobs")))) {
+                       while (true) {
+                               String m = is.readLine();
+                               
+                               if (m == null || !m.trim().equals("mob2.3"))
+                                       break;
+                               
+                               String name= is.readLine();
+                               int x = Integer.valueOf(is.readLine());
+                               int y = Integer.valueOf(is.readLine());
+                               
+                               o.println("type.Mobile=" + name);
+                               o.println("map=dusk");
+                               o.println("x=" + x);
+                               o.println("y=" + y);
+                               o.println("=end");
+                       }
+               }
+               o.close();
+       }
+}
index f11b7e6..74dbcd6 100644 (file)
@@ -27,7 +27,6 @@ import duskz.io.tiled.Data;
 import duskz.io.tiled.Image;
 import duskz.io.tiled.Layer;
 import duskz.io.tiled.Map;
-import duskz.io.tiled.Properties;
 import duskz.io.tiled.Property;
 import duskz.io.tiled.Tile;
 import duskz.io.tiled.Tileset;
@@ -35,7 +34,6 @@ import java.awt.Graphics2D;
 import java.awt.image.BufferedImage;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
@@ -670,8 +668,12 @@ public class Tiled {
                //              new File("/home/notzed/house.jar"));
                //tiledToDusk(new File("/home/notzed/test-map.tmx"),
                //              new File("/home/notzed/test-map.bin"));
-               tiledToDusk(new File("/home/notzed/dusk/maps/do-drop-inn.tmx"),
-                               new File("/home/notzed/src/DuskRPG/DuskFiles/DuskX/defMaps/do-drop-inn"));
+               //tiledToDusk(new File("/home/notzed/dusk/maps/dusk.tmx"),
+               //              new File("/home/notzed/dusk/game/defMaps/dusk"));
+               //tiledToDusk(new File("/home/notzed/dusk/maps/do-drop-inn.tmx"),
+               //              new File("/home/notzed/src/DuskRPG/DuskFiles/DuskX/defMaps/do-drop-inn"));
                //testexport();
+               tiledToDusk(new File("/home/notzed/dusk/maps/main.tmx"),
+                               new File("/home/notzed/dusk/game/defMaps/main"));
        }
 }
index d9061c1..522bea6 100644 (file)
 package duskz.server;
 
 import duskz.protocol.DuskProtocol;
+import duskz.protocol.Wearing;
 import duskz.server.entity.Mob;
 import duskz.server.entity.Merchant;
 import duskz.server.entity.Sign;
 import duskz.server.entity.Item;
 import duskz.server.entity.Prop;
 import duskz.server.entity.DuskObject;
-import duskz.server.entity.Equipment;
 import duskz.server.entity.PlayerMerchant;
 import duskz.server.entity.LivingThing;
 import duskz.server.entity.TileMap;
@@ -2447,11 +2447,11 @@ public class Commands implements DuskProtocol {
 
                                        switch (itmStore.intType) {
                                                case (1): {
-                                                       where = Equipment.WIELD;
+                                                       where = Wearing.WIELD;
                                                        break;
                                                }
                                                case (2):
-                                                       where = itmStore.intKind + Equipment.ARMS;
+                                                       where = itmStore.intKind + Wearing.ARMS;
                                                        break;
                                                default:
                                                        return "You can't wear that";
index 86de141..2272517 100644 (file)
@@ -696,11 +696,13 @@ public class DuskEngine implements Runnable, DuskProtocol {
                }
        }
 
+       @Deprecated
        public synchronized long getID() {
                nextid++;
                return nextid;
        }
 
+       @Deprecated
        public void chatMessage(String msg, String from) {
                from = from.toLowerCase();
                log.printMessage(Log.ALWAYS, msg);
@@ -721,6 +723,7 @@ public class DuskEngine implements Runnable, DuskProtocol {
         * @param strFrom
         */
        // FIXME: move somewhere better
+       @Deprecated
        public void chatMessage(TileMap map, String inMessage, int locx, int locy, String strFrom) {
                strFrom = strFrom.toLowerCase();
                LivingThing thnStore;
@@ -739,6 +742,7 @@ public class DuskEngine implements Runnable, DuskProtocol {
                }
        }
 
+       @Deprecated
        public void chatMessage(String msg, String clan, String from) {
                from = from.toLowerCase();
                log.printMessage(Log.ALWAYS, msg);
@@ -751,6 +755,8 @@ public class DuskEngine implements Runnable, DuskProtocol {
        }
 
        // FIXME: move to livinghting?
+       // only ever called on a player source
+       @Deprecated
        public void refreshEntities(LivingThing refresh) {
                LinkedList<DuskObject> newEntities = new LinkedList<>();
 
@@ -841,6 +847,7 @@ public class DuskEngine implements Runnable, DuskProtocol {
        }
 
        // FIXME: move to map or livingthing?
+       @Deprecated
        public void addEntity(TileMap map, DuskObject add) {
                for (TileMap.MapData md : map.range(add.x, add.y, viewrange)) {
                        for (DuskObject o : md.entities) {
@@ -890,6 +897,7 @@ public class DuskEngine implements Runnable, DuskProtocol {
        }
 
        // FIXME: Move to map?
+       @Deprecated
        void notifyRemoved(DuskObject remove) {
                for (TileMap.MapData md : remove.map.range(remove.x, remove.y, viewrange)) {
                        for (DuskObject o : md.entities) {
@@ -1265,7 +1273,7 @@ public class DuskEngine implements Runnable, DuskProtocol {
                if (!lt.map.inside(inLocX, inLocY))
                        return false;
 
-               System.out.printf("can move to: %d,%d tid=%3d on map %s\n", inLocX, inLocY, lt.map.getTile(inLocX, inLocY), lt.map.name);
+               //System.out.printf("can move to: %d,%d tid=%3d on map %s\n", inLocX, inLocY, lt.map.getTile(inLocX, inLocY), lt.map.name);
 
                for (DuskObject o : lt.map.getEntities(inLocX, inLocY, null)) {
                        if (o.isLivingThing()) {
index f8646d9..663b871 100644 (file)
@@ -206,6 +206,8 @@ public class Faction {
                                                                }
                                                        }
                                                        intConfidence += relation * lt.getTotalPoints();
+                                                       System.out.printf("mob '%s' can see player '%s' relation %f confidence=%d\n", mob.name, lt.name, relation, intConfidence);
+                                                       System.out.printf("  player points total = %d armour %d damage %d component %f\n", lt.getTotalPoints(), lt.getArmorMod(), lt.getDamMod(), relation * lt.getTotalPoints());
                                                }
                                                if (lt.isMob()) {
                                                        double relation;
@@ -229,6 +231,8 @@ public class Faction {
                                                                }
                                                        }
                                                        intConfidence += relation * lt.getTotalPoints();
+                                                       System.out.printf("mob '%s' can see mob '%s' relation %f confidence=%d\n", mob.name, lt.name, relation, intConfidence);
+                                                       System.out.printf("  player points total = %d armour %d damage %d component %f\n", lt.getTotalPoints(), lt.getArmorMod(), lt.getDamMod(), relation * lt.getTotalPoints());
                                                }
                                        }
                                }
index b4e7736..0b02f64 100644 (file)
@@ -105,24 +105,6 @@ public abstract class DuskObject {
                return ID;
        }
 
-       /**
-        * Convert to on-wire entity format
-        *
-        * @param eng
-        * @return
-        */
-       public String toEntity() {
-               StringBuilder sb = new StringBuilder();
-
-               sb.append(name).append('\n');
-               sb.append(getEntityType()).append('\n');
-               sb.append(ID).append('\n');
-               sb.append(x).append('\n');
-               sb.append(y).append('\n');
-               sb.append(getImage()).append('\n');
-
-               return sb.toString();
-       }
 
        public EntityUpdateMessage toMessage(int msg) {
                EntityUpdateMessage en = new EntityUpdateMessage();
index f26fb1c..cc42628 100644 (file)
@@ -27,14 +27,14 @@ package duskz.server.entity;
 
 import duskz.protocol.DuskMessage;
 import duskz.protocol.ListMessage;
-import duskz.protocol.Wearing;
+import static duskz.protocol.Wearing.*;
 
 /**
  * Equipment contains all the Items a LivingThing is wearing.
  *
  * @author Tom Weingarten
  */
-public class Equipment implements Wearing {
+public class Equipment  {
 
        // Must match Wearing
        public static final String[] USER_NAMES = {
index 40d4009..9c8cdf6 100644 (file)
@@ -38,6 +38,7 @@ import duskz.protocol.EntityUpdateMessage;
 import duskz.protocol.ListMessage;
 import duskz.protocol.MapMessage;
 import duskz.protocol.TransactionMessage;
+import duskz.protocol.Wearing;
 import duskz.server.Battle;
 import duskz.server.BlockedIPException;
 import duskz.server.Commands;
@@ -95,7 +96,7 @@ public class LivingThing extends DuskObject implements Runnable, DuskProtocol {
        //(The script can change this to false to stop it from happening)
        //(This allows scripts to short-circuit in-game commands)
        public boolean isAlwaysCommands = true;
-       // File names for i/o
+       // File wornNames for i/o
        File file, backup;
        //ID data
        public String password,
@@ -263,37 +264,6 @@ public class LivingThing extends DuskObject implements Runnable, DuskProtocol {
                        return null;
        }
 
-       @Override
-       public String toEntity() {
-               StringBuilder sb = new StringBuilder();
-               if (isSleeping) {
-                       sb.append("<sleeping>");
-               }
-               if (isPlayer() && !clan.equals("none")) {
-                       sb.append('<');
-                       sb.append(clan);
-                       sb.append('>');
-               }
-               if (isPet() && hp < 0) {
-                       sb.append("<wounded>");
-               }
-               for (String s : flags) {
-                       sb.append('<');
-                       sb.append(s);
-                       sb.append('>');
-               }
-               sb.append(name).append('\n');
-               sb.append(getEntityType()).append('\n');
-               sb.append(ID).append('\n');
-               sb.append(x).append('\n');
-               sb.append(y).append('\n');
-               sb.append(getImage()).append('\n');
-               if (isPlayer()) {
-                       sb.append(imagestep).append('\n');
-               }
-               return sb.toString();
-       }
-
        @Override
        public EntityUpdateMessage toMessage(int msg) {
                EntityUpdateMessage en = super.toMessage(msg);
@@ -743,58 +713,58 @@ public class LivingThing extends DuskObject implements Runnable, DuskProtocol {
                                break;
                        // FIXME: do i need these 'old' versions?
                        case "wield":
-                               wornItems.wear(Equipment.WIELD, game.getItem(in.readLine()));
+                               wornItems.wear(Wearing.WIELD, game.getItem(in.readLine()));
                                break;
                        case "arms":
-                               wornItems.wear(Equipment.ARMS, game.getItem(in.readLine()));
+                               wornItems.wear(Wearing.ARMS, game.getItem(in.readLine()));
                                break;
                        case "legs":
-                               wornItems.wear(Equipment.LEGS, game.getItem(in.readLine()));
+                               wornItems.wear(Wearing.LEGS, game.getItem(in.readLine()));
                                break;
                        case "torso":
-                               wornItems.wear(Equipment.TORSO, game.getItem(in.readLine()));
+                               wornItems.wear(Wearing.TORSO, game.getItem(in.readLine()));
                                break;
                        case "waist":
-                               wornItems.wear(Equipment.WAIST, game.getItem(in.readLine()));
+                               wornItems.wear(Wearing.WAIST, game.getItem(in.readLine()));
                                break;
                        case "neck":
-                               wornItems.wear(Equipment.NECK, game.getItem(in.readLine()));
+                               wornItems.wear(Wearing.NECK, game.getItem(in.readLine()));
                                break;
                        case "skull":
-                               wornItems.wear(Equipment.SKULL, game.getItem(in.readLine()));
+                               wornItems.wear(Wearing.SKULL, game.getItem(in.readLine()));
                                break;
                        case "eyes":
-                               wornItems.wear(Equipment.EYES, game.getItem(in.readLine()));
+                               wornItems.wear(Wearing.EYES, game.getItem(in.readLine()));
                                break;
                        case "hands":
-                               wornItems.wear(Equipment.HANDS, game.getItem(in.readLine()));
+                               wornItems.wear(Wearing.HANDS, game.getItem(in.readLine()));
                                break;
                        case "wield2":
-                               parseWear(in, Equipment.WIELD);
+                               parseWear(in, Wearing.WIELD);
                                break;
                        case "arms2":
-                               parseWear(in, Equipment.ARMS);
+                               parseWear(in, Wearing.ARMS);
                                break;
                        case "legs2":
-                               parseWear(in, Equipment.LEGS);
+                               parseWear(in, Wearing.LEGS);
                                break;
                        case "torso2":
-                               parseWear(in, Equipment.TORSO);
+                               parseWear(in, Wearing.TORSO);
                                break;
                        case "waist2":
-                               parseWear(in, Equipment.WAIST);
+                               parseWear(in, Wearing.WAIST);
                                break;
                        case "neck2":
-                               parseWear(in, Equipment.NECK);
+                               parseWear(in, Wearing.NECK);
                                break;
                        case "skull2":
-                               parseWear(in, Equipment.SKULL);
+                               parseWear(in, Wearing.SKULL);
                                break;
                        case "eyes2":
-                               parseWear(in, Equipment.EYES);
+                               parseWear(in, Wearing.EYES);
                                break;
                        case "hands2":
-                               parseWear(in, Equipment.HANDS);
+                               parseWear(in, Wearing.HANDS);
                                break;
                        case "nofollow":
                                noFollow = true;
@@ -950,7 +920,7 @@ public class LivingThing extends DuskObject implements Runnable, DuskProtocol {
                        for (Condition cond : conditions) {
                                rafPlayerFile.writeBytes("condition\n" + cond.name + "\n" + cond.ticksPast + "\n" + cond.duration + "\n");
                        }
-                       for (int i = 0; i < Equipment.WEARING_COUNT; i++) {
+                       for (int i = 0; i < Wearing.WEARING_COUNT; i++) {
                                Item item = wornItems.getWorn(i);
                                if (item != null) {
                                        rafPlayerFile.writeBytes(Equipment.USER_NAMES[i] + "\n" + item.name + "\n" + item.lngDurability + "\n" + item.intUses + "\n");
@@ -1843,7 +1813,7 @@ public class LivingThing extends DuskObject implements Runnable, DuskProtocol {
        }
 
        public int getDamMod() {
-               Item item = wornItems.getWorn(Equipment.WIELD);
+               Item item = wornItems.getWorn(Wearing.WIELD);
                if (item != null)
                        return item.intMod;
                return 100;
@@ -1854,7 +1824,7 @@ public class LivingThing extends DuskObject implements Runnable, DuskProtocol {
        }
 
        public int getRange() {
-               Item item = wornItems.getWorn(Equipment.WIELD);
+               Item item = wornItems.getWorn(Wearing.WIELD);
 
                if (item == null) {
                        return 1;
@@ -1872,7 +1842,7 @@ public class LivingThing extends DuskObject implements Runnable, DuskProtocol {
                        return;
                }
 
-               Item item = wornItems.damageItem(Equipment.WIELD, damage);
+               Item item = wornItems.damageItem(Wearing.WIELD, damage);
                if (item != null) {
                        chatMessage("Your " + item.name + " breaks.");
                        onUnwear(item);
@@ -1893,8 +1863,8 @@ public class LivingThing extends DuskObject implements Runnable, DuskProtocol {
                        return;
                }
 
-               for (int i = Equipment.ARMS; i < Equipment.WEARING_COUNT; i++) {
-                       Item item = wornItems.damageItem(Equipment.WIELD, damage);
+               for (int i = Wearing.ARMS; i < Wearing.WEARING_COUNT; i++) {
+                       Item item = wornItems.damageItem(Wearing.WIELD, damage);
                        if (item != null) {
                                chatMessage("Your " + item.name + " breaks.");
                                onUnwear(item);
@@ -2111,7 +2081,7 @@ public class LivingThing extends DuskObject implements Runnable, DuskProtocol {
                if (index != -1) {
                        unwear(index);
                } else if (strStore.equalsIgnoreCase("all")) {
-                       for (int i = 0; i < Equipment.WEARING_COUNT; i++) {
+                       for (int i = 0; i < Wearing.WEARING_COUNT; i++) {
                                unwear(i);
                        }
                } else {
index 52297a4..2de820c 100644 (file)
@@ -25,6 +25,7 @@
  */
 package duskz.server.entity;
 
+import duskz.protocol.Wearing;
 import duskz.server.Condition;
 import duskz.server.DuskEngine;
 import duskz.server.Faction;
@@ -221,31 +222,31 @@ public class Mob extends LivingThing {
                                dblGroupRelation = Double.valueOf(in.readLine()).doubleValue();
                                break;
                        case "wield":
-                               parseWear(in, Equipment.WIELD);
+                               parseWear(in, Wearing.WIELD);
                                break;
                        case "arms":
-                               parseWear(in, Equipment.ARMS);
+                               parseWear(in, Wearing.ARMS);
                                break;
                        case "legs":
-                               parseWear(in, Equipment.LEGS);
+                               parseWear(in, Wearing.LEGS);
                                break;
                        case "torso":
-                               parseWear(in, Equipment.TORSO);
+                               parseWear(in, Wearing.TORSO);
                                break;
                        case "waist":
-                               parseWear(in, Equipment.WAIST);
+                               parseWear(in, Wearing.WAIST);
                                break;
                        case "neck":
-                               parseWear(in, Equipment.NECK);
+                               parseWear(in, Wearing.NECK);
                                break;
                        case "skull":
-                               parseWear(in, Equipment.SKULL);
+                               parseWear(in, Wearing.SKULL);
                                break;
                        case "eyes":
-                               parseWear(in, Equipment.EYES);
+                               parseWear(in, Wearing.EYES);
                                break;
                        case "hands":
-                               parseWear(in, Equipment.HANDS);
+                               parseWear(in, Wearing.HANDS);
                                break;
                        case "faction":
                                String strFaction = in.readLine();
index 3bf654a..0e15b35 100644 (file)
@@ -605,7 +605,6 @@ public class TileMap implements Iterable<TileMap.MapData> {
        /**
         * Implements an iterator which follows a 'looking' path
         *
-        * TODO: it should probably use Bresenhams line algorithm
         */
        private class LookIterator implements Iterator<MapData> {
 
diff --git a/DuskServer/src/duskz/server/entityz/Ability.java b/DuskServer/src/duskz/server/entityz/Ability.java
new file mode 100644 (file)
index 0000000..3b70c3e
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+/**
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Ability {
+
+       public final String name;
+       private int level;
+
+       public Ability(String name) {
+               this.name = name;
+       }
+
+       public int getLevel() {
+               return level;
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Active.java b/DuskServer/src/duskz/server/entityz/Active.java
new file mode 100644 (file)
index 0000000..e3e8af1
--- /dev/null
@@ -0,0 +1,1793 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+// fixme copyrihgts
+package duskz.server.entityz;
+
+import duskz.protocol.DuskMessage;
+import duskz.protocol.Wearing;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+/**
+ * An active, 'living thing'. Active objects can move and participate in battles.
+ *
+ * Notes:
+ * the player update code needs to track if they were over a merchant
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public abstract class Active extends Thing {
+
+       /**
+        * Privilege level
+        */
+       private int privs;
+       /**
+        * Experience. Check: do mobs and pets need this?
+        */
+       private int exp;
+       /**
+        * Cash on hand
+        */
+       private long gold;
+       //
+       // TODO: make this part of the protocol?
+       //
+       public static final int STAT_HP = 0;
+       public static final int STAT_HPMAX = 1;
+       public static final int STAT_MP = 2;
+       public static final int STAT_MPMAX = 3;
+       public static final int STAT_STR = 4;
+       public static final int STAT_INT = 5;
+       public static final int STAT_DEX = 6;
+       public static final int STAT_CON = 7;
+       public static final int STAT_WIS = 8;
+       // These are pseudo-stats calculated dynamically
+       // Should always be at the end of fixed ones
+       public static final int STAT_DAMAGE = 9;
+       public static final int STAT_RANGE = 10;
+       public static final int STAT_ARC = 11;
+       public static final String[] stat_keys = {"hp", "hpmax", "mp", "mpmax", "str", "int", "dex", "con", "wis"};
+       /**
+        * Player stats. Keep this private (once mobile fixed up)
+        * Keyed by STAT_* constants.
+        */
+       protected int stats[] = new int[12];
+       protected int bonus[] = new int[12];
+       // object? player?
+       // I don' think mobs can have clans - factions are used instead, clan/race
+       // candiates for class beteen active and player/pet
+       String clan;
+       /**
+        * Race, mobs can't have races.
+        */
+       Race race;
+       Equipment wornItems = new Equipment();
+       Inventory inventory = new Inventory();
+       /**
+        * Sleeping state
+        */
+       private boolean sleeping;
+       /**
+        * Tracks actives following in a pack
+        */
+       protected Pack pack;
+       /**
+        * Does this leader allow following at the moment.
+        */
+       boolean canLead = true;
+       /**
+        * Allowed to move
+        */
+       protected boolean moveable = true;
+       /**
+        * "step" of image: basically last direction moved.
+        */
+       protected int imageStep;
+       /**
+        * Live conditions
+        */
+       private final ConditionList conditions = new ConditionList();
+       /**
+        * Arbitrary variables for script use.
+        */
+       protected final VariableList variables = new VariableList();
+       /**
+        * If fighting, current battle
+        */
+       protected Battle battle;
+       /**
+        * Damage done, accumulates until you lose, where it is used to give
+        * bonus experience to your the winners
+        */
+       int damageDone;
+       //
+       private final HashMap<String, Ability> skills = new HashMap<>();
+       // FIXME: SpellAbility subclasses Ability
+       private final HashMap<String, Ability> spells = new HashMap<>();
+       /**
+        * Pending moves, processed one at a time during movement tick
+        */
+       final private LinkedList<String> moveQueue = new LinkedList<>();
+       /**
+        * Pending commands, this is only used during battle
+        */
+       final private LinkedList<String[]> battleCommands = new LinkedList<>();
+       /**
+        * Tick of last state change, such as sleeping, etc.
+        */
+       int flagsChanged;
+       /**
+        * Tick of last condition change
+        */
+       int conditionsChanged;
+
+       Active(Game game) {
+               super(game);
+       }
+
+       Wearable parseWearable(String value) {
+               // format is name,durability,uses
+
+               System.out.println("parse wearable: " + value);
+
+               String[] args = value.split(",");
+
+               if (args[0].equals("none"))
+                       return null;
+
+               Wearable w = (Wearable) game.createItem(args[0].toLowerCase());
+
+               if (w != null) {
+                       w.durability = Long.valueOf(args[1]);
+                       w.uses = Integer.valueOf(args[2]);
+               }
+               return w;
+       }
+
+       // TODO: this probably needs some 'exporter' on Thing
+       Holdable parseHoldable(String value) {
+               // format is name,durability,uses ... but durability is only used on Wearables
+               // FIXME: fix exporter ... not so easy, everything is an item, even weapons.
+
+               System.out.println("parse holdable: " + value);
+
+               String[] args = value.split(",");
+
+               if (args[0].equals("none"))
+                       return null;
+
+               Holdable h = (Holdable) game.createItem(args[0].toLowerCase());
+
+               // blah ... nice eh?
+               if (h instanceof Wearable) {
+                       ((Wearable) h).durability = Long.valueOf(args[1]);
+               }
+               h.uses = Integer.valueOf(args[2]);
+
+               return h;
+       }
+
+       /* *
+        * I/o stuff
+        */
+       @Override
+       void setProperty(String name, String value) {
+               if (variables.setProperty(name, value))
+                       return;
+               if (conditions.setProperty(name, value))
+                       return;
+               switch (name) {
+                       case "hp":
+                               stats[STAT_HP] = Integer.valueOf(value);
+                               break;
+                       case "maxhp":
+                               stats[STAT_HPMAX] = Integer.valueOf(value);
+                               break;
+                       case "mp":
+                               stats[STAT_MP] = Integer.valueOf(value);
+                               break;
+                       case "maxmp":
+                               stats[STAT_MPMAX] = Integer.valueOf(value);
+                               break;
+                       case "str":
+                               stats[STAT_STR] = Integer.valueOf(value);
+                               break;
+                       case "int":
+                               stats[STAT_INT] = Integer.valueOf(value);
+                               break;
+                       case "dex":
+                               stats[STAT_DEX] = Integer.valueOf(value);
+                               break;
+                       case "con":
+                               stats[STAT_CON] = Integer.valueOf(value);
+                               break;
+                       case "wis":
+                               stats[STAT_WIS] = Integer.valueOf(value);
+                               break;
+                       case "hpbon":
+                               bonus[STAT_HP] = Integer.valueOf(value);
+                               break;
+                       case "maxhpbon":
+                               bonus[STAT_HPMAX] = Integer.valueOf(value);
+                               break;
+                       case "mpbon":
+                               bonus[STAT_MP] = Integer.valueOf(value);
+                               break;
+                       case "maxmpbon":
+                               bonus[STAT_MPMAX] = Integer.valueOf(value);
+                               break;
+                       case "strbon":
+                               bonus[STAT_STR] = Integer.valueOf(value);
+                               break;
+                       case "intbon":
+                               bonus[STAT_INT] = Integer.valueOf(value);
+                               break;
+                       case "dexbon":
+                               bonus[STAT_DEX] = Integer.valueOf(value);
+                               break;
+                       case "conbon":
+                               bonus[STAT_CON] = Integer.valueOf(value);
+                               break;
+                       case "wisbon":
+                               bonus[STAT_WIS] = Integer.valueOf(value);
+                               break;
+                       case "exp":
+                               exp = Integer.valueOf(value);
+                               break;
+                       case "cash":
+                               gold = Long.valueOf(value);
+                               break;
+                       case "race":
+                               race = game.getRace(value);
+                               break;
+                       case "clan":
+                               clan = value;
+                               break;
+                       case "wield":
+                               wornItems.wear(Wearing.WIELD, parseWearable(value));
+                               break;
+                       case "arms":
+                               wornItems.wear(Wearing.ARMS, parseWearable(value));
+                               break;
+                       case "legs":
+                               wornItems.wear(Wearing.LEGS, parseWearable(value));
+                               break;
+                       case "torso":
+                               wornItems.wear(Wearing.TORSO, parseWearable(value));
+                               break;
+                       case "waist":
+                               wornItems.wear(Wearing.WAIST, parseWearable(value));
+                               break;
+                       case "neck":
+                               wornItems.wear(Wearing.NECK, parseWearable(value));
+                               break;
+                       case "skull":
+                               wornItems.wear(Wearing.SKULL, parseWearable(value));
+                               break;
+                       case "eyes":
+                               wornItems.wear(Wearing.EYES, parseWearable(value));
+                               break;
+                       case "hands":
+                               wornItems.wear(Wearing.HANDS, parseWearable(value));
+                               break;
+                       // FIXME: should it specify the item type?
+                       case "item":
+                               inventory.add(parseHoldable(value));
+                               break;
+
+                       //
+                       // any more missing?
+                       default:
+                               super.setProperty(name, value);
+                               break;
+               }
+               // FIXME: spells, skills, inventory?
+       }
+
+       @Override
+       protected void writeProperties(BufferedWriter out) throws IOException {
+               super.writeProperties(out);
+               for (int i = 0; i < stat_keys.length; i++) {
+                       writeProperty(out, stat_keys[i], stats[i]);
+               }
+               for (int i = 0; i < stat_keys.length; i++) {
+                       writeProperty(out, stat_keys[i] + "bon", bonus[i]);
+               }
+               writeProperty(out, "exp", exp);
+               writeProperty(out, "gold", gold);
+               writeProperty(out, "clan", clan);
+               // FIXME: hack for mobile, race doesn't apply
+               for (int i = 0; i < Wearing.WEARING_COUNT; i++) {
+                       Wearable w = wornItems.getWorn(i);
+                       if (w != null) {
+                               writeProperty(out, Wearing.wornNames[i], w.name + "," + w.durability + "," + w.uses);
+                       }
+               }
+               if (race != null)
+                       writeProperty(out, "race", race.name);
+               variables.writeProperties(out);
+               conditions.writeProperties(out);
+       }
+
+       public int getStat(int key) {
+               return stats[key] + race.stats[key] + bonus[key];
+       }
+
+       public void setStat(int key, int value) {
+               // TODO: range check?
+               stats[key] = value;
+       }
+
+       public void addStat(int key, int value) {
+               setStat(key, stats[key] + value);
+       }
+       
+       public int getBonus(int key) {
+               return bonus[key];
+       }
+
+       public int getHP() {
+               return getStat(STAT_HP);
+       }
+
+       public int getHPMax() {
+               return getStat(STAT_HPMAX);
+       }
+
+       public int getMP() {
+               return getStat(STAT_MP);
+       }
+
+       public int getMPMax() {
+               return getStat(STAT_MPMAX);
+       }
+
+       public int getSTR() {
+               return getStat(STAT_STR);
+       }
+
+       public int getINT() {
+               return getStat(STAT_INT);
+       }
+
+       public int getDEX() {
+               return getStat(STAT_DEX);
+       }
+
+       public int getCON() {
+               return getStat(STAT_CON);
+       }
+
+       public int getWIS() {
+               return getStat(STAT_WIS);
+       }
+
+       public int getRange() {
+               Weapon item = (Weapon) wornItems.getWorn(Wearing.WIELD);
+
+               return item != null ? item.range : 1;
+       }
+
+       public int getRangeWithBonus() {
+               return getRange() + bonus[STAT_RANGE];
+       }
+
+       public long getGold() {
+               return gold;
+       }
+
+       /**
+        * Add or remove gold to/from the purse.
+        *
+        * If removing (amouunt &lt; 0), the operation only proceeds if there
+        * is sufficient money available.
+        *
+        * @param amount
+        * @return true if the gold was added, false if removing the gold would lead
+        * to a deficit.
+        */
+       public boolean addGold(int amount) {
+               // FIXME: locks
+               if (gold + amount < 0)
+                       return false;
+               gold += amount;
+               return true;
+       }
+
+       public int getExp() {
+               return exp;
+       }
+
+       public boolean addExp(int amount) {
+               if (exp + amount < 0)
+                       return false;
+               // FIXME: locks? or is += atomic in java, hmm.
+               exp += amount;
+               return true;
+       }
+
+       public boolean isMoveable() {
+               return moveable;
+       }
+
+       public void setMoveable(boolean moveable) {
+               this.moveable = moveable;
+       }
+
+       public boolean isPlayer() {
+               return getType() == TYPE_PLAYER;
+       }
+
+       public boolean isFighting() {
+               return battle != null;
+       }
+
+       public boolean isSleeping() {
+               return sleeping;
+       }
+
+       public void setSleeping(boolean sleeping) {
+               if (this.sleeping != sleeping) {
+                       this.sleeping = sleeping;
+                       flagsChanged();
+               }
+       }
+
+       /**
+        * Whether the object is alive, e.g. connection, etc.
+        * not the same as player death, todo: rename it
+        *
+        * @return
+        */
+       public boolean isAlive() {
+               // FIXME: implement isAlive
+               return true;
+               //throw new UnsupportedOperationException();
+       }
+
+       public List<String> getActiveConditions() {
+               return conditions.getActiveConditions();
+       }
+
+       public int getBattleSide() {
+               if (battle != null)
+                       return battle.getSide(this);
+               return 0;
+       }
+
+       public int getSkillLevel(String name) {
+               Ability a = skills.get(name);
+               if (a != null)
+                       return a.getLevel();
+               return 0;
+       }
+
+       /**
+        * This thing caused damage to another player.
+        *
+        * This implements battle accounting and weapon damage
+        *
+        * @param amount
+        */
+       public void causedDamage(int amount) {
+               damageDone += amount;
+               weaponDamage(amount);
+       }
+
+       /**
+        * Invoked on attackor to age weapon
+        *
+        * @param amount
+        */
+       public void weaponDamage(int amount) {
+               Wearable item = wornItems.damageItem(Wearing.WIELD, amount);
+               if (item != null) {
+                       chatMessage("Your " + item.name + " breaks.");
+                       // FIXME: onUnwear(item);
+                       //if (isPlayer()) {
+                       //      updateEquipment();
+                       //      updateStats();
+                       //}
+               }
+       }
+
+       public void receivedDamage(int amount) {
+               setStat(STAT_HP, stats[STAT_HP] - amount);
+               armourDamage(amount);
+       }
+
+       public void armourDamage(int damage) {
+               int total = wornItems.armourCount();
+               if (total == 0) {
+                       return;
+               }
+
+               for (int i = Wearing.ARMS; i < Wearing.WEARING_COUNT; i++) {
+                       Wearable item = wornItems.damageItem(i, damage / total);
+                       if (item != null) {
+                               chatMessage("Your " + item.name + " breaks.");
+                               //FIXME: onUnwear(item);
+                               //updateStats();
+                               //updateEquipment();
+                       }
+               }
+       }
+
+       /**
+        * Process one queued move
+        */
+       public void moveTick() {
+               String dir;
+
+               synchronized (moveQueue) {
+                       if (moveQueue.isEmpty())
+                               return;
+
+                       dir = moveQueue.removeFirst();
+               }
+
+               System.out.println("move " + name + " tick: " + dir);
+
+               switch (dir.charAt(0)) {
+                       case 'n':
+                               moveTo(x, y - 1, 0);
+                               break;
+                       case 's':
+                               moveTo(x, y + 1, 2);
+                               break;
+                       case 'w':
+                               moveTo(x - 1, y, 4);
+                               break;
+                       case 'e':
+                               moveTo(x + 1, y, 6);
+                               break;
+               }
+       }
+
+       /**
+        * Called every 'item type' tick. Something like every 250ms.
+        *
+        * @param tick
+        */
+       public void tick(int tick) {
+               // Every second
+               if (tick % 4 == 0) {
+                       if (conditions.checkConditions(this, tick))
+                               conditionsChanged = tick;
+               }
+               // Every 18 seconds
+               if (tick % 73 == 0) {
+                       //if (lt.battle == null) {
+                       int hpinc, mpinc;
+                       if (isSleeping()) {
+                               hpinc = 3 + getCON();
+                               mpinc = 3 + getWIS();
+                       } else {
+                               hpinc = 1 + getCON() / 2;
+                               mpinc = 1 + getWIS() / 2;
+                       }
+                       setStat(STAT_HP, Math.min(stats[STAT_HP] + hpinc, stats[STAT_HPMAX]));
+                       setStat(STAT_MP, Math.min(stats[STAT_MP] + mpinc, stats[STAT_MPMAX]));
+                       //      lt.updateInfo();
+                       //      lt.savePlayer();
+                       //}
+               }
+               // TODO: move tick
+       }
+
+       /**
+        * Called at the end of a turn to update visibility to this object/fire off
+        * updates, etc.
+        *
+        * @param tick
+        */
+       public void visibilityTick(int tick) {
+       }
+       int viewRange = 6;
+
+       boolean visibleRange(int tx, int ty) {
+               return Math.abs(x - tx) <= viewRange
+                               && Math.abs(y - ty) <= viewRange;
+       }
+
+       /**
+        * Execute the canSeeActive script - can this active
+        * see the other at this time
+        *
+        * @param other
+        * @return
+        */
+       public boolean onCanSeeActive(Active other) {
+               System.out.println("not implemented: onCanSeeActive");
+               return true;
+       }
+
+       /**
+        * Execute the canMoveActive script - can this active
+        * occupy the same location as other
+        *
+        * @param blocking
+        * @return
+        */
+       public boolean onCanMoveActive(Active blocking) {
+               System.out.println("not implemented: onCanMoveActive");
+
+               return false;
+       }
+
+       /**
+        * Execute location and tile canSee scripts
+        *
+        * @param lx
+        * @param ly
+        * @return
+        */
+       public boolean onCanSee(int lx, int ly) {
+               return game.onLocationVisible(this, lx, ly, map.getTile(lx, ly));
+       }
+
+       /**
+        * Check if the player can see the location, uses a line of sight check
+        *
+        * @param tx
+        * @param ty
+        * @return
+        */
+       public boolean canSee(int tx, int ty) {
+               if (!visibleRange(tx, ty))
+                       return false;
+               //if (!blnLineOfSight) {
+               //      return true;
+               //}
+
+               for (TileMap.MapData md : map.look(x, y, tx, ty)) {
+                       if (!onCanSee(md.x, md.y))
+                               return false;
+               }
+               return true;
+       }
+
+       /**
+        * Can this active move to the given location.
+        *
+        * @param tx
+        * @param ty
+        * @return
+        */
+       public boolean canMoveTo(int tx, int ty) {
+
+               if (!this.map.inside(tx, ty))
+                       return false;
+
+               //System.out.printf("can move to: %d,%d tid=%3d on map %s\n", tx, ty, this.map.getTile(tx, ty), this.map.name);
+
+               for (Thing o : this.map.getEntities(tx, ty, null)) {
+                       if (o instanceof Active) {
+                               Active a = (Active) o;
+                               if (!onCanMoveActive(a))
+                                       return false;
+                       }
+               }
+
+               return game.onLocationAble(this, tx, ty, map.getTile(tx, ty));
+       }
+
+       abstract public void send(DuskMessage msg);
+
+       public abstract void chatMessage(Active from, String clan, String msg);
+
+       /**
+        * Send a message to player.
+        *
+        * @param msg if null, nothing is sent.
+        */
+       void chatMessage(String msg) {
+               if (msg != null)
+                       chatMessage(null, null, msg);
+       }
+
+       void chatBattle(String msg) {
+               // FIXME:P right code
+               chatMessage(null, null, msg);
+       }
+
+       /**
+        * Send a localised chat message from the given object
+        *
+        * @param msg
+        */
+       public void localisedChat(String msg) {
+               //log.printMessage(Log.ALWAYS, inMessage);
+
+               for (TileMap.MapData md : map.range(x, y, game.viewRange)) {
+                       for (Thing o : md.entities) {
+                               if (o.getType() == Thing.TYPE_PLAYER) {
+                                       Player p = (Player) o;
+
+                                       p.chatMessage(this, null, msg);
+                               }
+                       }
+               }
+       }
+
+       public List<Active> getPackMembers() {
+               // FIXME: threads
+               if (pack != null)
+                       return pack.members;
+               else
+                       return Arrays.asList(this);
+       }
+
+       public void travelTo(int destX, int destY, boolean goon) {
+               if (pack != null && !pack.isLeader(this)) {
+                       chatMessage("You can't move while you're following someone.");
+               }
+               // TODO: verify logic
+               //if (master != null) {
+               //      if (!isPet() || master.following == this) {
+               //              return "You can't move while you're following someone.";
+               //      }
+               //}
+               if (goon && !canMoveTo(destX, destY)) {
+                       chatMessage("You can't move onto that location.");
+                       return;
+               }
+               if (Math.abs(destX - x) > game.viewRange || Math.abs(destY - y) > game.viewRange) {
+                       chatMessage("Too far away");
+                       return;
+               }
+               synchronized (moveQueue) {
+                       moveQueue.clear();
+
+                       TileMap.MoveListener ml = new TileMap.MoveListener() {
+                               @Override
+                               public boolean canMoveto(TileMap.MapData md) {
+                                       return canMoveTo(md.x, md.y);
+                               }
+                       };
+                       int mflags = goon ? 0 : TileMap.SKIP_END;
+
+                       for (TileMap.MoveData md : map.move(x, y, destX, destY, mflags, ml)) {
+                               moveQueue.add(md.direction);
+                       }
+               }
+       }
+
+       public void enqueueMove(String dir) {
+               if (pack != null
+                               && !pack.isLeader(this)) {
+                       chatMessage("You can't move while following someone");
+                       return;
+               }
+               synchronized (moveQueue) {
+                       moveQueue.add(dir);
+               }
+       }
+
+       public boolean hasPendingMoves() {
+               synchronized (moveQueue) {
+                       return !moveQueue.isEmpty();
+               }
+       }
+
+       public void clearMoveQueue() {
+               synchronized (moveQueue) {
+                       moveQueue.clear();
+               }
+       }
+
+       /**
+        * Jump to a new location/map.
+        *
+        * No checks are performed on validity of jump
+        *
+        * @param map
+        * @param newx
+        * @param newy
+        * @returns true if the location or map changed
+        */
+       protected boolean jumpTo(TileMap map, int newx, int newy) {
+               if (this.map == map && newx == x && newy == y)
+                       return false;
+
+               // TODO: if this is the leader, warp everyone to the new location?
+
+               // FIXME: threads or something
+               if (this.map == (map)) {
+                       map.moveEntity(this, newx, newy);
+               } else {
+                       this.map.removeEntity(this);
+                       this.map = map;
+                       this.x = newx;
+                       this.y = newy;
+                       this.map.addEntity(this);
+               }
+               return true;
+       }
+
+       /**
+        * Move player to new location.
+        *
+        * TODO: it seems to me, that if this is a leader, it should
+        * move all it's followers right away without needing the
+        * recursion.
+        *
+        * @param newx
+        * @param newy
+        * @param newStep TODO: this should just be a direction code, other code
+        * can decide if it means changing the picture or something
+        * @return
+        */
+       protected boolean moveTo(int newx, int newy, int newStep) {
+               //System.out.printf("%s: move to %d,%d moveable=%s sleeping=%s can=%s\n",
+               //              name, newx, newy, moveable, sleeping, canMoveTo(newx, newy));
+               /**
+                * l    * Allow gods to move outside of map (probably not!)
+                */
+               if (privs < 5 && !map.inside(newx, newy))
+                       return false;
+               /**
+                * Check if we can move at all
+                */
+               if (privs <= 1 && (!moveable || isSleeping() || !canMoveTo(newx, newy)))
+                       return false;
+
+               int oldx = x;
+               int oldy = y;
+
+               map.moveEntity(this, newx, newy);
+
+               imageStep = newStep;
+
+               game.onLocationAction(this, x, y, map.getTile(x, y));
+
+               Active following = pack != null ? pack.getFollowing(this) : null;
+
+               //move follower
+               if (following != null) {
+                       following.followTo(this, oldx, oldy);
+               }
+
+               return true;
+       }
+
+       protected boolean followTo(Active leader, int oldx, int oldy) {
+               //don't move follower if leader has moved onto follower's tile
+               if ((x == leader.x) && (y == leader.y)) {
+                       return false;
+               } else if (Math.abs(x - oldx) + Math.abs(y - oldy) > 1) {
+                       leader.chatMessage(name + " is no longer following you.");
+                       chatMessage("You are no longer following " + leader.name + ".");
+
+                       pack.removeFollower(this);
+                       return false;
+               } else if (y > oldy) {
+                       moveTo(x, y - 1, 0); // n
+               } else if (y < oldy) {
+                       moveTo(x, y + 1, 2); // s
+               } else if (x > oldx) {
+                       moveTo(x - 1, y, 4); // w
+               } else if (x < oldx) {
+                       moveTo(x + 1, y, 6); // e
+               }
+               return true;
+       }
+
+       /**
+        * This is a script helper to find a nearby visible object by name
+        * or number.
+        *
+        * @param name if parseable as a number, used as ID instead.
+        * @return
+        */
+       protected Thing findVisibleObject(String name) {
+               try {
+                       long id = Long.valueOf(name);
+                       Thing t = game.getThing(id);
+                       boolean see = true;
+
+                       if (t instanceof Active) {
+                               see &= onCanSeeActive((Active) t);
+                       }
+
+                       if (see && canSee(t.x, t.y)) {
+                               return t;
+                       }
+                       return null;
+               } catch (NumberFormatException x) {
+               }
+
+               // search by name
+               for (TileMap.MapData md : map.range(x, y, game.viewRange)) {
+                       for (Thing t : md.entities) {
+                               if (t.name.equalsIgnoreCase(name)) {
+                                       boolean see = true;
+
+                                       if (t instanceof Active) {
+                                               see &= onCanSeeActive((Active) t);
+                                       }
+
+                                       if (see && canSee(t.x, t.y)) {
+                                               return t;
+                                       }
+                               }
+                       }
+               }
+               return null;
+       }
+
+       public void createBattle(Active enemy) {
+               if (canAttackEnemy(enemy))
+                       attackEnemy(enemy);
+       }
+
+       // can attackEnemy a given target?
+       protected boolean canAttackEnemy(Active enemy) {
+               String msg;
+
+               if (isSleeping()) {
+                       msg = ("You can't do that while you're sleeping");
+               } else if (isFighting()) {
+                       msg = ("You're already busy fighting!");
+               } else if (enemy.ID == this.ID) {
+                       msg = ("You can't fight yourself!");
+               } else if (enemy.getType() == TYPE_PET) {
+                       msg = ("You can't attack pets.");
+               } else if (distanceL1(enemy) > getRangeWithBonus()) {
+                       System.out.println("attack enemy =" + enemy.name + " distance =" + distanceL1(enemy) + " range = " + getRangeWithBonus());
+                       msg = ("They're too far away.");
+               } else if (!game.onCanAttack(this, enemy)) {
+                       msg = ("You can't attack them.");
+               } else {
+                       return true;
+               }
+
+               chatMessage(msg);
+               return false;
+       }
+
+       /**
+        * Attack enemy, must already have been checked using canAttackEnemy()
+        *
+        * @param enemy
+        */
+       protected void attackEnemy(Active enemy) {
+               if (!enemy.isFighting()) {
+                       game.addBattle(new Battle(game, this, enemy));
+               } else {
+                       int side = 3 - enemy.battle.getSide(enemy);
+
+                       for (Active a : getPackMembers()) {
+                               enemy.battle.addToBattle(a, side);
+                       }
+               }
+       }
+
+       public int getArmourMod() {
+               return wornItems.armourMod();
+       }
+
+       public int getArmourModWithBonus() {
+               return getArmourMod() + ((getDEX()) / 10) + bonus[STAT_ARC];
+       }
+
+       public int getDamageMod() {
+               Wearable item = wornItems.getWorn(Wearing.WIELD);
+               if (item != null)
+                       return item.mod;
+               return 100;
+       }
+
+       public int getDamageModWithBonus() {
+               return getDamageMod() + bonus[STAT_DAMAGE];
+       }
+
+       int getTotalSkillLevel() {
+               int total = 0;
+
+               for (Ability skill : skills.values())
+                       total += skill.getLevel();
+               for (Ability spell : spells.values())
+                       total += spell.getLevel();
+               return total;
+       }
+
+       /**
+        * Get character points
+        *
+        * @return
+        */
+       public int getCP() {
+               int result = stats[STAT_WIS]
+                               + stats[STAT_INT]
+                               + stats[STAT_STR]
+                               + stats[STAT_DEX]
+                               + stats[STAT_CON]
+                               + stats[STAT_HPMAX] / 10
+                               + stats[STAT_MPMAX] / 10
+                               + getTotalSkillLevel();
+               return result;
+       }
+
+       /**
+        * get total points
+        *
+        * @return
+        */
+       public int getTP() {
+               int result = getCP()
+                               + getArmourMod()
+                               + (getDamageMod() - 100);
+               return result;
+       }
+
+       /**
+        * Test skill against a random hit rate
+        *
+        * @param name
+        * @return
+        */
+       public boolean testSkill(String name) {
+               return Math.random() * 100 + 1 < getSkillLevel(name);
+       }
+
+       protected int damageRoll(Active target, int range) {
+               int attackerTotal = getSTR();
+
+               double damage = (attackerTotal / 2.0)
+                               * (Math.random() + 0.5)
+                               * (getDamageModWithBonus() / 100)
+                               - (target.getArmourModWithBonus());
+
+               return (int) damage;
+       }
+
+       protected boolean dodgeRoll(Active target, int attackModifier) {
+               int attackedTotal = Math.min(100, target.getDEX());
+               int attackerTotal = Math.min(100, this.getDEX() + attackModifier);
+
+               return ((Math.random() * 100)
+                               < ((target.getSkillLevel("Dodge") * .75)
+                               + (.25 * (attackedTotal - attackerTotal))));
+       }
+
+       protected void performAttack(Battle battle, Active target, int range, StringBuilder s) {
+               // The farther away the target, the harder they are to hit.
+               // The more skilled the attecker is in ranged combat, the less range affects ability to hit.
+               // The farther away the target, the easier it is for them to dodge.
+               // The more skilled the attecker is in ranged combat, the harder it is for the target to dodge.
+               int r2 = 0;
+               if (range > 1) {
+//                     r2= thnAttacking.getSkill("ranged combat") - (int)((range * range - 1) * (double)(100/engGame.viewrange));
+                       r2 = this.getSkillLevel("ranged combat");
+               } else {
+                       r2 = this.getSkillLevel("close combat");
+               }
+               if (r2 < 0) {
+                       s.append(this.name).append(" missed.");
+                       battle.hitMessage(this, target, 0, "Missed!");
+               } else if (dodgeRoll(target, r2)) {
+                       s.append(target.name).append(" dodged ").append(this.name).append("'s attack");
+                       battle.hitMessage(this, target, 0, "Dodged!");
+               } else {
+                       // FIXME: audio
+                       //if (game.battlesound != -1) {
+                       //      game.playSound(attackor.map, game.battlesound, attackee.x, attackee.y);
+                       //}
+                       int i = damageRoll(target, range);
+                       if (i < 0) {
+                               i = 0;
+                       }
+                       s.append(this.name + " did " + i + " to " + target.name);
+
+                       target.receivedDamage(i);
+                       this.causedDamage(i);
+
+                       battle.hitMessage(this, target, i, "Hit!");
+               }
+       }
+
+       void splitMoney(double modifier, ArrayList<Active> opponents) {
+               int lost = (int) (modifier * gold);
+               if (lost > 0) {
+                       chatBattle("You have lost " + lost + " gp.");
+
+                       // FIXME: synchronised
+                       addGold(-lost);
+                       int share = lost / opponents.size();
+                       for (Active a : opponents) {
+                               a.addGold(share);
+                               // TODO: virtual method for bounty?
+                               if (a.isPlayer() && share != 0) {
+                                       a.chatMessage("You get " + share + " gp.");
+                               }
+                       }
+               }
+       }
+
+       void splitExp(double modifier, ArrayList<Active> opponents) {
+               int lost = (int) (modifier * this.exp);
+
+               this.chatBattle("You have lost " + lost + " exp.");
+               this.addExp(-lost);
+               this.damageDone = 0;
+
+               double totalPpoints, sidepoints = 0;
+
+               totalPpoints = this.getTP();
+               for (Active a : opponents) {
+                       sidepoints += a.getTP();
+               }
+
+               for (Active a : opponents) {
+                       // TODO: virtual method for bounty?
+                       if (!(a.getType() == TYPE_MOBILE)) {
+                               double damageMod = Math.min(getHPMax(), a.damageDone);
+                               double gained = (game.expGainMod
+                                               * (((totalPpoints / sidepoints)
+                                               + (2.0 * damageMod / getHPMax() * totalPpoints / a.getTP())) / 3.0));
+                               int igained = (int) gained;
+                               a.chatMessage("You get " + igained + " exp.");
+                               a.addExp(igained);
+                       }
+                       a.damageDone = 0;
+               }
+       }
+
+       public void enterBattle(Battle battle) {
+               assert (this.battle == null);
+
+               this.battle = battle;
+               moveable = false;
+               flagsChanged();
+       }
+
+       /**
+        * If fighting, add the command to the fighting command queue,
+        *
+        * Certain command issued during battle are handled from the
+        * battle even loop.
+        *
+        * TODO: is this just for some sort of thread-save
+        * synchronisation purpose, or does it have a better reason?
+        *
+        * It is useful for flee command, but otherwise?
+        *
+        * @param cmd
+        * @return true if the command was queued
+        */
+       boolean addBattleCommand(String... cmd) {
+               if (isFighting()) {
+                       synchronized (battleCommands) {
+                               battleCommands.add(cmd);
+                       }
+                       return true;
+               }
+               return false;
+       }
+
+       boolean addExpiditedBattleCommand(String... cmd) {
+               if (isFighting()) {
+                       synchronized (battleCommands) {
+                               battleCommands.addFirst(cmd);
+                       }
+                       return true;
+               }
+               return false;
+       }
+
+       /**
+        * Retrieve the next pending battle command
+        *
+        * @return
+        */
+       String[] pollBattleCommand() {
+               synchronized (battleCommands) {
+                       if (battleCommands.isEmpty())
+                               return null;
+                       return battleCommands.remove();
+               }
+
+       }
+
+       /**
+        * Called during battle to implement a single attack
+        *
+        * @param battle
+        * @param target
+        */
+       public void attack(Battle battle, Active target, int range) {
+               if (range > getRangeWithBonus()) {
+                       // out of range
+                       battle.chatMessage(name + " is out of range");
+               } else {
+                       StringBuilder msg = new StringBuilder();
+
+                       performAttack(battle, target, range, msg);
+                       if (testSkill("double attack")) {
+                               msg.append(",");
+                               performAttack(battle, target, range, msg);
+                       }
+                       if (testSkill("triple attack")) {
+                               msg.append(",");
+                               performAttack(battle, target, range, msg);
+                       }
+                       if (testSkill("quadruple attack")) {
+                               msg.append(",");
+                               performAttack(battle, target, range, msg);
+                       }
+                       msg.append(".");
+                       battle.chatMessage(msg.toString());
+               }
+       }
+
+       /**
+        * Called during battle to flee.
+        *
+        * This will end the battle participation of this player.
+        *
+        * @param battle
+        * @param opponents
+        */
+       public void fleeBattle(Battle battle, ArrayList<Active> opponents) {
+               clearFollow();
+
+               // FIXME: chatBattle
+               chatMessage("You have fled from battle");
+               splitMoney(game.goldFleeMod, opponents);
+               splitExp(game.expFleeMod, opponents);
+
+               endBattle();
+               //updateInfo();
+               //updateStats();
+               //updateActions();
+               //playMusic(0);
+       }
+
+       /**
+        * Called at the end of each battle turn. Includes a list of changes to the battlefield during
+        * that time.
+        *
+        * For updating clients with flags and shit.
+        *
+        * @param battle
+        * @param joined objects joined battle
+        * @param left objects left battle
+        */
+       public void endBattleTurn(Battle battle, ArrayList<Active> joined, ArrayList<Active> left) {
+       }
+
+       /**
+        * Lets the active know that an opponent has left the battle.
+        *
+        * @param battle
+        * @param loser
+        */
+       public void leftBattle(Battle battle, Active loser) {
+       }
+
+       /**
+        * Thing was killed in battle, distribute bounty to opponents
+        *
+        * @param battle
+        * @param winner
+        * @param opponents
+        */
+       abstract public void killedBattle(Battle battle, Active winner, ArrayList<Active> opponents);
+
+       public void endBattle() {
+               this.battle = null;
+
+               moveable = true;
+
+               flagsChanged();
+               // update everything
+               //      lt.clearFlags();
+               //      lt.battle = null;
+               //      lt.battleSide = 0;
+               //      lt.isMoveable = true;
+               //      lt.updateInfo();
+               //      lt.updateStats();
+               //      lt.updateActions();
+               //      lt.playMusic(0);
+       }
+
+       public boolean isCanLead() {
+               return canLead;
+       }
+
+       public void follow(Active master) {
+               String msg;
+
+               if (isSleeping()) {
+                       msg = "You can't do that while you're sleeping";
+               } else if (distanceL1(master) > 1) {
+                       msg = ("They're too far away.");
+               } else if (pack != null) {
+                       msg = ("You're already following someone. Leave them first.");
+               } else if (this.ID == master.ID) {
+                       msg = ("You can't follow yourself.");
+               } else if (!isCanLead()) {
+                       msg = ("They won't let you follow them.");
+               } else {
+                       if (master.pack == null) {
+                               master.pack = new Pack();
+                               master.pack.addFollower(master);
+                       }
+                       pack = master.pack;
+                       pack.addFollower(this);
+                       // set pack changed?
+                       return;
+               }
+
+               chatMessage(msg);
+       }
+
+       void followCommand(String whom) {
+               Thing thing = findVisibleObject(whom);
+               String msg = null;
+
+               if (thing == null) {
+                       msg = "You don't see that here.";
+               } else if (thing instanceof Active) {
+                       follow((Active) thing);
+               } else {
+                       msg = "That's not something you can follow.";
+               }
+
+               chatMessage(msg);
+       }
+
+       /**
+        * Leave the group if you're in one
+        */
+       void leaveCommand() {
+               if (pack != null) {
+                       Active leader = pack.getLeader(this);
+                       if (leader != this) {
+                               pack.removeFollower(this);
+                               pack.removeFollower(leader);
+                               pack = null;
+                               leader.pack = null;
+                       }
+               }
+       }
+
+       /**
+        * Force thing to be removed from the pack
+        *
+        * TODO: check it's needed
+        */
+       @Deprecated
+       public void clearFollow() {
+               if (pack != null)
+                       pack.members.remove(this);
+       }
+
+       protected void flagsChanged() {
+               flagsChanged = game.getClock();
+       }
+
+       public boolean jumpTo(String mapName, String mapAlias) {
+               System.out.println("script: jumpto(" + mapName + ", " + mapAlias + ")");
+               TileMap newmap = game.getMap(mapName);
+
+               if (newmap != null) {
+                       Location l = newmap.locationForAlias(mapAlias);
+
+                       if (l != null) {
+                               return jumpTo(newmap, l.x, l.y);
+                       }
+               }
+               return false;
+       }
+
+       /* *
+        * So all this 'get the player object to implemeent the commands' seemed
+        * like a good idea, but the code will bloat.
+        * 
+        * having it reusable is useful for scripts though.
+        * 
+        * Perhaps it shoudl be put into antoher class for reusability stake.
+        */
+       public void fleeCommand() {
+               if (isFighting()) {
+                       synchronized (battleCommands) {
+                               battleCommands.addFirst(new String[]{"flee"});
+                       }
+               } else {
+                       chatMessage("You're not fighting anyone");
+               }
+       }
+
+       public void sleepCommand() {
+               String msg = null;
+
+               if (isSleeping()) {
+                       // otherwise i get spew from tired script: might need to think script i/face
+                       //msg = "You are already asleep";
+               } else if (isFighting()) {
+                       msg = "Not while you're fighting!";
+               } else if (pack != null && !pack.getLeader(this).isSleeping()) {
+                       msg = "You can't sleep if you're following someone who's awake.";
+               } else {
+                       msg = "You fall asleep.";
+                       setSleeping(true);
+               }
+
+               chatMessage(msg);
+       }
+
+       public void awakeCommand() {
+               String msg;
+
+               if (isSleeping()) {
+                       setSleeping(false);
+                       msg = "You wake up";
+               } else {
+                       msg = "You are already awake";
+               }
+               chatMessage(msg);
+       }
+
+       public void gossipCommand(String text) {
+               // noop, see player
+               chatMessage("Only players can use the gossip/clan/tell channels.");
+       }
+
+       public void sayCommand(String text) {
+               // noop, see player
+       }
+
+       public void attackCommand(String who) {
+               String msg;
+
+               Thing enemy = findVisibleObject(who);
+               if (enemy == null) {
+                       msg = ("You don't see that here.");
+               } else if (!(enemy instanceof Active)) {
+                       msg = ("You can't fight that.");
+               } else {
+                       this.createBattle((Active) enemy);
+                       return;
+               }
+
+               chatMessage(msg);
+       }
+
+       public void lookCommand(String what) {
+               // TODO: put all this 'check sleeping' etc into a common function with
+               // a list of things to check.
+               if (isSleeping()) {
+                       chatMessage("You can't do that while you're sleeping");
+               } else {
+                       Thing thing = findVisibleObject(what);
+
+                       if (thing == null) {
+                               chatMessage("You don't see that here.");
+                       } else {
+                               thing.look(this);
+                       }
+               }
+       }
+
+       @Override
+       public void look(Active viewer) {
+               chatMessage(viewer.name + " is looking at you.");
+               viewer.chatMessage(name + " has " + getCP() + "cp and " + getHP() + "/" + getHPMax() + "hp.");
+               if (description != null) {
+                       viewer.chatMessage("Their description is: " + description);
+               }
+               final String[] formats = {
+                       "They are wielding %s.",
+                       "They are wearing %s on their arms.",
+                       "They are wearing %s on their legs.",
+                       "They are wearing %s on their torso.",
+                       "They are wearing %s on their waist.",
+                       "They are wearing %s on their neck.",
+                       "They are wearing %s on their skull.",
+                       "They are wearing %s on their eyes.",
+                       "They are wearing %s on their hands."
+               };
+               for (int i = 0; i < formats.length; i++) {
+                       Wearable item = wornItems.getWorn(i);
+                       if (item != null)
+                               viewer.chatMessage(String.format(formats[i], item.description));
+               }
+       }
+
+       /**
+        * Wear the item. The item must be in the active's current inventory.
+        *
+        * The item will be worn at the correct location.
+        *
+        * @param w
+        */
+       public void wearItem(Wearable w) {
+               // FIXME: threads.
+               if (inventory.contains(w)) {
+                       Wearable old = wornItems.wear(w.getWearing(), w);
+
+                       if (old != null) {
+                               inventory.add(old);
+                       }
+                       inventory.remove(w);
+               }
+       }
+
+       public void wearCommand(String what) {
+               Holdable h = inventory.get(what);
+               String msg = null;
+               if (h != null) {
+                       if (h instanceof Wearable) {
+                               wearItem((Wearable) h);
+                       } else {
+                               msg = "You'd look pretty stupid trying to wear that!";
+                       }
+               } else
+                       msg = "You don't have that";
+
+               chatMessage(msg);
+       }
+
+       /**
+        * Unwear item at position index. This is always called for the unwear
+        * command.
+        *
+        * @param index
+        */
+       public void unwearAt(int index) {
+               Wearable w = wornItems.unwear(index);
+
+               // fIXME: onunwear/etc.
+               if (w != null)
+                       inventory.add(w);
+       }
+
+       /**
+        * Allows un-wearing by position or name.
+        *
+        * @param what
+        */
+       public void unwearCommand(String what) {
+               if (what.equalsIgnoreCase("all")) {
+                       for (int i = 0; i < Wearing.WEARING_COUNT; i++) {
+                               unwearAt(i);
+                       }
+               } else {
+                       int index = Equipment.toIndex(what);
+
+                       if (index == -1)
+                               index = wornItems.getWornIndex(what);
+
+                       if (index != -1) {
+                               unwearAt(index);
+                       } else {
+                               chatMessage("You're not wearing that.");
+                       }
+               }
+       }
+
+       public void getCommand(String what) {
+               Thing thing = findVisibleObject(what);
+               String msg = null;
+
+               if (thing == null)
+                       msg = "You don't see that here.";
+               else if (!(thing instanceof Holdable))
+                       msg = "You can't take that.";
+               else if (distanceL1(thing) >= 2)
+                       msg = "That's too far away.";
+               else {
+                       Holdable item = (Holdable) thing;
+
+                       game.removeThing(thing);
+                       inventory.add(item);
+
+                       game.onItem(this, item, "get");
+               }
+
+               chatMessage(msg);
+       }
+
+       public void dropCommand(String what) {
+               Holdable h = inventory.get(what);
+               String msg = null;
+
+               if (h == null) {
+                       if (wornItems.isWearing(what)) {
+                               msg = ("You're wearing that, you cannot drop it.");
+                       } else {
+                               msg = ("You don't have that");
+                       }
+               } else if (game.haveOnItem(h, "drop")) {
+                       game.onItem(this, h, "drop");
+               } else {
+                       inventory.remove(h);
+                       if (h.cost == 0) {
+                               msg = h.name + " vanishes into thin air.";
+                       } else {
+                               game.addThing(h, map, x, y);
+                       }
+               }
+
+               chatMessage(msg);
+       }
+
+       public void useCommand(String what) {
+               Holdable item = inventory.get(what);
+               String msg = null;
+
+               // TODO: check type?
+
+               if (item == null)
+                       msg = "You don't have that.";
+               else if (!game.haveOnItem(item, "use"))
+                       msg = "That cannot be used.";
+               else if (item.uses == 0)
+                       msg = "That is used up.";
+               else {
+                       if (item.uses != -1)
+                               item.uses--;
+                       game.onItem(this, item, "use");
+               }
+
+               chatMessage(msg);
+       }
+
+       public void drinkCommand(String what) {
+               Holdable item = inventory.get(what);
+
+               if (item != null && item.getType() != TYPE_DRINK) {
+                       chatMessage("You can't drink that.");
+               } else
+                       useCommand(what);
+       }
+
+       public void eatCommand(String what) {
+               Holdable item = inventory.get(what);
+
+               if (item != null && item.getType() != TYPE_FOOD) {
+                       chatMessage("You can't eat that.");
+               } else
+                       useCommand(what);
+       }
+
+       public void castCommand(String name) {
+               throw new UnsupportedOperationException();
+       }
+
+       public Shop visitingShop() {
+               for (Thing thing : map.getEntities(x, y, null)) {
+                       if (thing.getType() == TYPE_PLAYER_SHOP
+                                       || thing.getType() == TYPE_GAME_SHOP) {
+                               return (Shop) thing;
+                       }
+               }
+               return null;
+       }
+
+       void buyCommand(String what, int quantity) {
+               Shop shop = visitingShop();
+               if (shop != null) {
+                       shop.buy(this, what, quantity);
+               } else {
+                       chatMessage("Buy from whom?");
+               }
+       }
+
+       void sellCommand(String what, int quantity) {
+               Shop shop = visitingShop();
+               if (shop != null) {
+                       shop.sell(this, inventory.getAll(what, quantity));
+               } else {
+                       chatMessage("Sell from whom?");
+               }
+       }
+
+       /**
+        * This is the public script API ... probably want to move it to another wrapper class for security reasons
+        */
+       public void emote(String value) {
+               // "thing value"
+               chatMessage(value);
+       }
+
+       public void chat(String value) {
+               // "thing says value" ??
+               chatMessage(value);
+       }
+
+       public void setCondition(String name, int duration) {
+               Condition c = new Condition(name, duration);
+               conditions.setCondition(c);
+       }
+
+       public void addINT(int inc) {
+               addStat(STAT_INT, inc);
+       }
+
+       public void addSTR(int inc) {
+               addStat(STAT_STR, inc);
+       }
+
+       public List<Holdable> getAllItems(String name) {
+               return inventory.getAll(name, Integer.MAX_VALUE);
+       }
+
+       public void addItem(Holdable item) {
+               if (item != null) {
+                       inventory.add(item);
+                       game.onItem(this, item, "get");
+               }
+       }
+
+       /**
+        * Remove the item from the inventory.
+        *
+        * @param item
+        * @return true if the item was in the inventory and could be removed.
+        */
+       public boolean removeItem(Holdable item) {
+               // FIXME: what if worn?
+               if (item != null) {
+                       return inventory.remove(item);
+               }
+               return false;
+       }
+
+       public void sleep() {
+               sleepCommand();
+       }
+
+       public int getInteger(String key) {
+               return variables.getInteger(key, 0);
+       }
+
+       public void setInteger(String key, int value) {
+               variables.put(key, value);
+       }
+
+       /**
+        * check if the active has the item inventory or worn
+        *
+        * @param key
+        * @return
+        */
+       public boolean isOwner(String key) {
+               return inventory.get(key) != null
+                               || wornItems.isWearing(key);
+       }
+
+       public boolean isWearing(String key) {
+               return wornItems.isWearing(key);
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Armour.java b/DuskServer/src/duskz/server/entityz/Armour.java
new file mode 100644 (file)
index 0000000..c9edd10
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.Wearing;
+import java.io.BufferedWriter;
+import java.io.IOException;
+
+/**
+ * Defense
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Armour extends Wearable {
+
+       /**
+        * Position the armour is worn, from Wearable
+        */
+       int worn;
+
+       public Armour(Game game) {
+               super(game);
+       }
+
+       public int getType() {
+               return TYPE_ARMOUR;
+       }
+
+       @Override
+       public int getWearing() {
+               return worn;
+       }
+
+       @Override
+       void setProperty(String name, String value) {
+               switch (name) {
+                       case "worn":
+                               worn = Wearing.getCode(value);
+                               break;
+                       default:
+                               super.setProperty(name, value);
+               }
+       }
+
+       @Override
+       protected void writeProperties(BufferedWriter out) throws IOException {
+               super.writeProperties(out);
+
+               writeProperty(out, "worn", Wearing.wornNames[worn]);
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Battle.java b/DuskServer/src/duskz/server/entityz/Battle.java
new file mode 100644 (file)
index 0000000..1637f06
--- /dev/null
@@ -0,0 +1,352 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+/**
+ * Changes
+ * Feb-2013 Michael Zucchi - Pretty major cleanup and parameterisation of code.
+ * Mar-2013 Michael Zucchi - changed server protocol
+ * Mar-2013 Michael Zucchi - big refactor, moved most of the logic to Active
+ *
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.DuskProtocol;
+import duskz.protocol.ListMessage;
+import java.util.ArrayList;
+
+/**
+ * Battle represents a fight between two sides comprised of Actives.
+ *
+ * TODO: Remove the general chat stuff and use specific message types where appropriate
+ *
+ * @author Tom Weingarten
+ */
+public class Battle implements DuskProtocol {
+
+       private ArrayList<Active> side1 = new ArrayList<>(),
+                       side2 = new ArrayList<>();
+       private Game game;
+       private boolean fighting = true,
+                       playerInSide1 = false,
+                       playerInSide2 = false;
+       /**
+        * Track players who left during this turn
+        */
+       private ArrayList<Active> left = new ArrayList<>();
+       /**
+        * Players who joined during the last turn
+        */
+       private ArrayList<Active> joined = new ArrayList<>();
+
+       public Battle(Game game, Active inpla1, Active inpla2) {
+               try {
+                       this.game = game;
+
+                       for (Active a : inpla2.getPackMembers()) {
+                               playerInSide2 = addToBattle(2, side2, side1, a, playerInSide2, playerInSide1);
+                       }
+                       for (Active a : inpla1.getPackMembers()) {
+                               playerInSide1 = addToBattle(1, side1, side2, a, playerInSide1, playerInSide2);
+                       }
+
+                       inpla1.localisedChat("-" + inpla1.name + " has attacked " + inpla2.name);
+                       //      FIXME: inpla1.startBattle(inpla2);
+                       //      inpla2.startBattle(inpla1);
+               } catch (Exception e) {
+                       fighting = false;
+                       System.out.println("erorr in battle new: " + e);
+                       //      game.log.printError("Battle()", e);
+               }
+       }
+
+       public boolean isFighting() {
+               return fighting;
+       }
+
+       public int getSide(Active thing) {
+               if (side1.contains(thing))
+                       return 1;
+               if (side2.contains(thing))
+                       return 2;
+               return 0;
+       }
+
+       /*
+        * Used in scripts
+        */
+       public static Active getEnemy(Active lt) {
+               return null;
+               //      if (lt.battleSide == 1) {
+               //              return (Active) lt.battle.vctSide2.get(0);
+               //      } else if (lt.battleSide == 2) {
+               //              return (Active) lt.battle.vctSide1.get(0);
+               //      } else {
+               //              return null;
+               //      }
+       }
+
+       public void addToBattle(Active lt, int side) {
+               if (side == 1) {
+                       playerInSide1 = addToBattle(1, side1, side2, lt, playerInSide1, playerInSide2);
+               } else {
+                       playerInSide2 = addToBattle(2, side2, side1, lt, playerInSide2, playerInSide1);
+               }
+       }
+
+       boolean addToBattle(int sideid, ArrayList<Active> side, ArrayList<Active> opponents, Active added, boolean playerSide, boolean playerOpponent) {
+               if (added.isPlayer()) {
+                       // This is already checked: do I need to do it again??
+                       // FIXME: yes i do, or some variation of it.
+
+                       /*
+                        if (playerOpponent) {
+                        if (added.clan.equals("none")) {
+                        added.chatMessage("Players who are not in clans cannot fight other players.");
+                        added.removeFromGroup();
+                        return playerSide;
+                        }
+                        Active thnStore = side.get(0);
+                        if (thnStore.clan.equals("none")) {
+                        added.chatMessage("Players who are not in clans cannot fight other players.");
+                        added.removeFromGroup();
+                        return playerSide;
+                        }
+                        }*/
+                       playerSide = true;
+                       // FIXME: put in enterBattle?
+                       //if (game.blnMusic) {
+                       //      added.playMusic(1);
+                       //}
+               }
+               chatMessage(added.name + " has joined the battle.");
+               side.add(added);
+
+               added.enterBattle(this);
+
+               joined.add(added);
+
+               return playerSide;
+       }
+
+       /**
+        * Attack everything in side1 against side2
+        *
+        * @param list1
+        * @param list2
+        * @return true if the battle continues
+        */
+       boolean attackSide(ArrayList<Active> list1, ArrayList<Active> list2, ArrayList<Active> fled) {
+               int range;
+               Active target;
+               StringBuilder msg = new StringBuilder();
+
+               checkCommands(list1, list2, fled);
+               if (list1.isEmpty()) {
+                       endBattle();
+                       return false;
+               }
+
+               for (Active attackor : list1) {
+                       msg.setLength(0);
+
+                       // run attackEnemy script, now in attackor.attack()
+                       //attackor.onFight();
+
+                       /**
+                        * Find the nearest target.
+                        *
+                        * Allow them to move if they are not in
+                        * direct contact with any opponent.
+                        *
+                        * TODO: should this use the attack range instead?
+                        *
+                        */
+                       range = Integer.MAX_VALUE;
+                       target = null;
+                       for (Active t : list2) {
+                               int distance = attackor.distanceL1(t);
+                               if (distance < range) {
+                                       range = distance;
+                                       target = t;
+                               }
+                       }
+                       attackor.setMoveable(range > 1);
+
+                       /*
+                        End the battle if the closest target is off of the screen.
+                        */
+                       if (range > game.viewRange) {
+                               endBattle();
+                               return false;
+                       }
+
+                       attackor.attack(this, target, range);
+               }
+               return true;
+       }
+
+       void endBattle(ArrayList<Active> list) {
+               for (Active lt : list) {
+                       lt.endBattle();
+               }
+       }
+
+       void endBattle() {
+               fighting = false;
+               chatMessage("You have won the battle.");
+               endBattle(side1);
+               endBattle(side2);
+       }
+
+       void flee(Active thing, ArrayList<Active> list, ArrayList<Active> opponents, ArrayList<Active> fled) {
+               fled.add(thing);
+               list.remove(thing);
+
+               thing.fleeBattle(this, opponents);
+
+               if (side2.size() == 0 || side1.size() == 0) {
+                       endBattle();
+                       return;
+               }
+       }
+
+       void chatMessage(ArrayList<Active> side1, ArrayList<Active> side2, String msg) {
+               for (Active a : side1) {
+                       a.chatBattle(msg);
+               }
+       }
+
+       void chatMessage(String strStore) {
+               chatMessage(side1, side2, strStore);
+               chatMessage(side2, side1, strStore);
+       }
+
+       void battleMessage(ArrayList<Active> side1, ListMessage lm) {
+               for (Active a : side1) {
+                       a.send(lm);
+                       //} else if (a.isPet()) {
+                       //      if (a.getMaster().battle != a.battle) {
+               }
+       }
+
+       /**
+        * New battle interface with more detailed messages
+        *
+        * @param type
+        * @param msg
+        */
+       void battleMessage(ListMessage msg) {
+               battleMessage(side1, msg);
+               battleMessage(side2, msg);
+       }
+
+       void hitMessage(Active attackor, Active attackee, int delta, String what) {
+               ListMessage lm = new ListMessage(MSG_BATTLE_UPDATE);
+
+               lm.add(FIELD_BATTLE_TARGET, attackee.ID);
+               lm.add(FIELD_BATTLE_DAMAGE, delta);
+               lm.add(FIELD_BATTLE_HP, attackee.getHP());
+               lm.add(FIELD_BATTLE_MAXHP, attackee.getHPMax());
+               lm.add(FIELD_BATTLE_SOURCE, attackor.ID);
+               lm.add(FIELD_BATTLE_WHAT, what);
+               battleMessage(lm);
+       }
+
+       void checkCommands(ArrayList<Active> list, ArrayList<Active> opponents, ArrayList<Active> fled) {
+               for (Active lt : list) {
+                       String[] argv = lt.pollBattleCommand();
+
+                       if (argv != null) {
+                               switch (argv[0]) {
+                                       case "flee":
+                                               flee(lt, list, opponents, fled);
+                                               break;
+                                       case "cast":
+                                               lt.castCommand(argv[1]);
+                                               break;
+                                       case "use":
+                                               lt.useCommand(argv[1]);
+                                               break;
+                                       case "eat":
+                                               lt.eatCommand(argv[1]);
+                                               break;
+                                       case "drink":
+                                               lt.drinkCommand(argv[1]);
+                                               break;
+                               }
+                       }
+               }
+       }
+
+       void checkDeath(ArrayList<Active> list1, ArrayList<Active> list2, ArrayList<Active> killed) {
+               // FIXME: verify this works.  this protects the second call 
+               if (list1.isEmpty() || list2.isEmpty())
+                       return;
+
+               Active front1 = list1.get(0);
+               Active front2 = list2.get(0);
+
+               if (front2.getHP() <= 0 || !front2.isAlive()) {
+                       list2.remove(0);
+
+                       killed.add(front2);
+
+                       front2.killedBattle(this, front1, list1);
+               }
+       }
+
+       void endTurn(ArrayList<Active> list1, ArrayList<Active> joined, ArrayList<Active> left) {
+               for (Active a : list1) {
+                       a.endBattleTurn(this, joined, left);
+               }
+       }
+
+       public void run() {
+
+               if (!attackSide(side1, side2, left)) {
+                       return;
+               }
+               if (!attackSide(side2, side1, left)) {
+                       return;
+               }
+
+               checkDeath(side1, side2, left);
+               checkDeath(side2, side1, left);
+
+               if (side2.isEmpty() || side1.isEmpty()) {
+                       endBattle();
+                       return;
+               }
+
+               endTurn(side1, joined, left);
+               endTurn(side2, joined, left);
+       }
+
+       /**
+        * Must only be called from a battle callback or internally
+        *
+        * @param thing
+        */
+       void removeParticipant(Active thing) {
+               side1.remove(thing);
+               side2.remove(thing);
+               left.remove(thing);
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Commands.java b/DuskServer/src/duskz/server/entityz/Commands.java
new file mode 100644 (file)
index 0000000..4799972
--- /dev/null
@@ -0,0 +1,319 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+/**
+ * Changes
+ * Feb-2013 Michael Zucchi - modernised java
+ * Mar-2013 Michael Zucchi - changed server protocol
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.DuskProtocol;
+import java.util.ArrayList;
+
+/**
+ * Command parser.
+ *
+ * This only implements unprivileged commands, each user only
+ * gets a command parser suitable for their privilege level.
+ *
+ * Each command parser is attached to a single Active.
+ *
+ * FIXME: merge with PlayerCommands
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Commands implements DuskProtocol {
+
+       private Active lt;
+
+       public Commands(Active lt) {
+               this.lt = lt;
+       }
+
+       /**
+        * Simple command line parser, handles quotes (' or "),
+        * literal escapes within or without quotes, and stripping
+        * whitespace.
+        *
+        * @param cmdLine
+        * @return list of unquoted and separated arguments
+        */
+       protected String[] parseArgs(String cmdLine) {
+               StringBuilder arg = new StringBuilder();
+               ArrayList<String> args = new ArrayList<>();
+
+               char q = 0;
+               int e = 0;
+               for (int i = 0; i < cmdLine.length(); i++) {
+                       char c = cmdLine.charAt(i);
+
+                       if (e > 0) {
+                               // escape
+                               arg.append(c);
+                               e--;
+                       } else if (q == 0) {
+                               // unquoted text
+                               if (c == '"' || c == '\'') {
+                                       q = c;
+                               } else if (c == ' ' || c == '\t') {
+                                       if (arg.length() != 0) {
+                                               args.add(arg.toString());
+                                               arg.setLength(0);
+                                       }
+                               } else if (c == '\\') {
+                                       e = 1;
+                               } else {
+                                       arg.append(c);
+                               }
+                       } else {
+                               // quoted text
+                               if (c == q) {
+                                       q = 0;
+                                       if (arg.length() != 0) {
+                                               args.add(arg.toString());
+                                               arg.setLength(0);
+                                       }
+                               } else if (c == '\\') {
+                                       e = 1;
+                               } else {
+                                       arg.append(c);
+                               }
+                       }
+               }
+               if (arg.length() != 0) {
+                       args.add(arg.toString());
+               }
+
+               return args.toArray(new String[args.size()]);
+       }
+       static final String alias_key = ";:'./";
+       static final String alias_values[] = {"gossip", "clan", "say", "emote", "tell"};
+
+       public void execute(String cmdline) throws Exception {
+               String argv[] = parseArgs(cmdline);
+
+               if (argv.length < 1) {
+                       lt.chatMessage("Huh?");
+                       return;
+               }
+
+               // TODO: scripted commands here
+               String cmd; // command name
+               String text; // all arguments unprocessed
+
+               int alias = alias_key.indexOf(argv[0].charAt(0));
+               if (alias >= 0) {
+                       cmd = alias_values[alias];
+                       // TODO: strip thing from cmdline properly
+                       text = cmdline.substring(1).trim();
+               } else {
+                       cmd = argv[0].toLowerCase();
+                       text = cmdline.substring(cmd.length()).trim();
+               }
+
+               String msg = null;
+
+               switch (cmd) {
+                       case "north":
+                       case "n":
+                               lt.clearMoveQueue();
+                               lt.enqueueMove("n");
+                               return;
+                       case "south":
+                       case "s":
+                               lt.clearMoveQueue();
+                               lt.enqueueMove("s");
+                               return;
+                       case "west":
+                       case "w":
+                               lt.clearMoveQueue();
+                               lt.enqueueMove("w");
+                               return;
+                       case "east":
+                       case "e":
+                               lt.clearMoveQueue();
+                               lt.enqueueMove("e");
+                               return;
+                       case "goto": {
+                               int destX;
+                               int destY;
+                               try {
+                                       destX = Integer.parseInt(argv[1]);
+                                       destY = Integer.parseInt(argv[2]);
+                                       lt.travelTo(destX, destY, true);
+                               } catch (NumberFormatException e) {
+                                       msg = "goto requires two numbers.";
+                               }
+                               break;
+                       }
+                       case "flee":
+                               lt.fleeCommand();
+                               break;
+                       case "sleep":
+                               lt.sleepCommand();
+                               break;
+                       case "wake":
+                               lt.awakeCommand();
+                               break;
+                       case "gossip":
+                               if (text.length() == 0) {
+                                       msg = "Gossip what?";
+                               } else if (text.length() > lt.game.messageLimit)
+                                       msg = "That message was goo long.";
+                               else {
+                                       lt.gossipCommand(text);
+                               }
+                               break;
+                       /// clan
+                       case "say":
+                               if (text.length() == 0) {
+                                       msg = "Say what?";
+                               } else if (text.length() > lt.game.messageLimit) {
+                                       msg = "That message was goo long.";
+                               } else {
+                                       lt.sayCommand(text);
+                               }
+                               break;
+                       case "kill":
+                       case "attack":
+                       case "a":
+                               if (argv.length != 2) {
+                                       msg = "Attack what?";
+                               } else {
+                                       lt.attackCommand(argv[1]);
+                               }
+                               break;
+                       case "look":
+                               if (argv.length != 2) {
+                                       msg = "Look at what?";
+                               } else {
+                                       lt.lookCommand(argv[1]);
+                               }
+                               break;
+                       case "wear": {
+                               if (argv.length < 2) {
+                                       msg = "Wear what?";
+                               } else {
+                                       for (int i = 1; i < argv.length; i++) {
+                                               lt.wearCommand(argv[i]);
+                                       }
+                               }
+                               break;
+                       }
+                       case "unwear": {
+                               if (argv.length < 2) {
+                                       msg = "Unwear what?";
+                               } else {
+                                       for (int i = 1; i < argv.length; i++) {
+                                               lt.unwearCommand(argv[i]);
+                                       }
+                               }
+                               break;
+                       }
+                       case "drop": {
+                               if (argv.length < 2) {
+                                       msg = "Drop what?";
+                               } else {
+                                       for (int i = 1; i < argv.length; i++) {
+                                               lt.dropCommand(argv[i]);
+                                       }
+                               }
+                               break;
+                       }
+                       case "get": {
+                               if (argv.length < 2) {
+                                       msg = "Get what?";
+                               } else {
+                                       for (int i = 1; i < argv.length; i++) {
+                                               lt.getCommand(argv[i]);
+                                       }
+                               }
+                               break;
+                       }
+                       case "use": {
+                               // TODO: battle check inside active?
+                               if (argv.length < 2) {
+                                       msg = "Use what?";
+                               } else if (lt.isFighting()) {
+                                       lt.addBattleCommand(argv);
+                               } else {
+                                       for (int i = 1; i < argv.length; i++) {
+                                               lt.useCommand(argv[i]);
+                                       }
+                               }
+                               break;
+                       }
+                       case "follow": {
+                               if (argv.length < 2) {
+                                       msg = "Follow whom?";
+                               } else {
+                                       lt.followCommand(argv[1]);
+                               }
+                               break;
+                       }
+                       case "leave": {
+                               lt.leaveCommand();
+                               break;
+                       }
+                       case "buy": {
+                               if (argv.length < 2) {
+                                       msg = "Buy what?";
+                               } else if (argv.length < 3) {
+                                       msg = "How many of what do you wish to buy?";
+                               } else {
+                                       try {
+                                               int quantity = Integer.valueOf(argv[1]);
+
+                                               if (quantity <= 0) {
+                                                       msg = "Quanitity must be at least one.";
+                                               } else
+                                                       lt.buyCommand(argv[2], quantity);
+                                       } catch (NumberFormatException ex) {
+                                               msg = "I don't know how many that is.";
+                                       }
+                               }
+                               break;
+                       }
+                       case "sell": {
+                               if (argv.length < 2) {
+                                       msg = "Sell what?";
+                               } else if (argv.length < 3) {
+                                       msg = "How many of what do you wish to sell?";
+                               } else {
+                                       try {
+                                               int quantity = Integer.valueOf(argv[1]);
+
+                                               if (quantity <= 0) {
+                                                       msg = "Quanitity must be at least one.";
+                                               } else
+                                                       lt.sellCommand(argv[2], quantity);
+                                       } catch (NumberFormatException ex) {
+                                               msg = "I don't know how many that is.";
+                                       }
+                               }
+                               break;
+                       }
+               }
+               
+               if (msg != null)
+                       lt.chatMessage(msg);
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Condition.java b/DuskServer/src/duskz/server/entityz/Condition.java
new file mode 100644 (file)
index 0000000..55bd81b
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import java.util.Objects;
+
+/**
+ * Conditions are *user-visible* things that occur for a time,
+ * they can invoke scripts for start/occurance/finish.
+ *
+ * Scripts and code can use other mechanisms for hidden values.
+ *
+ * Script actions are located by convention based on the name.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Condition {
+
+       /**
+        * Name of description.
+        */
+       public String name;
+       /**
+        * Long description
+        */
+       //public String description;
+       /**
+        * Total duration (left, after reload) of condition
+        */
+       int duration;
+       /**
+        * How many ticks between triggering an occurance, <= 0 to disable
+        */
+       int rate = 1;
+       /**
+        * Start of condition, -1 means new condition.
+        */
+       int start = -1;
+       int end = -1;
+
+       public Condition() {
+       }
+
+       public Condition(String name, int duration) {
+               this.name = name;
+               this.duration = duration;
+       }
+
+       public Condition(String name, int duration, int rate) {
+               this.name = name;
+               this.duration = duration;
+               this.rate = rate;
+       }
+
+       void onStart(Active host) {
+               host.game.onCondition(host, this, "start");
+       }
+
+       void onOccurance(Active host) {
+               host.game.onCondition(host, this, "tick");
+       }
+
+       void onEnd(Active host) {
+               host.game.onCondition(host, this, "end");
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof Condition) {
+                       Condition c = (Condition) obj;
+
+                       return c.name.equals(name);
+               } else
+                       return false;
+       }
+
+       @Override
+       public int hashCode() {
+               int hash = 3;
+               hash = 17 * hash + Objects.hashCode(this.name);
+               return hash;
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/ConditionList.java b/DuskServer/src/duskz/server/entityz/ConditionList.java
new file mode 100644 (file)
index 0000000..2ed0916
--- /dev/null
@@ -0,0 +1,136 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class ConditionList {
+
+       private final HashMap<String, Condition> conditions = new HashMap<>();
+       private final List<Condition> endedConditions = new ArrayList<>();
+
+       /* ***************
+        * Conditions
+        * ***************/
+       public synchronized void setCondition(Condition c) {
+               Condition old = conditions.put(c.name.toLowerCase(), c);
+               c.start = -1;
+
+               if (old != null)
+                       endedConditions.add(old);
+       }
+
+       public List<String> getActiveConditions() {
+               return new ArrayList<>(conditions.keySet());
+       }
+
+       /**
+        * Clear a condition by name.
+        *
+        * @param name
+        * @return true if there are no active conditions
+        */
+       public synchronized boolean clearCondition(String name) {
+               Condition old = conditions.remove(name.toLowerCase());
+               if (old != null) {
+                       endedConditions.add(old);
+               }
+               return conditions.isEmpty();
+       }
+
+       /**
+        * Iterate through all conditions running their actions.
+        *
+        * This also handles starting, occurance, and ending conditions
+        *
+        * @return true if the set of active conditions changed.
+        */
+       public synchronized boolean checkConditions(Active host, int tick) {
+               Condition[] list = conditions.values().toArray(new Condition[conditions.size()]);
+               boolean changed = false;
+
+               // End the pending ones
+               for (Condition c : endedConditions) {
+                       c.onEnd(host);
+                       changed = true;
+               }
+               endedConditions.clear();
+
+               for (Condition c : list) {
+                       if (c.start == -1) {
+                               c.start = tick;
+                               c.end = tick + c.duration;
+                               c.onStart(host);
+                               changed = true;
+                       }
+                       if (c.rate > 0 && ((tick - c.start) % c.rate) == 0) {
+                               c.onOccurance(host);
+                       }
+                       if (tick >= c.end) {
+                               conditions.remove(c.name);
+                               c.onEnd(host);
+                               changed = true;
+                       }
+               }
+
+               return changed;
+       }
+
+       /**
+        * i/o
+        */
+       public boolean setProperty(String name, String value) {
+               if (name.equals("condition")) {
+                       int c1 = value.indexOf(',');
+                       int c2 = value.indexOf(',', c1 + 1);
+
+                       Condition c = new Condition();
+                       c.name = value.substring(0, c1);
+                       c.duration = Integer.valueOf(value.substring(c1 + 1, c2));
+                       c.rate = Integer.valueOf(value.substring(c2 + 1));
+
+                       conditions.put(c.name.toLowerCase(), c);
+                       return true;
+               }
+               return false;
+       }
+
+       public void writeProperties(BufferedWriter out) throws IOException {
+               for (Condition c : conditions.values()) {
+                       out.write("condition=");
+
+                       // FIXME: i need the duration remaining, which is now - start?
+                       out.write(c.name);
+                       out.write(',');
+                       out.write(String.valueOf(c.duration));
+                       out.write(',');
+                       out.write(String.valueOf(c.rate));
+                       out.write('\n');
+               }
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Container.java b/DuskServer/src/duskz/server/entityz/Container.java
new file mode 100644 (file)
index 0000000..ddabc4d
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.Wearing;
+
+/**
+ * A container that can hold other items - simply increases carrying capacity.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Container extends Holdable {
+
+       public Container(Game game) {
+               super(game);
+       }
+       int volumeLimit;
+       int weightLimit;
+
+       @Override
+       public int getType() {
+               return TYPE_CONTAINER;
+       }
+
+       @Override
+       public int getWearing() {
+               // FIXME: food?
+               return Wearing.INVENTORY;
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Converter.java b/DuskServer/src/duskz/server/entityz/Converter.java
new file mode 100644 (file)
index 0000000..e367e40
--- /dev/null
@@ -0,0 +1,550 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.Wearing;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Convert old things to new things
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Converter {
+
+       static void add(BufferedWriter dst, String name, String value) {
+       }
+
+       public static class PropWriter extends BufferedWriter {
+
+               public PropWriter(Writer out) {
+                       super(out);
+               }
+
+               void add(String name, String value) throws IOException {
+                       append(name);
+                       append("=");
+                       append(value);
+                       append('\n');
+               }
+       }
+
+       static void addWear(String name, BufferedReader in, PropWriter dst) throws IOException {
+               dst.add(name, in.readLine().toLowerCase() + "," + in.readLine() + "," + in.readLine());
+       }
+
+       static boolean parsePlayer(String type, BufferedReader in, PropWriter dst) throws IOException {
+               switch (type.toLowerCase()) {
+                       case "timestamp":
+                       case "privs":
+                       case "clan":
+                       case "race":
+                       case "title":
+                       case "description":
+                       case "map":
+                       case "x":
+                       case "y":
+                       case "hp":
+                       case "maxhp":
+                       case "mp":
+                       case "maxmp":
+                       case "cash":
+                       case "exp":
+                       case "image":
+                               dst.add(type, in.readLine());
+                               break;
+                       case "petname": // but it has pet too?  which one?
+                       case "pet":
+                               dst.add("pet", in.readLine());
+                               break;
+                       case "master":
+                               dst.add(type, in.readLine());
+                               break;
+                       case "skill":
+                               dst.add(type, in.readLine() + "," + in.readLine());
+                               //type = in.readLine();
+                               //addToSkill(type, Byte.parseByte(in.readLine()));
+                               break;
+                       case "condition": {
+                               // name,passed,duration
+                               dst.add(type, in.readLine() + "," + in.readLine() + "," + in.readLine());
+                               // FIXME: i/o to Condition
+                               //      Condition cndStore = game.getCondition(in.readLine());
+                               //      cndStore.ticksPast = Integer.parseInt(in.readLine());
+                               //      cndStore.duration = Integer.parseInt(in.readLine());
+                               //      if (cndStore.duration < -1) { // only necessary to repair
+                               //              cndStore.duration = -1; // after bug fix, can go away
+                               //      }
+                               //      addCondition(cndStore);
+                               break;
+                       }
+                       case "item": {
+                               dst.add(type, in.readLine());
+                               //Item itmStore = game.getItem(in.readLine());
+                               //if (itmStore != null) {
+                               //      itemList.addElement(itmStore);
+                               //}
+                               break;
+                       }
+                       case "item2": {
+                               addWear("item", in, dst);
+                               //for compatibility. Will be replaced with item later
+                               //Item itmStore = game.getItem(in.readLine());
+                               //if (itmStore != null) {
+                               //      itmStore.lngDurability = Long.parseLong(in.readLine());
+                               //      itmStore.intUses = Integer.parseInt(in.readLine());
+                               //      itemList.addElement(itmStore);
+                               //}
+                               break;
+                       }
+
+                       case "sp":
+                               dst.add("mp", in.readLine());
+                               break;
+                       case "maxsp":
+                               dst.add("maxmp", in.readLine());
+                               break;
+                       case "stre":
+                       case "str":
+                               dst.add("str", in.readLine());
+                               break;
+                       case "inte":
+                       case "int":
+                               dst.add("int", in.readLine());
+                               break;
+                       case "dext":
+                               dst.add("dex", in.readLine());
+                               break;
+                       case "cons":
+                               dst.add("con", in.readLine());
+                               break;
+                       case "wisd":
+                               dst.add("wis", in.readLine());
+                               break;
+                       // FIXME: do i need these 'old' versions?
+                       case "wield":
+                       case "arms":
+                       case "legs":
+                       case "torso":
+                       case "waist":
+                       case "neck":
+                       case "skull":
+                       case "eyes":
+                       case "hands":
+                               dst.add(type.toLowerCase(), in.readLine() + ",,");
+                               break;
+                       case "wield2":
+                               addWear("wield", in, dst);
+                               break;
+                       case "arms2":
+                               addWear("arms", in, dst);
+                               break;
+                       case "legs2":
+                               addWear("legs", in, dst);
+                               break;
+                       case "torso2":
+                               addWear("torso", in, dst);
+                               break;
+                       case "waist2":
+                               addWear("waist", in, dst);
+                               break;
+                       case "neck2":
+                               addWear("neck", in, dst);
+                               break;
+                       case "skull2":
+                               addWear("skull", in, dst);
+                               break;
+                       case "eyes2":
+                               addWear("eyes", in, dst);
+                               break;
+                       case "hands2":
+                               addWear("hands", in, dst);
+                               break;
+                       case "nofollow":
+                               dst.add(type, "true");
+                               break;
+                       case "nopopup":
+                       case "audiooff":
+                       case "coloroff":
+                               break;
+                       case "nochannel":
+                               dst.add(type, in.readLine());
+                               break;
+                       default:
+                               return false;
+               }
+               return true;
+       }
+
+       static boolean parseMob(String type, BufferedReader in, PropWriter dst) throws IOException {
+               // everything in player, plus ...
+
+
+               switch (type.toLowerCase()) {
+                       case "bravery":
+                       case "grouprelation":
+                       case "faction":
+                               dst.add(type, in.readLine());
+                               break;
+                       case "giveitem":
+                               dst.add(type, in.readLine().toLowerCase() + "," + in.readLine());
+                               break;
+                       case "givegp":
+                               dst.add("givegold", in.readLine());
+                               break;
+                       // nice: mob format is different ... 
+                       case "wield":
+                       case "arms":
+                       case "legs":
+                       case "torso":
+                       case "waist":
+                       case "neck":
+                       case "skull":
+                       case "eyes":
+                       case "hands":
+                               addWear(type.toLowerCase(), in, dst);
+                               break;
+                       case "onbattle":
+                               dst.add("onBattle", in.readLine());
+                               break;
+                       default:
+                               return false;
+               }
+               return true;
+       }
+
+       static boolean parseRace(String type, BufferedReader in, PropWriter dst) throws IOException {
+               switch (type) {
+                       case "hp":
+                       case "mp":
+                       case "image":
+                               dst.add(type, in.readLine());
+                               break;
+                       case "stre":
+                       case "str":
+                               dst.add("str", in.readLine());
+                               break;
+                       case "inte":
+                       case "int":
+                               dst.add("int", in.readLine());
+                               break;
+                       case "dext":
+                               dst.add("dex", in.readLine());
+                               break;
+                       case "cons":
+                               dst.add("con", in.readLine());
+                               break;
+                       case "wisd":
+                               dst.add("wis", in.readLine());
+                               break;
+                       // i don't think these are used.
+                       case "range":
+                       case "hp_limit":
+                       case "mp_limit":
+                       case "exp_limit":
+                       case "stre_limit":
+                       case "inte_limit":
+                       case "dext_limit":
+                       case "cons_limit":
+                       case "wisd_limit":
+                       default:
+                               return false;
+               }
+               return true;
+       }
+
+       static void convertPlayer(File src, File dst) throws IOException {
+               System.out.println("converting: " + src);
+               try (BufferedReader in = new BufferedReader(new FileReader(src));
+                               PropWriter out = new PropWriter(new FileWriter(dst))) {
+                       String line;
+
+                       out.add("name", src.getName());
+                       if (!src.getName().equals("default"))
+                               out.add("password", in.readLine());
+
+                       while ((line = in.readLine()) != null) {
+                               line = line.trim();
+                               if (line.equals("."))
+                                       break;
+                               if (line.equals("") || line.startsWith("#"))
+                                       continue;
+                               if (!parsePlayer(line, in, out)) {
+                                       System.out.println(" unknown parameter: " + line);
+                               }
+                       }
+               }
+               //      engGame.log.printError("parseUserFile():Parsing \"" + strStore + "\" from " + entityName + "'s file", e);
+       }
+
+       static void convertMob(File src, File dst) throws IOException {
+               System.out.println("converting: " + src);
+               try (BufferedReader in = new BufferedReader(new FileReader(src));
+                               PropWriter out = new PropWriter(new FileWriter(dst))) {
+                       String line;
+
+                       out.add("name", src.getName());
+
+                       while ((line = in.readLine()) != null) {
+                               line = line.trim();
+                               if (line.equals("."))
+                                       break;
+                               if (line.equals("") || line.startsWith("#"))
+                                       continue;
+                               if (!(parseMob(line, in, out)
+                                               || parsePlayer(line, in, out))) {
+                                       System.out.println(" unknown parameter: " + line);
+                               }
+                       }
+               }
+               //      engGame.log.printError("parseUserFile():Parsing \"" + strStore + "\" from " + entityName + "'s file", e);
+       }
+
+       static void convertRace(File src, File dst) throws IOException {
+               System.out.println("converting: " + src);
+               try (BufferedReader in = new BufferedReader(new FileReader(src));
+                               PropWriter out = new PropWriter(new FileWriter(dst))) {
+                       String line;
+
+                       out.add("name", src.getName());
+
+                       while ((line = in.readLine()) != null) {
+                               line = line.trim();
+                               if (line.equals("."))
+                                       break;
+                               if (line.equals("") || line.startsWith("#"))
+                                       continue;
+                               if (!parseRace(line, in, out)) {
+                                       System.out.println(" unknown parameter: " + line);
+                               }
+                       }
+               }
+       }
+
+       static boolean parseItem(String type, BufferedReader in, Holdable item) throws IOException {
+               switch (type) {
+                       case "type":
+                               // discard type
+                               in.readLine();
+                               break;
+                       case "description":
+                               item.description = in.readLine();
+                               break;
+                       case "kind":
+                               String w = in.readLine().toLowerCase();
+                               switch (w) {
+                                       case "arms":
+                                               ((Armour) item).worn = Wearing.ARMS;
+                                               break;
+                                       case "legs":
+                                               ((Armour) item).worn = Wearing.LEGS;
+                                               break;
+                                       case "torso":
+                                               ((Armour) item).worn = Wearing.TORSO;
+                                               break;
+                                       case "waist":
+                                               ((Armour) item).worn = Wearing.WAIST;
+                                               break;
+                                       case "neck":
+                                               ((Armour) item).worn = Wearing.NECK;
+                                               break;
+                                       case "skull":
+                                               ((Armour) item).worn = Wearing.SKULL;
+                                               break;
+                                       case "eyes":
+                                               ((Armour) item).worn = Wearing.EYES;
+                                               break;
+                                       case "hands":
+                                               ((Armour) item).worn = Wearing.HANDS;
+                                               break;
+                                       default:
+                                               System.out.println("  unknown armour location: " + w);
+                                               break;
+                               }
+                               break;
+                       case "cost":
+                               item.cost = Integer.valueOf(in.readLine());
+                               break;
+                       case "durability":
+                               ((Wearable) item).durability = Long.valueOf(in.readLine());
+                               break;
+                       case "uses":
+                               item.uses = Integer.valueOf(in.readLine());
+                               break;
+                       case "mod": {
+                               int mod = Integer.valueOf(in.readLine());
+                               if (item instanceof Wearable) {
+                                       ((Wearable) item).mod = mod;
+                               } else if (mod != 0) {
+                                       System.out.println(" setting mod on non-wearable item = " + mod);
+                               }
+                               break;
+                       }
+                       case "range":
+                               ((Weapon) item).range = Integer.valueOf(in.readLine());
+                               break;
+                       case "image":
+                               item.image = Integer.valueOf(in.readLine());
+                               break;
+                       case "onuse":
+                               item.onUse = in.readLine();
+                               break;
+                       case "onwear":
+                               ((Wearable) item).onWear = in.readLine();
+                               break;
+                       case "onunwear":
+                               ((Wearable) item).onUnwear = in.readLine();
+                               break;
+                       case "onget":
+                               item.onGet = in.readLine();
+                               break;
+                       case "ondrop":
+                               item.onDrop = in.readLine();
+                               break;
+                       default:
+                               return false;
+               }
+               return true;
+       }       //      engGame.log.printError("parseUserFile():Parsing \"" + strStore + "\" from " + entityName + "'s file", e);
+
+       static void convertItem(File src, File dst) throws IOException {
+               System.out.println("converting: " + src);
+               String type = null;
+               // First find type
+               try (BufferedReader in = new BufferedReader(new FileReader(src))) {
+                       String line;
+
+                       while ((line = in.readLine()) != null) {
+                               if (line.toLowerCase().equals("type")) {
+                                       type = in.readLine().trim().toLowerCase();
+                               }
+                       }
+               }
+               if (type == null)
+                       type = "item";
+               //type = armor
+               //type = drink
+               //type = food
+               //type = item
+               //type = weapon
+
+               Holdable item;
+               switch (type) {
+                       case "armor":
+                               item = new Armour(null);
+                               break;
+                       case "drink":
+                               item = new Drink(null);
+                               break;
+                       case "food":
+                               item = new Food(null);
+                               break;
+                       case "item":
+                               item = new Item(null);
+                               break;
+                       case "weapon":
+                               item = new Weapon(null);
+                               break;
+                       default:
+                               System.out.println("unknown type: " + type);
+                               return;
+               }
+
+               //System.out.println(" type = " + type);
+
+               // hang on, items don't have an x/y when saved ...
+               TileMap mainmap = new TileMap("main", 0, 0);
+
+               try (BufferedReader in = new BufferedReader(new FileReader(src));
+                               BufferedWriter out = new BufferedWriter(new FileWriter(dst))) {
+                       String line;
+
+                       item.name = src.getName();
+
+                       while ((line = in.readLine()) != null) {
+                               line = line.trim();
+                               if (line.equals("."))
+                                       break;
+                               if (line.equals("") || line.startsWith("#"))
+                                       continue;
+                               if (!parseItem(line.toLowerCase(), in, item)) {
+                                       System.out.println(" unknown parameter: " + line);
+                               }
+                       }
+
+                       item.map = mainmap;
+
+                       out.append("type.");
+                       out.append(item.getClass().getSimpleName());
+                       out.append("=");
+                       out.append(item.name);
+                       out.append('\n');
+                       
+                       item.writeProperties(out);
+
+                       out.append("=end");
+               }
+       }
+
+       static public void main(String[] args) {
+               File old = new File("/home/notzed/src/DuskRPG/DuskFiles/DuskX");
+               File game = new File("/home/notzed/dusk/game");
+
+               if (true) {
+                       for (File f : new File(old, "users").listFiles()) {
+                               try {
+                                       convertPlayer(f, new File(game, "players/" + f.getName()));
+                               } catch (IOException ex) {
+                                       Logger.getLogger(Converter.class.getName()).log(Level.SEVERE, null, ex);
+                               }
+                       }
+                       for (File f : new File(old, "defMobs").listFiles()) {
+                               try {
+                                       convertMob(f, new File(game, "defMobs/" + f.getName()));
+                               } catch (IOException ex) {
+                                       Logger.getLogger(Converter.class.getName()).log(Level.SEVERE, null, ex);
+                               }
+                       }
+                       for (File f : new File(old, "defRaces").listFiles()) {
+                               try {
+                                       convertRace(f, new File(game, "defRaces/" + f.getName()));
+                               } catch (IOException ex) {
+                                       Logger.getLogger(Converter.class.getName()).log(Level.SEVERE, null, ex);
+                               }
+                       }
+               }
+               for (File f : new File(old, "defItems").listFiles()) {
+                       try {
+                               convertItem(f, new File(game, "defItems/" + f.getName()));
+                       } catch (Exception ex) {
+                               ex.printStackTrace(System.out);
+                       }
+               }
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Drink.java b/DuskServer/src/duskz/server/entityz/Drink.java
new file mode 100644 (file)
index 0000000..ab6f597
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.Wearing;
+
+/**
+ * Liquid consumables
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Drink extends Holdable {
+
+       public Drink(Game game) {
+               super(game);
+       }
+
+       public int getType() {
+               return TYPE_DRINK;
+       }
+
+       @Override
+       public int getWearing() {
+               // FIXME: food?
+               return Wearing.INVENTORY;
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Equipment.java b/DuskServer/src/duskz/server/entityz/Equipment.java
new file mode 100644 (file)
index 0000000..81986b5
--- /dev/null
@@ -0,0 +1,226 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+/**
+ * Changes
+ * Feb-2013 Michael Zucchi - modernised java, big refactor.
+ * Mar-2013 Michael Zucchi - changed server protocol
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.DuskMessage;
+import duskz.protocol.ListMessage;
+import duskz.protocol.Wearing;
+import static duskz.protocol.Wearing.*;
+
+/**
+ * Equipment contains all the Items a LivingThing is wearing.
+ *
+ * @author Tom Weingarten
+ */
+public class Equipment {
+
+       // Must match Wearing
+       public static final String[] USER_NAMES = {
+               "wield2", "arms2", "legs2", "torso2", "waist2", "neck2", "skull2", "eyes2", "hands2"
+       };
+       public static final float[] ARMOUR_MOD = {
+               0f, 0.05f, 0.05f, 0.40f, 0.15f, 0.05f, 0.20f, 0.05f, 0.05f
+       };
+       private Wearable[] worn = new Wearable[WEARING_COUNT];
+
+       /**
+        * Encode for network.
+        *
+        * @return
+        */
+       public String toEntity() {
+               StringBuilder sb = new StringBuilder();
+
+               for (int i = 0; i < worn.length; i++) {
+                       Wearable item = worn[i];
+                       if (item != null)
+                               sb.append(item.name).append('\n');
+                       else
+                               sb.append("none\n");
+               }
+               return sb.toString();
+       }
+
+       public DuskMessage toMessage(int msgid) {
+               ListMessage msg = new ListMessage(msgid);
+
+               for (int i = 0; i < worn.length; i++) {
+                       Wearable item = worn[i];
+                       if (item != null)
+                               msg.add(i, item.name);
+               }
+
+               return msg;
+       }
+
+       public Wearable getWorn(int where) {
+               return worn[where];
+       }
+       
+       public int getWornIndex(String what) {
+               for (int i=0;i<worn.length;i++) {
+                       Wearable item = worn[i];
+                       
+                       if (item != null && what.equalsIgnoreCase(item.name))
+                               return i;
+               }
+               return -1;
+       }
+
+       public boolean isWearing(String what) {
+               for (Wearable item : worn) {
+                       if (item != null && what.equalsIgnoreCase(item.name))
+                               return true;
+               }
+               return false;
+       }
+
+       /**
+        * Where a sepecific item
+        *
+        * @param where
+        * @param item if null, the item at position 'where' is removed, if any
+        * @return
+        */
+       public Wearable wear(int where, Wearable item) {
+               if (item != null) {
+                       if (item.getWearing() != where) {
+                               // FIXME: cachable exception?
+                               throw new RuntimeException("can't wear an item " + item.name
+                                               + " type " + item.getClass().getSimpleName()
+                                               + " at " + Wearing.wornNames[where]
+                                               + " should be " + Wearing.wornNames[item.getWearing()]);
+                       }
+               }
+               Wearable old = worn[where];
+
+               worn[where] = item;
+               return old;
+       }
+
+       /**
+        * Unwear item at specific location
+        *
+        * @param where
+        * @return item if worn
+        */
+       public Wearable unwear(int where) {
+               Wearable old = worn[where];
+
+               worn[where] = null;
+               return old;
+       }
+       
+       /**
+        * Unwear an item of a sepcific name
+        *
+        * @param name
+        * @return item if worn
+        */
+       public Wearable unwearByName(String name) {
+               for (int i = 0; i < worn.length; i++) {
+                       Wearable item = worn[i];
+
+                       if (item != null && name.equalsIgnoreCase(item.name)) {
+                               worn[i] = null;
+                               return item;
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Convert to wield type index
+        *
+        * @param where
+        * @return
+        */
+       public static int toIndex(String where) {
+               switch (where.toLowerCase()) {
+                       case "wielded":
+                               return WIELD;
+                       case "arms":
+                               return ARMS;
+                       case "legs":
+                               return LEGS;
+                       case "torso":
+                               return TORSO;
+                       case "waist":
+                               return WAIST;
+                       case "neck":
+                               return NECK;
+                       case "skull":
+                               return SKULL;
+                       case "eyes":
+                               return EYES;
+                       case "hands":
+                               return HANDS;
+               }
+               return -1;
+       }
+
+       public int armourCount() {
+               int count = 0;
+               for (int i = ARMS; i < worn.length; i++) {
+                       if (worn[i] != null)
+                               count++;
+               }
+               return count;
+       }
+
+       public int armourMod() {
+               int mod = 0;
+
+               for (int i = 0; i < worn.length; i++) {
+                       Wearable item = worn[i];
+
+                       if (item != null) {
+                               mod += ARMOUR_MOD[i] * item.mod;
+                       }
+               }
+               return mod;
+       }
+
+       /**
+        * Damage the specified item if it exists
+        *
+        * @param damage
+        * @return the weapon if it was destroyed
+        */
+       public Wearable damageItem(int index, int damage) {
+               Wearable item = worn[index];
+
+               if (item != null && item.durability != -1) {
+                       item.durability -= damage;
+                       if (item.durability < 0) {
+                               worn[index] = null;
+                               return item;
+                       }
+               }
+               return null;
+       }
+       // TODO: load/save functions here too?
+}
diff --git a/DuskServer/src/duskz/server/entityz/Faction.java b/DuskServer/src/duskz/server/entityz/Faction.java
new file mode 100644 (file)
index 0000000..e00a56c
--- /dev/null
@@ -0,0 +1,289 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+/**
+ * Changes
+ * Feb-2013 Michael Zucchi - Moved to new object structure/clean up.
+ */
+/**
+ * a Faction represents a group of mobs.
+ *
+ * @author Tom Weingarten
+ */
+package duskz.server.entityz;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * Faction is a group of mobs. TODO: should it override/merge the normal group thing
+ */
+public class Faction {
+
+       final public String name;
+       /**
+        * This is keyed on both player names and clans. Seems it should be two
+        * separate tables
+        */
+       private final HashMap<String, Relation> relations = new HashMap<>();
+       private Game game;
+       private boolean changed = false;
+
+       public Faction(String name, Game game) {
+               this.name = name;
+               this.game = game;
+       }
+
+       public void load() throws IOException {
+               // FIXME: chagne faction format
+               File file = new File(game.getRoot(), "factions/" + name);
+               try (BufferedReader br = new BufferedReader(new FileReader(file))) {
+                       String line = br.readLine();
+                       while (line != null && !line.equals(".")) {
+                               if (line.equalsIgnoreCase("relation")) {
+                                       Relation r = new Relation(br.readLine(), Double.valueOf(br.readLine()));
+                                       relations.put(r.name, r);
+                               }
+                               line = br.readLine();
+                       }
+               }
+       }
+
+       synchronized void saveFactionData() {
+               /*
+                ** Only save faction data if it has changed.
+                */
+               if (!changed) {
+                       return;
+               }
+               File file = new File(game.getRoot(), "factions/" + name);
+               try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
+                       for (Relation r : relations.values()) {
+                               bw.write("relation\n" + r.name + "\n" + r.level + "\n");
+                       }
+                       bw.write(".\n");
+                       changed = false;
+               } catch (IOException e) {
+                       //game.log.printError("saveFactionData()", e);
+               }
+       }
+
+       double getRelationValue(String name) {
+               Relation r = relations.get(name.toLowerCase());
+
+               return r != null ? r.level : 0;
+       }
+
+       Relation getRelation(String name) {
+               return relations.get(name.toLowerCase());
+       }
+
+       boolean updateRelation(String name, int delta) {
+               Relation rel = getRelation(name);
+
+               if (rel == null) {
+                       rel = new Relation(name, 0);
+                       relations.put(name, rel);
+               }
+
+               return rel.updateRelation(delta);
+       }
+
+       void killedByEnemy(Mobile died, Active killer) {
+               if (game.isPreference(Game.PREF_AI)) {
+                       int delta = killer.getCP() - died.getCP();
+
+                       changed |= updateRelation(killer.name, delta);
+
+                       if (killer.clan != null && !killer.clan.equals("none")) {
+                               changed |= updateRelation(killer.clan, delta);
+                       }
+               }
+       }
+
+       void runAI(Mobile mob) {
+               if (!game.isPreference(Game.PREF_AI)) {
+                       mob.canSeePlayer = false;
+                       return;
+               }
+
+               //Battle AI (inside Battle class)
+               if (mob.isFighting()) {
+                       return;
+               }
+
+               // FIXME: can this be moved to DuskEngine somehow?
+
+               //Default AI
+               int intConfidence = 0;
+               Active enemy = null;
+               double enemyrelation = 0;
+               boolean visiblePlayer = false;
+               for (TileMap.MapData md : mob.map.range(mob.x, mob.y, game.viewRange - 1)) {
+                       for (Thing o : md.entities) {
+                               if (o.getType() == Thing.TYPE_MOBILE || o.getType() == Thing.TYPE_PLAYER) {
+                                       Active lt = (Active) o;
+
+                                       if (mob.onCanSeeActive(lt) && mob.canSee(lt.x, lt.y)) {
+                                               if (o.getType() == Thing.TYPE_PLAYER) {
+                                                       visiblePlayer = true;
+                                                       double relation = getRelationValue(lt.name);
+                                                       if (!(lt.clan == null || lt.clan.equals("none"))) {
+                                                               relation = (relation + getRelationValue(lt.clan)) / 2;
+                                                       }
+                                                       if (relation < 0 && (enemy == null || enemyrelation < relation)) {
+                                                               enemy = lt;
+                                                               enemyrelation = relation;
+                                                       }
+                                                       intConfidence += relation * lt.getTP();
+                                                       System.out.printf("mob '%s' can see player '%s' relation %f confidence=%d\n", mob.name, lt.name, relation, intConfidence);
+                                                       System.out.printf("  player points total = %d armour %d damage %d cmponent %f\n", lt.getTP(), lt.getArmourMod(), lt.getDamageMod(), relation * lt.getTP());
+                                               } else {
+                                                       double relation;
+
+                                                       Mobile m = (Mobile) lt;
+                                                       if (m.name.equals(mob.name)) {
+                                                               relation = m.groupRelation;
+                                                       } else {
+                                                               relation = getRelationValue(m.name);
+                                                       }
+                                                       intConfidence += relation * m.getTP();
+                                                       if (relation < 0 && (enemy == null || enemyrelation > relation)) {
+                                                               enemy = m;
+                                                               enemyrelation = relation;
+                                                       }
+                                                       intConfidence += relation * lt.getTP();
+                                                       System.out.printf("mob '%s' can see mob '%s' relation %f confidence=%d\n", mob.name, lt.name, relation, intConfidence);
+                                                       System.out.printf("  player points total = %d armour %d damage %d component %f\n", lt.getTP(), lt.getArmourMod(), lt.getDamageMod(), relation * lt.getTP());
+                                               }
+                                       }
+                               }
+                       }
+               }
+               mob.canSeePlayer = visiblePlayer;
+               if (!visiblePlayer) {
+                       return;
+               }
+               if (enemy != null) {
+                       double delta = (mob.getTP() + intConfidence) * mob.bravery * -1 * enemyrelation;
+                       int enemycp = enemy.getTP();
+                       //Fight/flee
+                       if (enemycp < delta) {
+                               if (enemycp > delta - (delta * 0.1 * Math.random())) {
+                                       return;
+                               }
+                               //Fight
+                               if (enemy.distanceL1(mob) <= mob.getRangeWithBonus()) {
+                                       System.out.println(mob.name + " close enough, going into battle distance: " + enemy.distanceL1(mob) + " range: " + mob.getRangeWithBonus());
+                                       // close enough to attack, so stop moving
+                                       mob.clearMoveQueue();
+                                       mob.createBattle(enemy);
+                                       /*
+                                        try {
+                                        // TODO: just call duskEngine.newBattle directly?
+                                        Commands.parseCommand(mob, game, "a " + enemy.name);
+                                        } catch (Exception e) {
+                                        game.log.printError("runAI():" + mob.name + " had an error attacking " + enemy.name, e);
+                                        }*/
+                               } else {
+                                       System.out.println("Mob ai: move to enemy " + enemy.name + " " + enemy.x + "," + enemy.y);
+                                       mob.travelTo(enemy.x, enemy.y, false);
+                               }
+                       } else {
+                               if (enemycp < delta + (delta * 0.1 * Math.random())) {
+                                       return;
+                               }
+                               //Flee
+                               int destX = mob.x + Integer.signum(mob.x - enemy.x) * game.viewRange;
+                               int destY = mob.y + Integer.signum(mob.y - enemy.y) * game.viewRange;
+                               System.out.println("Mob ai: flee from " + enemy.name + " " + destX + "," + destY);
+                               mob.travelTo(destX, destY, true);
+                       }
+               }
+
+               //If no enemies
+               if ((int) (Math.random() * 25) == 1) {
+                       if ((int) (Math.random() * 2) == 1) {
+                               if ((int) (Math.random() * 2) == 1) {
+                                       mob.enqueueMove("e");
+                               } else {
+                                       mob.enqueueMove("w");
+                               }
+                       } else {
+                               if ((int) (Math.random() * 2) == 1) {
+                                       mob.enqueueMove("s");
+                               } else {
+                                       mob.enqueueMove("n");
+                               }
+                       }
+               }
+       }
+
+       /**
+        * a Relation represents a feeling held by one faction for another faction, a
+        * player, or a clan. The mob AI bases it's decisions around Relations.
+        *
+        * @author Tom Weingarten
+        */
+       static class Relation {
+
+               String name;
+               double level = 2; //-1 to 1
+
+               Relation(String inName, double inLevel) {
+                       name = inName.toLowerCase();
+                       level = inLevel;
+               }
+
+               /**
+                *
+                * Uses an optimized form of the function:
+                * ((1.03^delta) + (1.03^-delta)) / (2 + (1.03^delta) + (1.03^-delta))
+                *
+                * But I don't know why ...
+                *
+                * @param delta
+                * @returns true if the level changes
+                */
+               boolean updateRelation(int delta) {
+                       double old = level;
+
+                       if (delta == 0) {
+                               level -= (.5) * (1D + level);
+                       } else if (delta > 0) {
+                               level -= ((((Math.pow(1.03, delta)) / (Math.pow(1.03, delta) + 2))) / 2) * (1 + level);
+                       } else {
+                               level -= ((Math.pow(1.03, (-1 * delta)) / (Math.pow(1.03, (-1 * delta)) + 2)) / 2) * (1 + level);
+                       }
+
+                       return old != level;
+               }
+
+               @Override
+               public String toString() {
+                       return "Relation '" + name + "' = " + level;
+               }
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Food.java b/DuskServer/src/duskz/server/entityz/Food.java
new file mode 100644 (file)
index 0000000..2e880d4
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.Wearing;
+import static duskz.server.entityz.Thing.TYPE_FOOD;
+
+/**
+ * Items that can be eaten
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Food extends Holdable {
+
+       public Food(Game game) {
+               super(game);
+       }
+
+       public int getType() {
+               return TYPE_FOOD;
+       }
+
+       @Override
+       public int getWearing() {
+               // FIXME: food?
+               return Wearing.INVENTORY;
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Game.java b/DuskServer/src/duskz/server/entityz/Game.java
new file mode 100644 (file)
index 0000000..6fce193
--- /dev/null
@@ -0,0 +1,879 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.server.BlockedIPException;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.Future;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+/**
+ * Main game engine
+ *
+ * Handles game setup, prefs, loading/restoring state, and the main
+ * game clock.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Game {
+
+       public final Log log;
+       // prefs stuff
+       public int port = 7480;
+       public int viewRange = 6;
+       public int mapSize = viewRange * 2 + 1;
+       public String rcName = "somedusk";
+       public double goldFleeMod = 1.0 / 64.0;  // fiXME ;lcheck
+       public double goldLoseMod = 1.0 / 16.0;
+       public double expFleeMod = 1.0 / 64.0;
+       public double expLoseMod = 1.0 / 16.0;
+       public double expGainMod = 18;
+       public int messageLimit = 256;
+       // How many ticks between battle steps
+       public int battleRate = 3;//10
+       /**
+        * Time between user messages
+        */
+       public int floodLimit = 500;
+       //
+       private ScriptManager scriptManager = new ScriptManager();
+       //
+       private File root;
+       private File confDir;
+       private File tileActionDir;
+       private File tileAbleDir;
+       private File tileVisibleDir;
+       private File locationActionDir;
+       private File locationAbleDir;
+       private File locationVisibleDir;
+       //
+       private File onScriptDir;
+       private File onConditionDir;
+       // Maps a tile number to a script
+       private HashMap<Integer, String> tileAction = new HashMap<>();
+       private HashMap<Integer, String> tileAble = new HashMap<>();
+       private HashMap<Integer, String> tileVisible = new HashMap<>();
+       // Just indicates which scripts are available, could be a cache or something
+       private HashSet<String> locationAction = new HashSet<>();
+       private HashSet<String> locationAble = new HashSet<>();
+       private HashSet<String> locationVisible = new HashSet<>();
+       /*
+        * Preference keys
+        */
+       public final static String PREF_AI = "ai";
+       // Used for mob info cache
+       HashMap<String, Mobile> allMobs = new HashMap<>();
+       // Used for item info cache, this needs a clone or equivalent ...
+       //HashMap<String, Holdable> allItems = new HashMap<>();
+       // Factions
+       HashMap<String, Faction> factions = new HashMap<>();
+       // do i want any of this now?  hmm.
+       ThingTable<Mobile> mobs;
+       ThingTable<GameShop> gameShops;
+       ThingTable<Sign> signs;
+       ThingTable<Prop> props;
+       //
+       HashMap<String, Race> races = new HashMap<>();
+       // All players in game
+       HashMap<String, Player> players = new HashMap<>();
+       // All things in game
+       HashMap<Long, Thing> things = new HashMap<>();
+       // All actives in game
+       HashMap<Long, Active> actives = new HashMap<>();
+       //
+       private final HashMap<String, TileMap> maps = new HashMap<>();
+       //
+       private ArrayList<Battle> battles = new ArrayList<>();
+
+       public Game() {
+               log = new Log(System.out);
+               log.setLogLevel(Log.DEBUG);
+       }
+
+       public File getRoot() {
+               return root;
+       }
+
+       public void init(File root) throws IOException {
+               this.root = root;
+
+               confDir = new File(root, "config");
+               tileActionDir = new File(root, "onTileAction");
+               tileAbleDir = new File(root, "onTileAble");
+               tileVisibleDir = new File(root, "onTileVisible");
+
+               locationActionDir = new File(root, "onLocationAction");
+               locationAbleDir = new File(root, "onLocationAble");
+               locationVisibleDir = new File(root, "onLocationVisible");
+
+               onScriptDir = new File(root, "onItem");
+               onConditionDir = new File(root, "onCondition");
+
+               mobs = new ThingTable<>(new File(root, "mobs"));
+               gameShops = new ThingTable<>(new File(root, "gameShops"));
+               signs = new ThingTable<>(new File(root, "signs"));
+               props = new ThingTable<>(new File(root, "props"));
+
+               // Load preferences
+               // TODO: load preferences
+
+               // Load all races (move to 'racelist' class?)
+               {
+                       File path = new File(root, "defRaces");
+                       if (!path.isDirectory())
+                               throw new FileNotFoundException("Unable to find races");
+
+                       for (File file : path.listFiles()) {
+                               try {
+                                       Race race = Race.loadRace(file);
+                                       races.put(race.name, race);
+                               } catch (IOException ex) {
+                                       log.printf(ex, "Loading map %s failed", file.getName());
+                               }
+                       }
+               }
+
+               // Load all maps
+               {
+                       File path = new File(root, "defMaps");
+                       if (!path.isDirectory())
+                               throw new FileNotFoundException("Unable to find maps");
+                       for (File file : path.listFiles()) {
+                               if (file.getName().endsWith(".alias"))
+                                       continue;
+                               try {
+                                       TileMap map = TileMap.loadLayered(file);
+                                       maps.put(map.name, map);
+                                       log.printf(Log.VERBOSE, "Map: %s size=%dx%dx%d", map.name, map.getCols(), map.getRows(), map.getLayerCount());
+                               } catch (IOException ex) {
+                                       log.printf(ex, "Loading map %s failed", file.getName());
+                               }
+                       }
+               }
+
+               // Load tile scripts
+               // TODO: actually verify scripts exist at least?
+               {
+                       File path = new File(root, "tileScriptMap");
+                       try (PropertyLoader pl = new PropertyLoader(path)) {
+                               for (PropertyLoader.PropertyEntry pe : pl) {
+                                       String[] line = pe.name.split("\\.");
+                                       Integer id = Integer.valueOf(line[0]);
+
+                                       switch (line[1]) {
+                                               case "visible":
+                                                       tileVisible.put(id, pe.value);
+                                                       break;
+                                               case "able":
+                                                       tileAble.put(id, pe.value);
+                                                       break;
+                                               case "action":
+                                                       tileAction.put(id, pe.value);
+                                                       break;
+                                       }
+                               }
+                       }
+               }
+
+               // Find out the names of the scripts available
+               {
+                       String[] dirs = {"onLocationAble", "onLocationAction", "onLocationVisible"};
+                       HashSet[] sets = {locationAble, locationAction, locationVisible};
+
+                       for (int i = 0; i < dirs.length; i++) {
+                               sets[i].addAll(Arrays.asList(new File(root, dirs[i]).list()));
+                       }
+               }
+
+               // load all factions (before mobs)
+               {
+                       File path = new File(root, "factions");
+                       if (!path.isDirectory())
+                               throw new FileNotFoundException("Unable to find factions");
+                       for (File file : path.listFiles()) {
+                               try {
+                                       Faction f = new Faction(file.getName(), this);
+                                       f.load();
+                                       factions.put(f.name, f);
+                                       log.printf(Log.VERBOSE, "Faction: %s", f.name);
+                               } catch (IOException ex) {
+                                       log.printf(ex, "Loading faction %s failed", file.getName());
+                               }
+                       }
+               }
+
+               // load all mob types into cache/check validity
+               // hmm, not using this yet?
+               {
+                       File path = new File(root, "defMobs");
+                       if (!path.isDirectory())
+                               throw new FileNotFoundException("Unable to find mobs");
+                       for (File file : path.listFiles()) {
+                               try {
+                                       Mobile mob = new Mobile(this);
+                                       mob.load(file);
+                                       allMobs.put(mob.name, mob);
+                                       //log.printf(Log.VERBOSE, "Mob class: %s", mob.name);
+                               } catch (Exception ex) {
+                                       log.printf(ex, "Loading mob %s failed", file.getName());
+                               }
+                       }
+               }
+
+
+               // Load all active objects
+               // FIXME: has to copy to various indices
+               mobs.restoreState(this, new File(root, "defMobs"));
+               gameShops.restoreState(this);
+               signs.restoreState(this);
+               props.restoreState(this);
+
+               // HACK: just copy to indices now
+               for (Mobile m : mobs.values()) {
+                       System.out.println("add mob " + m.name + " on " + m.map.name);
+                       addThing(m);
+               }
+               for (Sign s : signs.values()) {
+                       System.out.println("add sign: " + s.name + " at " + s.x + ", " + s.y + " on map " + s.map.name);
+                       addThing(s);
+               }
+               for (Shop s : gameShops.values()) {
+                       System.out.println("add shop: " + s.name + " at " + s.x + ", " + s.y + " on map " + s.map.name);
+                       addThing(s);
+               }
+
+               scriptManager.start();
+
+               // We're ready, time for 'onstart' script               
+               runScript(confDir, "onStart", "game", this);
+       }
+
+       // dont think i want this now
+       public void addEveryTick(Active lt) {
+       }
+
+       public void removeEveryTick(Active lt) {
+       }
+
+       public void addTickHandler(Runnable r, int ratio) {
+               // add the tick handler to the queue
+               // if the queue was empty, wake up the tick thread
+       }
+
+       void restoreState() {
+       }
+
+       void saveState() {
+               mobs.saveState(this);
+               gameShops.saveState(this);
+               signs.saveState(this);
+               props.saveState(this);
+       }
+
+       public TileMap getMap(String name) {
+               return maps.get(name);
+       }
+
+       /**
+        * Create a copy of an item prototype
+        *
+        * @param name
+        * @return item, or null if it coulnd't be loaded/found
+        */
+       public Holdable createItem(String name) {
+               Thing.ThingResolver resolver = new Thing.EmptyResolver();
+
+               try (BufferedReader in = new BufferedReader(new FileReader(new File(root, "defItems/" + name.toLowerCase())))) {
+                       return (Holdable) Thing.restoreState(this, in, resolver);
+               } catch (IOException ex) {
+                       log.printf(ex, "Loading item: %s", name);
+               }
+               return null;
+       }
+
+       /**
+        * Look up a boolean preference.
+        *
+        * @param name
+        * @return
+        */
+       public boolean isPreference(String name) {
+               switch (name) {
+                       case PREF_AI:
+                               return true;
+               }
+               // FIXME: implement properly
+               return true;
+       }
+
+       /**
+        * Send a global chat from a given player. Calls chatMessage() on
+        * all active players.
+        *
+        * @param from may be null
+        * @param clan may be null
+        * @param msg
+        */
+       public void globalChat(Player from, String clan, String msg) {
+               //log.printMessage(Log.ALWAYS, msg);
+               for (Player player : players.values()) {
+                       player.chatMessage(from, clan, msg);
+               }
+       }
+
+       // this shouldn't be needed ...
+       public void cleanup() {
+       }
+
+       public void addBattle(Battle battle) {
+               battles.add(battle);
+       }
+
+       public Thing getThing(long id) {
+               return things.get(id);
+       }
+
+       /**
+        * Remove object from game
+        * Do not call this directly for players.
+        *
+        * @param thing
+        */
+       void removeThing(Thing thing) {
+               assert (things.containsKey(thing.ID));
+
+               switch (thing.getType()) {
+                       case Thing.TYPE_PLAYER:
+                               players.remove(((Player) thing).name);
+                               actives.remove(thing.ID);
+                               break;
+                       case Thing.TYPE_MOBILE:
+                               mobs.remove((Mobile) thing);
+                               actives.remove(thing.ID);
+                               break;
+                       case Thing.TYPE_PET:
+                               actives.remove(thing.ID);
+                               break;
+               }
+
+               thing.map.removeEntity(thing);
+               things.remove(thing.ID);
+
+       }
+
+       public void addThing(Thing t) {
+               things.put(t.ID, t);
+
+               switch (t.getType()) {
+                       case Thing.TYPE_PLAYER: {
+                               Player player = (Player) t;
+                               players.put(player.name, player);
+                               actives.put(player.ID, player);
+                               break;
+                       }
+                       case Thing.TYPE_MOBILE:
+                               mobs.add((Mobile) t);
+                               actives.put(t.ID, (Active) t);
+                               // Mobs are not addded to the map directly, respawn does it
+                               return;
+                       case Thing.TYPE_PET:
+                               actives.put(t.ID, (Active) t);
+                               break;
+               }
+               t.map.addEntity(t);
+       }
+
+       /**
+        * Public interfac to remove things from the game
+        *
+        * @param h
+        */
+       public void removeItem(Holdable h) {
+               removeThing(h);
+       }
+
+       /**
+        * Add a thing to game
+        *
+        * @param t
+        * @param map
+        * @param x
+        * @param y
+        */
+       public void addThing(Thing t, TileMap map, int x, int y) {
+               // FIXME: threads
+               t.map = map;
+               t.x = x;
+               t.y = y;
+               addThing(t);
+       }
+
+       /**
+        * runs a script that returns nothing
+        *
+        * @param base
+        * @param name
+        * @param args
+        */
+       public void runScript(File base, String name, Object... args) {
+               if (name == null)
+                       return;
+
+               try {
+                       Future<Object> res = scriptManager.runScript(new File(base, name), args);
+
+                       res.get();
+               } catch (FileNotFoundException ex) {
+               } catch (Exception ex) {
+                       Logger.getLogger(Game.class.getName()).log(Level.SEVERE, null, ex);
+               }
+       }
+
+       /**
+        * Runs a script returning a boolean result.
+        *
+        * @param def default value for script with error or no script
+        * @param base
+        * @param name name of script. The special names 'true', and 'false' are handled directly.
+        * @param args
+        * @return
+        */
+       public boolean runScriptBoolean(boolean def, File base, String name, Object... args) {
+               if (name == null)
+                       return def;
+
+               if (name.equals("true"))
+                       return true;
+               else if (name.equals("false"))
+                       return false;
+
+               System.out.println("run bool script: " + base.getName() + "/" + name);
+
+               try {
+                       Future<Boolean> res = scriptManager.runScript(new File(base, name), args);
+
+                       System.out.println(" = " + res.get());
+
+                       return res.get();
+               } catch (FileNotFoundException ex) {
+                       System.out.println("Script not found: " + base.getName() + "/" + name);
+               } catch (Exception ex) {
+                       Logger.getLogger(Game.class.getName()).log(Level.SEVERE, null, ex);
+               }
+               return def;
+       }
+
+       /**
+        * TODO: do i want to pass the map name to the tile script too?
+        * TODO: can i improve the mapping? e.g. give the tile a name,
+        * and use the name to find the appropriate scripts?
+        *
+        * @param trigger
+        * @param tileid
+        */
+       public void onTileAction(Active trigger, int tileid) {
+               String name = tileAction.get(tileid);
+
+               if (name != null) {
+                       runScript(tileActionDir, name,
+                                       "game", this,
+                                       "trigger", trigger);
+               }
+       }
+
+       public boolean onTileVisible(Active trigger, int tileid) {
+               String name = tileVisible.get(tileid);
+
+               if (name != null) {
+                       return runScriptBoolean(false, tileVisibleDir, name,
+                                       "game", this,
+                                       "trigger", trigger);
+               }
+               return false;
+       }
+
+       public boolean onTileAble(Active trigger, int tileid) {
+               String name = tileAble.get(tileid);
+
+               //System.out.printf("onTileTable tid=%d script=%s\n", tileid, name);
+
+               if (name != null) {
+                       return runScriptBoolean(false, tileAbleDir, name,
+                                       "game", this,
+                                       "trigger", trigger);
+               }
+               return false;
+       }
+
+       private void onMoveAction(Active trigger, int x, int y) {
+               String alias = trigger.map.jumpAlias(x, y);
+               if (alias != null) {
+                       TileMap map = trigger.map;
+                       Location l = map.locationForAlias(alias);
+                       if (l == null) {
+                               for (TileMap scan : maps.values()) {
+                                       l = scan.locationForAlias(alias);
+                                       if (l != null) {
+                                               map = scan;
+                                               break;
+                                       }
+                               }
+                       }
+                       if (l != null) {
+                               System.out.printf("direct jump to '%s'  map %s %d,%d\n",
+                                               alias, map.name, l.x, l.y);
+                               trigger.jumpTo(map, l.x, l.y);
+                               return;
+                       } else {
+                               log.printf(Log.INFO, "Location alias not found: %s, map %s %d,%d", alias, trigger.map.name, x, y);
+                       }
+               }
+
+               runScript(locationActionDir, trigger.map.locationActionScript(x, y),
+                               "game", this,
+                               "trigger", trigger);
+       }
+
+       private boolean haveMoveAble(Active trigger, int x, int y) {
+               return trigger.map.locationAbleScript(x, y) != null;
+       }
+
+       private boolean onMoveAble(Active trigger, int x, int y) {
+               return runScriptBoolean(false, locationAbleDir, trigger.map.locationAbleScript(x, y),
+                               "game", this,
+                               "trigger", trigger);
+       }
+
+       private boolean haveMoveVisible(Active trigger, int x, int y) {
+               return trigger.map.locationVisibleScript(x, y) != null;
+               //return locationVisible.contains(trigger.map.locationVisibleScript(x, y));
+       }
+
+       private boolean onMoveVisible(Active trigger, int x, int y) {
+               return runScriptBoolean(false, locationVisibleDir, trigger.map.locationVisibleScript(x, y),
+                               "game", this,
+                               "trigger", trigger);
+       }
+
+       /**
+        * Check scripts to see if trigger can move onto location.
+        * If present, the location script overrides the tile script.
+        *
+        * @param trigger
+        * @param x
+        * @param y
+        * @param tileid
+        * @return
+        */
+       public boolean onLocationAble(Active trigger, int x, int y, int tileid) {
+               //System.out.printf("onLocationAble %d,%d %d havemove=%s onmove=%s ontile=%s\n",
+               //              x, y, tileid, haveMoveAble(trigger, x, y),
+               //              onMoveAble(trigger, x, y),
+               //              onTileAble(trigger, tileid));
+               if (haveMoveAble(trigger, x, y))
+                       return onMoveAble(trigger, x, y);
+               else
+                       return onTileAble(trigger, tileid);
+       }
+
+       public boolean onLocationVisible(Active trigger, int x, int y, int tileid) {
+               //System.out.printf("onLocationVisible %d,%d %d havemove=%s onmove=%s ontile=%s\n",
+               //              x, y, tileid, haveMoveVisible(trigger, x, y),
+               //              onMoveVisible(trigger, x, y),
+               //              onTileVisible(trigger, tileid));
+
+               if (haveMoveVisible(trigger, x, y))
+                       return onMoveVisible(trigger, x, y);
+               else
+                       return onTileVisible(trigger, tileid);
+       }
+
+       public void onLocationAction(Active trigger, int x, int y, int tileid) {
+               // Do I always do both?  Possibly not?
+               onTileAction(trigger, tileid);
+               onMoveAction(trigger, x, y);
+       }
+
+       public boolean onCanAttack(Active attacking, Active attacked) {
+               // TODO: would a race or mob based attack script fit here?
+               return runScriptBoolean(true, confDir, "canAttack", "attacking", attacking, "attacked", attacked);
+       }
+
+       /**
+        * Calls the onPlayerStart script.
+        *
+        * @param player
+        * @return true if the player is allowed to start
+        */
+       public boolean onPlayerStart(Player player) {
+               return runScriptBoolean(true, confDir, "onPlayerStart", "player", player.getCommands());
+       }
+
+       public boolean haveOnItem(Holdable trigger, String what) {
+               return new File(onScriptDir, trigger.name + "." + what).exists();
+       }
+
+       /**
+        * Perform on item callback
+        *
+        * @param owner
+        * @param item
+        * @param what "get" or "drop", etc.
+        */
+       public void onItem(Active owner, Holdable item, String what) {
+               runScript(onScriptDir, item.name + "." + what,
+                               "game", this,
+                               "owner", owner,
+                               "item", item);
+       }
+
+       public boolean haveOnCondition(Holdable trigger, String what) {
+               return new File(onConditionDir, trigger.name + "." + what).exists();
+       }
+
+       /**
+        * Perform on item callback
+        *
+        * @param owner
+        * @param item
+        * @param what "get" or "drop", etc.
+        */
+       public void onCondition(Active owner, Condition cond, String what) {
+               runScript(onConditionDir, cond.name + "." + what,
+                               "game", this,
+                               "owner", owner,
+                               "condition", cond);
+       }
+       private int tick;
+
+       public int getClock() {
+               return tick;
+       }
+
+       /**
+        * Main game loop.
+        *
+        * 1. all actives are moved active.moveTick()
+        * 2. run per-tick activities
+        * 3. run player updates
+        *
+        * @param tick
+        */
+       void gameTick(int tick) {
+               this.tick = tick;
+               // TODO: the order of this might need tweaking
+               for (Active a : actives.values()) {
+                       a.moveTick();
+               }
+               for (Active a : actives.values()) {
+                       a.tick(tick);
+               }
+               //if (tick % 10 == 0) {
+               if (tick % battleRate == 0) {
+                       ArrayList<Battle> done = new ArrayList<>();
+                       for (Battle b : battles) {
+                               if (b.isFighting()) {
+                                       b.run();
+                               } else {
+                                       done.add(b);
+                               }
+                       }
+                       battles.removeAll(done);
+               }
+
+               for (Active a : actives.values()) {
+                       a.visibilityTick(tick);
+               }
+       }
+
+       void onDeath(Player player, Active winner) {
+               runScript(new File(root, "scripts"), "onDeath", "trigger", player, "killer", winner);
+       }
+
+       boolean playerExists(String name) {
+               return new File(root, "players/" + name.toLowerCase()).exists();
+       }
+
+       boolean petExists(String name) {
+               return new File(root, "pets/" + name.toLowerCase()).exists();
+       }
+
+       // TODO: String insecure for password
+       boolean checkPassword(String name, String pass, String address) throws TooManyTriesException, BlockedIPException {
+
+               // TODO: handle ip blocked checking here
+
+               Player p = new Player(this, null);
+               try {
+                       p.load(new File(root, "players/" + name.toLowerCase()));
+               } catch (IOException ex) {
+                       return false;
+               }
+
+               if (p.password == null) {
+                       // Some error
+                       log.printf(Log.ERROR, "User file has no password: %s", name);
+                       return false;
+               }
+
+               if (pass.equals(p.password)) {
+                       return true;
+               } else if (logPasswordFailure(name, address)) {
+                       throw new TooManyTriesException();
+               } else {
+                       try {
+                               Thread.sleep(3000);
+                       } catch (InterruptedException ex) {
+                               Logger.getLogger(Game.class.getName()).log(Level.SEVERE, null, ex);
+                       }
+                       return false;
+               }
+       }
+
+       /**
+        * Return true if login limit exceeded. If so, then mark address as banned.
+        *
+        * @param name
+        * @param address
+        * @return
+        */
+       boolean logPasswordFailure(String name, String address) {
+               return false;
+       }
+
+       void logPasswordSuccess(String player, String address) {
+               //throw new UnsupportedOperationException("Not supported yet.");
+       }
+       int namecap = 12;
+
+       boolean isGoodName(String player) {
+               if (player == null)
+                       return false;
+
+               String lcplayer = player.toLowerCase();
+
+               // config?
+               Pattern reserved = Pattern.compile("^god|default$");
+               // config?
+               Pattern valid = Pattern.compile("^[A-Za-z]+$");
+
+               if (reserved.matcher(lcplayer).matches()
+                               || !valid.matcher(player).matches()) {
+                       return false;
+               }
+
+               try (BufferedReader br = new BufferedReader(new FileReader(new File(confDir, "dirtyWordFile")))) {
+                       String strDirtyWord = br.readLine();
+                       while (strDirtyWord != null) {
+                               if (lcplayer.indexOf(strDirtyWord) != -1) {
+                                       return false;
+                               }
+                               strDirtyWord = br.readLine();
+                       }
+               } catch (IOException x) {
+                       x.printStackTrace();
+               }
+
+               return true;
+       }
+
+       /**
+        * Registers the player with the game.
+        * Checks to see if the player is already connected - logs out other player.
+        *
+        * @param player
+        * @throws BlockedIPException
+        */
+       void registerPlayer(Player player) {
+               logPasswordSuccess(player.name, player.getAddress());
+
+               Player old = players.get(player.name);
+               if (old != null) {
+                       old.chatMessage("Logged in again from: " + player.getAddress());
+                       old.logout();
+               }
+
+               //throw new UnsupportedOperationException("Not supported yet.");
+               players.put(player.name, player);
+               things.put(player.ID, player);
+               actives.put(player.ID, player);
+               player.map.addEntity(player);
+
+               try {
+                       System.out.println("added player:");
+                       BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
+                       player.writeProperties(bw);
+                       bw.flush();
+               } catch (IOException ex) {
+                       Logger.getLogger(Game.class.getName()).log(Level.SEVERE, null, ex);
+               }
+
+       }
+
+       void unregisterPlayer(Player player) {
+               // TODO threading
+               player.map.removeEntity(player);
+               players.remove(player.name);
+               things.remove(player.ID);
+               actives.remove(player.ID);
+       }
+
+       public List<String> getRaceNames() {
+               ArrayList<String> l = new ArrayList<>(races.size());
+
+               for (Race r : races.values()) {
+                       l.add(r.name);
+               }
+               return l;
+       }
+
+       Race getRace(String value) {
+               return races.get(value);
+       }
+
+       public Faction getFaction(String name) {
+               return factions.get(name);
+       }
+
+       public static void main(String[] args) throws IOException {
+               Game g = new Game();
+
+               g.init(new File("/home/notzed/dusk/game"));
+
+               String[] names = {"z", "fuckface", "god", "root", "default"};
+               for (String n : names) {
+                       System.out.println("good name: " + n + " " + g.isGoodName(n));
+               }
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/GameServer.java b/DuskServer/src/duskz/server/entityz/GameServer.java
new file mode 100644 (file)
index 0000000..c327271
--- /dev/null
@@ -0,0 +1,178 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+/**
+ * Special thanks to:
+ * Randall Leeds
+ * Vittorio Alberto Floris
+ * Ian Macphail
+ *
+ * Changes
+ * Michael Zucchi Mar-2013 - changed to new backend, implemented
+ * different clock timing.
+ */
+package duskz.server.entityz;
+
+import duskz.server.Log;
+import java.io.File;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This is the main entry point.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class GameServer {
+
+       // ms per tick: TODO: move into game prefs
+       int tickRate = 100; // 250
+       boolean cancelled = false;
+       ServerSocket serverSocket;
+       Socket socket;
+       Game game;
+       MasterClock clockThread;
+       ConnectionManager connectionThread;
+
+       /**
+        * Creates a new DuskServer object;
+        */
+       public static void main(String args[]) {
+               // TODO: parse arguments for game data location
+               File path = new File("/home/notzed/dusk/game");
+
+               GameServer server = new GameServer();
+
+               server.start(path);
+       }
+
+       private void abort() {
+               game.log.printf(Log.ALWAYS, "Aborted shutdown.");
+               System.exit(0);
+       }
+
+       protected void shutdown() {
+               try {
+                       cancelled = true;
+                       clockThread.interrupt();
+                       connectionThread.interrupt();
+                       clockThread.join();
+                       connectionThread.join();
+               } catch (InterruptedException ex) {
+                       Logger.getLogger(GameServer.class.getName()).log(Level.SEVERE, null, ex);
+               }
+       }
+
+       /**
+        * Creates a DuskEngine object, then a ServerSocket object, then
+        * starts a new thread to accept incoming connections.
+        */
+       public GameServer() {
+               // game location?
+               game = new Game();
+               clockThread = new MasterClock();
+               connectionThread = new ConnectionManager();
+       }
+
+       public void start(File path) {
+               System.out.println("Initialising game at: " + path);
+
+               try {
+                       game.init(path);
+                       serverSocket = new ServerSocket(game.port, 25);
+               } catch (Exception e) {
+                       game.log.printf(e, "Server init failed", e);
+                       e.printStackTrace();
+                       abort();
+               }
+
+
+               connectionThread.start();
+               clockThread.start();
+       }
+
+       /**
+        * Accepts incoming connections.
+        */
+       /**
+        * Accepts connections.
+        * TODO: handle ip blocking, throttling etc. here
+        * rather than in playerconnection
+        */
+       class ConnectionManager extends Thread {
+
+               public void run() {
+                       System.out.println("Accepting connections on port " + game.port + ".");
+
+                       while (true) {
+                               try {
+                                       Socket accept = serverSocket.accept();
+                                       accept.setSoTimeout(30000);
+
+                                       PlayerConnection pc = new PlayerConnection(game, accept);
+
+                                       pc.start();
+                               } catch (Exception e) {
+                                       game.log.printf(e, "Error accepting connection", e);
+                                       abort();
+                                       return;
+                               }
+                       }
+               }
+       }
+
+       class MasterClock extends Thread {
+
+               int tick = 0;
+
+               @Override
+               public void run() {
+                       long start = System.currentTimeMillis() + 1000;
+
+                       System.out.println("Starting game clock, " + tickRate + "ms per tock.");
+
+                       while (!cancelled) {
+                               try {
+                                       long target = start + tick * tickRate;
+                                       long now = System.currentTimeMillis();
+                                       long delay = (target - now);
+
+                                       if (delay > 0) {
+                                               sleep(delay);
+                                       } else if (delay < -500) {
+                                               System.out.println("warning: clock failing to keep up, delay: " + (-delay));
+                                       }
+
+                                       game.gameTick(tick);
+
+                                       // battles?
+
+                               } catch (InterruptedException ex) {
+                               } catch (Exception ex) {
+                                       ex.printStackTrace();
+                               } finally {
+                                       tick += 1;
+                               }
+                       }
+               }
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/GameShop.java b/DuskServer/src/duskz/server/entityz/GameShop.java
new file mode 100644 (file)
index 0000000..fab7450
--- /dev/null
@@ -0,0 +1,125 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import java.util.List;
+
+/**
+ * A game provided shop.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class GameShop extends Shop {
+
+       public GameShop(Game game) {
+               super(game);
+       }
+
+       @Override
+       void setProperty(String name, String value) {
+               // train is just an alias at the moment
+               if (name.equals("train"))
+                       name = "item";
+
+               super.setProperty(name, value);
+       }
+
+       @Override
+       public int getType() {
+               return TYPE_GAME_SHOP;
+       }
+
+       @Override
+       public void buy(Active buyer, String what, int quantity) {
+               Holdable h = items.get(what);
+
+               if (h == null) {
+                       buyer.chatMessage(name + " doesn't sell those.");
+                       return;
+               }
+               if (h instanceof Training) {
+                       Training t = (Training) h;
+                       if (!buyer.addExp(-h.cost * quantity)) {
+                               buyer.chatMessage("You can't afford that many.");
+                       } else {
+                               switch (t.skill) {
+                                       case "hp":
+                                               buyer.addStat(Active.STAT_HPMAX, quantity * 10);
+                                               buyer.chatMessage("You're magically able to withstand more damage.");
+                                               break;
+                                       case "mp":
+                                               buyer.addStat(Active.STAT_MPMAX, quantity * 10);
+                                               buyer.chatMessage("You're magically able to cast more magic.");
+                                               break;
+                                       case "int":
+                                               buyer.addStat(Active.STAT_INT, quantity);
+                                               buyer.chatMessage("You begin to think clearer.");
+                                               break;
+                                       case "str":
+                                               buyer.addStat(Active.STAT_STR, quantity);
+                                               buyer.chatMessage("You feel stronger.");
+                                               break;
+                                       case "dex":
+                                               buyer.addStat(Active.STAT_DEX, quantity);
+                                               buyer.chatMessage("You gain an extra spring in your step.");
+                                               break;
+                                       case "wis":
+                                               buyer.addStat(Active.STAT_WIS, quantity);
+                                               buyer.chatMessage("You feel wiser.");
+                                               break;
+                                       case "con":
+                                               buyer.addStat(Active.STAT_CON, quantity);
+                                               buyer.chatMessage("You feel better equipped to face the world.");
+                                               break;
+                                       default:
+                                               buyer.chatMessage("BUG: I don't know how to train " + t.skill);
+                                               buyer.addExp(h.cost * quantity);
+                                               break;
+                               }
+                       }
+               } else {
+                       if (buyer.getGold() < h.cost * quantity) {
+                               buyer.chatMessage("You can't afford that many.");
+                       } else {
+                               while (quantity > 0) {
+                                       Holdable s = game.createItem(what);
+
+                                       if (buyer.addGold(-h.cost)) {
+                                               buyer.addItem(s);
+                                       }
+                                       quantity -= 1;
+                               }
+                       }
+               }
+       }
+
+       @Override
+       public void sell(Active customer, List<Holdable> items) {
+               // TODO: I really want to look up the item somewhere to see what
+               // this shop will buy the items for ...?
+
+               for (Holdable h : items) {
+                       if (customer.removeItem(h)) {
+                               customer.addGold(h.cost / 2);
+                       }
+               }
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Holdable.java b/DuskServer/src/duskz/server/entityz/Holdable.java
new file mode 100644 (file)
index 0000000..ddc6689
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+
+/**
+ * Base class for all items.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public abstract class Holdable extends Thing {
+
+       int cost;
+       //int image;
+       int volume;
+       int weight;
+       String onGet, onDrop;
+       int uses = -1;
+       String onUse;
+
+       Holdable(Game game) {
+               super(game);
+       }
+
+       abstract public int getWearing();
+
+       public String getUnits() {
+               return "gp";
+       }
+
+       @Override
+       public void look(Active viewer) {
+               viewer.chatMessage("You see " + description + ".");
+       }
+
+       @Override
+       void setProperty(String name, String value) {
+               switch (name) {
+                       case "cost":
+                               cost = Integer.parseInt(value);
+                               break;
+                       case "weight":
+                               weight = Integer.parseInt(value);
+                               break;
+                       case "volume":
+                               volume = Integer.parseInt(value);
+                               break;
+                       case "onGet": // FIXME: use conventions to find script from name
+                               onGet = value;
+                               break;
+                       case "onDrop": // FIXME: use conventions to find script from name
+                               onDrop = value;
+                               break;
+                       case "uses":
+                               uses = Integer.parseInt(value);
+                               break;
+                       case "onUse": // FIXME: use conventions to find script from name
+                               onUse = value;
+                               break;
+                       default:
+                               super.setProperty(name, value);
+               }
+       }
+
+       @Override
+       protected void writeProperties(BufferedWriter out) throws IOException {
+               writeProperty(out, "name", name);
+               writeProperty(out, "description", description);
+               //writeProperty(out, "map", map.name);
+               //writeProperty(out, "x", x);
+               //writeProperty(out, "y", y);
+               writeProperty(out, "image", image);
+
+               writeProperty(out, "cost", cost);
+               writeProperty(out, "weight", weight);
+               writeProperty(out, "volume", volume);
+               writeProperty(out, "onGet", onGet);
+               writeProperty(out, "onDrop", onDrop);
+
+               if (uses != -1)
+                       writeProperty(out, "uses", uses);
+               writeProperty(out, "onUse", onUse);
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Inventory.java b/DuskServer/src/duskz/server/entityz/Inventory.java
new file mode 100644 (file)
index 0000000..0e6c185
--- /dev/null
@@ -0,0 +1,105 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.TransactionMessage;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A list of holdables.
+ *
+ * Original game had table by type, but that seems somewhat pointless overhead.
+ *
+ * This is a bit of a mis-mash right now just trying to get the code working
+ *
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Inventory {
+
+       // do i need a name index?  rarely accessed, short list, who cares
+       final private ArrayList<Holdable> items = new ArrayList<>();
+
+       public TransactionMessage createTransactionMessage(int name, float scale) {
+               TransactionMessage msg = new TransactionMessage(name);
+
+               // TODO: count occurances here for player inventory?
+
+               for (Holdable h : items) {
+                       // FIXME: how to represent skills
+
+                       int cost = (int) (h.cost * scale);
+                       
+                       msg.add(h.getWearing(), h.name, 1, cost, h.getUnits());
+               }
+
+               return msg;
+       }
+
+       public void describeTo(Active viewer) {
+               if (items.isEmpty())
+                       viewer.chatMessage("Nothing at the moment.");
+               else {
+                       // hack: shopping list
+                       for (Holdable h : items) {
+                               viewer.chatMessage(String.format("%-20s %s", h.name, h.description));
+                       }
+               }
+       }
+
+       /**
+        * Return the first item of the given type
+        * @param key
+        * @return 
+        */
+       public Holdable get(String key) {
+               for (Holdable h : items) {
+                       if (key.equalsIgnoreCase(h.name))
+                               return h;
+               }
+               return null;
+       }
+
+       public void add(Holdable h) {
+               items.add(h);
+       }
+
+       public boolean remove(Holdable h) {
+               return items.remove(h);
+       }
+
+       public List<Holdable> getAll(String key, int maximum) {
+               ArrayList<Holdable> list = new ArrayList<>();
+               for (Holdable h : items) {
+                       if (h.name.equalsIgnoreCase(key)) {
+                               list.add(h);
+                               if (list.size() >= maximum)
+                                       break;
+                       }
+               }
+               return list;
+       }
+
+       public boolean contains(Holdable h) {
+               return items.contains(h);
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Item.java b/DuskServer/src/duskz/server/entityz/Item.java
new file mode 100644 (file)
index 0000000..049c306
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.Wearing;
+
+/**
+ * Normal item whose behaviour is defined by scripts
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Item extends Holdable {
+
+       public Item(Game game) {
+               super(game);
+       }
+
+       public int getType() {
+               return TYPE_ITEM;
+       }
+
+       @Override
+       public int getWearing() {
+               return Wearing.INVENTORY;
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Location.java b/DuskServer/src/duskz/server/entityz/Location.java
new file mode 100644 (file)
index 0000000..f41962d
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+/**
+ * Track position.
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Location {
+
+       public int x;
+       public int y;
+
+       public Location() {
+       }
+
+       public Location(int x, int y) {
+               this.x = x;
+               this.y = y;
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof Location) {
+                       Location l = (Location) obj;
+                       return l.x == x && l.y == y;
+               } else
+                       return false;
+       }
+
+       @Override
+       public int hashCode() {
+               int hash = 3;
+               hash = 97 * hash + this.x;
+               hash = 97 * hash + this.y;
+               return hash;
+       }
+
+       public class Global extends Location {
+
+               public TileMap map;
+
+               public Global(TileMap map, int x, int y) {
+                       super(x, y);
+
+                       this.map = map;
+               }
+
+               public Global(TileMap map, Location l) {
+                       super(l.x, l.y);
+
+                       this.map = map;
+               }
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Log.java b/DuskServer/src/duskz/server/entityz/Log.java
new file mode 100644 (file)
index 0000000..35fb38f
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+/**
+ * Changes
+ * Michael Zucchi 2013-Mar - redone for new i/o api like printf. Fixed the
+ * priority orderings of the messages.
+ */
+package duskz.server.entityz;
+
+import java.io.PrintStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Simple logging interface.
+ */
+public class Log {
+
+       public static final int DEBUG = 0;
+       public static final int ALWAYS = 1;
+       public static final int VERBOSE = 2;
+       public static final int INFO = 3;
+       public static final int ERROR = 4;
+       private static final String[] msg = {
+               "DEBUG",
+               "ALWAYS",
+               "VERBOSE",
+               "INFO",
+               "ERROR"
+       };
+       private static final String LOG_SEP = "::";
+       private PrintStream out;
+       private int logLevel = DEBUG;
+       private SimpleDateFormat formatter;
+
+       public Log(PrintStream ps) {
+               formatter = new SimpleDateFormat("EEE MMM dd hh:mm:ss yyyy", Locale.ROOT);
+               out = ps == null ? System.out : ps;
+       }
+
+       public void setLogLevel(int newlevel) {
+               newlevel = Math.max(newlevel, ALWAYS);
+               logLevel = Math.min(newlevel, ERROR);
+       }
+
+       public int getLogLevel() {
+               return logLevel;
+       }
+
+       private void printTimeStamp(int level) {
+               out.print(msg[level]);
+               out.print(":");
+               out.print(formatter.format(new Date()));
+               out.print(":");
+               if (logLevel <= DEBUG) {
+                       out.print("thread=" + Thread.currentThread().getName());
+                       out.print(":");
+               }
+       }
+
+       public void printf(int level, String strMessage, Object... args) {
+               if (level >= logLevel) {
+                       printTimeStamp(level);
+                       out.printf(strMessage, args);
+                       out.println();
+               }
+       }
+
+       public void printf(Exception ex, String strMessage, Object... args) {
+               printTimeStamp(ERROR);
+               out.printf(strMessage, args);
+               out.print(": ");
+               if (logLevel <= DEBUG) {
+                       ex.printStackTrace(out);
+               } else {
+                       out.println(ex.toString());
+               }
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Mobile.java b/DuskServer/src/duskz/server/entityz/Mobile.java
new file mode 100644 (file)
index 0000000..1176024
--- /dev/null
@@ -0,0 +1,315 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.DuskMessage;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedList;
+
+/**
+ * Non player character
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Mobile extends Active {
+
+       /**
+        * Home location for mobile
+        */
+       int homeX, homeY;
+       /**
+        * Items given when killed by a player
+        */
+       ArrayList<GiveItem> giveItems = new ArrayList<>();
+       double bravery;
+       double groupRelation;
+       Faction faction;
+       boolean canSeePlayer;
+       // causes respawn on first tick
+       int respawn = 1;
+
+       public Mobile(Game game) {
+               super(game);
+       }
+
+       // onBattle script      
+       @Override
+       public int getType() {
+               return TYPE_MOBILE;
+       }
+
+       @Override
+       public boolean isAlive() {
+               if (respawn > 0)
+                       return false;
+               return super.isAlive();
+       }
+
+       @Override
+       void setProperty(String name, String value) {
+               switch (name) {
+                       case "faction":
+                               this.faction = game.getFaction(value);
+                               break;
+                       case "giveitem": {
+                               int c = value.indexOf(',');
+                               if (c > 0) {
+                                       giveItems.add(new GiveItem(value.substring(0, c), Double.valueOf(value.substring(c + 1))));
+                               }
+                               break;
+                       }
+                       case "bravery":
+                               bravery = Double.valueOf(value);
+                               break;
+                       case "grouprelation":
+                               groupRelation = Double.valueOf(value);
+                               break;
+                       default:
+                               super.setProperty(name, value);
+               }
+       }
+
+       @Override
+       protected void writeProperties(BufferedWriter out) throws IOException {
+               super.writeProperties(out);
+               if (faction != null)
+                       writeProperty(out, "faction", faction.name);
+               writeProperty(out, "bravery", bravery);
+               writeProperty(out, "grouprelation", groupRelation);
+               for (GiveItem gi : giveItems) {
+                       writeProperty(out, "giveitem", gi.name + "," + String.valueOf(gi.probability));
+               }
+       }
+
+       // FIXME: need some way of verifying loading, this isn't quite useful
+       // because loaded isn't called for load() only reconstruct()
+       @Override
+       protected void loaded() throws IOException {
+               homeX = x;
+               homeY = y;
+               if (faction == null)
+                       //throw new IOException("Missing faction on " + name);
+                       System.out.println("Missing faction on " + name);
+               /*
+                setStat(STAT_HP, getHPMax());
+                setStat(STAT_MP, getMPMax());
+
+                // debug
+                System.out.println("dump of loaded mob:");
+                BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
+                writeProperties(bw);
+                bw.flush();*/
+       }
+
+       // FIXME: this is a hack as mobiles don't have a race.
+       // race is common to pet and player only ...?
+       public int getStat(int key) {
+               return stats[key] + bonus[key];
+       }
+
+       @Override
+       protected boolean canAttackEnemy(Active enemy) {
+               switch (enemy.getType()) {
+                       case TYPE_PLAYER: {
+                               Player nme = (Player) enemy;
+
+                               if (nme.visitingShop() != null) {
+                                       return false;
+                               }
+
+                               break;
+                       }
+                       case TYPE_MOBILE:
+                               // free for all?
+                               // FIXME: can mobiles attackEnemy mobiles?  check ai logic
+                               break;
+                       case TYPE_PET:
+                               return false;
+               }
+               return super.canAttackEnemy(enemy);
+       }
+
+       @Override
+       public void send(DuskMessage msg) {
+               // don't care except for debugging?
+       }
+
+       @Override
+       public void chatMessage(Active from, String clan, String msg) {
+               // don't care except for debugging?
+               System.out.println("mob: " + name + "=" + msg);
+       }
+
+       @Override
+       protected boolean moveTo(int newx, int newy, int newstep) {
+               // So, this I just don't get, shouldn't it already be checked in the 'can move to' stuff somewhere?
+               // This checks that the mob doesn't go out of it's home area
+               // Maybe it's so that path finding works
+               if (Math.abs(newx - homeX) > game.viewRange
+                               || Math.abs(newy - homeY) > game.viewRange)
+                       return false;
+
+               return super.moveTo(newx, newy, newstep);
+       }
+
+       /**
+        * Noop, mob weapons invincible
+        *
+        * @param amount
+        */
+       @Override
+       public void weaponDamage(int amount) {
+               // noop
+       }
+
+       @Override
+       public void armourDamage(int damage) {
+               // noop
+       }
+
+       /**
+        * Enemy out of range, mob will try to get closer to target,
+        * if they are not already moving.
+        *
+        * @param target
+        */
+       @Override
+       public void attack(Battle battle, Active target, int range) {
+               // FIXME: Invoke "onBatte"
+
+               if (range > getRangeWithBonus()) {
+                       battle.chatMessage(name + " is out of range, moving closer.  distance=" + range + " range=" + getRangeWithBonus() + " moveable=" + isMoveable());
+                       travelTo(target.x, target.y, false);
+               } else {
+                       clearMoveQueue();
+                       super.attack(battle, target, range);
+               }
+       }
+
+       @Override
+       public void killedBattle(Battle battle, Active winner, ArrayList<Active> opponents) {
+               chatMessage(this.name + " is killed.");
+               splitMoney(1, opponents);
+               splitExp(0, opponents);
+
+               // FIXME: respawnspeed
+               respawn = 100;
+               map.removeEntity(this);
+               endBattle();
+
+               for (Active a : opponents) {
+                       switch (a.getType()) {
+                               case TYPE_PET:
+                                       break;
+                               case TYPE_PLAYER:
+                               case TYPE_MOBILE:
+                                       if (faction != null) {
+                                               faction.killedByEnemy(this, a);
+                                       }
+                                       break;
+                       }
+               }
+               if (winner.isPlayer()) {
+                       System.out.println("Player won, checking give items");
+                       for (GiveItem gi : giveItems) {
+                               System.out.println(" item " + gi.name + " prob " + gi.probability);
+                               if (Math.random() < gi.probability) {
+                                       Holdable h = game.createItem(gi.name);
+
+                                       winner.chatMessage("You got a " + h.name + ".");
+                                       winner.addItem(h);
+                               }
+                       }
+               }
+       }
+
+       @Override
+       public void endBattle() {
+               super.endBattle();
+       }
+
+       @Override
+       public void sayCommand(String text) {
+               localisedChat("Mob " + name + " says: " + text);
+       }
+
+       void respawn() {
+               respawn = 0;
+               x = homeX;
+               y = homeY;
+               setStat(STAT_HP, getHPMax());
+               setStat(STAT_MP, getMPMax());
+
+               map.addEntity(this);
+       }
+
+       @Override
+       public void tick(int tick) {
+
+               if (respawn == 1) {
+                       respawn();
+               } else if (respawn > 0) {
+                       respawn -= 1;
+                       return;
+               }
+
+               LinkedList l;
+
+               if (tick % 4 == 0) {
+                       if (faction != null && canSeePlayer) {
+                               faction.runAI(this);
+                       }
+               }
+
+               super.tick(tick);
+       }
+
+       @Override
+       public void visibilityTick(int tick) {
+               if (respawn > 0)
+                       return;
+
+               boolean seePlayer = false;
+
+               for (TileMap.MapData md : map.range(x, y, game.viewRange)) {
+                       if (!md.entities.isEmpty() && canSee(md.x, md.y)) {
+                               for (Thing thing : md.entities) {
+                                       if (thing instanceof Player)
+                                               seePlayer = true;
+                               }
+                       }
+               }
+               canSeePlayer = seePlayer;
+       }
+
+       static class GiveItem {
+
+               String name;
+               double probability;
+
+               public GiveItem(String name, double probability) {
+                       this.name = name;
+                       this.probability = probability;
+               }
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Pack.java b/DuskServer/src/duskz/server/entityz/Pack.java
new file mode 100644 (file)
index 0000000..7ed1720
--- /dev/null
@@ -0,0 +1,121 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import java.util.ArrayList;
+
+/**
+ * Tracks a group of things following other things
+ *
+ * I think they all have to be players or pets, maybe they should
+ * sub-class off a common thing.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Pack {
+       /*
+        * Required functionality:
+        *  - ordered list of followers
+        *  - notifications
+        *  - grou tests, e.g. member of, clan inside
+        */
+
+       ArrayList<Active> members = new ArrayList<>();
+
+       public boolean contains(Active member) {
+               return members.contains(member);
+       }
+
+       /**
+        * Returns true if any member is clanless
+        *
+        * @return
+        */
+       public boolean containsClanless() {
+               for (Active a : members) {
+                       if (a.clan.equals("none"))
+                               return true;
+               }
+               return false;
+       }
+
+       /**
+        * Adds follower onto the end of the chain of followers
+        *
+        * @param follower
+        */
+       public void addFollower(Active follower) {
+               if (!members.isEmpty()) {
+                       for (Active a : members) {
+                               a.chatMessage("You are now being followed by " + follower.name + ".");
+                       }
+                       follower.chatMessage("You are now following " + members.get(members.size() - 1).name + ".");
+               }
+               members.add(follower);
+       }
+
+       public boolean isLeader(Active a) {
+               assert (members.size() > 1);
+
+               return members.get(0).ID == a.ID;
+       }
+
+       public Active getFollowing(Active leader) {
+               int i = members.indexOf(leader);
+
+               assert (members.size() > 1);
+               assert (i != -1);
+
+               if (i + 1 < members.size())
+                       return members.get(i + 1);
+               return null;
+       }
+
+       Active getLeader(Active follower) {
+               int i = members.indexOf(follower);
+
+               if (i == 0)
+                       return follower;
+
+               return members.get(i - 1);
+       }
+
+       /**
+        * Remove someone who is following.
+        *
+        * FIXME: if this leavs the group emtpy, then what?
+        *
+        * @param follower
+        */
+       void removeFollower(Active follower) {
+               if (isLeader(follower)) {
+                       // cant do that
+               } else {
+                       Active leader = getLeader(follower);
+
+                       members.remove(follower);
+                       for (Active a : members) {
+                               a.chatMessage(follower.name + " is no longer following you.");
+                       }
+                       follower.chatMessage("You are no longer following " + leader.name);
+               }
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Pet.java b/DuskServer/src/duskz/server/entityz/Pet.java
new file mode 100644 (file)
index 0000000..ac262e0
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.DuskMessage;
+import java.util.ArrayList;
+
+/**
+ * Pet, only Players can have pets
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Pet extends Active {
+
+       /**
+        * The pet's master.
+        */
+       Player master;
+
+       public Pet(Game game) {
+               super(game);
+       }
+
+       @Override
+       public int getType() {
+               return TYPE_PET;
+       }
+
+       @Override
+       public boolean isCanLead() {
+               return master.isCanLead();
+       }
+
+       @Override
+       public void send(DuskMessage msg) {
+               master.send(msg);
+       }
+
+       @Override
+       public void chatMessage(Active from, String clan, String msg) {
+               master.chatMessage(from, clan, msg);
+       }
+
+       @Override
+       public boolean canAttackEnemy(Active enemy) {
+               master.chatMessage(null, null, "Pets cannot lead battle");
+               return false;
+       }
+
+       @Override
+       public void follow(Active master) {
+               if (this.master.ID != master.ID) {
+                       chatMessage("You can only follow your owner.");
+                       return;
+               }
+               super.follow(master);
+       }
+
+       @Override
+       void leaveCommand() {
+               chatMessage("You cannot leave your master unless he unfollows you.");
+       }
+       
+       @Override
+       protected boolean followTo(Active leader, int oldx, int oldy) {
+
+               assert (leader.ID == master.ID);
+
+               if (leader.ID != master.ID) {
+                       throw new RuntimeException("pets can only follow masters");
+               }
+
+               boolean moved = jumpTo(map, oldx, oldy);
+
+               return moved;
+       }
+
+       @Override
+       public void fleeBattle(Battle battle, ArrayList<Active> opponents) {
+               endBattle();
+       }
+
+       @Override
+       public void killedBattle(Battle battle, Active winner, ArrayList<Active> opponents) {
+               battle.chatMessage(name + " is wounded.");
+               chatMessage("You have been wounded.");
+               splitMoney(game.goldLoseMod, opponents);
+               splitExp(game.expLoseMod, opponents);
+               endBattle();
+       }
+
+       @Override
+       public void sayCommand(String text) {
+               localisedChat("Pet " + name + " says: " + text);
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Player.java b/DuskServer/src/duskz/server/entityz/Player.java
new file mode 100644 (file)
index 0000000..66d9bf2
--- /dev/null
@@ -0,0 +1,544 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.DuskMessage;
+import duskz.protocol.DuskProtocol;
+import duskz.protocol.EntityUpdateMessage;
+import duskz.protocol.ListMessage;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Human player
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Player extends Active implements DuskProtocol {
+
+       private HashSet<String> ignore = new HashSet<>();
+       // This shoudln't be stored like this ...
+       protected String password;
+       /**
+        * Privilege level
+        */
+       int privs;
+       /**
+        * Players can have 1 pet
+        */
+       Pet pet;
+       /*
+        * State tracking
+        */
+       private boolean hasMoved = false;
+       /**
+        *
+        */
+       // TODO: check
+       public boolean audioon = true,
+                       coloron = true,
+                       popup = true,
+                       highlight = true;
+       /**
+        *
+        */
+       boolean alive;
+       /**
+        * Abuse control - muting until time
+        */
+       public long muteLimit;
+       /**
+        * Flood control for players
+        */
+       public long lastMessageStamp = 0;
+       /**
+        * Used to optimise network traffic
+        */
+       PlayerState state;
+       /**
+        * Connection to client
+        */
+       private final PlayerConnection connection;
+       /**
+        * Helper for commands
+        */
+       private final PlayerCommands commands;
+       /**
+        * Helper for command line
+        */
+       private final Commands cmdline;
+
+       public Player(Game game, PlayerConnection connection) {
+               super(game);
+               this.connection = connection;
+               this.commands = new PlayerCommands(game, this);
+               this.cmdline = new Commands(this);
+
+               this.state = new PlayerState(this);
+               alive = connection != null;
+       }
+
+       @Override
+       public int getType() {
+               return TYPE_PLAYER;
+       }
+
+       @Override
+       public int getImage() {
+               // + imagestep?
+               return race.image;
+       }
+
+       void conditionsChanged() {
+               state.conditions = game.getClock();
+       }
+
+       void actionsChanged() {
+               state.actions = game.getClock();
+       }
+
+       void statsChanged() {
+               state.stats = game.getClock();
+       }
+
+       void scoreChanged() {
+               state.score = game.getClock();
+       }
+
+       void wornChanged() {
+               // Wearing items always changes the inventory state
+               state.worn = game.getClock();
+               state.inventory = game.getClock();
+       }
+
+       void inventoryChanged() {
+               state.inventory = game.getClock();
+       }
+
+       @Override
+       public boolean addGold(int amount) {
+               if (super.addGold(amount)) {
+                       scoreChanged();
+                       return true;
+               }
+               return false;
+       }
+
+       @Override
+       public boolean addExp(int amount) {
+               if (super.addExp(amount)) {
+                       scoreChanged();
+                       return true;
+               }
+               return false;
+       }
+
+       @Override
+       public void setStat(int key, int value) {
+               super.setStat(key, value);
+               if (key <= STAT_MPMAX)
+                       scoreChanged();
+               else
+                       statsChanged();
+       }
+
+       @Override
+       public void setSleeping(boolean sleeping) {
+               super.setSleeping(sleeping);
+               actionsChanged();
+       }
+
+       @Override
+       public void wearItem(Wearable w) {
+               super.wearItem(w);
+               wornChanged();
+
+       }
+
+       @Override
+       public void unwearAt(int index) {
+               super.unwearAt(index);
+               wornChanged();
+       }
+
+       public PlayerCommands getCommands() {
+               return commands;
+       }
+
+       @Override
+       public DuskMessage createUpdateMessage(int name) {
+               EntityUpdateMessage en = (EntityUpdateMessage) super.createUpdateMessage(name);
+               // FIXME: move the base stuff to Active.
+               StringBuilder sb = new StringBuilder();
+               // Hmmm, should sleeping be a condition?
+               // use flags for now ...
+               //if (isSleeping()) {
+               //      sb.append("<sleeping>");
+               //}
+               if (!clan.equals("none")) {
+                       sb.append('<');
+                       sb.append(clan);
+                       sb.append('>');
+               }
+               // FIXME: status flags
+               // Hang on, shouldn't that be conditions?
+               //if (isPet() && hp < 0) {
+               //      sb.append("<wounded>");
+               //}
+               //for (String s : flags) {
+               //      sb.append('<');
+               //      sb.append(s);
+               //      sb.append('>');
+               //}
+               sb.append(this.name);
+
+               en.entityName = sb.toString();
+               en.imageStep = (short) imageStep;
+
+               return en;
+       }
+
+       @Override
+       void setProperty(String name, String value) {
+               switch (name) {
+                       case "password":
+                               password = value;
+                               break;
+                       default:
+                               super.setProperty(name, value);
+               }
+       }
+
+       @Override
+       protected void writeProperties(BufferedWriter out) throws IOException {
+               // Want this to be first line
+               writeProperty(out, "password", password);
+               super.writeProperties(out);
+
+               writeProperty(out, "privs", privs);
+               if (pet != null)
+                       writeProperty(out, "pet", pet.name);
+
+       }
+
+       public String getAddress() {
+               return connection.getAddress();
+       }
+
+       // Groups, clans, etc, can only be done on players!
+       public boolean isSameGroup(Active other) {
+               if (pack != null)
+                       return pack.contains(other);
+               return false;
+       }
+
+       public boolean isClanless() {
+               return clan == null || clan.equals("none");
+       }
+
+       /**
+        * Whether the acti
+        * FIXME: Who can have clans? if it's only players, fix Active to account for that.
+        *
+        * @return true if the active or any in it's pack are clanless.
+        */
+       public boolean isClanlessGroup() {
+               if (isClanless())
+                       return true;
+               else if (pack != null)
+                       return pack.containsClanless();
+               else
+                       return false;
+       }
+
+       /**
+        * Tracks if a move occured, and
+        *
+        * @param newLocX
+        * @param newLocY
+        * @param dir
+        * @param newStep
+        * @return
+        */
+       @Override
+       protected boolean moveTo(int newLocX, int newLocY, int newStep) {
+               hasMoved = super.moveTo(newLocX, newLocY, newStep);
+               return hasMoved;
+       }
+
+       @Override
+       protected boolean canAttackEnemy(Active enemy) {
+               switch (enemy.getType()) {
+                       case TYPE_PLAYER: {
+                               Player nme = (Player) enemy;
+
+                               if (isSameGroup(nme)) {
+                                       chatMessage(null, null, "You can't attack a member of your group.");
+                                       return false;
+                               }
+
+                               if (nme.isClanlessGroup()) {
+                                       chatMessage(null, null, "You can't fight them.");
+                                       return false;
+                               }
+
+                               // I don't see why the isFighting test is needed here.
+                               if (!enemy.isFighting()) {
+                                       if (this.isClanless()) {
+                                               chatMessage(null, null, "Players who are not in clans cannot fight other players.");
+                                               return false;
+                                       }
+
+                                       if (nme.visitingShop() != null) {
+                                               chatMessage(null, null, "You cannot attack players who are shopping.");
+                                               return false;
+                                       }
+                               }
+                               break;
+                       }
+                       case TYPE_MOBILE:
+                               // free for all?
+                               break;
+                       case TYPE_PET:
+                               chatMessage(null, null, "You can't attack pets.");
+                               return false;
+               }
+               return super.canAttackEnemy(enemy);
+       }
+
+       @Override
+       public void enterBattle(Battle battle) {
+               super.enterBattle(battle);
+
+               actionsChanged();
+       }
+
+       @Override
+       public void endBattle() {
+               super.endBattle();
+
+               actionsChanged();
+       }
+
+       @Override
+       public void fleeBattle(Battle battle, ArrayList<Active> opponents) {
+               super.fleeBattle(battle, opponents);
+
+               if (pet != null) {
+                       battle.removeParticipant(pet);
+                       pet.fleeBattle(battle, opponents);
+               }
+       }
+
+       @Override
+       public void killedBattle(Battle battle, Active winner, ArrayList<Active> opponents) {
+               clearFollow();
+
+               battle.chatMessage(name + " is killed.");
+               chatBattle("You have died.");
+
+               splitMoney(game.goldLoseMod, opponents);
+               splitExp(game.expLoseMod, opponents);
+               endBattle();
+               game.globalChat(null, null, name + " has been killed by " + winner.name);
+
+               // FIXME: on player death script
+               game.onDeath(this, winner);
+
+               if (pet != null) {
+                       // how to pass this to battle?
+                       //      list2.remove(front2.getFollowing());
+                       battle.removeParticipant(pet);
+
+                       pet.damageDone = 0;
+                       pet.endBattle();
+                       // FIXME: warp pet to player location
+                       //      pet.changeLocBypass(map, x, y);
+               }
+       }
+
+       @Override
+       public void send(DuskMessage msg) {
+               if (isPlayer() && alive) {
+                       connection.send(msg);
+               }
+       }
+
+       public void chatMessage(Active from, String clan, String msg) {
+               if (from != null && from.getType() == TYPE_PLAYER && ignore.contains(from.name))
+                       return;
+
+               if (clan != null && !clan.equals(this.clan))
+                       return;
+
+               send(DuskMessage.create(MSG_CHAT, msg));
+
+               // TODO: charmer?
+       }
+
+       @Override
+       public void tick(int tick) {
+               if (tick >= muteLimit)
+                       muteLimit = 0;
+
+               super.tick(tick);
+       }
+
+       boolean isVoiceAllowed() {
+               if (muteLimit != 0) {
+                       chatMessage("You can't do that when nochanneled.");
+                       return false;
+               } else {
+                       long last = lastMessageStamp;
+                       lastMessageStamp = System.currentTimeMillis();
+                       if ((lastMessageStamp - last) < game.floodLimit) {
+                               chatMessage("No flooding.");
+                               return false;
+                       }
+                       return true;
+               }
+       }
+
+       @Override
+       public void gossipCommand(String text) {
+               if (isVoiceAllowed()) {
+                       String title = name;
+                       if (privs > 2) {
+                               //                      && hasCondition("invis")
+                               //                      && hasCondition("invis2")) {
+                               title = "A god";
+                       }
+                       game.globalChat(this, null, title + " gossips: " + text);
+               }
+       }
+
+       @Override
+       public void sayCommand(String text) {
+               if (isVoiceAllowed()) {
+                       String title = name;
+                       if (privs > 2) {
+                               //                      && hasCondition("invis")
+                               //                      && hasCondition("invis2")) {
+                               title = "A god";
+                       }
+                       localisedChat(title + " says: " + text);
+               }
+       }
+
+       @Override
+       public void visibilityTick(int tick) {
+               super.visibilityTick(tick);
+
+               state.updatePlayer(tick);
+       }
+
+       @Override
+       public void follow(Active master) {
+               if (master.getType() != TYPE_PLAYER
+                               && master.getType() != TYPE_PET) {
+                       chatMessage("You can only follow players.");
+                       return;
+               }
+
+               super.follow(master);
+       }
+
+       @Override
+       void leaveCommand() {
+               boolean linkpet = false;
+               if (pet != null) {
+                       // Not really happy with this design here
+                       if (pack.contains(pet)) {
+                               pack.removeFollower(pet);
+                               linkpet = true;
+                       }
+               }
+
+               super.leaveCommand();
+
+               // This seems a bit ugly ...
+               if (linkpet) {
+                       pack = new Pack();
+                       pet.pack = pack;
+                       pack.addFollower(this);
+                       pack.addFollower(pet);
+               }
+       }
+
+       void initMap() {
+               ListMessage lm = new ListMessage(MSG_INIT_MAP);
+
+               lm.add(FIELD_MAP_WIDTH, game.mapSize);
+               lm.add(FIELD_MAP_HEIGHT, game.mapSize);
+               // FIXME: depends on client info
+               lm.add(FIELD_MAP_ASSETLOCATION, game.rcName);
+
+               send(lm);
+       }
+
+       void startup() {
+               // Sent initial stuff
+               initMap();
+
+               // Add me: no, do it at next game update loop
+               //send(createUpdateMessage(MSG_ADD_ENTITY));
+
+               game.onPlayerStart(this);
+       }
+
+       void logout() {
+               game.unregisterPlayer(this);
+               // FIXME: save player
+               chatMessage("Goodbyte.");
+               connection.shutdown();
+       }
+
+       void parseCommand(String cmd) {
+               System.out.println(name + ": parse command: " + cmd);
+               try {
+                       cmdline.execute(cmd);
+               } catch (Exception ex) {
+                       // FIXME: log
+                       game.log.printf(ex, "executing: " + cmd);
+                       Logger.getLogger(Player.class.getName()).log(Level.SEVERE, null, ex);
+               }
+       }
+
+       @Override
+       public void addItem(Holdable item) {
+               super.addItem(item);
+
+               inventoryChanged();
+       }
+
+       @Override
+       public boolean removeItem(Holdable item) {
+               if (super.removeItem(item)) {
+                       inventoryChanged();
+                       return true;
+               }
+               return false;
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/PlayerCommands.java b/DuskServer/src/duskz/server/entityz/PlayerCommands.java
new file mode 100644 (file)
index 0000000..0184bf5
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+/**
+ * This is a wrapper for the player object, which is passed to scripts.
+ *
+ * Defines a cleaner external interface, and the intention is that this
+ * is a safer sandboxed set of interfaces.
+ *
+ * TODO: this is probably where i want the commands to live too simply
+ * for a matter of maintainability
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class PlayerCommands {
+       private final Game game;
+
+       final private Player p;
+
+       public PlayerCommands(Game game, Player p) {
+               this.game = game;
+               this.p = p;
+       }
+
+       public void chat(String msg) {
+               p.chatMessage(msg);
+       }
+
+       public String getString(String name, String def) {
+               return p.variables.getString(name, def);
+       }
+
+       public String getString(String name) {
+               return p.variables.getString(name, null);
+       }
+
+       public void setString(String name, String value) {
+               p.variables.put(name, value);
+       }
+
+       public void setInt(String name, int value) {
+               p.variables.put(name, value);
+       }
+
+       public int getInt(String name, int def) {
+               return p.variables.getInteger(name, def);
+       }
+
+       public int getInt(String name) {
+               return p.variables.getInteger(name, 0);
+       }
+       
+       /**
+        * Jump to a location
+        * @param mapName
+        * @param mapAlias
+        * @return true if the jump happened.
+        */
+       public boolean jumpTo(String mapName, String mapAlias) {
+               System.out.println("script: jumpto(" + mapName + ", " + mapAlias + ")");
+               TileMap map = game.getMap(mapName);
+               
+               if (map != null) {
+                       Location l = map.locationForAlias(mapAlias);
+                       
+                       if (l != null) {
+                               return p.jumpTo(map, l.x, l.y);
+                       }
+               }
+               return false;
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/PlayerConnection.java b/DuskServer/src/duskz/server/entityz/PlayerConnection.java
new file mode 100644 (file)
index 0000000..be959dc
--- /dev/null
@@ -0,0 +1,487 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.DuskMessage;
+import duskz.protocol.DuskProtocol;
+import static duskz.protocol.DuskProtocol.MSG_AUTH;
+import static duskz.protocol.DuskProtocol.MSG_COMMAND;
+import static duskz.protocol.DuskProtocol.MSG_QUERY;
+import duskz.protocol.EntityListMessage;
+import duskz.protocol.ListMessage;
+import duskz.server.BlockedIPException;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.util.HashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Wraps incomming/outgoing communications
+ *
+ * The receiver thread handles login and creates the player object.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class PlayerConnection implements DuskProtocol {
+
+       private final Game game;
+       private final Socket socket;
+       private final ReceiverThread receiver;
+       private final SenderThread sender;
+       boolean isStopped;
+       Player player;
+
+       public PlayerConnection(Game game, Socket socket) throws IOException {
+               this.game = game;
+               this.socket = socket;
+               receiver = new ReceiverThread(socket);
+               sender = new SenderThread(socket);
+       }
+
+       public void start() {
+               receiver.start();
+               sender.start();
+       }
+
+       public String getAddress() {
+               // TODO: the connect code had the following, why?
+               //              String strIP = socket.getInetAddress().toString();
+               //int ip = strIP.indexOf("/");
+               //strIP = strIP.substring(ip + 1, strIP.length());
+
+               if (socket != null
+                               && socket.isConnected())
+                       return socket.getInetAddress().toString();
+               else
+                       return null;
+       }
+
+       void send(DuskMessage msg) {
+               sender.send(msg);
+       }
+
+       void shutdown() {
+               // Do I want to save the player here/
+               System.out.println("shutting down player connection");
+
+               try {
+                       // shutdown everything
+                       isStopped = true;
+                       sender.interrupt();
+                       receiver.interrupt();
+                       try {
+                               sender.join();
+                       } catch (InterruptedException x) {
+                       }
+                       socket.close();
+               } catch (IOException ex) {
+                       Logger.getLogger(PlayerConnection.class.getName()).log(Level.SEVERE, null, ex);
+               }
+       }
+
+       void abort() {
+               // definitely no-save exit
+               shutdown();
+       }
+
+       public class ReceiverThread extends Thread implements DuskProtocol {
+
+               final private DataInputStream instream;
+
+               public ReceiverThread(Socket socket) throws IOException {
+                       instream = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
+               }
+
+               DuskMessage readMessage() throws IOException {
+                       DuskMessage msg;
+
+                       do {
+                               synchronized (instream) {
+                                       msg = DuskMessage.receiveMessage(instream);
+                               }
+
+                               // Redirect queries to whomever is waiting for it
+                               if (msg.name == MSG_QUERY) {
+                                       EntityListMessage qmsg = (EntityListMessage) msg;
+                                       PendingQuery f = sender.getPendingQuery(qmsg.id);
+                                       f.setResponse(qmsg);
+                               }
+                       } while (msg.name == MSG_QUERY);
+
+                       return msg;
+               }
+               final static int ASK_NEW_RACE = 0;
+
+               private DuskMessage getRaceQuery() {
+                       ListMessage racem = new ListMessage(ASK_NEW_RACE);
+
+                       racem.add(FIELD_QUERY_PROMPT, "Choose race");
+                       racem.add(FIELD_QUERY_OPTIONS, game.getRaceNames());
+
+                       return racem;
+               }
+
+               private DuskMessage getNewPlayerInfo() {
+                       ListMessage np = new ListMessage(FIELD_AUTH_NEWPLAYER);
+                       np.add(getRaceQuery());
+
+                       return np;
+               }
+
+               /**
+                * Check if the user can be created, unique name, and all requirements met
+                *
+                * @param name
+                * @param np NEWPLAYER message containing new player infos.
+                * @param res a NEWPLAYER message populated iwth missing bits
+                * @return true if ok
+                */
+               boolean canCreate(String name, ListMessage np, ListMessage res) {
+                       boolean ok = true;
+                       ListMessage newp = null;
+                       String race;
+
+                       // Check if we have the info we need to create the user - i.e. race so far
+                       if (np == null
+                                       || (race = np.getString(ASK_NEW_RACE, null)) == null
+                                       || game.getRace(race) == null) {
+                               newp = new ListMessage(FIELD_AUTH_NEWPLAYER);
+                               newp.add(getRaceQuery());
+                               res.add(newp);
+                               ok = false;
+                       }
+
+                       ok &= !game.playerExists(name);
+                       ok &= !game.petExists(name);
+
+                       return ok;
+               }
+
+               Player loadPlayer(String name, String password) throws IOException, BlockedIPException {
+                       Player player = new Player(game, PlayerConnection.this);
+
+                       try {
+                               player.load(new File(game.getRoot(), "players/default"));
+                       } catch (IOException x) {
+                       }
+
+                       player.load(new File(game.getRoot(), "players/" + name));
+
+                       return player;
+               }
+
+               Player createPlayer(String name, String password, ListMessage np) throws IOException, BlockedIPException {
+                       String race = np.getString(ASK_NEW_RACE, null);
+
+                       Player player = new Player(game, PlayerConnection.this);
+
+                       try {
+                               player.load(new File(game.getRoot(), "players/default"));
+                       } catch (IOException x) {
+                       }
+
+                       player.setProperty("name", name);
+                       // this will load the race
+                       player.setProperty("race", race);
+                       player.setProperty("password", password);
+
+                       if (player.race == null)
+                               throw new IOException("Unknown race");
+
+                       player.save(new File(game.getRoot(), "players/" + name));
+
+                       return player;
+               }
+
+               Player handleLogin() {
+                       String address = getAddress();
+
+                       // Handle auth state.
+                       try {
+                               while (true) {
+                                       DuskMessage dm = readMessage();
+                                       ListMessage res = new ListMessage(MSG_AUTH);
+
+                                       // TODO: FIELD_AUTH_CLIENT stuff
+
+                                       switch (dm.name) {
+                                               case MSG_AUTH: {
+                                                       ListMessage lm = (ListMessage) dm;
+                                                       String player = lm.getString(FIELD_AUTH_PLAYER, null);
+                                                       String pass = lm.getString(FIELD_AUTH_PASS, null);
+                                                       ListMessage newp = (ListMessage) lm.getMessage(FIELD_AUTH_NEWPLAYER);
+
+                                                       // Don't give detailed reasons for failure (e.g. 'player not found') for security
+
+                                                       if (player == null || pass == null) {
+                                                               // Just send new player info
+                                                               res.add(-1L, FIELD_AUTH_RESULT, AUTH_LOGIN_INCOMPLETE);
+                                                               res.add(FIELD_AUTH_REASON, "Login failed.");
+                                                               res.add(getNewPlayerInfo());
+                                                       } else if (newp == null && game.playerExists(player)) {
+                                                               // Try normal login
+                                                               if (game.checkPassword(player, pass, address)) {
+                                                                       return loadPlayer(player, pass);
+                                                               } else {
+                                                                       res.add(-1L, FIELD_AUTH_RESULT, AUTH_LOGIN_FAILED);
+                                                                       res.add(FIELD_AUTH_REASON, "Login failed.");
+                                                               }
+                                                       } else if (!game.isGoodName(player)) {
+                                                               // disallowed name
+                                                               res.add(-1L, FIELD_AUTH_RESULT, AUTH_LOGIN_FAILED);
+                                                               res.add(FIELD_AUTH_REASON, "Name is not allowed, try again.");
+                                                               res.add(getNewPlayerInfo());
+                                                       } else {
+                                                               System.out.println("? new player " + player);
+                                                               // Trying to create a player
+                                                               if (canCreate(player, newp, res)) {
+                                                                       System.out.println("  creating player\n");
+                                                                       return createPlayer(player, pass, newp);
+                                                               } else {
+                                                                       if (res.get(FIELD_AUTH_NEWPLAYER) != null) {
+                                                                               res.add(-1L, FIELD_AUTH_RESULT, AUTH_LOGIN_INCOMPLETE);
+                                                                               res.add(FIELD_AUTH_REASON, "Insufficient information to create player.");
+                                                                       } else {
+                                                                               res.add(-1L, FIELD_AUTH_RESULT, AUTH_LOGIN_EXISTS);
+                                                                               res.add(FIELD_AUTH_REASON, "A player with that name already exists.");
+                                                                       }
+                                                               }
+                                                       }
+                                                       break;
+                                               }
+                                               default: // Anything else is a protocol error
+                                                       res.add(-1L, FIELD_AUTH_RESULT, AUTH_LOGIN_FAILED);
+                                                       res.add(FIELD_AUTH_REASON, "Unexpected message.");
+                                                       break;
+                                       }
+
+                                       sender.send(res);
+                               }
+                       } catch (BlockedIPException ex) {
+                               sender.send("There's already a player connected from your IP address.");
+                       } catch (IOException ex) {
+                               sender.send("IO Error: " + ex);
+                       } catch (ClassCastException ex) {
+                               sender.send("Loading error: " + ex);
+                       } catch (TooManyTriesException ex) {
+                               sender.send("Too many login failures, try again in an hour.");
+                       } catch (Exception ex) {
+                               sender.send("Unexpected error: " + ex);
+                               ex.printStackTrace();
+                       }
+                       abort();
+                       return null;
+               }
+
+               @Override
+               public void run() {
+                       player = handleLogin();
+
+                       if (player != null) {
+                               ListMessage res = new ListMessage(MSG_AUTH);
+                               res.add(player.ID, FIELD_AUTH_RESULT, AUTH_LOGIN_OK);
+                               res.add(FIELD_AUTH_REASON, "Login ok.");
+                               player.send(res);
+
+                               setName("Player thread: " + player.name);
+                               sender.setName("Ouptut thread: " + player.name);
+
+                               // log successful connection, etc.
+                               //
+                               player.startup();
+
+                               // Do this afterwards, as it then goes into the game update loop
+                               game.registerPlayer(player);
+
+                               while (!isStopped) {
+                                       try {
+                                               DuskMessage dm = readMessage();
+
+                                               switch (dm.name) {
+                                                       case MSG_COMMAND: {
+                                                               DuskMessage.StringMessage sm = (DuskMessage.StringMessage) dm;
+
+                                                               player.parseCommand(sm.value);
+                                                               break;
+                                                       }
+                                                       default:
+                                                               // anything else is bogus
+                                                               System.out.println("Unexpected server command (ignored):");
+                                                               dm.format(System.out);
+                                               }
+                                       } catch (SocketTimeoutException e) {
+                                               sender.send(DuskMessage.create(MSG_PING));
+                                       } catch (Exception e) {
+                                               //game.log.printError("LivingThing.run():" + name + " disconnected", e);
+                                               e.printStackTrace();
+                                               player.logout();
+                                               return;
+                                       }
+                               }
+                               shutdown();
+                       }
+               }
+       }
+
+       public class SenderThread extends Thread implements DuskProtocol {
+
+               private DataOutputStream outstream;
+               long qid;
+               final HashMap<Long, PendingQuery> pendingQuestions = new HashMap<>();
+               final public LinkedBlockingDeque<DuskMessage> messageQueue = new LinkedBlockingDeque<>();
+
+               public SenderThread(Socket socket) throws IOException {
+                       outstream = new DataOutputStream(new BufferedOutputStream(socket.getOutputStream()));
+               }
+
+               synchronized long getQuestionID() {
+                       return qid++;
+               }
+
+               PendingQuery getPendingQuery(long id) {
+                       synchronized (pendingQuestions) {
+                               return pendingQuestions.get(id);
+                       }
+               }
+
+               /**
+                * This asks a query asynchronously
+                * FIXME: abnormal shutdown requires the queries to be flushed
+                *
+                * @param query
+                * @return a future used to retrieve the query
+                */
+               public Future<EntityListMessage> askQuestion(EntityListMessage query) throws IOException {
+                       PendingQuery pq = new PendingQuery(query);
+
+                       if (query.name != MSG_QUERY)
+                               throw new RuntimeException("Trying to ask non-query message");
+
+                       synchronized (pendingQuestions) {
+                               query.id = qid++;
+                               pendingQuestions.put(query.id, pq);
+                       }
+
+                       messageQueue.add(query);
+
+                       // Just discard messages until we get a response
+                       // And what if we don't?
+                       if (Thread.currentThread() == this) {
+                               do {
+                                       receiver.readMessage();
+                               } while (pq.response == null);
+                       }
+
+                       return pq;
+               }
+
+               public void send(DuskMessage dm) {
+                       messageQueue.add(dm);
+               }
+
+               public void send(String msg) {
+                       messageQueue.add(new DuskMessage.StringMessage(MSG_CHAT, msg));
+               }
+
+               public void run() {
+                       DuskMessage msg;
+
+                       while (!isStopped) {
+                               try {
+                                       msg = messageQueue.take();
+
+                                       // low level protocol dump
+                                       msg.format(System.out);
+
+                                       try {
+                                               msg.sendMessage(outstream);
+                                               outstream.flush();
+                                       } catch (IOException e) {
+                                               e.printStackTrace();
+                                               player.logout();
+                                       }
+                               } catch (InterruptedException ex) {
+                                       //Logger.getLogger(SendThread.class.getName()).log(Level.SEVERE, null, ex);
+                               }
+                       }
+               }
+       }
+
+       static class PendingQuery implements Future<EntityListMessage> {
+
+               EntityListMessage query;
+               EntityListMessage response;
+
+               public PendingQuery(EntityListMessage query) {
+                       this.query = query;
+               }
+
+               synchronized void setResponse(EntityListMessage response) {
+                       this.response = response;
+                       notify();
+               }
+
+               @Override
+               public boolean cancel(boolean mayInterruptIfRunning) {
+                       throw new UnsupportedOperationException("Not supported yet.");
+               }
+
+               @Override
+               public boolean isCancelled() {
+                       throw new UnsupportedOperationException("Not supported yet.");
+               }
+
+               @Override
+               public boolean isDone() {
+                       throw new UnsupportedOperationException("Not supported yet.");
+               }
+
+               @Override
+               public synchronized EntityListMessage get() throws InterruptedException, ExecutionException {
+                       while (response == null) {
+                               wait();
+                       }
+                       return response;
+               }
+
+               @Override
+               public EntityListMessage get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+                       if (response == null) {
+                               wait(unit.convert(timeout, unit));
+                               if (response == null)
+                                       throw new TimeoutException();
+                       }
+                       return response;
+               }
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/PlayerShop.java b/DuskServer/src/duskz/server/entityz/PlayerShop.java
new file mode 100644 (file)
index 0000000..9238b60
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import java.util.List;
+
+/**
+ * A player provided shop, the list of items can be exhausted.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class PlayerShop extends Shop {
+
+       /**
+        * Gold in till (it appears unused in previous game?)
+        */
+       private long gold;
+       /**
+        * Owner of shop
+        */
+       private Player owner;
+
+       public PlayerShop(Game game) {
+               super(game);
+       }
+
+       @Override
+       public int getType() {
+               return TYPE_PLAYER_SHOP;
+       }
+
+       private void buy(Active buyer, List<Holdable> all) {
+               for (Holdable h : all) {
+                       items.remove(h);
+                       buyer.addItem(h);
+               }
+               inventoryChanged = game.getClock();
+       }
+
+       @Override
+       public void buy(Active buyer, String what, int quantity) {
+               List<Holdable> all = items.getAll(what, quantity);
+
+               if (all.isEmpty()) {
+                       buyer.chatMessage(name + " doesn't have that for sale.");
+               } else if (all.size() < quantity) {
+                       buyer.chatMessage(name + " doesn't have that many to sell.");
+               } else if (buyer == owner) {
+                       buy(buyer, all);
+               } else if (buyer.addGold(-all.get(0).cost * quantity * 3 / 4)) {
+                       gold += all.get(0).cost * quantity * 3 / 4;
+                       buy(buyer, all);
+               } else {
+                       buyer.chatMessage("You can't afford that.");
+               }
+       }
+
+       @Override
+       public void sell(Active customer, List<Holdable> all) {
+               if (owner != customer)
+                       customer.chatMessage("You cannot sell to this shop.");
+               else {
+                       for (Holdable h : all) {
+                               items.add(h);
+                               customer.removeItem(h);
+                       }
+                       inventoryChanged = game.getClock();
+               }
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/PlayerState.java b/DuskServer/src/duskz/server/entityz/PlayerState.java
new file mode 100644 (file)
index 0000000..ed0e04e
--- /dev/null
@@ -0,0 +1,367 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.DuskMessage;
+import duskz.protocol.DuskProtocol;
+import static duskz.protocol.DuskProtocol.FIELD_ENTITY_FLAGS;
+import static duskz.protocol.DuskProtocol.MSG_UPDATE_ENTITY;
+import duskz.protocol.EntityListMessage;
+import duskz.protocol.ListMessage;
+import duskz.protocol.MapMessage;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * This tracks the state of the player client, to optimise network transfers.
+ *
+ * Each integer field contains the last tick the item was changed, this is
+ * to allow for polled clients.
+ *
+ * The map update reqiures more information, so the location of the last update
+ * is recorded.
+ *
+ * TODO: pets need most of this monitored too
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class PlayerState implements DuskProtocol {
+
+       Player player;
+       /**
+        * Slowly changing stats, str, dex, etc.
+        */
+       protected int stats;
+       /**
+        * hp/mp, gold or exp changed
+        */
+       protected int score;
+       /**
+        * Condition list changed
+        */
+       protected int conditions;
+       /**
+        * Skill list/level changed
+        */
+       protected int skills;
+       /**
+        * Spell list/level changed
+        */
+       protected int spells;
+       /**
+        * Item list changed
+        */
+       protected int inventory;
+       /**
+        * worn items changed
+        */
+       protected int worn;
+       /**
+        * something affecting actions changed
+        */
+       protected int actions;
+       /**
+        * Last updated location
+        */
+       TileMap map;
+       int x;
+       int y;
+       /**
+        * If we were last over a shop
+        */
+       Shop overShop;
+       /**
+        * All visible things
+        */
+       final HashMap<Long, VisibleThing> visibleThings = new HashMap<>();
+       /*
+        * Last update tick
+        */
+       int lastUpdate = -1;
+
+       public PlayerState(Player player) {
+               this.player = player;
+       }
+
+       /**
+        * Send the player all updates since last tick
+        *
+        * @param player
+        */
+       public void updatePlayer(int tick) {
+               boolean moved = player.x != x || player.y != y || player.map != map;
+
+               x = player.x;
+               y = player.y;
+               map = player.map;
+
+               updateVisibleThings(tick);
+
+               if (moved) {
+                       updateMap();
+                       updateCheckShop();
+               } else if (overShop != null && overShop.inventoryChanged >= lastUpdate) {
+                       player.send(overShop.createTransactionMessage(MSG_UPDATE_MERCHANT));
+               }
+
+               if (actions >= lastUpdate)
+                       updateActions();
+
+               if (worn >= lastUpdate)
+                       updateWorn();
+
+               if (inventory >= lastUpdate)
+                       updateInventory();
+
+               if (conditions >= lastUpdate)
+                       updateConditions();
+
+               lastUpdate = tick;
+       }
+
+       void updateMap() {
+               int r = player.game.viewRange;
+
+               System.out.printf("update map %d,%d map %s\n", x, y, map.name);
+
+               int width = r * 2 + 1;
+               int height = r * 2 + 1;
+               short[] tiles = null;
+               int nlayers = map.getLayerCount();
+               int nused = 0;
+               short[][] layers = new short[nlayers][];
+               int groundLayer = 0;
+               for (int l = 0; l < nlayers; l++) {
+                       if (tiles == null)
+                               tiles = new short[width * height];
+
+                       short[] visible = map.getRegion(l, x - r, y - r, width, height, tiles);
+
+                       if (visible != null) {
+                               tiles = null;
+
+                               if (l == map.getGroundLayer())
+                                       groundLayer = nused;
+
+                               layers[nused++] = visible;
+                       }
+               }
+
+               player.send(new MapMessage(MSG_UPDATE_MAP, width, height, x, y, groundLayer, nused, layers));
+       }
+
+       /**
+        * Check if the player is on a shop square, and if so update the client
+        * state and send off the shop details. If the player has left a shop
+        * square, then send a leaving update.
+        */
+       void updateCheckShop() {
+               Shop thing = player.visitingShop();
+
+               if (thing != null) {
+                       overShop = thing;
+                       player.send(overShop.createTransactionMessage(MSG_UPDATE_MERCHANT));
+               } else if (overShop != null) {
+                       player.send(DuskMessage.create(MSG_EXIT_MERCHANT));
+                       overShop = null;
+               }
+       }
+
+       void updateActions() {
+               DuskMessage.StringListMessage list = new DuskMessage.StringListMessage(MSG_UPDATE_ACTIONS);
+
+               if (player.isFighting()) {
+                       list.add("flee");
+               } else {
+                       if (player.isSleeping()) {
+                               list.add("wake");
+                       } else {
+                               list.add("sleep");
+                       }
+               }
+               player.send(list);
+       }
+
+       void updateWorn() {
+               player.send(player.wornItems.toMessage(MSG_EQUIPMENT));
+       }
+
+       void updateInventory() {
+               // FIXME: make player item sale price ratio adjustable
+               player.send(player.inventory.createTransactionMessage(MSG_INVENTORY, 0.5f));
+       }
+
+       void updateConditions() {
+       }
+
+       /**
+        * Update the visibleThings list, and queue updates to the client.
+        *
+        * Note that this is the ONLY way that visibleThings should ever be changed.
+        */
+       void updateVisibleThings(int tick) {
+               HashMap<Long, VisibleThing> left = (HashMap<Long, VisibleThing>) visibleThings.clone();
+               ArrayList<VisibleThing> newlyVisible = new ArrayList<>();
+
+               //System.out.println("checking visible range");
+
+               // FIXME: I think this is missing something here, i.e. some sort of object visibility check
+
+               for (TileMap.MapData md : map.range(x, y, player.game.viewRange)) {
+                       if (!md.entities.isEmpty() && player.canSee(md.x, md.y)) {
+                               for (Thing thing : md.entities) {
+                                       VisibleThing here = left.remove(thing.ID);
+                                       if (here == null) {
+                                               newlyVisible.add(new VisibleThing(thing));
+                                       } else {
+                                               here.updateIfChanged(tick);
+                                       }
+                               }
+                       }
+               }
+
+               for (VisibleThing thing : newlyVisible) {
+                       // Add thing to client
+                       //send(thing.thing.createUpdateMessage(MSG_ADD_ENTITY));
+                       visibleThings.put(thing.thing.ID, thing);
+                       thing.updateIfChanged(tick);
+               }
+               // anything left over must be removed
+               for (long id : left.keySet()) {
+                       player.send(DuskMessage.EntityMessage.create(id, MSG_REMOVE_ENTITY));
+                       visibleThings.remove(id);
+               }
+       }
+
+       private void addStat(ListMessage lm, int field, int index) {
+               int total = player.getStat(index);
+               int bonus = player.getBonus(index);
+               addStat(lm, field, total - bonus, bonus);
+       }
+
+       private void addStat(ListMessage lm, int field, int base, int bonus) {
+               lm.add(field, base);
+               lm.add(field, bonus);
+       }
+
+       protected ListMessage buildStats(int msg) {
+               ListMessage lm = new ListMessage(msg);
+
+               lm.add(FIELD_INFO_CASH, player.getGold());
+               lm.add(FIELD_INFO_EXP, player.getExp());
+               addStat(lm, FIELD_INFO_STR, Active.STAT_STR);
+               addStat(lm, FIELD_INFO_INT, Active.STAT_INT);
+               addStat(lm, FIELD_INFO_DEX, Active.STAT_DEX);
+               addStat(lm, FIELD_INFO_CON, Active.STAT_CON);
+               addStat(lm, FIELD_INFO_WIS, Active.STAT_WIS);
+               addStat(lm, FIELD_INFO_DAM, player.getDamageMod(), player.getBonus(Active.STAT_DAMAGE));
+               addStat(lm, FIELD_INFO_ARC, player.getArmourMod(), player.getBonus(Active.STAT_ARC));
+
+               // What else?
+
+               return lm;
+       }
+
+       class VisibleThing {
+
+               Thing thing;
+               int clientX = -10;
+               int clientY = -10;
+
+               public VisibleThing(Thing thing) {
+                       this.thing = thing;
+               }
+
+               public void updateIfChanged(int tick) {
+                       int dx = thing.x - clientX;
+                       int dy = thing.y - clientY;
+                       DuskMessage msg = null;
+
+                       clientX = thing.x;
+                       clientY = thing.y;
+
+                       /*
+                        if (thing instanceof Active
+                        && ((Active) thing).isStateChanged(tick)) {
+                        System.out.println("sending remove/add for changed entity");
+                        // FIXME: this should just update the flags or whatever
+                        player.send(DuskMessage.EntityMessage.create(thing.ID, MSG_REMOVE_ENTITY));
+                        player.send(thing.createUpdateMessage(MSG_ADD_ENTITY));
+                        return;
+                        }*/
+
+                       if (dx != 0 || dy != 0) {
+                               System.out.println("Thing " + thing.name + " moved: " + dx + ", " + dy);
+
+                               // TODO: rather than relative move, send the specific location/faceing direction?
+
+                               if (dx == 0) {
+                                       if (dy == 1) {
+                                               msg = DuskMessage.create(thing.ID, MSG_MOVE, (byte) 's');
+                                       } else if (dy == -1) {
+                                               msg = DuskMessage.create(thing.ID, MSG_MOVE, (byte) 'n');
+                                       }
+                               }
+                               if (dy == 0) {
+                                       if (dx == 1) {
+                                               msg = DuskMessage.create(thing.ID, MSG_MOVE, (byte) 'e');
+                                       } else if (dx == -1) {
+                                               msg = DuskMessage.create(thing.ID, MSG_MOVE, (byte) 'w');
+                                       }
+                               }
+                               if (msg == null) {
+                                       // Need to remove it first to move it further
+                                       player.send(DuskMessage.EntityMessage.create(thing.ID, MSG_REMOVE_ENTITY));
+                                       msg = thing.createUpdateMessage(MSG_ADD_ENTITY);
+                               }
+
+                               player.send(msg);
+                       }
+
+                       // FIXME: this could include location?
+                       if (thing instanceof Active) {
+                               Active a = (Active) thing;
+
+                               if (a.flagsChanged >= lastUpdate
+                                               || a.conditionsChanged >= lastUpdate) {
+                                       EntityListMessage elm = new EntityListMessage(thing.ID, MSG_UPDATE_ENTITY);
+
+                                       if (a.flagsChanged >= lastUpdate) {
+                                               int flags = 0;
+                                               if (a.isFighting()) {
+                                                       flags = a.getBattleSide();
+                                               }
+                                               if (a.isSleeping())
+                                                       flags |= ENTITY_FLAG_SLEEPING;
+
+                                               elm.add(FIELD_ENTITY_FLAGS, flags);
+                                       }
+
+                                       if (a.conditionsChanged >= lastUpdate) {
+                                               elm.add(FIELD_ENTITY_CONDITIONS, a.getActiveConditions());
+                                       }
+
+                                       player.send(elm);
+                               }
+                       }
+               }
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Prop.java b/DuskServer/src/duskz/server/entityz/Prop.java
new file mode 100644 (file)
index 0000000..31822b6
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+/**
+ * Immovable object/graphic?
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Prop extends Thing {
+
+       public Prop(Game game) {
+               super(game);
+       }
+
+       @Override
+       public int getType() {
+               return TYPE_PROP;
+       }
+}
\ No newline at end of file
diff --git a/DuskServer/src/duskz/server/entityz/PropertyLoader.java b/DuskServer/src/duskz/server/entityz/PropertyLoader.java
new file mode 100644 (file)
index 0000000..0216bc6
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Slight abuse of iterators and autoclosable and iterable to be able to iterate
+ * through properties easily.
+ *
+ * Used as:
+ * try (PropertyLoader pl = new PropertyLoader(file)) {
+ * for (PropertyEntry pe: pl) {
+ * }
+ * }
+ *
+ * FIXME: maye just change to PropertyReader and use a readEntry() interface
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class PropertyLoader implements Iterable<PropertyLoader.PropertyEntry>, AutoCloseable {
+
+       BufferedReader reader;
+
+       public PropertyLoader(File src) throws FileNotFoundException {
+               reader = new BufferedReader(new FileReader(src));
+       }
+
+       /**
+        * Should only be called once for a given property loader.
+        *
+        * @return
+        */
+       @Override
+       public Iterator<PropertyEntry> iterator() {
+               return new PropertyIterator(reader);
+       }
+
+       @Override
+       public void close() throws IOException {
+               if (reader != null)
+                       reader.close();
+       }
+
+       static class PropertyIterator implements Iterator<PropertyEntry> {
+
+               BufferedReader reader;
+               String name;
+               PropertyEntry entry = new PropertyEntry();
+
+               public PropertyIterator(BufferedReader reader) {
+                       this.reader = reader;
+               }
+
+               @Override
+               public boolean hasNext() {
+                       try {
+                               if (reader == null)
+                                       return false;
+
+                               if (name != null)
+                                       return true;
+
+                               do {
+                                       String line = reader.readLine();
+
+                                       if (line == null)
+                                               return false;
+                                       else if (!line.startsWith("#")) {
+                                               int c = line.indexOf('=');
+
+                                               if (c != -1) {
+                                                       entry.value = line.substring(c + 1).trim();
+                                                       entry.name = name = line.substring(0, c).trim();
+                                               } else {
+                                                       System.out.println("unparsable property line: " + line);
+                                               }
+                                       }
+                               } while (name == null);
+
+                               return true;
+                       } catch (IOException ex) {
+                               Logger.getLogger(PropertyLoader.class.getName()).log(Level.SEVERE, null, ex);
+                               return false;
+                       }
+               }
+
+               @Override
+               public PropertyEntry next() {
+                       if (name == null)
+                               if (!hasNext()) {
+                                       throw new IndexOutOfBoundsException();
+                               }
+
+                       name = null;
+                       return entry;
+               }
+
+               @Override
+               public void remove() {
+                       throw new UnsupportedOperationException("Not supported yet.");
+               }
+       }
+
+       public static class PropertyEntry {
+
+               public String name;
+               public String value;
+
+               public int asInteger(int def) {
+                       try {
+                               return Integer.valueOf(value);
+                       } catch (NumberFormatException x) {
+                               return def;
+                       }
+               }
+       }
+
+       public static void main(String[] args) throws IOException {
+               File file = new File("mobs");
+               try (PropertyLoader pl = new PropertyLoader(file)) {
+                       for (PropertyEntry pe : pl) {
+                               System.out.println("entry: '" + pe.name + "' " + pe.value);
+                       }
+               }
+
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Race.java b/DuskServer/src/duskz/server/entityz/Race.java
new file mode 100644 (file)
index 0000000..e4984ef
--- /dev/null
@@ -0,0 +1,106 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import static duskz.server.entityz.Active.STAT_CON;
+import static duskz.server.entityz.Active.STAT_DEX;
+import static duskz.server.entityz.Active.STAT_HP;
+import static duskz.server.entityz.Active.STAT_HPMAX;
+import static duskz.server.entityz.Active.STAT_INT;
+import static duskz.server.entityz.Active.STAT_MP;
+import static duskz.server.entityz.Active.STAT_MPMAX;
+import static duskz.server.entityz.Active.STAT_STR;
+import static duskz.server.entityz.Active.STAT_WIS;
+import duskz.server.entityz.PropertyLoader.PropertyEntry;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Meta-data about race
+ *
+ * Races adjust the base values of attributes.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Race {
+
+       public String name;
+       public int image;
+       // Uses STAT_* constants from Active
+       public int stats[] = new int[12];
+
+       Race() {
+       }
+
+       boolean setProperty(String name, String value) {
+               switch (name) {
+                       case "name":
+                               this.name = value;
+                               break;
+                       case "image":
+                               image = Integer.valueOf(value);
+                               break;
+                       case "hp":
+                               stats[STAT_HP] = Integer.valueOf(value);
+                               break;
+                       case "maxhp":
+                               stats[STAT_HPMAX] = Integer.valueOf(value);
+                               break;
+                       case "mp":
+                               stats[STAT_MP] = Integer.valueOf(value);
+                               break;
+                       case "maxmp":
+                               stats[STAT_MPMAX] = Integer.valueOf(value);
+                               break;
+                       case "str":
+                               stats[STAT_STR] = Integer.valueOf(value);
+                               break;
+                       case "int":
+                               stats[STAT_INT] = Integer.valueOf(value);
+                               break;
+                       case "dex":
+                               stats[STAT_DEX] = Integer.valueOf(value);
+                               break;
+                       case "con":
+                               stats[STAT_CON] = Integer.valueOf(value);
+                               break;
+                       case "wis":
+                               stats[STAT_WIS] = Integer.valueOf(value);
+                               break;
+                       default:
+                               return false;
+               }
+               return true;
+       }
+
+       public static Race loadRace(File file) throws IOException {
+               Race r = new Race();
+
+               r.name = file.getName();
+               try (PropertyLoader pl = new PropertyLoader(file)) {
+                       for (PropertyEntry pe: pl) {
+                               r.setProperty(pe.name, pe.value);
+                       }
+               }
+               
+               return r;
+       }
+}
@@ -20,7 +20,7 @@
 /**
  * Changes
  */
-package duskz.server;
+package duskz.server.entityz;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -78,6 +78,9 @@ public class ScriptManager {
 
        public ScriptManager() {
                Permissions perms = new Permissions();
+               
+               // FIXME: fix security manager for the actual paths used
+               
                //perms.add(new AllPermission());
                perms.add(new FilePermission("scripts/*", "read,write"));
                //      perms.add(new NetPermission("*"));
@@ -114,11 +117,11 @@ public class ScriptManager {
                new WatchThread().start();
        }
 
-       public Future<?> runScript(String script, Object... args) {
+       public <T> Future<T> runScript(String script, Object... args) throws ClassCastException {
                return pool.submit(new ScriptData(script, args));
        }
 
-       public Future<?> runScript(File script, Object... args) throws FileNotFoundException {
+       public <T> Future<T> runScript(File script, Object... args) throws FileNotFoundException, ClassCastException {
                return pool.submit(new ScriptData(script, args));
        }
 
@@ -179,7 +182,7 @@ public class ScriptManager {
                        this.script = script;
                }
 
-               public ScriptData(File scriptFile, Object... args) throws FileNotFoundException {
+               public ScriptData(File scriptFile, Object... args) throws FileNotFoundException, ClassCastException {
                        for (int i = 0; i < args.length; i += 2) {
                                this.args.put((String) args[i], args[i + 1]);
                        }
diff --git a/DuskServer/src/duskz/server/entityz/Shop.java b/DuskServer/src/duskz/server/entityz/Shop.java
new file mode 100644 (file)
index 0000000..8eff71a
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.TransactionMessage;
+import java.util.List;
+
+/**
+ * Shops contain sellable items
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public abstract class Shop extends Thing {
+
+       Inventory items = new Inventory();
+       /**
+        * Tick of last inventory change.
+        */
+       int inventoryChanged;
+
+       Shop(Game game) {
+               super(game);
+       }
+
+       @Override
+       void setProperty(String name, String value) {
+               switch (name) {
+                       case "item":
+                               Holdable h = game.createItem(value);
+                               if (h != null) {
+                                       System.out.println(this.name + " sells " + h.name);
+                                       items.add(h);
+                               } else
+                                       game.log.printf(Log.ERROR, "Shop: %s, unkown item: %s", this.name, value);
+                               break;
+                       default:
+                               super.setProperty(name, value);
+               }
+       }
+
+       /**
+        * Create a transactionmessage representing the shop stores.
+        *
+        * @param name
+        * @return
+        */
+       public TransactionMessage createTransactionMessage(int name) {
+               return items.createTransactionMessage(name, 1);
+       }
+
+       @Override
+       public void look(Active viewer) {
+               if (description != null)
+                       viewer.chatMessage("You see " + name + ", " + description);
+               else
+                       viewer.chatMessage("You see " + name);
+               viewer.chatMessage("Currently for sale:");
+
+               items.describeTo(viewer);
+
+               /*
+                *                                      } else if (objStore.isPlayerMerchant()) {
+                lt.chatMessage("You see a merchant that sells");
+                PlayerMerchant pmrStore = (PlayerMerchant) objStore;
+                boolean blnEmptyMerchant = true;
+                for (LinkedList<Item> list : pmrStore.vctItems.values()) {
+                Item item = (Item) list.element();
+                cmdline = item.name;
+                String strSpacer = "\t";
+                if (cmdline.length() < 11) {
+                strSpacer = "\t\t";
+                }
+                lt.chatMessage("\t" + cmdline + strSpacer + item.description);
+                blnEmptyMerchant = false;
+                }
+                if (blnEmptyMerchant) {
+                lt.chatMessage("\tNothing at the moment.");
+                }
+                } else if (objStore.isMerchant()) {
+                lt.chatMessage("You see a merchant that sells");
+                Merchant mrcStore = (Merchant) objStore;
+                boolean blnEmptyMerchant = true;
+                for (String item : mrcStore.items) {
+                Item itmStore = game.getItem(item);
+                if (itmStore != null) {
+                String strSpacer = "\t";
+                if (item.length() < 11) {
+                strSpacer = "\t\t";
+                }
+                lt.chatMessage("\t" + item + strSpacer + itmStore.description);
+                blnEmptyMerchant = false;
+                } else {
+                if (item.equals("pet")) {
+                lt.chatMessage("\tpets.");
+                blnEmptyMerchant = false;
+                } else {
+                item = item.substring(6, item.length());
+                lt.chatMessage("\ttraining in " + item);
+                blnEmptyMerchant = false;
+                }
+                }
+                }
+                if (blnEmptyMerchant) {
+                lt.chatMessage("\tNothing.");
+                }
+                return null;
+                }
+
+                */
+       }
+
+       abstract public void buy(Active buyer, String what, int quantity);
+
+       abstract public void sell(Active customer, List<Holdable> items);
+}
diff --git a/DuskServer/src/duskz/server/entityz/Sign.java b/DuskServer/src/duskz/server/entityz/Sign.java
new file mode 100644 (file)
index 0000000..d8bc925
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+
+/**
+ * Sign
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+class Sign extends Thing {
+
+       public Sign(Game game) {
+               super(game);
+       }
+
+       @Override
+       public int getType() {
+               return TYPE_SIGN;
+       }
+
+       @Override
+       void writeState(BufferedWriter out) throws IOException {
+               super.writeState(out);
+               writeProperty(out, "description", description);
+       }
+
+       @Override
+       public void look(Active viewer) {
+               viewer.chatMessage("The sign says " + description + ".");
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Thing.java b/DuskServer/src/duskz/server/entityz/Thing.java
new file mode 100644 (file)
index 0000000..0a891af
--- /dev/null
@@ -0,0 +1,450 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.DuskMessage;
+import duskz.protocol.EntityUpdateMessage;
+import duskz.server.entityz.PropertyLoader.PropertyEntry;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.Comparator;
+
+/**
+ * Base object for all dusk things
+ *
+ * Handles common fields, creation and typeing, and i/o.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public abstract class Thing implements Cloneable {
+
+       /**
+        * Every non-abstract sub-class of thing needs a unique
+        * number. Used to simplify some code decisions.
+        */
+       public static final int TYPE_PLAYER = 0;
+       public static final int TYPE_PET = 1;
+       public static final int TYPE_MOBILE = 2;
+       public static final int TYPE_SIGN = 3;
+       public static final int TYPE_PROP = 4;
+       public static final int TYPE_GAME_SHOP = 5;
+       public static final int TYPE_PLAYER_SHOP = 6;
+       public static final int TYPE_ITEM = 7;
+       public static final int TYPE_FOOD = 8;
+       public static final int TYPE_DRINK = 9;
+       public static final int TYPE_WEAPON = 10;
+       public static final int TYPE_ARMOUR = 11;
+       public static final int TYPE_CONTAINER = 12;
+       public static final int TYPE_TRAINING = 13;
+       public final static Comparator<Thing> cmpName = new Comparator<Thing>() {
+               @Override
+               public int compare(Thing o1, Thing o2) {
+                       return o1.name.compareTo(o2.name);
+               }
+       };
+       protected final Game game;
+       /**
+        * Game unique id
+        */
+       public final long ID;
+       /**
+        * Name
+        */
+       public String name;
+       /**
+        * Description
+        */
+       public String description = null;
+       /**
+        * Hide the name from players.
+        *
+        * boolean isHideName;
+        * /**
+        * Map object belongs to
+        */
+       public TileMap map;
+       /**
+        * Location on map
+        */
+       public int x, y;
+       /**
+        * Current image id. TODO: players get images from race instead.
+        */
+       protected int image;
+       //
+       static long nextid = 1;
+       static Integer idlock = new Integer(0);
+
+       /**
+        * Create a new globally unique id
+        *
+        * @return
+        */
+       public static long createID() {
+               synchronized (idlock) {
+                       return nextid++;
+               }
+       }
+
+       public Thing(Game game) {
+               this.ID = createID();
+               this.game = game;
+       }
+
+       /*
+        * 
+        */
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof Thing) {
+                       return ((Thing) obj).ID == ID;
+               } else
+                       return false;
+       }
+
+       @Override
+       public int hashCode() {
+               int hash = 3;
+               hash = 61 * hash + (int) (this.ID ^ (this.ID >>> 32));
+               return hash;
+       }
+
+       public int getImage() {
+               return image;
+       }
+
+       /**
+        * For efficient decision making, return TYPE_* types
+        *
+        * @return
+        */
+       public abstract int getType();
+
+       /**
+        * Calculate the l1 or manhatten distance to another thing
+        *
+        * @param other
+        * @return
+        */
+       public int distanceL1(Thing other) {
+               return Math.abs(other.x - x) + Math.abs(other.y - y);
+       }
+
+       /**
+        * Create an EntityUpdate message with the given name
+        *
+        * @param msg
+        * @return
+        */
+       public DuskMessage createUpdateMessage(int name) {
+               EntityUpdateMessage en = new EntityUpdateMessage();
+
+               en.name = name;
+               en.id = ID;
+               en.entityName = this.name;
+               en.entityType = (byte) getType();
+               en.x = (short) x;
+               en.y = (short) y;
+               en.image = (short) this.getImage();
+               en.imageStep = -1;
+
+               return en;
+       }
+
+       /**
+        * Used by loader to read descriptor files
+        *
+        * @param name
+        * @param value
+        */
+       void setProperty(String name, String value) {
+               switch (name) {
+                       case "name":
+                               this.name = value;
+                               break;
+                       case "description":
+                               this.description = value;
+                               break;
+                       case "map":
+                               this.map = game.getMap(value);
+                               break;
+                       case "x":
+                               this.x = Integer.valueOf(value);
+                               break;
+                       case "y":
+                               this.y = Integer.valueOf(value);
+                               break;
+                       case "image":
+                               this.image = Integer.valueOf(value);
+                               break;
+                       default:
+                               //System.out.println("unknown property: " + name + " on class " + getClass().getSimpleName());
+                               break;
+               }
+       }
+       // FIXME: move to a PropertyWriter class
+
+       protected void writeProperty(BufferedWriter out, String name, String value) throws IOException {
+               if (value != null) {
+                       out.write(name);
+                       out.write('=');
+                       out.write(value);
+                       out.write('\n');
+               }
+       }
+
+       protected void writeProperty(BufferedWriter out, String name, int value) throws IOException {
+               writeProperty(out, name, String.valueOf(value));
+       }
+
+       protected void writeProperty(BufferedWriter out, String name, long value) throws IOException {
+               writeProperty(out, name, String.valueOf(value));
+       }
+
+       protected void writeProperty(BufferedWriter out, String name, double value) throws IOException {
+               writeProperty(out, name, String.valueOf(value));
+       }
+
+       /**
+        * Write out the object properties, i.e. for saving the entire object.
+        *
+        * @param out
+        * @throws IOException
+        */
+       protected void writeProperties(BufferedWriter out) throws IOException {
+               writeProperty(out, "name", name);
+               writeProperty(out, "description", description);
+               writeProperty(out, "map", map.name);
+               writeProperty(out, "x", x);
+               writeProperty(out, "y", y);
+               writeProperty(out, "image", image);
+       }
+
+       /**
+        * Write out live game state - parameters which allow an instance of the object
+        * to be restored later.
+        *
+        * For mobs it will be their location, for players it is nothing as they are
+        * always stored in full separately.
+        *
+        * The default implementation saves out the name and location.
+        *
+        * @param out
+        * @throws IOException
+        */
+       void writeState(BufferedWriter out) throws IOException {
+               writeProperty(out, "name", name);
+               writeProperty(out, "x", x);
+               writeProperty(out, "y", y);
+       }
+
+       void writeHeader(BufferedWriter out) throws IOException {
+               out.append("type.");
+               out.append(getClass().getSimpleName());
+               out.append('=');
+               out.append(name);
+               out.append('\n');
+       }
+
+       void writeFooter(BufferedWriter out) throws IOException {
+               out.append("=end\n");
+       }
+
+       /**
+        * Save the state to a stream in a way which can also restore the same object.
+        * this will call writeState which should write out volatile fields, it doesn't
+        * need to write out fields that are loaded from a prototype.
+        *
+        * @param out
+        * @throws IOException
+        */
+       public static void saveState(Thing thing, BufferedWriter out) throws IOException {
+               thing.writeHeader(out);
+               thing.writeState(out);
+               thing.writeFooter(out);
+       }
+
+       static Thing createThing(Game game, String type) throws ClassNotFoundException {
+               try {
+                       Class<Thing> c = (Class<Thing>) Class.forName("duskz.server.entityz." + type);
+                       return c.getConstructor(Game.class).newInstance(game);
+               } catch (Exception ex) {
+                       ex.printStackTrace();
+                       throw new ClassNotFoundException("Failed", ex);
+               }
+       }
+
+       public interface ThingResolver {
+
+               Thing resolve(Game game, String klass, String name);
+       }
+
+       public static class PrototypeResolver implements ThingResolver {
+
+               File base;
+
+               /**
+                * Create a new thing resolver which loads a prototype
+                * from a file.
+                *
+                * @param base
+                */
+               public PrototypeResolver(File base) {
+                       this.base = base;
+               }
+
+               @Override
+               public Thing resolve(Game game, String klass, String name) {
+                       Thing thing = null;
+                       try {
+                               thing = createThing(game, klass);
+                               thing.name = name;
+                               thing.load(new File(base, name));
+                       } catch (ClassNotFoundException | IOException ex) {
+                               System.out.println("Cannot load prototype: " + ex);
+                       }
+                       return thing;
+               }
+       }
+
+       public static class EmptyResolver implements ThingResolver {
+
+               /**
+                * Create a new resolver which just instantiates an empty
+                * object.
+                */
+               public EmptyResolver() {
+               }
+
+               @Override
+               public Thing resolve(Game game, String klass, String name) {
+                       Thing thing = null;
+                       try {
+                               thing = createThing(game, klass);
+                               thing.name = name;
+                       } catch (ClassNotFoundException ex) {
+                               System.out.println("Cannot load prototype: " + ex);
+                       }
+                       return thing;
+               }
+       }
+       // todo this might need to load some object prototype first
+
+       public static Thing restoreState(Game game, BufferedReader in, ThingResolver resolve) throws IOException {
+               String line;
+               Thing thing = null;
+
+               while ((line = in.readLine()) != null) {
+                       line = line.trim();
+                       int col = line.indexOf('=');
+
+                       if (thing == null) {
+                               if (line.startsWith("type.") && col > 0) {
+                                       String type = line.substring(5, col);
+                                       String name = line.substring(col + 1);
+
+                                       thing = resolve.resolve(game, type, name);
+                               } else {
+                                       continue;
+                               }
+                       } else if (line.equals("=end")) {
+                               break;
+                       } else if (col > 0) {
+                               String name = line.substring(0, col);
+                               String value = line.substring(col + 1);
+                               try {
+                                       thing.setProperty(name, value);
+                               } catch (NumberFormatException ex) {
+                               }
+                       }
+               }
+               if (thing != null)
+                       thing.loaded();
+               return thing;
+       }
+
+       /**
+        * Save whole state to file
+        *
+        * @param path
+        * @throws IOException
+        */
+       public void save(File path) throws IOException {
+               // Write to temp file first, and if ok, overwrite old one
+               File tmp = new File(path.getParentFile(), path.getName() + "~");
+
+               try (BufferedWriter out = new BufferedWriter(new FileWriter(tmp))) {
+                       writeHeader(out);
+                       // writeProperty(getFileVersion()?))
+                       writeProperties(out);
+                       writeFooter(out);
+               }
+
+               Files.move(tmp.toPath(), path.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
+       }
+
+       /**
+        * Load whole state from a file
+        *
+        * This works with the typed fields as well as not.
+        *
+        * @param path
+        * @throws IOException
+        */
+       public void load(File path) throws IOException {
+               try (PropertyLoader pl = new PropertyLoader(path)) {
+                       for (PropertyEntry pe : pl) {
+                               if (pe.name.startsWith("type.")) {
+                                       if (pe.name.startsWith("type." + getClass().getSimpleName())) {
+                                               continue;
+                                       } else {
+                                               throw new IOException("Trying to load " + pe.name.substring(5) + " into " + getClass().getSimpleName());
+                                       }
+                               } else if (pe.name.equals("=end")) {
+                                       break;
+                               }
+                               try {
+                                       setProperty(pe.name, pe.value);
+                               } catch (NumberFormatException ex) {
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Called after an object has been fully loaded or restored.
+        * Can validate/setup stuff.
+        */
+       protected void loaded() throws IOException {
+       }
+
+       public void look(Active viewer) {
+               if (description != null)
+                       viewer.chatMessage("You see " + description + ".");
+               else
+                       viewer.chatMessage("You see nothing special.");
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/ThingTable.java b/DuskServer/src/duskz/server/entityz/ThingTable.java
new file mode 100644 (file)
index 0000000..b0e5b52
--- /dev/null
@@ -0,0 +1,152 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Manages things, indices thereof, etc.
+ *
+ * Ideally it should be possible to implement different ways including
+ * live disk backing.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class ThingTable<T extends Thing> {
+
+       // By name index
+       final private HashMap<String, HashSet<T>> byName = new HashMap<>();
+       // By id index
+       final private HashMap<Long, T> byID = new HashMap<>();
+       // backing store
+       private final File path;
+
+       public ThingTable(File path) {
+               this.path = path;
+       }
+
+       void restoreState(Game game, File prototypeBase) {
+               Thing.ThingResolver resolver = new Thing.PrototypeResolver(prototypeBase);
+               
+               try (BufferedReader in = new BufferedReader(new FileReader(path))) {
+                       T thing;
+
+                       while ((thing = (T) Thing.restoreState(game, in, resolver)) != null) {
+                               add(thing);
+                       }
+               } catch (IOException ex) {
+                       ex.printStackTrace();
+               }
+       }
+       void restoreState(Game game) {
+               Thing.ThingResolver resolver = new Thing.EmptyResolver();
+               
+               try (BufferedReader in = new BufferedReader(new FileReader(path))) {
+                       T thing;
+
+                       while ((thing = (T) Thing.restoreState(game, in, resolver)) != null) {
+                               add(thing);
+                       }
+               } catch (IOException ex) {
+                       ex.printStackTrace();
+               }
+       }
+
+       void saveState(Game game) {
+               try (BufferedWriter out = new BufferedWriter(new FileWriter(path))) {
+
+                       for (T o : byID.values()) {
+                               Thing.saveState(o, out);
+                       }
+               } catch (IOException ex) {
+               }
+       }
+
+       // Shoudln't be allowed for concurrency reasons
+       @Deprecated
+       public Collection<T> values() {
+               return byID.values();
+       }
+       
+       public void add(T thing) {
+               HashSet<T> set = byName.get(thing.name);
+               if (set == null) {
+                       set = new HashSet<>();
+                       byName.put(thing.name, set);
+               }
+               set.add(thing);
+               byID.put(thing.ID, thing);
+       }
+
+       public void remove(T thing) {
+               HashSet<T> set = byName.get(thing.name);
+
+               set.remove(thing);
+               if (set.isEmpty())
+                       byName.remove(thing.name);
+
+               byID.remove(thing.ID);
+       }
+
+       /**
+        * Get the thing with the given id.
+        *
+        * @param id
+        * @return
+        */
+       public T get(long id) {
+               return byID.get(id);
+       }
+
+       /**
+        * Get the first item with the given name
+        *
+        * @param name
+        * @return
+        */
+       public T get(String name) {
+               HashSet<T> set = byName.get(name);
+
+               if (set != null)
+                       return set.iterator().next();
+               else
+                       return null;
+       }
+
+       /**
+        * Get all items with the same name
+        *
+        * @param name
+        * @return
+        */
+       public Set<T> getAll(String name) {
+               return byName.get(name);
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/TileMap.java b/DuskServer/src/duskz/server/entityz/TileMap.java
new file mode 100644 (file)
index 0000000..ac23ed7
--- /dev/null
@@ -0,0 +1,1122 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+/**
+ * Changes
+ */
+package duskz.server.entityz;
+
+import duskz.server.entityz.PropertyLoader.PropertyEntry;
+import duskz.util.Maths;
+import java.io.DataInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.PriorityQueue;
+
+/**
+ * Low level map management and helpers.
+ * 
+ * Some of the helpers provide higher level functionality like path finding.
+ *
+ * @author notzed
+ */
+public class TileMap implements Iterable<TileMap.MapData> {
+
+       /**
+        * Name of map for referencing
+        */
+       public final String name;
+       /**
+        * Size
+        */
+       private int rows, cols;
+       /**
+        * Tile map
+        */
+       private short tiles[];
+       /**
+        * Index of Entities by location.
+        */
+       private HashMap<Location, LinkedList<Thing>> entities = new HashMap<>();
+       //private Thing entities[];
+       /**
+        * Aliases for locations, by location and name
+        */
+       private HashMap<Location, String> aliases = new HashMap<>();
+       private HashMap<String, Location> aliasesByName = new HashMap<>();
+       /**
+        * Scripts for locations
+        */
+       private HashMap<Location, String> ableScript = new HashMap<>();
+       private HashMap<Location, String> visibleScript = new HashMap<>();
+       private HashMap<Location, String> actionScript = new HashMap<>();
+       /**
+        * Aliases for jumps. These are implicit actions on a location
+        */
+       private HashMap<Location, String> jumpTable = new HashMap<>();
+       /**
+        * Layers
+        */
+       private int groundLayer;
+       private TileLayer layers[];
+       /**
+        * privileges for each cell. This appears to be unimplemented in Dusk so
+        * isn't here either.
+        */
+       protected short privs[];
+       /**
+        * Ownership for each cell. This appears to be unimplemented in Dusk so
+        * isn't here either.
+        */
+       protected int owner[];
+       /**
+        * Flags for iterators
+        */
+       public final static int SKIP_START = 1;
+       public final static int SKIP_END = 2;
+
+       /**
+        * Create a new empty map of the given size
+        *
+        * @param cols
+        * @param rows
+        */
+       public TileMap(String name, int cols, int rows) {
+               this.name = name;
+               this.rows = rows;
+               this.cols = cols;
+
+               tiles = new short[rows * cols];
+               //entities = new Thing[rows * cols];
+       }
+
+       public int getRows() {
+               return rows;
+       }
+
+       public int getCols() {
+               return cols;
+       }
+
+       public int getLayerCount() {
+               return layers.length;
+       }
+
+       public int getGroundLayer() {
+               return groundLayer;
+       }
+
+       public void saveMap(File path) throws IOException {
+               path.delete();
+               try (RandomAccessFile rafFile = new RandomAccessFile(path, "rw")) {
+                       rafFile.writeInt(cols);
+                       rafFile.writeInt(rows);
+                       for (int x = 0; x < cols; x++) {
+                               for (int y = 0; y < rows; y++) {
+                                       rafFile.writeShort(getTile(x, y));
+                               }
+                       }
+               }
+       }
+
+       static class TileLayer {
+
+               int x, y;
+               int width, height;
+               private short[] tiles;
+
+               public TileLayer(int x, int y, int width, int height) {
+                       this.x = x;
+                       this.y = y;
+                       this.width = width;
+                       this.height = height;
+                       this.tiles = new short[width * height];
+               }
+
+               public TileLayer(int x, int y, int width, int height, short[] tiles) {
+                       this.x = x;
+                       this.y = y;
+                       this.width = width;
+                       this.height = height;
+                       this.tiles = tiles;
+               }
+
+               public boolean inside(int tx, int ty) {
+                       tx -= this.x;
+                       ty -= this.y;
+                       return tx >= 0 && tx < width
+                                       && ty >= 0 && ty < height;
+               }
+
+               public short getTile(int tx, int ty) {
+                       tx -= this.x;
+                       ty -= this.y;
+                       if (tx >= 0 && tx < width
+                                       && ty >= 0 && ty < height) {
+                               return tiles[tx + ty * width];
+                       } else
+                               return 0;
+               }
+       }
+       public static final int FORMAT_BYTE = 0;
+       public static final int FORMAT_SHORT = 1;
+       // 'mapz'
+       // TODO: gzip?
+       public static final int MAGIC_LAYERED = 0x6d61707a;
+
+       /**
+        * Load a layered map. Format is:
+        * magic: int
+        * version: int 0
+        * flags: int (0 - short)
+        * width: int must contain all layers
+        * height: int must contain all layers
+        * layer ground: int index of ground layer, ground layer must be same size as map
+        * layer count: int
+        * then layer count {
+        * layer x: int
+        * layer y: int
+        * layer width: int
+        * layer height: int
+        * layer data: short width*height in row major format
+        * }
+        *
+        * @param path
+        * @return
+        * @throws IOException
+        */
+       public static TileMap loadLayered(File path) throws IOException {
+               TileMap map;
+
+               try (DataInputStream mapFile = new DataInputStream(new FileInputStream(path))) {
+                       int magic = mapFile.readInt();
+                       int version = mapFile.readInt();
+                       int flags = mapFile.readInt();
+                       int cols = mapFile.readInt();
+                       int rows = mapFile.readInt();
+                       int groundLayer = mapFile.readInt();
+                       int layerCount = mapFile.readInt();
+
+                       if (magic != MAGIC_LAYERED
+                                       || version != 0) {
+                               throw new IOException("Invalid format/magic/unknown version");
+                       }
+
+                       System.out.println("Load map: " + path);
+                       System.out.printf(" size: %dx%d\n", cols, rows);
+                       System.out.printf(" groundLayer: %d\n", groundLayer);
+                       System.out.printf(" layerCount: %d\n", layerCount);
+
+                       map = new TileMap(path.getName(), cols, rows);
+
+                       map.groundLayer = groundLayer;
+                       map.layers = new TileLayer[layerCount];
+                       for (int l = 0; l < layerCount; l++) {
+                               int tx = mapFile.readInt();
+                               int ty = mapFile.readInt();
+                               int twidth = mapFile.readInt();
+                               int theight = mapFile.readInt();
+                               TileLayer tl;
+
+                               System.out.printf("  layer %2d: at %3d,%3d size %3dx%3d\n", l, tx, ty, twidth, theight);
+                               if (l == groundLayer)
+                                       tl = new TileLayer(tx, ty, twidth, theight, map.tiles);
+                               else
+                                       tl = new TileLayer(tx, ty, twidth, theight);
+
+                               map.layers[l] = tl;
+
+                               for (int i = 0; i < twidth * theight; i++) {
+                                       tl.tiles[i] = mapFile.readShort();
+                               }
+                       }
+               }
+
+               // Format is alias.x.y=name
+               File aliasPath = new File(path.getParentFile(), path.getName() + ".alias");
+               try (PropertyLoader pl = new PropertyLoader(aliasPath)) {
+                       for (PropertyEntry pe : pl) {
+                               String[] line = pe.name.split("\\.");
+                               int x = Integer.valueOf(line[0]);
+                               int y = Integer.valueOf(line[1]);
+                               Location l = new Location(x, y);
+
+                               switch (line[2]) {
+                                       case "alias":
+                                               map.aliases.put(l, pe.value);
+                                               map.aliasesByName.put(pe.value, l);
+                                               break;
+                                       //case "script":
+                                       //      map.seeScript.put(l, pe.value);
+                                       //      map.moveScript.put(l, pe.value);
+                                       //      map.locationActionScript.put(l, pe.value);
+                                       //      break;
+                                       case "visible":
+                                               map.visibleScript.put(l, pe.value);
+                                               break;
+                                       case "able":
+                                               map.ableScript.put(l, pe.value);
+                                               break;
+                                       case "action":
+                                               map.actionScript.put(l, pe.value);
+                                               break;
+                                       case "goto":
+                                               map.jumpTable.put(l, pe.value);
+                                               break;
+                               }
+                       }
+               } catch (NullPointerException | IndexOutOfBoundsException x) {
+                       throw new IOException("Error in map alias file " + aliasPath);
+               } catch (FileNotFoundException x) {
+                       // don't care
+               }
+
+               return map;
+       }
+
+       public void saveAlias(File path) throws IOException {
+               File tmp = new File(path.getParentFile(), path.getName() + ".alias~");
+               File file = new File(path.getParentFile(), path.getName() + ".alias");
+               
+               // how do i preserve comments?
+               throw new UnsupportedOperationException("Not implemented yet");
+       }
+
+       // Map in row major format (i.e. more efficient)
+       public static TileMap loadMapX(File path) throws IOException {
+               TileMap map;
+
+               try (DataInputStream mapFile = new DataInputStream(new FileInputStream(path))) {
+                       int cols = mapFile.readInt();
+                       int rows = mapFile.readInt();
+                       map = new TileMap(path.getName(), cols, rows);
+                       for (int y = 0; y < rows; y++) {
+                               for (int x = 0; x < cols; x++) {
+                                       map.setTile(x, y, mapFile.readShort());
+                               }
+                       }
+
+                       map.layers = new TileLayer[1];
+                       map.layers[0] = new TileLayer(0, 0, cols, rows, map.tiles);
+               }
+               return map;
+       }
+
+       public static TileMap loadMap(File path, int format) throws IOException {
+               TileMap map;
+
+               try (RandomAccessFile mapFile = new RandomAccessFile(path, "r")) {
+                       int cols = mapFile.readInt();
+                       int rows = mapFile.readInt();
+                       map = new TileMap(path.getName(), cols, rows);
+                       for (int x = 0; x < cols; x++) {
+                               for (int y = 0; y < rows; y++) {
+                                       if (format == FORMAT_BYTE)
+                                               map.setTile(x, y, mapFile.readByte());
+                                       else
+                                               map.setTile(x, y, mapFile.readShort());
+                               }
+                       }
+                       map.layers = new TileLayer[1];
+                       map.layers[0] = new TileLayer(0, 0, cols, rows, map.tiles);
+               }
+               return map;
+       }
+
+       /**
+        * Create a new map
+        *
+        * @param newcols
+        * @param newrows
+        * @return
+        */
+       public synchronized void resize(int newcols, int newrows) {
+               short[] ntiles = new short[newcols * newrows];
+               //Thing[] nentities = new Thing[newcols * newrows];
+
+               int rx = Math.min(newcols, cols);
+               int ry = Math.min(newrows, rows);
+
+               for (int y = 0; y < ry; y++) {
+                       for (int x = 0; x < rx; x++) {
+                               int indexa = x + y * cols;
+                               int indexb = x + y * newcols;
+
+                               ntiles[indexb] = tiles[indexa];
+                               //nentities[indexb] = entities[indexa];
+                       }
+               }
+
+               tiles = ntiles;
+               //entities = nentities;
+               cols = newcols;
+               rows = newrows;
+       }
+
+       public boolean inside(int x, int y) {
+               return x >= 0 && x < cols
+                               && y >= 0 && y < rows;
+       }
+
+       public boolean inside(int layer, int x, int y) {
+               return x >= 0 && x < cols
+                               && y >= 0 && y < rows;
+       }
+
+       /**
+        * Calculate if the layer is empty over the given region
+        *
+        * @param layer
+        * @param x
+        * @param y
+        * @param width
+        * @param height
+        * @return
+        */
+       public boolean empty(int layer, int x, int y, int width, int height) {
+               TileLayer tl = layers[layer];
+
+               for (int ty = y; ty < y + height; ty++) {
+                       for (int tx = x; tx < x + width; tx++) {
+                               if (tl.getTile(tx, ty) != 0)
+                                       return false;
+                       }
+               }
+               return true;
+       }
+
+       /**
+        * Get the region bounded by the supplied coordinates.
+        * If the region is all empty, then null is returned.
+        *
+        * @param layer
+        * @param x
+        * @param y
+        * @param width
+        * @param height
+        * @param region if supplied use this array otherwise allocate one
+        * @return
+        */
+       public short[] getRegion(int layer, int tx, int ty, int width, int height, short[] region) {
+               TileLayer tl = layers[layer];
+               if (region == null)
+                       region = new short[width * height];
+               boolean empty = true;
+
+               for (int y = 0; y < height; y++) {
+                       for (int x = 0; x < width; x++) {
+                               short t = tl.getTile(tx + x, ty + y);
+
+                               region[x + width * y] = t;
+
+                               empty &= t == 0;
+                       }
+               }
+               if (empty)
+                       return null;
+               else
+                       return region;
+       }
+
+       public void setTile(int x, int y, int t) {
+               tiles[x + y * cols] = (short) t;
+       }
+
+       public short getTile(int x, int y) {
+               return tiles[x + y * cols];
+       }
+
+       public short getTile(int layer, int x, int y) {
+               return layers[layer].getTile(x, y);
+       }
+
+       public synchronized List<Thing> getEntities(int x, int y, List<Thing> list) {
+               if (list == null)
+                       list = new ArrayList<>();
+               //Thing o = entities[x + y * cols];
+               //while (o != null) {
+               //      list.add(o);
+               //      o = o.getNext();
+               //}
+               LinkedList<Thing> ll = entities.get(new Location(x, y));
+               if (ll != null)
+                       list.addAll(ll);
+
+               return list;
+       }
+
+       public synchronized List<Thing> getEntities(Location l, List<Thing> list) {
+               if (list == null)
+                       list = new ArrayList<>();
+               //Thing o = entities[x + y * cols];
+               //while (o != null) {
+               //      list.add(o);
+               //      o = o.getNext();
+               //}
+               LinkedList<Thing> ll = entities.get(l);
+               if (ll != null)
+                       list.addAll(ll);
+
+               return list;
+       }
+
+       public synchronized void addEntity(Thing o) {
+               if (inside(o.x, o.y)) {
+                       //int index = o.x + o.y * cols;
+                       //entities[index] = Thing.append(entities[index], o);
+                       Location l = new Location(o.x, o.y);
+                       LinkedList<Thing> ll = entities.get(l);
+                       if (ll == null) {
+                               ll = new LinkedList<>();
+                               entities.put(l, ll);
+                       }
+                       if (ll.contains(o)) {
+                               System.out.println("adding the same thing twice: " + o);
+                       } else {
+                               ll.add(o);
+                       }
+               }
+       }
+
+       public synchronized void removeEntity(Thing o) {
+               if (inside(o.x, o.y)) {
+                       //int index = o.x + o.y * cols;
+                       //entities[index] = Thing.remove(entities[index], o);
+                       Location l = new Location(o.x, o.y);
+                       LinkedList<Thing> ll = entities.get(l);
+                       ll.remove(o);
+                       if (ll.isEmpty()) {
+                               entities.remove(l);
+                       }
+               }
+       }
+
+       public synchronized void moveEntity(Thing o, int x, int y) {
+               if (inside(x, y)) {
+                       removeEntity(o);
+                       o.x = x;
+                       o.y = y;
+                       addEntity(o);
+               }
+       }
+
+       /**
+        * Get an iterable over a range - allows foreach support
+        *
+        * @param x0
+        * @param y0
+        * @param x1
+        * @param y1
+        * @return
+        */
+       public Iterable<MapData> range(int x0, int y0, int x1, int y1) {
+               return new MapIterable(x0, y0, x1, y1);
+       }
+
+       /**
+        * Get an iterable over a range with a given centre and radius
+        *
+        * @param x
+        * @param y
+        * @param radius
+        * @return
+        */
+       public Iterable<MapData> range(int x, int y, int radius) {
+               return new MapIterable(x - radius, y - radius, x + radius + 1, y + radius + 1);
+       }
+
+       /**
+        * Get an iterable which will iterate over the looking path
+        *
+        * @param sx
+        * @param sy
+        * @param ex
+        * @param ey
+        * @param flags SKIP_END, SKIP_START to skip end/start locations
+        * (UNIMPLEMENTED)
+        * @return
+        */
+       public Iterable<MapData> look(int sx, int sy, int ex, int ey, int flags) {
+               return new LookIterable(sx, sy, ex, ey, flags);
+       }
+
+       public Iterable<MapData> look(int sx, int sy, int ex, int ey) {
+               return new LookIterable(sx, sy, ex, ey, 0);
+       }
+
+       public Iterable<MoveData> move(int sx, int sy, int ex, int ey, int flags, MoveListener l) {
+               return new MoveIterable(sx, sy, ex, ey, flags, l);
+       }
+
+       public Location locationForAlias(String name) {
+               // FIXME: aliases need to be global!
+               return aliasesByName.get(name);
+       }
+
+       public String aliasForLocation(int x, int y) {
+               return aliases.get(new Location(x, y));
+       }
+
+       public String locationAbleScript(int x, int y) {
+               return ableScript.get(new Location(x, y));
+       }
+
+       public String locationVisibleScript(int x, int y) {
+               return visibleScript.get(new Location(x, y));
+       }
+
+       public String locationActionScript(int x, int y) {
+               return actionScript.get(new Location(x, y));
+       }
+
+       public String jumpAlias(int x, int y) {
+               return jumpTable.get(new Location(x, y));
+       }
+
+       public void setJumpAlias(int tx, int ty, String alias) {
+               jumpTable.put(new Location(tx, ty), alias);
+       }
+
+       public void setAlias(int x, int y, String alias) {
+               Location l = new Location(x, y);
+
+               if (alias == null || alias.equals("")) {
+                       alias = aliases.remove(l);
+                       if (alias != null)
+                               aliasesByName.remove(alias);
+               } else {
+                       aliases.put(l, alias);
+                       aliasesByName.put(alias, l);
+               }
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof TileMap) {
+                       return name.equals(((TileMap) obj).name);
+               } else
+                       return false;
+       }
+
+       @Override
+       public int hashCode() {
+               int hash = 7;
+               hash = 97 * hash + Objects.hashCode(this.name);
+               return hash;
+       }
+
+       private class MapIterable implements Iterable<MapData> {
+
+               int x0, y0, x1, y1;
+
+               public MapIterable(int x0, int y0, int x1, int y1) {
+                       this.x0 = x0;
+                       this.y0 = y0;
+                       this.x1 = x1;
+                       this.y1 = y1;
+               }
+
+               @Override
+               public Iterator<MapData> iterator() {
+                       return new MapIterator(x0, y0, x1, y1);
+               }
+       }
+
+       private class LookIterable implements Iterable<MapData> {
+
+               int sx, sy, ex, ey;
+               int flags;
+
+               public LookIterable(int sx, int sy, int ex, int ey, int flags) {
+                       this.sx = sx;
+                       this.sy = sy;
+                       this.ex = ex;
+                       this.ey = ey;
+                       this.flags = flags;
+               }
+
+               @Override
+               public Iterator<MapData> iterator() {
+                       return new LookIterator(sx, sy, ex, ey, flags);
+               }
+       }
+
+       private class MoveIterable implements Iterable<MoveData> {
+
+               int sx, sy, ex, ey;
+               int flags;
+               MoveListener l;
+
+               public MoveIterable(int sx, int sy, int ex, int ey, int flags, MoveListener l) {
+                       this.sx = sx;
+                       this.sy = sy;
+                       this.ex = ex;
+                       this.ey = ey;
+                       this.flags = flags;
+                       this.l = l;
+               }
+
+               @Override
+               public Iterator<MoveData> iterator() {
+                       return new MoveIterator(sx, sy, ex, ey, flags, l);
+               }
+       }
+
+       public Iterator<MapData> getIterator(int x0, int y0, int x1, int y1) {
+               return new MapIterator(x0, y0, x1, y1);
+       }
+
+       @Override
+       public Iterator<MapData> iterator() {
+               return getIterator(0, 0, cols, rows);
+       }
+
+       public class MapData extends Location {
+
+               public final List<Thing> entities = new ArrayList<>();
+               // Tileid of ground layer
+               public short tile;
+               // Tileid of visible layers in correct order
+               public short[] layers = new short[TileMap.this.layers.length];
+               // lessen gc load by re-using
+               //private Location l = new Location(0, 0);
+
+               protected void setData(int x, int y) {
+                       this.x = x;
+                       this.y = y;
+                       //l.x = x;
+                       //l.y = y;
+                       this.entities.clear();
+                       if (inside(x, y)) {
+                               this.tile = getTile(x, y);
+                               getEntities(this, this.entities);
+                       } else {
+                               this.tile = 0;
+                       }
+                       for (int l = 0; l < layers.length; l++) {
+                               layers[l] = TileMap.this.layers[l].getTile(x, y);
+                       }
+               }
+       }
+
+       public class MoveData extends MapData {
+
+               public String direction;
+       }
+
+       private class MapIterator implements Iterator<MapData> {
+
+               int x0, y0, x1, y1;
+               int x, y;
+               MapData data = new MapData();
+
+               public MapIterator(int x0, int y0, int x1, int y1) {
+                       this.x0 = Maths.clamp(x0, 0, cols);
+                       this.y0 = Maths.clamp(y0, 0, rows);
+                       this.x1 = Maths.clamp(x1, 0, cols);
+                       this.y1 = Maths.clamp(y1, 0, rows);
+                       this.x = -1;
+                       this.y = -1;
+               }
+
+               @Override
+               public boolean hasNext() {
+                       return (x + 1) < x1
+                                       || (y + 1) < y1;
+               }
+
+               @Override
+               public MapData next() {
+                       // FIXME: this needs to iterate in x first then y
+                       if (x == -1) {
+                               x = x0;
+                               y = y0;
+                       } else if (y + 1 < y1) {
+                               y++;
+                       } else {
+                               y = y0;
+                               x++;
+                       }
+
+                       data.setData(x, y);
+
+                       return data;
+               }
+
+               @Override
+               public void remove() {
+                       throw new UnsupportedOperationException("Not supported yet.");
+               }
+       }
+
+       /**
+        * Implements an iterator which follows a 'looking' path
+        *
+        * TODO: it should probably use Bresenhams line algorithm
+        */
+       private class LookIterator implements Iterator<MapData> {
+
+               int x, y;
+               final int sx, sy;
+               final int ex, ey;
+               int flags;
+               boolean there = false;
+               MapData data = new MapData();
+
+               public LookIterator(int sx, int sy, int ex, int ey, int flags) {
+                       this.x = sx;
+                       this.y = sy;
+                       this.sx = sx;
+                       this.sy = sy;
+                       this.ex = ex;
+                       this.ey = ey;
+                       this.flags = flags;
+
+                       diffx = Math.abs(ex - sx);
+                       diffy = Math.abs(ey - sy);
+                       stepx = Integer.signum(ex - sx);
+                       stepy = Integer.signum(ey - sy);
+                       err = diffx - diffy;
+               }
+
+               @Override
+               public boolean hasNext() {
+                       return !there;
+               }
+               // Bresenham algorithm data, from wikipedia
+               int diffx, diffy;
+               int stepx, stepy;
+               int err;
+
+               void lineStep() {
+                       int e2 = 2 * err;
+                       if (e2 > -diffy) {
+                               err -= diffy;
+                               x += stepx;
+                       }
+                       if (e2 < diffx) {
+                               err += diffx;
+                               y += stepy;
+                       }
+               }
+
+               @Override
+               public MapData next() {
+                       there = x == ex && y == ey;
+
+                       // FIXME: impelement?
+                       //if ((flags & SKIP_START) != 0 && sx == x && sy == y) {
+                       //      lineStep();
+                       //}
+
+                       data.setData(x, y);
+
+                       if (!there) {
+                               lineStep();
+                       }
+                       return data;
+               }
+
+               public MapData nextOld() {
+                       there = x == ex && y == ey;
+
+                       data.setData(x, y);
+
+                       if (!there) {
+                               int stepx = Integer.signum(ex - y);
+                               int stepy = Integer.signum(ey - y);
+                               int dx = Math.abs(ex - x);
+                               int dy = Math.abs(ey - y);
+
+                               if (dx > dy) {
+                                       x += stepx;
+                               } else if (dx < dy) {
+                                       y += stepy;
+                               } else {
+                                       x += stepx;
+                                       y += stepy;
+                               }
+                       }
+
+                       return data;
+               }
+
+               @Override
+               public void remove() {
+                       throw new UnsupportedOperationException("Not supported yet.");
+               }
+       }
+
+       public interface MoveListener {
+
+               public boolean canMoveto(MapData md);
+       }
+
+       /**
+        * Iterator for movement.
+        */
+       private static class MoveInfo implements Comparable<MoveInfo> {
+
+               int x, y;
+               float cost;
+               float estimate;
+               String direction;
+               MoveInfo parent;
+
+               public MoveInfo(int x, int y, float cost, String direction) {
+                       this.x = x;
+                       this.y = y;
+                       this.cost = cost;
+                       this.direction = direction;
+               }
+
+               @Override
+               public boolean equals(Object obj) {
+                       MoveInfo o = (MoveInfo) obj;
+
+                       return x == o.x && y == o.y;
+               }
+
+               @Override
+               public int hashCode() {
+                       int hash = 5;
+                       hash = 59 * hash + this.x;
+                       hash = 59 * hash + this.y;
+                       return hash;
+               }
+
+               @Override
+               public int compareTo(MoveInfo o) {
+                       return Float.compare(cost, o.cost);
+               }
+       }
+
+       /**
+        * An iterator which steps through the individual moves to get to a
+        * destination.
+        *
+        * Implemented using A* algorithm, so can handle obstructions.
+        *
+        * TODO: Limit the search space.
+        */
+       private class MoveIterator implements Iterator<MoveData> {
+
+               int x, y;
+               final int sx, sy;
+               final int ex, ey;
+               int flags;
+               boolean there = false;
+               private final MoveListener l;
+               // quick hack version
+               Iterator<MoveInfo> iterator;
+               MoveData data = new MoveData();
+
+               public MoveIterator(int sx, int sy, int ex, int ey, int flags, MoveListener l) {
+                       this.x = sx;
+                       this.y = sy;
+                       this.sx = sx;
+                       this.sy = sy;
+                       this.ex = ex;
+                       this.ey = ey;
+                       this.flags = flags;
+                       this.l = l;
+
+                       // A* requires the whole path to be calculated ahead of time.
+
+                       List<MoveInfo> path = findPath();
+
+                       System.out.printf("Finding path from %d,%d to %d,%d: ", sx, sy, ex, ey);
+
+                       if (path != null) {
+                               for (MoveInfo mi : path) {
+                                       System.out.print(" ");
+                                       System.out.print(mi.direction);
+                               }
+                               System.out.println();
+
+                               if (!path.isEmpty() && (flags & SKIP_END) != 0) {
+                                       path.remove(path.size() - 1);
+                               }
+
+                               iterator = path.iterator();
+                       } else {
+                               System.out.println("No path found!");
+                       }
+               }
+
+               float estimateCost(int sx, int sy, int ex, int ey) {
+                       return (float) Math.sqrt((ex - sx) * (ex - sx) + (ey - sy) * (ey - sy));
+               }
+
+               List<MoveInfo> constructPath(List<MoveInfo> list, MoveInfo n) {
+                       if (list == null)
+                               list = new ArrayList<>();
+
+                       if (n.parent != null) {
+                               constructPath(list, n.parent);
+                               list.add(n);
+                       }
+
+                       return list;
+               }
+
+               void moveIf(List<MoveInfo> list, int x, int y, float cost, String dir) {
+                       data.setData(x, y);
+
+                       if (l.canMoveto(data)) {
+                               list.add(new MoveInfo(x, y, cost + 1, dir));
+                       }
+               }
+
+               List<MoveInfo> getNeighbours(MoveInfo n) {
+                       List<MoveInfo> list = new ArrayList<>(4);
+
+                       moveIf(list, n.x + 1, n.y, n.cost, "e");
+                       moveIf(list, n.x - 1, n.y, n.cost, "w");
+                       moveIf(list, n.x, n.y + 1, n.cost, "s");
+                       moveIf(list, n.x, n.y - 1, n.cost, "n");
+
+                       return list;
+               }
+
+               // A* algorithm from here:
+               //http://www.peachpit.com/articles/article.aspx?p=101142&seqNum=2
+               public List<MoveInfo> findPath() {
+
+                       PriorityQueue<MoveInfo> openList = new PriorityQueue();
+                       HashSet<MoveInfo> closedList = new HashSet<>();
+
+                       MoveInfo startNode = new MoveInfo(sx, sy, 0, null);
+                       startNode.estimate = estimateCost(sx, sy, ex, ey);
+                       startNode.parent = null;
+                       openList.add(startNode);
+
+                       while (!openList.isEmpty()) {
+                               MoveInfo node = openList.poll();
+                               if (node.x == ex && node.y == ey) {
+                                       // construct the path from start to goal
+                                       return constructPath(null, node);
+                               }
+
+                               List<MoveInfo> neighbors = getNeighbours(node);
+                               for (int i = 0; i < neighbors.size(); i++) {
+                                       MoveInfo nnode = neighbors.get(i);
+                                       boolean isOpen = openList.contains(nnode);
+                                       boolean isClosed = closedList.contains(nnode);
+                                       float costFromStart = node.cost + 1;
+
+                                       // check if the neighbor node has not been
+                                       // traversed or if a shorter path to this
+                                       // neighbor node is found.
+                                       if ((!isOpen && !isClosed)
+                                                       || costFromStart < nnode.cost) {
+                                               nnode.parent = node;
+                                               nnode.cost = costFromStart;
+                                               nnode.estimate = estimateCost(nnode.x, nnode.y, ex, ey);
+                                               if (isClosed) {
+                                                       closedList.remove(nnode);
+                                               }
+                                               if (!isOpen) {
+                                                       openList.add(nnode);
+                                               }
+                                       }
+                               }
+                               closedList.add(node);
+                       }
+
+                       // no path found
+                       return null;
+               }
+
+               @Override
+               public boolean hasNext() {
+                       if (iterator != null)
+                               return iterator.hasNext();
+                       else
+                               return false;
+               }
+
+               @Override
+               public MoveData next() {
+                       MoveInfo mi = iterator.next();
+
+                       data.setData(mi.x, mi.y);
+                       data.direction = mi.direction;
+
+                       return data;
+               }
+
+               @Override
+               public void remove() {
+                       throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+               }
+       }
+
+       public static void main(String[] args) {
+               TileMap map = new TileMap("test", 16, 16);
+
+               /*
+                for (MapData md : map.look(5, 5, 10, 3)) {
+                System.out.printf(" %d,%d\n", md.x, md.y);
+                }
+                for (MapData md : map.look(15, 15, 0, 0)) {
+                System.out.printf(" %d,%d\n", md.x, md.y);
+                }
+                for (MapData md : map.look(15, 0, 0, 15)) {
+                System.out.printf(" %d,%d\n", md.x, md.y);
+                }*/
+
+
+               MoveListener l = new MoveListener() {
+                       @Override
+                       public boolean canMoveto(MapData md) {
+                               //System.out.printf("can move %d,%d\n", md.x, md.y);
+                               return md.tile == 0;
+                       }
+               };
+               System.out.println("no obstacles");
+               for (MoveData md : map.move(0, 0, 10, 10, 0, l)) {
+                       System.out.printf(" %d,%d %s\n", md.x, md.y, md.direction);
+               }
+
+               // put an obstacle in the way
+               for (int x = 0; x < 11; x++) {
+                       map.setTile(x, 5, 1);
+               }
+               System.out.println("line in the way");
+               for (MoveData md : map.move(0, 0, 10, 10, SKIP_END, l)) {
+                       System.out.printf(" %d,%d %s\n", md.x, md.y, md.direction);
+               }
+               System.out.println("line in the way to last");
+               for (MoveData md : map.move(0, 0, 1, 1, 0, l)) {
+                       System.out.printf(" %d,%d %s\n", md.x, md.y, md.direction);
+               }
+
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/TooManyTriesException.java b/DuskServer/src/duskz/server/entityz/TooManyTriesException.java
new file mode 100644 (file)
index 0000000..a7fb13d
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+/**
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class TooManyTriesException extends SecurityException {
+       
+}
diff --git a/DuskServer/src/duskz/server/entityz/Training.java b/DuskServer/src/duskz/server/entityz/Training.java
new file mode 100644 (file)
index 0000000..acd3416
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.Wearing;
+
+/**
+ * This is a pseudo-object that can only be held by a game-shop
+ * and represents training.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Training extends Holdable {
+
+       String skill;
+
+       public Training(Game game) {
+               super(game);
+       }
+
+       @Override
+       public String getUnits() {
+               return "exp";
+       }
+       
+       @Override
+       void setProperty(String name, String value) {
+               switch (name) {
+                       case "skill":
+                               skill = value;
+                               break;
+                       default:
+                               super.setProperty(name, value);
+                               break;
+               }
+       }
+
+       @Override
+       public int getWearing() {
+               // TODO: maybe add a training class
+               return Wearing.INVENTORY;
+       }
+
+       @Override
+       public int getType() {
+               return TYPE_TRAINING;
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/VariableList.java b/DuskServer/src/duskz/server/entityz/VariableList.java
new file mode 100644 (file)
index 0000000..0d4aa29
--- /dev/null
@@ -0,0 +1,155 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+/**
+ * Maps tracks per-user variables. Available for arbitrary script use.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class VariableList {
+
+       private HashMap<String, Object> map = new HashMap<>();
+
+       public void put(String name, int v) {
+               map.put(name, v);
+               System.out.println("put integer " + name + " = " + v + " ? " + map.get(name));
+       }
+
+       public void put(String name, long v) {
+               map.put(name, v);
+       }
+
+       public void put(String name, double v) {
+               map.put(name, v);
+       }
+
+       public void put(String name, String v) {
+               map.put(name, v);
+       }
+
+       public int getInteger(String name, int def) {
+               Number x = (Number) map.get(name);
+               System.out.println("getInteger " + name + " = " + x);
+               if (x != null)
+                       return x.intValue();
+               else
+                       return def;
+       }
+
+       public long getLong(String name, long def) {
+               Number x = (Number) map.get(name);
+               if (x != null)
+                       return x.longValue();
+               else
+                       return def;
+       }
+
+       public double getDouble(String name, double def) {
+               Number x = (Number) map.get(name);
+               if (x != null)
+                       return x.doubleValue();
+               else
+                       return def;
+       }
+
+       public String getString(String name, String def) {
+               String x = (String) map.get(name);
+               if (x != null)
+                       return x;
+               else
+                       return def;
+       }
+
+       /**
+        * Set a property value as created by writeProperties
+        *
+        * @param name will be"varDouble", "varInteger", etc.
+        * @param value will be name:value
+        */
+       public boolean setProperty(String name, String value) {
+               int col = value.indexOf(':');
+
+               if (col > 0) {
+                       String vname = value.substring(0, col);
+
+                       value = value.substring(col + 1);
+
+                       switch (name) {
+                               case "var.Integer":
+                                       map.put(vname, Integer.valueOf(value));
+                                       break;
+                               case "var.Long":
+                                       map.put(vname, Long.valueOf(value));
+                                       break;
+                               case "var.Double":
+                                       map.put(vname, Double.valueOf(value));
+                                       break;
+                               case "var.String":
+                                       put(vname, value);
+                                       break;
+                               default:
+                                       return false;
+                       }
+                       return true;
+               }
+               return false;
+       }
+
+       public void writeProperties(BufferedWriter out) throws IOException {
+               for (Entry<String, Object> entry : map.entrySet()) {
+                       out.write("var.");
+                       out.write(entry.getValue().getClass().getSimpleName());
+                       out.write('=');
+                       out.write(entry.getKey());
+                       out.write(':');
+                       out.write(String.valueOf(entry.getValue()));
+                       out.write('\n');
+               }
+       }
+
+       public static void main(String[] args) throws IOException {
+               VariableList v = new VariableList();
+
+               v.put("int", 1);
+               v.put("long", 1L);
+               v.put("double", 1.0);
+               v.put("string", "some string");
+
+               BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
+               v.writeProperties(bw);
+               bw.flush();
+
+               VariableList v2 = new VariableList();
+               v2.setProperty("var.Double", "shit:99.0");
+               v2.writeProperties(bw);
+               bw.flush();
+
+               System.out.println("shit = " + v2.getDouble("shit", -1));
+
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Weapon.java b/DuskServer/src/duskz/server/entityz/Weapon.java
new file mode 100644 (file)
index 0000000..964c58a
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import duskz.protocol.Wearing;
+import static duskz.server.entityz.Thing.TYPE_WEAPON;
+import java.io.BufferedWriter;
+import java.io.IOException;
+
+/**
+ * Attack weapon
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public class Weapon extends Wearable {
+
+       int range = 1;
+
+       public Weapon(Game game) {
+               super(game);
+       }
+
+       public int getType() {
+               return TYPE_WEAPON;
+       }
+
+       @Override
+       public int getWearing() {
+               return Wearing.WIELD;
+       }
+
+       @Override
+       void setProperty(String name, String value) {
+               switch (name) {
+                       case "range":
+                               range = Integer.parseInt(value);
+                               break;
+                       default:
+                               super.setProperty(name, value);
+               }
+       }
+
+       @Override
+       protected void writeProperties(BufferedWriter out) throws IOException {
+               super.writeProperties(out);
+
+               if (range != 1)
+                       writeProperty(out, "range", range);
+       }
+}
diff --git a/DuskServer/src/duskz/server/entityz/Wearable.java b/DuskServer/src/duskz/server/entityz/Wearable.java
new file mode 100644 (file)
index 0000000..97d535b
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * This file is part of DuskZ, a graphical mud engine.
+ *
+ * Copyright (C) 2000 Tom Weingarten <captaint@home.com>
+ * Copyright (C) 2013 Michael Zucchi <notzed@gmail.com>
+ *
+ * DuskZ is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * DuskZ 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with DuskZ; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+package duskz.server.entityz;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+
+/**
+ * Base class for items that can be worn, and wear out over time.
+ *
+ * @author Michael Zucchi <notzed@gmail.com>
+ */
+public abstract class Wearable extends Holdable {
+
+       int mod;
+       long durability;
+       String onWear, onUnwear;
+
+       Wearable(Game game) {
+               super(game);
+       }
+
+       @Override
+       void setProperty(String name, String value) {
+               switch (name) {
+                       case "mod":
+                               mod = Integer.parseInt(value);
+                               break;
+                       case "durability":
+                               durability = Long.parseLong(value);
+                               break;
+                       case "onWear":
+                               onWear = value;
+                               break;
+                       case "onUnwear":
+                               onUnwear = value;
+                               break;
+                       default:
+                               super.setProperty(name, value);
+               }
+       }
+
+       @Override
+       protected void writeProperties(BufferedWriter out) throws IOException {
+               super.writeProperties(out);
+
+               writeProperty(out, "mod", mod);
+               writeProperty(out, "durability", durability);
+               writeProperty(out, "onWear", onWear);
+               writeProperty(out, "onUnwear", onUnwear);
+       }
+}