From: notzed@gmail.com Date: Sun, 15 Sep 2013 00:13:33 +0000 (+0000) Subject: Checkpoint of "work as is". X-Git-Tag: dusk-0.1~4 X-Git-Url: https://code.zedzone.au/cvs?a=commitdiff_plain;h=521065bac7306692809d66bf8acd684c0c211f92;p=duskz Checkpoint of "work as is". git-svn-id: file:///home/notzed/svn/duskz/trunk@17 b8b59bfb-1aa4-4687-8f88-a62eeb14c21e --- diff --git a/DuskServer/CommandsWork.java b/DuskServer/CommandsWork.java new file mode 100644 index 0000000..8056dbd --- /dev/null +++ b/DuskServer/CommandsWork.java @@ -0,0 +1,2393 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 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 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; + } +} diff --git a/DuskServer/README b/DuskServer/README index 658caf2..ad770de 100644 --- a/DuskServer/README +++ b/DuskServer/README @@ -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 index 0000000..7d93ab8 --- /dev/null +++ b/DuskServer/TODO @@ -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 diff --git a/DuskServer/docs/dusk-script b/DuskServer/docs/dusk-script index 10570dc..858e647 100644 --- a/DuskServer/docs/dusk-script +++ b/DuskServer/docs/dusk-script @@ -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 index 0000000..3f0cafe --- /dev/null +++ b/DuskServer/docs/duskz-classes @@ -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 index 0000000..b69818d --- /dev/null +++ b/DuskServer/docs/duskz-javascript @@ -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 index 0000000..6d72f58 --- /dev/null +++ b/DuskServer/docs/duskz-script @@ -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 index 0000000..fdaf838 --- /dev/null +++ b/DuskServer/docs/duskz-state @@ -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 index 0000000..027ac01 --- /dev/null +++ b/DuskServer/src/duskz/io/Convert.java @@ -0,0 +1,61 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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(); + } +} diff --git a/DuskServer/src/duskz/io/Tiled.java b/DuskServer/src/duskz/io/Tiled.java index f11b7e6..74dbcd6 100644 --- a/DuskServer/src/duskz/io/Tiled.java +++ b/DuskServer/src/duskz/io/Tiled.java @@ -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")); } } diff --git a/DuskServer/src/duskz/server/Commands.java b/DuskServer/src/duskz/server/Commands.java index d9061c1..522bea6 100644 --- a/DuskServer/src/duskz/server/Commands.java +++ b/DuskServer/src/duskz/server/Commands.java @@ -26,13 +26,13 @@ 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"; diff --git a/DuskServer/src/duskz/server/DuskEngine.java b/DuskServer/src/duskz/server/DuskEngine.java index 86de141..2272517 100644 --- a/DuskServer/src/duskz/server/DuskEngine.java +++ b/DuskServer/src/duskz/server/DuskEngine.java @@ -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 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()) { diff --git a/DuskServer/src/duskz/server/Faction.java b/DuskServer/src/duskz/server/Faction.java index f8646d9..663b871 100644 --- a/DuskServer/src/duskz/server/Faction.java +++ b/DuskServer/src/duskz/server/Faction.java @@ -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()); } } } diff --git a/DuskServer/src/duskz/server/entity/DuskObject.java b/DuskServer/src/duskz/server/entity/DuskObject.java index b4e7736..0b02f64 100644 --- a/DuskServer/src/duskz/server/entity/DuskObject.java +++ b/DuskServer/src/duskz/server/entity/DuskObject.java @@ -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(); diff --git a/DuskServer/src/duskz/server/entity/Equipment.java b/DuskServer/src/duskz/server/entity/Equipment.java index f26fb1c..cc42628 100644 --- a/DuskServer/src/duskz/server/entity/Equipment.java +++ b/DuskServer/src/duskz/server/entity/Equipment.java @@ -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 = { diff --git a/DuskServer/src/duskz/server/entity/LivingThing.java b/DuskServer/src/duskz/server/entity/LivingThing.java index 40d4009..9c8cdf6 100644 --- a/DuskServer/src/duskz/server/entity/LivingThing.java +++ b/DuskServer/src/duskz/server/entity/LivingThing.java @@ -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(""); - } - if (isPlayer() && !clan.equals("none")) { - sb.append('<'); - sb.append(clan); - sb.append('>'); - } - if (isPet() && hp < 0) { - sb.append(""); - } - 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 { diff --git a/DuskServer/src/duskz/server/entity/Mob.java b/DuskServer/src/duskz/server/entity/Mob.java index 52297a4..2de820c 100644 --- a/DuskServer/src/duskz/server/entity/Mob.java +++ b/DuskServer/src/duskz/server/entity/Mob.java @@ -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(); diff --git a/DuskServer/src/duskz/server/entity/TileMap.java b/DuskServer/src/duskz/server/entity/TileMap.java index 3bf654a..0e15b35 100644 --- a/DuskServer/src/duskz/server/entity/TileMap.java +++ b/DuskServer/src/duskz/server/entity/TileMap.java @@ -605,7 +605,6 @@ public class TileMap implements Iterable { /** * Implements an iterator which follows a 'looking' path * - * TODO: it should probably use Bresenhams line algorithm */ private class LookIterator implements Iterator { diff --git a/DuskServer/src/duskz/server/entityz/Ability.java b/DuskServer/src/duskz/server/entityz/Ability.java new file mode 100644 index 0000000..3b70c3e --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Ability.java @@ -0,0 +1,38 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..e3e8af1 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Active.java @@ -0,0 +1,1793 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +/** + * 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 + */ +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 skills = new HashMap<>(); + // FIXME: SpellAbility subclasses Ability + private final HashMap spells = new HashMap<>(); + /** + * Pending moves, processed one at a time during movement tick + */ + final private LinkedList moveQueue = new LinkedList<>(); + /** + * Pending commands, this is only used during battle + */ + final private LinkedList 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 < 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 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 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 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 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 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 joined, ArrayList 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 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 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 index 0000000..c9edd10 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Armour.java @@ -0,0 +1,68 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..1637f06 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Battle.java @@ -0,0 +1,352 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 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 left = new ArrayList<>(); + /** + * Players who joined during the last turn + */ + private ArrayList 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 side, ArrayList 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 list1, ArrayList list2, ArrayList 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 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 list, ArrayList opponents, ArrayList fled) { + fled.add(thing); + list.remove(thing); + + thing.fleeBattle(this, opponents); + + if (side2.size() == 0 || side1.size() == 0) { + endBattle(); + return; + } + } + + void chatMessage(ArrayList side1, ArrayList 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 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 list, ArrayList opponents, ArrayList 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 list1, ArrayList list2, ArrayList 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 list1, ArrayList joined, ArrayList 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 index 0000000..4799972 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Commands.java @@ -0,0 +1,319 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 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 index 0000000..55bd81b --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Condition.java @@ -0,0 +1,100 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..2ed0916 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/ConditionList.java @@ -0,0 +1,136 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +public class ConditionList { + + private final HashMap conditions = new HashMap<>(); + private final List 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 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 index 0000000..ddabc4d --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Container.java @@ -0,0 +1,47 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..e367e40 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Converter.java @@ -0,0 +1,550 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..ab6f597 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Drink.java @@ -0,0 +1,44 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..81986b5 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Equipment.java @@ -0,0 +1,226 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + * Copyright (C) 2013 Michael Zucchi + * + * 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 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 index 0000000..2e880d4 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Food.java @@ -0,0 +1,45 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..6fce193 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Game.java @@ -0,0 +1,879 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 tileAction = new HashMap<>(); + private HashMap tileAble = new HashMap<>(); + private HashMap tileVisible = new HashMap<>(); + // Just indicates which scripts are available, could be a cache or something + private HashSet locationAction = new HashSet<>(); + private HashSet locationAble = new HashSet<>(); + private HashSet locationVisible = new HashSet<>(); + /* + * Preference keys + */ + public final static String PREF_AI = "ai"; + // Used for mob info cache + HashMap allMobs = new HashMap<>(); + // Used for item info cache, this needs a clone or equivalent ... + //HashMap allItems = new HashMap<>(); + // Factions + HashMap factions = new HashMap<>(); + // do i want any of this now? hmm. + ThingTable mobs; + ThingTable gameShops; + ThingTable signs; + ThingTable props; + // + HashMap races = new HashMap<>(); + // All players in game + HashMap players = new HashMap<>(); + // All things in game + HashMap things = new HashMap<>(); + // All actives in game + HashMap actives = new HashMap<>(); + // + private final HashMap maps = new HashMap<>(); + // + private ArrayList 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 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 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 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 getRaceNames() { + ArrayList 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 index 0000000..c327271 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/GameServer.java @@ -0,0 +1,178 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..fab7450 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/GameShop.java @@ -0,0 +1,125 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 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 index 0000000..ddc6689 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Holdable.java @@ -0,0 +1,104 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..0e6c185 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Inventory.java @@ -0,0 +1,105 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +public class Inventory { + + // do i need a name index? rarely accessed, short list, who cares + final private ArrayList 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 getAll(String key, int maximum) { + ArrayList 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 index 0000000..049c306 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Item.java @@ -0,0 +1,43 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..f41962d --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Location.java @@ -0,0 +1,72 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..35fb38f --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Log.java @@ -0,0 +1,98 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 index 0000000..1176024 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Mobile.java @@ -0,0 +1,315 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +public class Mobile extends Active { + + /** + * Home location for mobile + */ + int homeX, homeY; + /** + * Items given when killed by a player + */ + ArrayList 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 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 index 0000000..7ed1720 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Pack.java @@ -0,0 +1,121 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +public class Pack { + /* + * Required functionality: + * - ordered list of followers + * - notifications + * - grou tests, e.g. member of, clan inside + */ + + ArrayList 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 index 0000000..ac262e0 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Pet.java @@ -0,0 +1,114 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 opponents) { + endBattle(); + } + + @Override + public void killedBattle(Battle battle, Active winner, ArrayList 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 index 0000000..66d9bf2 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Player.java @@ -0,0 +1,544 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +public class Player extends Active implements DuskProtocol { + + private HashSet 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(""); + //} + 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(""); + //} + //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 opponents) { + super.fleeBattle(battle, opponents); + + if (pet != null) { + battle.removeParticipant(pet); + pet.fleeBattle(battle, opponents); + } + } + + @Override + public void killedBattle(Battle battle, Active winner, ArrayList 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 index 0000000..0184bf5 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/PlayerCommands.java @@ -0,0 +1,90 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..be959dc --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/PlayerConnection.java @@ -0,0 +1,487 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 pendingQuestions = new HashMap<>(); + final public LinkedBlockingDeque 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 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 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 index 0000000..9238b60 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/PlayerShop.java @@ -0,0 +1,88 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 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 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 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 index 0000000..ed0e04e --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/PlayerState.java @@ -0,0 +1,367 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 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 left = (HashMap) visibleThings.clone(); + ArrayList 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 index 0000000..31822b6 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Prop.java @@ -0,0 +1,37 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..0216bc6 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/PropertyLoader.java @@ -0,0 +1,152 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +public class PropertyLoader implements Iterable, 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 iterator() { + return new PropertyIterator(reader); + } + + @Override + public void close() throws IOException { + if (reader != null) + reader.close(); + } + + static class PropertyIterator implements Iterator { + + 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 index 0000000..e4984ef --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Race.java @@ -0,0 +1,106 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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; + } +} diff --git a/DuskServer/src/duskz/server/ScriptManager.java b/DuskServer/src/duskz/server/entityz/ScriptManager.java similarity index 95% rename from DuskServer/src/duskz/server/ScriptManager.java rename to DuskServer/src/duskz/server/entityz/ScriptManager.java index 4e7ec06..ab9c5a9 100644 --- a/DuskServer/src/duskz/server/ScriptManager.java +++ b/DuskServer/src/duskz/server/entityz/ScriptManager.java @@ -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 Future runScript(String script, Object... args) throws ClassCastException { return pool.submit(new ScriptData(script, args)); } - public Future runScript(File script, Object... args) throws FileNotFoundException { + public Future 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 index 0000000..8eff71a --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Shop.java @@ -0,0 +1,133 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 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 items); +} diff --git a/DuskServer/src/duskz/server/entityz/Sign.java b/DuskServer/src/duskz/server/entityz/Sign.java new file mode 100644 index 0000000..d8bc925 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Sign.java @@ -0,0 +1,52 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..0a891af --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Thing.java @@ -0,0 +1,450 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 cmpName = new Comparator() { + @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 c = (Class) 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 index 0000000..b0e5b52 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/ThingTable.java @@ -0,0 +1,152 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +public class ThingTable { + + // By name index + final private HashMap> byName = new HashMap<>(); + // By id index + final private HashMap 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 values() { + return byID.values(); + } + + public void add(T thing) { + HashSet 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 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 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 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 index 0000000..ac23ed7 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/TileMap.java @@ -0,0 +1,1122 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 { + + /** + * 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> entities = new HashMap<>(); + //private Thing entities[]; + /** + * Aliases for locations, by location and name + */ + private HashMap aliases = new HashMap<>(); + private HashMap aliasesByName = new HashMap<>(); + /** + * Scripts for locations + */ + private HashMap ableScript = new HashMap<>(); + private HashMap visibleScript = new HashMap<>(); + private HashMap actionScript = new HashMap<>(); + /** + * Aliases for jumps. These are implicit actions on a location + */ + private HashMap 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 getEntities(int x, int y, List list) { + if (list == null) + list = new ArrayList<>(); + //Thing o = entities[x + y * cols]; + //while (o != null) { + // list.add(o); + // o = o.getNext(); + //} + LinkedList ll = entities.get(new Location(x, y)); + if (ll != null) + list.addAll(ll); + + return list; + } + + public synchronized List getEntities(Location l, List list) { + if (list == null) + list = new ArrayList<>(); + //Thing o = entities[x + y * cols]; + //while (o != null) { + // list.add(o); + // o = o.getNext(); + //} + LinkedList 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 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 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 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 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 look(int sx, int sy, int ex, int ey, int flags) { + return new LookIterable(sx, sy, ex, ey, flags); + } + + public Iterable look(int sx, int sy, int ex, int ey) { + return new LookIterable(sx, sy, ex, ey, 0); + } + + public Iterable 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 { + + 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 iterator() { + return new MapIterator(x0, y0, x1, y1); + } + } + + private class LookIterable implements Iterable { + + 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 iterator() { + return new LookIterator(sx, sy, ex, ey, flags); + } + } + + private class MoveIterable implements Iterable { + + 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 iterator() { + return new MoveIterator(sx, sy, ex, ey, flags, l); + } + } + + public Iterator getIterator(int x0, int y0, int x1, int y1) { + return new MapIterator(x0, y0, x1, y1); + } + + @Override + public Iterator iterator() { + return getIterator(0, 0, cols, rows); + } + + public class MapData extends Location { + + public final List 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 { + + 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 { + + 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 { + + 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 { + + int x, y; + final int sx, sy; + final int ex, ey; + int flags; + boolean there = false; + private final MoveListener l; + // quick hack version + Iterator 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 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 constructPath(List 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 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 getNeighbours(MoveInfo n) { + List 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 findPath() { + + PriorityQueue openList = new PriorityQueue(); + HashSet 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 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 index 0000000..a7fb13d --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/TooManyTriesException.java @@ -0,0 +1,28 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..acd3416 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Training.java @@ -0,0 +1,66 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..0d4aa29 --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/VariableList.java @@ -0,0 +1,155 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +public class VariableList { + + private HashMap 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 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 index 0000000..964c58a --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Weapon.java @@ -0,0 +1,68 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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 index 0000000..97d535b --- /dev/null +++ b/DuskServer/src/duskz/server/entityz/Wearable.java @@ -0,0 +1,70 @@ +/* + * This file is part of DuskZ, a graphical mud engine. + * + * Copyright (C) 2000 Tom Weingarten + * Copyright (C) 2013 Michael Zucchi + * + * 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 + */ +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); + } +}