summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Purger.java186
-rw-r--r--README.restart55
-rw-r--r--_news_colors.py168
-rw-r--r--aligncsv.cpp183
-rwxr-xr-xbin/restart-all21
-rw-r--r--bin/restart-config25
-rwxr-xr-xbin/restart-login3
-rwxr-xr-xbin/restart-pid44
-rwxr-xr-xbin/restart-world28
-rw-r--r--item_show.py29
-rwxr-xr-xmobxp30
-rw-r--r--monster-killing-values.py61
-rwxr-xr-xnews.py103
-rw-r--r--retab.sml98
-rw-r--r--showmagicspells.py64
-rwxr-xr-xshowvars.py110
-rwxr-xr-xtmx_converter.py371
-rw-r--r--web/README3
-rwxr-xr-xweb/main.py28
-rw-r--r--web/with_xml.py71
21 files changed, 1683 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f11fb40
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/aligncsv
+*.pyc
diff --git a/Purger.java b/Purger.java
new file mode 100644
index 0000000..5113438
--- /dev/null
+++ b/Purger.java
@@ -0,0 +1,186 @@
+/*
+ * Purger (c) 2006 Eugenio Favalli
+ * License: GPL, v2 or later
+ */
+
+import java.io.*;
+import java.text.*;
+import java.util.*;
+
+ public class Purger {
+
+ public static void main(String[] args) {
+ if (args.length != 2) {
+ System.err.println(
+ "Usage: java Purger <folder> <date>\n" +
+ " - folder: is the path to account.txt and athena.txt files.\n" +
+ " - date: accounts created before this date will be purged (dd/mm/yy or yyyy-mm-dd).");
+ return;
+ }
+
+ int accounts = 0;
+ int characters = 0;
+ int deletedCharacters = 0;
+ Set<String> activeAccounts = new HashSet<String>();
+
+ File folder = new File(args[0]);
+ // Do some sanity checking
+ if (!folder.exists()) {
+ System.err.println("Folder does not exist!");
+ return;
+ }
+ if (!folder.isDirectory()) {
+ System.err.println("Folder is not a folder!");
+ return;
+ }
+
+ File oldAccount = new File(folder, "account.txt");
+ File oldAthena = new File(folder, "athena.txt");
+ File newAccount = new File(folder, "account.txt.new");
+ File newAthena = new File(folder, "athena.txt.new");
+
+ DateFormat dateFormat;
+ Date purgeDate = null;
+
+ for (String format : new String[] {"dd/MM/yy", "yyyy-MM-dd"}) {
+ dateFormat = new SimpleDateFormat(format);
+
+ try {
+ purgeDate = dateFormat.parse(args[1]);
+ break;
+ } catch (ParseException e) {}
+ }
+
+ if (purgeDate == null) {
+ System.err.println("ERROR: Date format not recognized.");
+ return;
+ }
+
+ System.out.printf("Removing accounts unused since %tF\n", purgeDate);
+
+ String line;
+
+ // Remove accounts
+ try {
+ FileInputStream fin = new FileInputStream(oldAccount);
+ BufferedReader input = new BufferedReader(
+ new InputStreamReader(fin));
+ FileOutputStream fout = new FileOutputStream(newAccount);
+ PrintStream output = new PrintStream(fout);
+
+ while ((line = input.readLine()) != null) {
+ boolean copy = false;
+ String[] fields = line.split("\t");
+ // Check if we're reading a comment or the last line
+ if (line.substring(0, 2).equals("//") || fields[1].charAt(0) == '%') {
+ copy = true;
+ }
+ else {
+ // Server accounts should not be purged
+ if (fields[4].equals("S")) {
+ copy = true;
+ } else {
+ accounts++;
+ dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ try {
+ Date date = dateFormat.parse(fields[3]);
+ if (date.after(purgeDate)) {
+ activeAccounts.add(fields[0]);
+ copy = true;
+ }
+ else
+ {
+ System.out.println("Removing " + fields[1]);
+ }
+ }
+ catch (ParseException e) {
+ // Ignore accounts that haven't been used yet
+ if (fields[3].equals("-")) {
+ activeAccounts.add(fields[0]);
+ copy = true;
+ }
+ else {
+ System.err.println("ERROR: Wrong date format in account.txt. (" + accounts + ": " + line + ")");
+ System.out.println("Removing " + fields[1]);
+ }
+ }
+ catch (Exception e) {
+ e.printStackTrace();
+ return;
+ }
+ }
+ }
+ if (copy) {
+ try {
+ output.println(line);
+ }
+ catch (Exception e) {
+ System.err.println("ERROR: Unable to write file.");
+ }
+ }
+ }
+ input.close();
+ output.close();
+ }
+ catch (FileNotFoundException e ) {
+ System.err.println("ERROR: file " + oldAccount.getAbsolutePath() + " not found.");
+ return;
+ }
+ catch (Exception e) {
+ System.err.println("ERROR: unable to process account.txt");
+ e.printStackTrace();
+ return;
+ }
+
+ System.out.println("Removed " + (accounts - activeAccounts.size()) + "/" + accounts + " accounts.");
+
+ // Remove characters
+ try {
+ FileInputStream fin = new FileInputStream(oldAthena);
+ BufferedReader input = new BufferedReader(
+ new InputStreamReader(fin));
+ FileOutputStream fout = new FileOutputStream(newAthena);
+ PrintStream output = new PrintStream(fout);
+
+ while ((line = input.readLine()) != null) {
+ boolean copy = false;
+ String[] fields = line.split("\t");
+ // Check if we're reading a comment or the last line
+ if (line.substring(0, 2).equals("//")
+ || fields[1].charAt(0) == '%') {
+ copy = true;
+ }
+ else {
+ characters++;
+ String id = fields[1].substring(0, fields[1].indexOf(','));
+ if (activeAccounts.contains(id)) {
+ copy = true;
+ }
+ else {
+ deletedCharacters++;
+ }
+ }
+ if (copy) {
+ output.println(line);
+ }
+ }
+ input.close();
+ output.close();
+ }
+ catch (FileNotFoundException e ) {
+ System.err.println("ERROR: file " + oldAthena.getAbsolutePath() + " not found.");
+ return;
+ }
+ catch (Exception e) {
+ System.err.println("ERROR: unable to process athena.txt");
+ e.printStackTrace();
+ return;
+ }
+
+ System.out.println(
+ "Removed " + deletedCharacters + "/"
+ + characters + " characters.");
+ }
+
+}
+
diff --git a/README.restart b/README.restart
new file mode 100644
index 0000000..0bd147e
--- /dev/null
+++ b/README.restart
@@ -0,0 +1,55 @@
+There are 3 front-facing scripts for this new pidfile-based restart system.
+
+All of them belong in ~/bin/, although one of them is not actually a script.
+(it is source'd, which follows $PATH)
+
+restart-all Call this on first boot
+ if REBUILD is not empty, it will first pull, build, and install
+ from the tmw-eathena repository.
+ In bash you can do this like: REBUILD=sure restart-all
+restart-world Call this if you only want to restart one server.
+ The first argument is the directory, e.g. ~/tmwa-server-data/
+ After that, you may pass --manual or --auto to not-pull or pull
+ script updates. If neither is specified, the value of PULL is
+ used.
+ This does NOT use AUTO_WORLDS or MANUAL_WORLDS.
+restart-config Contains configuration settings.
+ SERVER_SOURCE is the location of the tmw-eathena clone.
+ LOGIN_WORLD is the location of the clone that contains the
+ account data. It does not necessarily have to correspond to
+ a world that actually starts, although it does in the
+ current configuration
+ AUTO_WORLDS is an array (space separated, surrounded by
+ parentheses) of world directories that will have updates
+ pulled when calling restart-all.
+ MANUAL_WORLDS is an array of world directories that will not
+ have updates pulled when calling restart-all
+ VERBOSE controls whether the servers will print their output
+ to the tty or have it redirected to /dev/null. Use if if
+ you have any problems. It is inspected by the low-level
+ command restart-pid.
+ REBUILD controls whether the server sources will be rebuilt
+ during restart-all.
+ PULL controls whether updates should be pulled by
+ restart-world. It is ignored if --auto or --manual
+ is specified, which is the case during restart-all.
+ All of these variables (except probably the arrays) can be
+ specified in the environment, but the values in restart-config
+ override them. However, since restart-config is a bash script,
+ you could conditionally set the variables by using if test ...
+
+There are also two commands you'll probably never have to call yourself:
+restart-login is self-explanatory and is usually called only by restart-all
+restart-pid is the low-level command that maintains the PID file and kills
+ the old servers. In order for the server to be killed, three
+ things must match: the PID, and name, and the user.
+ This will keep errors to a minimum in case PID files continue
+ to exist after the processes have died and new processes have
+ taken their IDs.
+
+ There's a theoretical case in which the replacing process will
+ be a new instance of the same process by the same user in a
+ different world directory. The odds of this happening are
+ theoretically 1 in 32767-ish, but in practice might be a bit
+ more common than that. If you find your newly-spawned children
+ did not survive, try running restart-all again.
diff --git a/_news_colors.py b/_news_colors.py
new file mode 100644
index 0000000..329a6b8
--- /dev/null
+++ b/_news_colors.py
@@ -0,0 +1,168 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+## _news_colors.py - colors that can be used in news
+##
+## Copyright © 2012 Ben Longbons <b.r.longbons@gmail.com>
+##
+## This file is part of The Mana World (Athena server)
+##
+## This program 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.
+##
+## This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+import cgi
+
+__all__ = ['make_html_colors_dict', 'make_txt_colors_dict']
+
+class Color(object):
+ __slots__ = ('txt', 'rgb')
+ def __init__(self, txt, rgb):
+ self.txt = txt
+ self.rgb = rgb
+
+color_dict = dict(
+ black = Color(txt='##0', rgb=0x000000),
+ red = Color(txt='##1', rgb=0xff0000),
+ green = Color(txt='##2', rgb=0x009000),
+ blue = Color(txt='##3', rgb=0x0000ff),
+ orange = Color(txt='##4', rgb=0xe0980e),
+ yellow = Color(txt='##5', rgb=0xf1dc27),
+ pink = Color(txt='##6', rgb=0xff00d8),
+ purple = Color(txt='##7', rgb=0x8415e2),
+ gray = Color(txt='##8', rgb=0x919191),
+ brown = Color(txt='##9', rgb=0x8e4c17),
+)
+
+class HtmlDate(object):
+ __slots__ = ()
+ def __format__(self, date):
+ return '<font color="#0000ff">%s</font>' % date
+
+class HtmlLink(object):
+ __slots__ = ()
+ def __format__(self, target):
+ target = cgi.escape(target, True)
+ return '<a href="%s">%s</a>' % (target, target)
+
+class HtmlSignature(object):
+ __slots__ = ()
+ def __format__(self, author):
+ return '-<font color="#009000">%s</font>' % author
+
+class HtmlTitle(object):
+ __slots__ = ()
+ def __format__(self, title):
+ # no color here
+ # (we really need someone to do CSS)
+ return '<b>%s</b>' % title
+
+def make_html_colors_dict():
+ r = {
+ 'date': HtmlDate(),
+ 'link': HtmlLink(),
+ 'author': HtmlSignature(),
+ 'title': HtmlTitle(),
+ 'ul' : '<ul>',
+ '/ul': '</ul>',
+ 'li' : '<li>',
+ '/li': '</li>',
+ }
+ for k, v in color_dict.items():
+ r[k] = '<font color="#%06x">' % v.rgb
+ r['/' + k] = '</font>'
+ return r
+
+# Here be dragons
+
+def make_txt_colors_dict():
+ return dict(generate_txt_colors())
+
+class StackPusher(object):
+ __slots__ = ('tag_stack', 'tag', 'txt_stack', 'txt')
+ def __init__(self, tag_stack, tag, txt_stack, txt):
+ self.tag_stack = tag_stack
+ self.tag = tag
+ self.txt_stack = txt_stack
+ self.txt = txt
+ def __format__(self, fmt):
+ assert fmt == ''
+ self.tag_stack.append(self.tag)
+ if self.txt_stack is not None:
+ self.txt_stack.append(self.txt)
+ return self.txt
+
+class StackPopper(object):
+ __slots__ = ('tag_stack', 'tag', 'txt_stack', 'txt')
+ def __init__(self, tag_stack, tag, txt_stack, txt):
+ self.tag_stack = tag_stack
+ self.tag = tag
+ self.txt_stack = txt_stack
+ self.txt = txt
+ def __format__(self, fmt):
+ assert fmt == ''
+ if len(self.tag_stack) <= 0:
+ raise SyntaxError('Unmatched {/%s}' % self.tag)
+ prev = self.tag_stack.pop()
+ if self.tag != prev:
+ raise SyntaxError('Mismatched {/%s} from {%s}' % (self.tag, prev))
+ if self.txt_stack is not None:
+ self.txt_stack.pop()
+ return self.txt_stack[-1]
+ return self.txt
+
+class TxtDate(object):
+ __slots__ = ('stack')
+ def __init__(self, stack):
+ self.stack = stack
+ def __format__(self, date):
+ return '##3' + date + self.stack[-1]
+
+class TxtLink(object):
+ __slots__ = ('stack')
+ def __init__(self, stack):
+ self.stack = stack
+ def __format__(self, target):
+ # the field labeled 'bug' should not be necessary ...
+ return '@@{link}|{text}@@{bug}'.format(link=target, text=target, bug=self.stack[-1])
+
+class TxtSignature(object):
+ __slots__ = ('stack')
+ def __init__(self, stack):
+ self.stack = stack
+ def __format__(self, author):
+ return '-##2' + author + self.stack[-1]
+
+class TxtTitle(object):
+ __slots__ = ('stack')
+ def __init__(self, stack):
+ self.stack = stack
+ def __format__(self, title):
+ return '##7' + title + self.stack[-1]
+
+def generate_txt_colors():
+ tag_stack = []
+ color_stack = ['##0'] # don't let color stack become empty
+ for k,v in color_dict.items():
+ yield k, StackPusher(tag_stack, k, color_stack, v.txt)
+ e = '/' + k
+ yield e, StackPopper(tag_stack, k, color_stack, v.txt)
+ yield 'date', TxtDate(color_stack)
+ yield 'link', TxtLink(color_stack)
+ yield 'author', TxtSignature(color_stack)
+ yield 'title', TxtTitle(color_stack)
+
+ yield 'ul', StackPusher(tag_stack, 'ul', None, '')
+ yield '/ul', StackPopper(tag_stack, 'ul', None, '')
+
+ yield 'li', StackPusher(tag_stack, 'li', None, '* ')
+ yield '/li', StackPopper(tag_stack, 'li', None, '')
diff --git a/aligncsv.cpp b/aligncsv.cpp
new file mode 100644
index 0000000..ef75ac0
--- /dev/null
+++ b/aligncsv.cpp
@@ -0,0 +1,183 @@
+#include <cerrno>
+#include <cstdio>
+#include <cstddef>
+
+#include <unistd.h>
+
+#include <vector>
+#include <string>
+
+// this configuration puts 2-5 spaces between entries (excluding headers)
+// and rounds the start of each field up to 4, for easier manual indenting
+// but force each field to be at least size 8
+const size_t min_pad = 2;
+const size_t align_pad = 4;
+const size_t min_size = 8;
+
+void add_pieces(std::vector<std::string>& line, std::vector<size_t>& sizes)
+{
+ // This would get rid of trailing commas,
+ // but that would break certain db.txt files.
+ // Instead we'll have to manually check whether it's empty when checking length
+// if (!line.empty() && line.back().empty())
+// line.pop_back();
+ size_t num_sizes = line.size();
+ if (!num_sizes) // line.empty()
+ return;
+ if (line[0].size() >= 2
+ && (line[0][0] == '#'
+ || (line[0][0] == '/'
+ && line[0][1] == '/')))
+ return;
+
+ if (num_sizes > sizes.size())
+ sizes.resize(num_sizes, 1UL);
+ for (size_t i = 0; i < num_sizes; ++i)
+ {
+ size_t elt_size = line[i].size();
+ if (!elt_size)// line[i].empty()
+ continue;
+ if (line[i][elt_size - 1] == ' ')
+ line[i].resize(--elt_size);
+ // mandatory padding and comma
+ elt_size += min_pad + 1;
+ if (elt_size < min_size)
+ elt_size = min_size;
+ if (elt_size > sizes[i])
+ // always true if we expanded sizes
+ sizes[i] = elt_size;
+ }
+}
+
+// the arguments may be the same file - the whole file is stored in memory
+void aligncsv(FILE *in, FILE *out, const char *name)
+{
+ bool newline = true;
+ bool can_split = true;
+ bool can_have_whitespace = false;
+ int c;
+ std::vector<std::vector<std::string> > contents;
+
+ while ((c = fgetc(in)) != -1)
+ {
+ if (c == '}' || c == '\n')
+ can_split = true;
+ if (c == '\n')
+ {
+ if (newline)
+ {
+ // preserve consecutive blank lines
+ contents.push_back(std::vector<std::string>());
+ }
+ newline = true;
+ continue;
+ }
+ if (c == '{')
+ can_split = false;
+ if (c == '\t')
+ c = ' ';
+ if (c == ' ')
+ {
+ if (!can_have_whitespace)
+ continue;
+ can_have_whitespace = false;
+ }
+ else
+ can_have_whitespace = true;
+ if (newline)
+ {
+ contents.push_back(std::vector<std::string>(1, std::string(1, c)));
+ newline = false;
+ }
+ else
+ {
+ if (can_split && c == ',')
+ {
+ can_have_whitespace = false;
+ contents.back().push_back(std::string());
+ }
+ else
+ contents.back().back() += c;
+ }
+ }
+
+ typedef std::vector<std::vector<std::string> >::iterator outer_it;
+ typedef std::vector<std::vector<std::string> >::const_iterator outer_cit;
+ typedef std::vector<size_t>::iterator pieces_it;
+ // at this point, each entry in a line:
+ // * does not start with whitespace
+ // * has one space in place of any previous run of whitespace
+ // * may end in a single space
+ // The last is fixed during add_pieces
+ std::vector<size_t> pieces;
+ for (outer_it it = contents.begin(), end = contents.end(); it != end; ++it)
+ add_pieces(*it, pieces);
+ for (pieces_it it = pieces.begin(), end = pieces.end(); it != end; ++it)
+ if (size_t trail = *it % align_pad)
+ *it += align_pad - trail;
+
+ if (in == out)
+ {
+ //rewind(out);
+ if (fseek(out, 0, SEEK_SET) == -1)
+ {
+ perror(name);
+ return;
+ }
+ if (ftruncate(fileno(out), 0) == -1)
+ {
+ perror(name);
+ return;
+ }
+ }
+ for (outer_cit oit = contents.begin(), oend = contents.end(); oit != oend; ++oit)
+ {
+ const std::vector<std::string>& inner = *oit;
+ size_t num_elems = inner.size();
+ // we have previously guaranteed that pieces[i].size() >= num_elems
+ for (size_t i = 0; i < num_elems; ++i)
+ {
+ // FIXME handle UTF-8 characters (here AND above?)
+ if (fputs(inner[i].c_str(), out) == -1)
+ {
+ perror(name);
+ return;
+ }
+ if (i != num_elems - 1)
+ {
+ if (fputc(',', out) == -1)
+ {
+ perror(name);
+ return;
+ }
+ size_t elem_length = inner[i].size() + 1;
+ while (elem_length++ < pieces[i])
+ {
+ if (fputc(' ', out) == -1)
+ {
+ perror(name);
+ return;
+ }
+ }
+ }
+ }
+ fputc('\n', out);
+ }
+}
+
+int main(int argc, char **argv)
+{
+ if (argc == 1)
+ aligncsv(stdin, stdout, "<stdio>");
+ for (int i = 1; i < argc; ++i)
+ {
+ FILE *f = fopen(argv[i], "r+");
+ if (!f)
+ {
+ perror(argv[i]);
+ continue;
+ }
+ aligncsv(f, f, argv[i]);
+ fclose(f);
+ }
+}
diff --git a/bin/restart-all b/bin/restart-all
new file mode 100755
index 0000000..5d7e242
--- /dev/null
+++ b/bin/restart-all
@@ -0,0 +1,21 @@
+#!/bin/bash -e
+source restart-config
+if test -n "$REBUILD"
+then
+ cd $SERVER_SOURCE
+ git pull
+ make
+ make install prefix=${HOME}
+fi
+
+restart-login $LOGIN_WORLD
+
+for world in ${AUTO_WORLDS[@]}
+do
+ restart-world $world --auto
+done
+
+for world in ${MANUAL_WORLDS[@]}
+do
+ restart-world $world --manual
+done
diff --git a/bin/restart-config b/bin/restart-config
new file mode 100644
index 0000000..e19ab31
--- /dev/null
+++ b/bin/restart-config
@@ -0,0 +1,25 @@
+## TMW restart script settings
+## This file must be in ~/bin/ even though it's not executable
+
+## Mandatory filepath settings
+SERVER_SOURCE=~/eathena
+LOGIN_WORLD=~/tmwa-server-data
+AUTO_WORLDS=(
+ ~/tmwa-server-data
+)
+MANUAL_WORLDS=(
+ ~/tmwa-server-test
+)
+## Boolean settings (nonempty for true)
+## if not specified here, the value from the environment is used,
+## which is probably empty (false). However, some scripts may
+## provide command-line options to override the defaults.
+
+## Should the servers print their output to the terminal?
+# VERBOSE=yep
+
+## Should server sources be rebuilt?
+# REBUILD=sure
+
+## Should server data be pulled?
+# PULL=certainly
diff --git a/bin/restart-login b/bin/restart-login
new file mode 100755
index 0000000..ebb5fe1
--- /dev/null
+++ b/bin/restart-login
@@ -0,0 +1,3 @@
+#!/bin/bash -e
+cd "$1"
+restart-pid tmwa-login
diff --git a/bin/restart-pid b/bin/restart-pid
new file mode 100755
index 0000000..498bdcc
--- /dev/null
+++ b/bin/restart-pid
@@ -0,0 +1,44 @@
+#!/bin/bash -e
+# do nasty work here
+# The job of this script is twofold:
+# 1. kill the existing server, if it exists
+# 2. write the PID file and start the new server
+
+source restart-config
+
+PROCESS=$1
+
+if test -f $PROCESS.pid
+then
+ # if the process ID may change its name (e.g. via exec),
+ # then remove '$PROCESS' on the following line
+ PID=$(pgrep $PROCESS -u $UID -F $PROCESS.pid || true)
+ if test -n "$PID"
+ then
+ kill $PID
+ echo waiting for $PID to die so I can restart $PROCESS
+ while
+ ! kill -s 0 $PID
+ do
+ echo -n .
+ sleep 1
+ done
+ # This shouldn't be necessary, but somehow is
+ sleep 2
+ echo
+ else
+ echo $PROCESS.pid does not point to a valid process - nothing killed
+ fi
+else
+ echo No PID file $PROCESS.pid found - nothing killed
+fi
+
+if test -z "$VERBOSE"
+then
+ exec >/dev/null 2>&1
+fi
+
+{
+ # $$ is not reset in subshells
+ exec sh -c 'echo $$ > '$PROCESS'.pid; exec ./'$PROCESS
+} &
diff --git a/bin/restart-world b/bin/restart-world
new file mode 100755
index 0000000..ccd9e45
--- /dev/null
+++ b/bin/restart-world
@@ -0,0 +1,28 @@
+#!/bin/bash -e
+cd "$1"
+shift
+
+source restart-config
+
+for ARG
+do
+ if [ "$ARG" = --auto ]
+ then
+ PULL=y
+ elif [ "$ARG" = --manual ]
+ then
+ PULL=
+ else
+ echo unknown argument
+ exit 1
+ fi
+done
+
+if test -n "$PULL"
+then
+ git pull
+ (cd world/map/conf; cat magic.conf.template | ./spells-build > magic.conf)
+fi
+
+restart-pid tmwa-char
+restart-pid tmwa-map
diff --git a/item_show.py b/item_show.py
new file mode 100644
index 0000000..1e14e77
--- /dev/null
+++ b/item_show.py
@@ -0,0 +1,29 @@
+#!/usr/bin/python
+#has to be executed in place, this folder
+
+
+def make_items():
+ items_file=open("../db/item_db.txt","r")
+ lines=items_file.readlines()
+ items_file.close();
+
+ items=[]
+ for line in lines:
+ array=line.split(",")
+ if len(array)>6 and not line.startswith("#") and not line.startswith("//"):
+ id=array[0]
+ name=array[1]
+ mbonus=array[10]
+ try:
+ int(mbonus)
+ items+=[(int(mbonus),name)]
+ except:
+ print line
+ return items;
+
+global_items=[]
+global_items=make_items();
+
+global_items.sort()
+for item in global_items:
+ print item
diff --git a/mobxp b/mobxp
new file mode 100755
index 0000000..49c74f2
--- /dev/null
+++ b/mobxp
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+IFS=$' \t\n,'
+while read
+do
+ if [ -z "$REPLY" ] || [ "${REPLY:0:1}" = '#' ] || [ "${REPLY:0:2}" = '//' ]
+ then
+ echo "$REPLY"
+ continue
+ fi
+
+ read ID Name Jname LV HP SP EXP JEXP Range1 ATK1 ATK2 DEF MDEF STR AGI VIT INT DEX LUK Range2 Range3 Scale Race Element Mode Speed Adelay Amotion Dmotion Drop1id Drop1per Drop2id Drop2per Drop3id Drop3per Drop4id Drop4per Drop5id Drop5per Drop6id Drop6per Drop7id Drop7per Drop8id Drop8per Item1 Item2 MEXP ExpPer MVP1id MVP1per MVP2id MVP2per MVP3id MVP3per mutationcount mutationstrength <<< "$REPLY"
+ if [ -z "$mutationstrength" ]
+ then
+ echo "$REPLY"
+ fi
+ BOSS=$(((Mode&32)>0))
+ ASSIST=$(((Mode&8)>0))
+ AGGRO=$(((Mode&4)>0))
+
+ MODE_BONUS=$((3*BOSS+1*ASSIST+1*AGGRO+2))
+ NEWJEXP=$(bc <<< "
+ 1 + $MODE_BONUS * $HP * (100 - $DEF) * sqrt(2500 * ($ATK1 + $ATK2) / $Adelay) / sqrt(sqrt($Speed)) / 15000
+ ") || {
+ echo "$REPLY"
+ continue
+ }
+ # echo $ID$'\t'$Name$'\t'Old:$'\t'$JEXP$'\t'New:$'\t'$NEWJEXP$'\t( '$BOSS $AGGRO $ASSIST')'
+ echo $ID, $Name, $Jname, $LV, $HP, $SP, $EXP, $NEWJEXP, $Range1, $ATK1, $ATK2, $DEF, $MDEF, $STR, $AGI, $VIT, $INT, $DEX, $LUK, $Range2, $Range3, $Scale, $Race, $Element, $Mode, $Speed, $Adelay, $Amotion, $Dmotion, $Drop1id, $Drop1per, $Drop2id, $Drop2per, $Drop3id, $Drop3per, $Drop4id, $Drop4per, $Drop5id, $Drop5per, $Drop6id, $Drop6per, $Drop7id, $Drop7per, $Drop8id, $Drop8per, $Item1, $Item2, $MEXP, $ExpPer, $MVP1id, $MVP1per, $MVP2id, $MVP2per, $MVP3id, $MVP3per, $mutationcount, $mutationstrength
+done
diff --git a/monster-killing-values.py b/monster-killing-values.py
new file mode 100644
index 0000000..9578804
--- /dev/null
+++ b/monster-killing-values.py
@@ -0,0 +1,61 @@
+#!/usr/bin/python
+#has to be executed in place, this folder
+
+
+def make_items():
+ items_file=open("../db/item_db.txt","r")
+ lines=items_file.readlines()
+ items_file.close();
+
+ items=[]
+ for line in lines:
+ array=line.split(",")
+ if len(array)>6 and not line.startswith("#") and not line.startswith("//"):
+ id=array[0]
+ sellprize=array[5]
+ try:
+ int(sellprize)
+ items+=[(int(id),int(sellprize))]
+ except:
+ print line
+ return items;
+
+def getvalueof(id):
+ for x in global_items:
+ if x[0]==id:
+ return int(x[1])
+ return 0
+
+def make_mobs():
+ mobfile=open("../db/mob_db.txt","r")
+ lines=mobfile.readlines()
+ mobfile.close();
+
+ mobs=[]
+ for line in lines:
+ array=line.split(",")
+ if len(array)>6 and not line.startswith("#"):
+ id=array[0]
+ name=array[1]
+ print name
+ print array[29:44]
+ sellprize = 0
+ #hardcoded -.- fix it !
+ sellprize += getvalueof(int(array[29]))*int(array[30])
+ sellprize += getvalueof(int(array[31]))*int(array[32])
+ sellprize += getvalueof(int(array[33]))*int(array[34])
+ sellprize += getvalueof(int(array[35]))*int(array[36])
+ sellprize += getvalueof(int(array[37]))*int(array[38])
+ sellprize += getvalueof(int(array[39]))*int(array[40])
+ sellprize += getvalueof(int(array[41]))*int(array[32])
+ sellprize += getvalueof(int(array[43]))*int(array[44])
+ mobs+=[(name,sellprize/1000.0)]
+ return mobs
+
+global_items=[]
+global_items=make_items();
+
+mobs=make_mobs();
+
+for mob in mobs:
+ print mob[1],mob[0]
diff --git a/news.py b/news.py
new file mode 100755
index 0000000..4391714
--- /dev/null
+++ b/news.py
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+## news.py - Generates news.
+##
+## Copyright © 2012 Ben Longbons <b.r.longbons@gmail.com>
+##
+## This file is part of The Mana World
+##
+## This program 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.
+##
+## This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from __future__ import print_function
+
+import sys
+import os
+from abc import ABCMeta, abstractmethod
+
+import _news_colors as colors
+
+class BasicWriter(object):
+ __slots__ = ('stream')
+ __metaclass__ = ABCMeta
+ def __init__(self, outfile):
+ self.stream = open(outfile, 'w')
+
+ @abstractmethod
+ def start(self):
+ pass
+
+ @abstractmethod
+ def put(self, entry):
+ pass
+
+ @abstractmethod
+ def finish(self):
+ pass
+
+class HtmlWriter(BasicWriter):
+ __slots__ = ()
+ def start(self):
+ self.stream.write('<!-- Generated by tools/news.py for index.php -->\n')
+ #self.stream.write('<pre>\n')
+ pass
+
+ def put(self, entry):
+ self.stream.write('<div>\n<p/>\n')
+ entry = entry.replace('\n\n', '\n<p/>\n')
+ entry = entry.format(**colors.make_html_colors_dict())
+ self.stream.write(entry)
+ self.stream.write('</div>\n')
+
+ def finish(self):
+ #self.stream.write('</pre>\n')
+ pass
+
+class TxtWriter(BasicWriter):
+ __slots__ = ()
+ def start(self):
+ pass
+ def put(self, entry):
+ entry = entry.replace('\n\n', '\n \n')
+ entry = entry.format(**colors.make_txt_colors_dict())
+ self.stream.write(entry)
+ self.stream.write(' \n \n')
+ def finish(self):
+ # DO NOT REMOVE
+ #self.stream.write('Did you really read down this far?\n')
+ pass
+
+def create_writers(outdir):
+ yield TxtWriter(os.path.join(outdir, 'news.txt'))
+ yield HtmlWriter(os.path.join(outdir, 'news.html'))
+
+def main(outdir, indir=None):
+ if indir is None:
+ indir = os.path.join(outdir, 'news.d')
+
+ out = list(create_writers(outdir))
+ for s in out:
+ s.start()
+ for entry in sorted(os.listdir(indir), reverse=True):
+ if not entry.endswith('.txt'):
+ continue
+ e = open(os.path.join(indir, entry)).read()
+ for s in out:
+ s.put(e)
+ for s in out:
+ s.finish()
+
+if __name__ == '__main__':
+ main(*sys.argv[1:])
diff --git a/retab.sml b/retab.sml
new file mode 100644
index 0000000..8e2a054
--- /dev/null
+++ b/retab.sml
@@ -0,0 +1,98 @@
+(*
+ * retab (c) 2009 The Mana World development team
+ * License: GPL, version 2 or later
+ *
+ * Compilation, e.g. (depends on SML implementation):
+ * mlton retab.sml
+ *
+ * Example usage:
+ * tools/retab < db/mob_db.txt > db/mob_db.txt.new && mv db/mob_db.txt.new db/mob_db.txt
+ *
+ * TODO:
+ * - Commas inside {} need to be seen as just one field when tabified
+ * - Commented lines should be left untabified
+ *)
+
+fun width (#"\t", i) = let val m = i mod 8 in if m = 0 then 8 else m end
+ | width (c, i) = 1
+
+fun encode_width (offset) (l) =
+ let fun expand ([], i) = []
+ | expand (c::tl, i) = let val w = width (c, i)
+ in (c, i, i + w) :: expand (tl, i + w)
+ end
+ in expand (l, offset)
+ end
+
+fun strip_blanks (#" "::tl) = strip_blanks (tl)
+ | strip_blanks (#"\t"::tl) = strip_blanks (tl)
+ | strip_blanks (#"\n"::tl) = strip_blanks (tl)
+ | strip_blanks (#"\r"::tl) = strip_blanks (tl)
+ | strip_blanks (other) = other
+
+fun clean (s) = rev (strip_blanks (rev (strip_blanks (s))))
+
+fun split_comma (#","::tl) = [#","] :: (split_comma (strip_blanks (tl)))
+ | split_comma ([]) = []
+ | split_comma (h::tl) = (case split_comma (tl) of
+ [] => [[h]]
+ | (h'::tl) => (h::h')::tl
+ )
+
+fun expand (l : char list list list, offset : int) : char list list list =
+ if List.all (null) (l) (* at the end of all rows *)
+ then l
+ else let fun splitlist ([]::tl) = let val (heads, tails) = splitlist (tl)
+ in ([]::heads, []::tails)
+ end
+ | splitlist ((hrow::tlrow)::tl) = let val (heads, tails) = splitlist (tl)
+ in (hrow::heads, tlrow::tails)
+ end
+ | splitlist ([]) = ([], [])
+ val (heads, tails) = splitlist (l)
+ val eheads = map (encode_width offset) heads
+
+ fun last_offset [] = offset
+ | last_offset (cell) = let val (_, _, x)::_ = rev (cell) in x end
+ val desired_final_offset = foldl Int.max offset (map last_offset (eheads))
+(*
+ val () = print ("start offset = " ^ Int.toString (offset) ^ "\n")
+ val () = print ("FINAL OFFSET = " ^ Int.toString (desired_final_offset) ^ "\n")
+*)
+ fun align (i) = ((i + 7) div 8) * 8
+ val desired_final_offset = align(desired_final_offset)
+(*
+ val () = print ("FINAL OFFSET (align) = " ^ Int.toString (desired_final_offset) ^ "\n")
+*)
+ fun fill (offset) = if offset >= desired_final_offset
+ then []
+ else #"\t" :: fill (align (offset - 7) + 8)
+ val fill = if List.all null tails
+ then fn _ => []
+ else fill
+ fun tabexpand ([]) = fill (offset)
+ | tabexpand (cell) = let val (_, _, finalwidth)::_ = rev (cell)
+ fun strip (x, _, _) = x
+ in rev (fill (finalwidth) @ rev (map strip (cell)))
+ end
+ val expanded_heads = map tabexpand (eheads)
+ val expanded_tails = expand (tails, desired_final_offset)
+ in ListPair.map op:: (expanded_heads, expanded_tails)
+ end
+
+fun align_strings (s : string list) = map (implode o List.concat) (expand (map ((map clean) o split_comma o explode) s, 0))
+
+(*val test = align_strings (["foo, bar, quux", "a,b", "0,01234567890,c"])*)
+
+fun println (s) = (print (s);
+ print "\n")
+
+fun read_input () =
+ case TextIO.inputLine (TextIO.stdIn) of
+ NONE => []
+ | SOME s => s :: read_input ()
+
+fun main (_, _) = app println (align_strings (read_input ()))
+
+val () = main ("", [])
+
diff --git a/showmagicspells.py b/showmagicspells.py
new file mode 100644
index 0000000..6909a72
--- /dev/null
+++ b/showmagicspells.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+#
+# showmagicspells.py
+#
+# Copyright 2010 Stefan Beller
+#
+# This program 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.
+#
+# This program 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 this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1301, USA.
+
+def handle_lines(lines):
+ def getInfo(line, info):
+ sp = line.split(" ")
+ if info in sp:
+ pos = sp.index(info)
+ return sp[pos+2].strip()
+ return ""
+
+ firstline = lines[0].split(" ")
+ pos=firstline.index("SPELL")
+ name= firstline[pos+1]
+ level = getInfo(lines[1],"level")
+ school = getInfo(lines[2],"school")
+
+ #print name,level,school
+ if not school in spells:
+ spells[school]=[]
+ spells[school]+=[(name,level,school)]
+
+def main():
+ fname = "../conf/magic.conf.template"
+ f=open(fname, "r");
+ lines=f.readlines();
+ f.close();
+
+ while lines :
+ line=lines[0]
+ if line.startswith("SPELL"):
+ handle_lines(lines);
+ if line.startswith("# LOCAL SPELL"):
+ handle_lines(lines);
+ if line.startswith("# SPELL"):
+ handle_lines(lines);
+ del lines[0]
+ return 0
+
+spells={}
+main()
+for x in spells:
+ print x
+ for y in spells[x]:
+ print "\t",y[1],y[0];
+
diff --git a/showvars.py b/showvars.py
new file mode 100755
index 0000000..376f804
--- /dev/null
+++ b/showvars.py
@@ -0,0 +1,110 @@
+#!/usr/bin/python
+
+
+# must be started in the npc dir
+
+import os
+import re
+from optparse import OptionParser
+parser = OptionParser()
+parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
+ help="show the occurrences of that var")
+
+parser.add_option("-f", "--file", dest="fname", default="",
+ help="inspect that file", metavar="FILE")
+
+parser.add_option("-l", "--localvariables", dest="localvars", action="store_true", default=False,
+ help="show local variables as well")
+
+(options, args) = parser.parse_args()
+
+def handleFile(fname):
+ f = open(fname)
+ lines = f.readlines();
+ f.close()
+ rm=[]
+ for l in lines:
+ #remove comments
+ line = l.split(r"//")[0]
+
+ sp = line.split()
+
+ # no set command?
+ if not "set" in sp:
+ continue
+
+ # ignore those lines printing messages
+ if 'mes "' in line:
+ continue
+
+ #ignore anything before the "set" command:
+ sp = sp[sp.index("set")+1:]
+ line = "".join(sp)
+ endpos = line.find(",")
+
+ #check for comma
+ if endpos>0:
+ #ok its a oneliner, the comma is in the same line:
+ varname = line[0:endpos].strip()
+ assignment = line[endpos+1:].strip()[:-1] # remove semicolon
+ if assignment != "0":
+ if varname.startswith("@") and not options.localvars:
+ continue
+ if varname.startswith("$"):
+ continue
+ if varname in allvars:
+ if not fname in allvars[varname]:
+ allvars[varname] += [fname]
+ else:
+ allvars[varname] = [fname]
+ else:
+ #print fname
+ if fname == "." + os.sep + "functions" + os.sep + "clear_vars.txt":
+ rm += [varname]
+
+ else:
+ # ok error, you need to check manually:
+ print "\tline:\t",line
+ return rm
+
+if options.fname:
+ path=options.fname
+else:
+ path=".."+os.sep+"npc"
+
+allvars = {}
+rmvars = []
+print "please check manully for vars in here:"
+os.chdir(path)
+
+for tpl in os.walk("."):
+ for fname in tpl[2]:
+ rmvars += handleFile(tpl[0]+os.sep+fname)
+
+unusedcounter=0
+usedcounter=0
+print "These variables are found in the scripts, which are deleted in clear_vars"
+for var in allvars:
+ if not var in rmvars:
+ continue
+
+ unusedcounter+=1
+ print "\t",var
+ if options.verbose:
+ for fname in allvars[var]:
+ print "\t","\t", fname
+
+
+print "These variables are valid variables of the scripts:"
+for var in allvars:
+ if var in rmvars:
+ continue
+
+ usedcounter+=1
+ print "\t",var
+ if options.verbose:
+ for fname in allvars[var]:
+ print "\t","\t", fname
+
+print "number of vars used:", usedcounter
+print "number of vars cleared:", unusedcounter
diff --git a/tmx_converter.py b/tmx_converter.py
new file mode 100755
index 0000000..379a4f3
--- /dev/null
+++ b/tmx_converter.py
@@ -0,0 +1,371 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+
+## tmx_converter.py - Extract walkmap, warp, and spawn information from maps.
+##
+## Copyright © 2012 Ben Longbons <b.r.longbons@gmail.com>
+##
+## This file is part of The Mana World
+##
+## This program 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.
+##
+## This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from __future__ import print_function
+
+import sys
+import os
+import posixpath
+import struct
+import xml.sax
+import base64
+import zlib
+
+dump_all = False # wall of text
+check_mobs = True # mob_db.txt
+
+# lower case versions of everything except 'spawn' and 'warp'
+other_object_types = set([
+ 'particle_effect',
+ 'npc', # not interpreted by client
+ 'script', # for ManaServ
+ 'fixme', # flag for things that didn't have a type before
+])
+
+# Somebody has put ManaServ fields in our data!
+other_spawn_fields = (
+ 'spawn_rate',
+)
+other_warp_fields = (
+)
+
+TILESIZE = 32
+SEPARATOR = '|'
+MESSAGE = 'This file is generated automatically. All manually changes will be removed when running the Converter.'
+CLIENT_MAPS = 'maps'
+SERVER_WLK = 'data'
+SERVER_NPCS = 'npc'
+SERVER_MOB_DB = 'db/mob_db.txt'
+NPC_MOBS = '_mobs.txt'
+NPC_WARPS = '_warps.txt'
+NPC_IMPORTS = '_import.txt'
+NPC_MASTER_IMPORTS = NPC_IMPORTS
+
+class State(object):
+ pass
+State.INITIAL = State()
+State.LAYER = State()
+State.DATA = State()
+State.FINAL = State()
+
+class Object(object):
+ __slots__ = (
+ 'name',
+ #'map',
+ 'x', 'y',
+ 'w', 'h',
+ )
+class Mob(Object):
+ __slots__ = (
+ 'monster_id',
+ 'max_beings',
+ 'ea_spawn',
+ 'ea_death',
+ ) + other_spawn_fields
+ def __init__(self):
+ self.max_beings = 1
+ self.ea_spawn = 0
+ self.ea_death = 0
+
+class Warp(Object):
+ __slots__ = (
+ 'dest_map',
+ 'dest_x',
+ 'dest_y',
+ 'dest_tile_x',
+ 'dest_tile_y',
+ ) + other_warp_fields
+
+class ContentHandler(xml.sax.ContentHandler):
+ __slots__ = (
+ 'locator', # keeps track of location in document
+ 'out', # open file handle to .wlk
+ 'state', # state of collision info
+ 'tilesets', # first gid of each tileset
+ 'buffer', # characters within a section
+ 'encoding', # encoding of layer data
+ 'compression', # compression of layer data
+ 'width', # width of the collision layer
+ 'height', # height of the collision layer
+ 'base', # base name of current map
+ 'npc_dir', # world/map/npc/<base>
+ 'mobs', # open file to _mobs.txt
+ 'warps', # open file to _warps.txt
+ 'imports', # open file to _import.txt
+ 'name', # name property of the current map
+ 'object', # stores properties of the latest <object> tag
+ 'mob_ids', # set of all mob types that spawn here
+ )
+ def __init__(self, out, npc_dir, mobs, warps, imports):
+ xml.sax.ContentHandler.__init__(self)
+ self.locator = None
+ self.out = open(out, 'w')
+ self.state = State.INITIAL
+ self.tilesets = set([0]) # consider the null tile as its own tileset
+ self.buffer = bytearray()
+ self.encoding = None
+ self.compression = None
+ self.width = None
+ self.height = None
+ self.base = posixpath.basename(npc_dir)
+ self.npc_dir = npc_dir
+ self.mobs = mobs
+ self.warps = warps
+ self.imports = imports
+ self.object = None
+ self.mob_ids = set()
+
+ def setDocumentLocator(self, loc):
+ self.locator = loc
+
+ # this method randomly cuts in the middle of a line; thus funky logic
+ def characters(self, s):
+ if not s.strip():
+ return
+ if self.state is State.DATA:
+ self.buffer += s.encode('ascii')
+
+ def startDocument(self):
+ pass
+
+ def startElement(self, name, attr):
+ if dump_all:
+ attrs = ' '.join('%s="%s"' % (k,v) for k,v in attr.items())
+ if attrs:
+ print('<%s %s>' % (name, attrs))
+ else:
+ print('<%s>' % name)
+
+ if self.state is State.INITIAL:
+ if name == u'property' and attr[u'name'].lower() == u'name':
+ self.name = attr[u'value']
+ self.mobs.write('// %s\n' % MESSAGE)
+ self.mobs.write('// %s mobs\n\n' % self.name)
+ self.warps.write('// %s\n' % MESSAGE)
+ self.warps.write('// %s warps\n\n' % self.name)
+
+ if name == u'tileset':
+ self.tilesets.add(int(attr[u'firstgid']))
+
+ if name == u'layer' and attr[u'name'].lower().startswith(u'collision'):
+ self.width = int(attr[u'width'])
+ self.height = int(attr[u'height'])
+ self.out.write(struct.pack('<HH', self.width, self.height))
+ self.state = State.LAYER
+ elif self.state is State.LAYER:
+ if name == u'data':
+ if attr.get(u'encoding','') not in (u'', u'csv', u'base64', u'xml'):
+ print('Bad encoding:', attr.get(u'encoding',''))
+ return
+ self.encoding = attr.get(u'encoding','')
+ if attr.get(u'compression','') not in (u'', u'none', u'zlib', u'gzip'):
+ print('Bad compression:', attr.get(u'compression',''))
+ return
+ self.compression = attr.get(u'compression','')
+ self.state = State.DATA
+ elif self.state is State.DATA:
+ self.out.write(chr(int(attr.get(u'gid',0)) not in self.tilesets))
+ elif self.state is State.FINAL:
+ if name == u'object':
+ obj_type = attr[u'type'].lower()
+ x = int(attr[u'x']) / TILESIZE;
+ y = int(attr[u'y']) / TILESIZE;
+ w = int(attr.get(u'width', 0)) / TILESIZE;
+ h = int(attr.get(u'height', 0)) / TILESIZE;
+ # I'm not sure exactly what the w/h shrinking is for,
+ # I just copied it out of the old converter.
+ # I know that the x += w/2 is to get centers, though.
+ if obj_type == 'spawn':
+ self.object = Mob()
+ if w > 1:
+ w -= 1
+ if h > 1:
+ h -= 1
+ x += w/2
+ y += h/2
+ elif obj_type == 'warp':
+ self.object = Warp()
+ x += w/2
+ y += h/2
+ w -= 2
+ h -= 2
+ else:
+ if obj_type not in other_object_types:
+ print('Unknown object type:', obj_type, file=sys.stderr)
+ self.object = None
+ return
+ obj = self.object
+ obj.x = x
+ obj.y = y
+ obj.w = w
+ obj.h = h
+ obj.name = attr[u'name']
+ elif name == u'property':
+ obj = self.object
+ if obj is None:
+ return
+ key = attr[u'name'].lower()
+ value = attr[u'value']
+ # Not true due to defaulting
+ #assert not hasattr(obj, key)
+ try:
+ value = int(value)
+ except ValueError:
+ pass
+ setattr(obj, key, value)
+
+ def add_warp_line(self, line):
+ self.warps.write(line)
+
+ def endElement(self, name):
+ if dump_all:
+ print('</%s>' % name)
+
+ if name == u'object':
+ obj = self.object
+ if isinstance(obj, Mob):
+ mob_id = obj.monster_id
+ if mob_id < 1000:
+ mob_id += 1002
+ if check_mobs:
+ try:
+ name = mob_names[mob_id]
+ except KeyError:
+ print('Warning: unknown mob ID: %d (%s)' % (mob_id, obj.name))
+ else:
+ if name != obj.name:
+ print('Warning: wrong mob name: %s (!= %s)' % (obj.name, name))
+ obj.name = name
+ self.mob_ids.add(mob_id)
+ self.mobs.write(
+ SEPARATOR.join([
+ '%s.gat,%d,%d,%d,%d' % (self.base, obj.x, obj.y, obj.w, obj.h),
+ 'monster',
+ obj.name,
+ '%d,%d,%d,%d,Mob%s::On%d\n' % (mob_id, obj.max_beings, obj.ea_spawn, obj.ea_death, self.base, mob_id),
+ ])
+ )
+ elif isinstance(obj, Warp):
+ nx = hasattr(obj, 'dest_tile_x')
+ ny = hasattr(obj, 'dest_tile_y')
+ ox = hasattr(obj, 'dest_x')
+ oy = hasattr(obj, 'dest_y')
+ assert nx == ny != ox == oy, 'Error: mixed coordinate properties exist.'
+
+ if ox:
+ obj.dest_tile_x = obj.dest_x / 32;
+ obj.dest_tile_y = obj.dest_y / 32;
+
+ self.warps.write(
+ SEPARATOR.join([
+ '%s.gat,%d,%d' % (self.base, obj.x, obj.y),
+ 'warp',
+ obj.name,
+ '%d,%d,%s.gat,%d,%d\n' % (obj.w, obj.h, obj.dest_map, obj.dest_tile_x, obj.dest_tile_y),
+ ])
+ )
+
+ if name == u'data':
+ if self.state is State.DATA:
+ if self.encoding == u'csv':
+ for x in self.buffer.split(','):
+ self.out.write(chr(int(x) not in self.tilesets))
+ elif self.encoding == u'base64':
+ data = base64.b64decode(str(self.buffer))
+ if self.compression == u'zlib':
+ data = zlib.decompress(data)
+ elif self.compression == u'gzip':
+ data = zlib.decompressobj().decompress('x\x9c' + data[10:-8])
+ for i in range(self.width*self.height):
+ self.out.write(chr(int(struct.unpack('<I',data[i*4:i*4+4])[0]) not in self.tilesets))
+ self.state = State.FINAL
+
+ def endDocument(self):
+ self.mobs.write('\n\n%s.gat,0,0,0|script|Mob%s|-1,\n{\n' % (self.base, self.base))
+ for mob_id in sorted(self.mob_ids):
+ self.mobs.write('On%d:\n set @mobID, %d;\n callfunc "MobPoints";\n end;\n\n' % (mob_id, mob_id))
+ self.mobs.write(' end;\n}\n')
+ self.imports.write('// Map %s: %s\n' % (self.base, self.name))
+ self.imports.write('// %s\n' % MESSAGE)
+ self.imports.write('map: %s.gat\n' % self.base)
+
+ npcs = os.listdir(self.npc_dir)
+ npcs.sort()
+ for x in npcs:
+ if x == NPC_IMPORTS:
+ continue
+ if x.startswith('.'):
+ continue
+ if x.endswith('.txt'):
+ self.imports.write('npc: %s\n' % posixpath.join(SERVER_NPCS, self.base, x))
+ pass
+
+def main(argv):
+ _, client_data, server_data = argv
+ tmx_dir = posixpath.join(client_data, CLIENT_MAPS)
+ wlk_dir = posixpath.join(server_data, SERVER_WLK)
+ npc_dir = posixpath.join(server_data, SERVER_NPCS)
+ if check_mobs:
+ global mob_names
+ mob_names = {}
+ with open(posixpath.join(server_data, SERVER_MOB_DB)) as mob_db:
+ for line in mob_db:
+ if not line.strip():
+ continue
+ if line.startswith('#'):
+ continue
+ if line.startswith('//'):
+ continue
+ k, v, _ = line.split(',', 2)
+ mob_names[int(k)] = v.strip()
+
+ npc_master = []
+ map_basenames = []
+
+ for arg in os.listdir(tmx_dir):
+ base, ext = posixpath.splitext(arg)
+
+ if ext == '.tmx':
+ map_basenames.append(base)
+ tmx = posixpath.join(tmx_dir, arg)
+ wlk = posixpath.join(wlk_dir, base + '.wlk')
+ this_map_npc_dir = posixpath.join(npc_dir, base)
+ os.path.isdir(this_map_npc_dir) or os.mkdir(this_map_npc_dir)
+ print('Converting %s to %s' % (tmx, wlk))
+ with open(posixpath.join(this_map_npc_dir, NPC_MOBS), 'w') as mobs:
+ with open(posixpath.join(this_map_npc_dir, NPC_WARPS), 'w') as warps:
+ with open(posixpath.join(this_map_npc_dir, NPC_IMPORTS), 'w') as imports:
+ xml.sax.parse(tmx, ContentHandler(wlk, this_map_npc_dir, mobs, warps, imports))
+ npc_master.append('import: %s\n' % posixpath.join(SERVER_NPCS, base, NPC_IMPORTS))
+
+ with open(posixpath.join(wlk_dir, 'resnametable.txt'), 'w') as resname:
+ for base in sorted(map_basenames):
+ resname.write('%s.gat#%s.wlk#\n' % (base, base))
+ with open(posixpath.join(npc_dir, NPC_MASTER_IMPORTS), 'w') as out:
+ out.write('// %s\n\n' % MESSAGE)
+ npc_master.sort()
+ for line in npc_master:
+ out.write(line)
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/web/README b/web/README
new file mode 100644
index 0000000..dc7bc55
--- /dev/null
+++ b/web/README
@@ -0,0 +1,3 @@
+This is a flask app to manage accounts.
+
+It implements a full-featured webserver, but is usually proxied by nginx.
diff --git a/web/main.py b/web/main.py
new file mode 100755
index 0000000..b7501c5
--- /dev/null
+++ b/web/main.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python2.6
+
+from flask import Flask
+
+from with_xml import Node
+
+app = Flask(__name__)
+
+@app.route('/')
+def index():
+ content = Node()
+ tag = content.tag
+ put = content.put
+ a = tag('a')
+ with tag('html'):
+ with tag('head'):
+ with tag('title'):
+ put('Title')
+ with tag('body'):
+ with tag('h1'):
+ put('Header')
+ put('This is ')
+ with a(href='http://google.com/'):
+ put('a link to Google.')
+ return str(content)
+
+if __name__ == '__main__':
+ app.run(debug=True)
diff --git a/web/with_xml.py b/web/with_xml.py
new file mode 100644
index 0000000..bc49a94
--- /dev/null
+++ b/web/with_xml.py
@@ -0,0 +1,71 @@
+''' A stupid little way of generating xml
+'''
+
+import re
+
+from flask import escape
+
+PRETTY = True
+
+class Context(object):
+ __slots__ = ('_node', '_name', '_kwargs')
+ pattern = re.compile(r'[A-Za-z]\w*')
+
+ def __init__(self, node, name, kwargs):
+ self._node = node
+ self._name = name
+ self._kwargs = kwargs
+
+ def __enter__(self):
+ _node = self._node
+ _buffer = _node._buffer
+ _node.nl()
+ _buffer.extend(['<', escape(self._name)])
+ for k, v in self._kwargs.iteritems():
+ assert Context.pattern.match(k)
+ _buffer.extend([' ', k, '="', escape(v), '"'])
+ _buffer.append('>')
+ _node._indent += 1
+ _node.nl()
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ _node = self._node
+ _buffer = _node._buffer
+ _node._indent -= 1
+ _node.nl()
+ if _buffer[-1] == '>' and _buffer[-3] != '</':
+ _buffer[-1] = '/>'
+ else:
+ _buffer.extend(['</', escape(self._name), '>'])
+ _node.nl()
+
+ def __call__(_self, **kwargs):
+ new_kwargs = dict(_self._kwargs)
+ new_kwargs.update(kwargs)
+ return Context(_self._node, _self._name, new_kwargs)
+
+class Node(object):
+ __slots__ = ('_buffer', '_indent')
+
+ def __init__(self):
+ self._buffer = ['<?xml version="1.0" encoding="utf-8"?>', '\n', '']
+ self._indent = 0
+
+ def tag(_self, _name, **kwargs):
+ return Context(_self, _name, kwargs)
+
+
+ def put(self, text):
+ self._buffer.append(escape(text))
+
+ def __str__(self):
+ return ''.join(self._buffer)
+
+ def nl(self):
+ if PRETTY:
+ _buffer = self._buffer
+ if _buffer[-2] == '\n':
+ _buffer.pop()
+ else:
+ _buffer.append('\n')
+ _buffer.extend([' ' * self._indent])