pykiss

Python Kisekae set viewer
git clone https://code.literati.org/pykiss.git
Log | Files | Refs | LICENSE

commit 32606013e90cd2a512ec873de3c07d086954c8fe
Author: Sean Lynch <seanl@literati.org>
Date:   Tue,  8 Jun 2010 09:44:50 -0700

Initial commit

Diffstat:
ALICENSE | 19+++++++++++++++++++
Agrammar.def | 27+++++++++++++++++++++++++++
Agrammar.g | 27+++++++++++++++++++++++++++
Agrammar.py | 230+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Akiss.py | 1046+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Akisscompiler.py | 367+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Akissparser.py | 188+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aparser.py | 24++++++++++++++++++++++++
Apykiss | 251+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awxkiss.py | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awxkiss.xrc | 19+++++++++++++++++++
11 files changed, 2341 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010 Sean R. Lynch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/grammar.def b/grammar.def @@ -0,0 +1,27 @@ +parser Grammar: + separator spaces: '\s+' ; + token num: '\d' ; + token obj: '#\d+' ; + token filename: '\S+' ; + token name: '\w+' ; + token var: '\w+' ; + token string: '"[^"]*"' ; + token int: '-?\d+' ; + + Field -> '\(' int ',' int '\)' ; + Memsize -> '=' int 'K' ; + Palettefile -> '%' filename ; + Bordercol -> '\[' int ; + Cel -> '#' int ( '.' int )? filename ( '\*' int)? ( ':' num? )? ; + Setinfo -> '$' num Pos+ ; + Pos -> int ',' int | '*' ; + + KissLine -> Field | Memsize | Palettefile | Bordercol | Cel | Setinfo ; + + EventHandler -> ';@EventHandler' ' + Event -> name '\(' ArgList? '\)' ; + Arg -> int | obj | var | string ; + ArgList -> Arg ( ',' Arg )* ; + FKiss -> ';@' Event* ; + + START -> KissLine+ ( EventHandler FKiss* ) ; diff --git a/grammar.g b/grammar.g @@ -0,0 +1,27 @@ +parser KiSSParser: + separator spaces: '\s+' ; + token num: '\d' int ; + token obj: '#\d+' ; + token filename: '\S+' ; + token name: '\w+' ; + token var: '\w+' ; + token string: '"[^"]*"' ; + token int: '-?\d+' int ; + + Field -> '\(' int ',' int '\)' ; + Memsize -> '=' int 'K' ; + Palettefile -> '%' filename ; + Bordercol -> '\[' int ; + Cel -> '#' int ( '.' int )? filename ( '\*' int)? ( ':' num? )? ; + Setinfo -> '$' num Pos+ ; + Pos -> int ',' int | '*' ; + + KissLine -> Field | Memsize | Palettefile | Bordercol | Cel | Setinfo ; + + EventHandler -> ';@EventHandler' ; + Event -> name '\(' ArgList? '\)' ; + Arg -> int | obj | var | string ; + ArgList -> Arg ( ',' Arg )* ; + FKiss -> ';@' Event* ; + + START -> KissLine+ ( EventHandler FKiss* ) ; diff --git a/grammar.py b/grammar.py @@ -0,0 +1,230 @@ + +#........[ TOY PARSER GENERATOR ].........................! +# ! ! +# Warning: This file was automatically generated by TPG ! | ! +# Do not edit this file unless you know what you do. ! | ! +# ! @ ! +#....................................................!!!!!!!!!!! +# +# For further information about TPG you can visit +# http://christophe.delord.free.fr/en/tpg + +import tpg.base +class KiSSParser(tpg.base.ToyParser,): + + def _init_scanner(self): + self._lexer = tpg.base._Scanner( + tpg.base._TokenDef(r"_tok_1", r"\("), + tpg.base._TokenDef(r"_tok_2", r","), + tpg.base._TokenDef(r"_tok_3", r"\)"), + tpg.base._TokenDef(r"_tok_4", r"="), + tpg.base._TokenDef(r"_kw_K", r"K"), + tpg.base._TokenDef(r"_tok_5", r"%"), + tpg.base._TokenDef(r"_tok_6", r"\["), + tpg.base._TokenDef(r"_tok_7", r"#"), + tpg.base._TokenDef(r"_tok_8", r"."), + tpg.base._TokenDef(r"_tok_9", r"\*"), + tpg.base._TokenDef(r"_tok_10", r":"), + tpg.base._TokenDef(r"_tok_11", r"$"), + tpg.base._TokenDef(r"_tok_12", r"*"), + tpg.base._TokenDef(r"_tok_13", r";@EventHandler"), + tpg.base._TokenDef(r"_tok_14", r";@"), + tpg.base._TokenDef(r"spaces", r"\s+", None, 1), + tpg.base._TokenDef(r"num", r"\d", int, 0), + tpg.base._TokenDef(r"obj", r"#\d+", None, 0), + tpg.base._TokenDef(r"filename", r"\S+", None, 0), + tpg.base._TokenDef(r"name", r"\w+", None, 0), + tpg.base._TokenDef(r"var", r"\w+", None, 0), + tpg.base._TokenDef(r"string", r"\"[^\"]*\"", None, 0), + tpg.base._TokenDef(r"int", r"-?\d+", int, 0), + ) + + def Field(self,): + """ Field -> '\(' int ',' int '\)' """ + self._eat('_tok_1') # \( + self._eat('int') + self._eat('_tok_2') # , + self._eat('int') + self._eat('_tok_3') # \) + + def Memsize(self,): + """ Memsize -> '=' int 'K' """ + self._eat('_tok_4') # = + self._eat('int') + self._eat('_kw_K') # K + + def Palettefile(self,): + """ Palettefile -> '%' filename """ + self._eat('_tok_5') # % + self._eat('filename') + + def Bordercol(self,): + """ Bordercol -> '\[' int """ + self._eat('_tok_6') # \[ + self._eat('int') + + def Cel(self,): + """ Cel -> '#' int ('.' int)? filename ('\*' int)? (':' num?)? """ + self._eat('_tok_7') # # + self._eat('int') + __p1 = self._cur_token + try: + self._eat('_tok_8') # . + self._eat('int') + except self.TPGWrongMatch: + self._cur_token = __p1 + self._eat('filename') + __p2 = self._cur_token + try: + self._eat('_tok_9') # \* + self._eat('int') + except self.TPGWrongMatch: + self._cur_token = __p2 + __p3 = self._cur_token + try: + self._eat('_tok_10') # : + __p4 = self._cur_token + try: + self._eat('num') + except self.TPGWrongMatch: + self._cur_token = __p4 + except self.TPGWrongMatch: + self._cur_token = __p3 + + def Setinfo(self,): + """ Setinfo -> '$' num Pos+ """ + self._eat('_tok_11') # $ + self._eat('num') + __p1 = self._cur_token + __n1 = 0 + while 1: + try: + self.Pos() + __n1 += 1 + __p1 = self._cur_token + except self.TPGWrongMatch: + if __n1 >= 1: + self._cur_token = __p1 + break + else: + self.WrongMatch() + + def Pos(self,): + """ Pos -> int ',' int | '*' """ + __p1 = self._cur_token + try: + self._eat('int') + self._eat('_tok_2') # , + self._eat('int') + except self.TPGWrongMatch: + self._cur_token = __p1 + self._eat('_tok_12') # * + + def KissLine(self,): + """ KissLine -> Field | Memsize | Palettefile | Bordercol | Cel | Setinfo """ + __p1 = self._cur_token + try: + try: + self.Field() + except self.TPGWrongMatch: + self._cur_token = __p1 + try: + self.Memsize() + except self.TPGWrongMatch: + self._cur_token = __p1 + self.Palettefile() + except self.TPGWrongMatch: + self._cur_token = __p1 + try: + self.Bordercol() + except self.TPGWrongMatch: + self._cur_token = __p1 + try: + self.Cel() + except self.TPGWrongMatch: + self._cur_token = __p1 + self.Setinfo() + + def EventHandler(self,): + """ EventHandler -> ';@EventHandler' """ + self._eat('_tok_13') # ;@EventHandler + + def Event(self,): + """ Event -> name '\(' ArgList? '\)' """ + self._eat('name') + self._eat('_tok_1') # \( + __p1 = self._cur_token + try: + self.ArgList() + except self.TPGWrongMatch: + self._cur_token = __p1 + self._eat('_tok_3') # \) + + def Arg(self,): + """ Arg -> int | obj | var | string """ + __p1 = self._cur_token + try: + try: + self._eat('int') + except self.TPGWrongMatch: + self._cur_token = __p1 + self._eat('obj') + except self.TPGWrongMatch: + self._cur_token = __p1 + try: + self._eat('var') + except self.TPGWrongMatch: + self._cur_token = __p1 + self._eat('string') + + def ArgList(self,): + """ ArgList -> Arg (',' Arg)* """ + self.Arg() + __p1 = self._cur_token + while 1: + try: + self._eat('_tok_2') # , + self.Arg() + __p1 = self._cur_token + except self.TPGWrongMatch: + self._cur_token = __p1 + break + + def FKiss(self,): + """ FKiss -> ';@' Event* """ + self._eat('_tok_14') # ;@ + __p1 = self._cur_token + while 1: + try: + self.Event() + __p1 = self._cur_token + except self.TPGWrongMatch: + self._cur_token = __p1 + break + + def START(self,): + """ START -> KissLine+ EventHandler FKiss* """ + __p1 = self._cur_token + __n1 = 0 + while 1: + try: + self.KissLine() + __n1 += 1 + __p1 = self._cur_token + except self.TPGWrongMatch: + if __n1 >= 1: + self._cur_token = __p1 + break + else: + self.WrongMatch() + self.EventHandler() + __p2 = self._cur_token + while 1: + try: + self.FKiss() + __p2 = self._cur_token + except self.TPGWrongMatch: + self._cur_token = __p2 + break + + diff --git a/kiss.py b/kiss.py @@ -0,0 +1,1046 @@ +#!/usr/bin/python +import re, sys, os, time, bisect, random, struct +from PIL import Image +from kissparser import KissParser + + +class ParseError(Exception): pass + +class Variable: + """A variable.""" + def __init__(self, name, value=0): + self.name = name + self.value = value + + def __cmp__(self, other): + return cmp(self.value, int(other)) + + def __int__(self): + return self.value + + +class MovementHandler: + def __init__(self): + self.handler0 = None # was no, is no + self.handler1 = None # was no, is yes + self.handler2 = None # was yes, is no + self.handler3 = None # was yes, is yes + + def test(self): + """Run the actual test. Should be overridden.""" + pass + + def setup(self): + self.prev_result = self.test() + + def check(self): + """Check the condition.""" + result = self.test() + if result: + if self.prev_result: + if self.handler3 is not None: self.handler3() + elif self.handler1 is not None: + self.handler1() + else: + if self.prev_result: + if self.handler2 is not None: self.handler2() + elif self.handler0 is not None: self.handler0() + + self.prev_result = result + + +class Collider(MovementHandler): + """Keep track of collisions between cels""" + def __init__(self, cel1, cel2): + MovementHandler.__init__(self) + self.cel1 = cel1 + self.cel2 = cel2 + + def test(self): + #print "Checking for collision between %s and %s" % (self.cel1.name, self.cel2.name) + + # Only check if both cels are visible + if not (self.cel1.is_visible() and self.cel2.is_visible()): return + + return self.cel1.collide(self.cel2) + +class Inout(MovementHandler): + """Keep track of in/out movement""" + def __init__(self, obj1, obj2): + MovementHandler.__init__(self) + self.obj1 = obj1 + self.obj2 = obj2 + + def test(self): + return self.obj1.rectcollide(self.obj2) + + +class Object: + def __init__(self, kiss, num, fixval): + self.kiss = kiss + self.num = num + self.pos = [(0, 0)] * 10 + self.cels = [] + self.fixval = fixval + self.colliders = [] + + def add_cel(self, cel): + self.cels.append(cel) + + def map(self): + for cel in self.cels: + cel.map() + + def unmap(self): + for cel in self.cels: + cel.unmap() + + def altmap(self): + for cel in self.cels: + cel.altmap() + + def setfixval(self, fixval): + self.fixval = fixval + + def set_pos(self, (x, y)): + w = self.kiss.width + h = self.kiss.height + l = w + t = h + r = 0 + b = 0 + for cel in self.cels: + x1 = x + cel.xoffset + y1 = y + cel.yoffset + if x1 < l: l = x1 + if y1 < t: t = y1 + if x1 + cel.width > r: r = x1 + cel.width + if y1 + cel.height > b: b = y1 + cel.height + + if l < 0: x -= l + elif r > w: x += (w-r) + if t < 0: y -= t + elif b > h: y += (h-b) + + self.pos[self.kiss.set] = (x, y) + + def get_pos(self): + return self.pos[self.kiss.set] + + def rectcollide(self, other): + for cel1 in self.cels: + if not cel1.is_visible(): continue + for cel2 in self.cels: + if cel2.is_visible(): + if cel1.quick_collide(cel2): return 1 + + return 0 + + +class Cel: + def __init__(self, name, obj, palfile, sets, trans): + self.name = name + self.obj = obj + self.palfile = palfile + self.transparent = trans + self.mapped = 1 + + if sets is None: self.visible = [1] * 10 + else: + self.visible = [0] * 10 + for set in sets: + self.visible[set] = 1 + + self.load() + + def set_transparent(self, reloffset): + # This may need to be overridden + self.transparent += reloffset + if self.transparent > 255: self.transparent = 255 + if self.transparent < 0: self.transparent = 0 + + def collide(self, other): + # First, check if the rectangles overlap + rect = self.quick_collide(other) + if rect: + l1, t1, r1, b1 = self.get_rect() + l2, t2, r2, b2 = other.get_rect() + l, t, r, b = rect + im1 = self.image + im2 = other.image + # Need to check nontransparent pixels now + # For now, we'll naively loop through all the pixels. + # x and y are in screen coordinates + # One loop for each possible combination of image types + if im1.mode == "RGBA": + if im2.mode == "RGBA": + for y in range(t, b): + for x in range(l, r): + if im1.getpixel((x-l1, y-t1))[3] and \ + im2.getpixel((x-l2, y-t2))[3]: return 1 + else: + for y in range(t, b): + for x in range(l, r): + if im1.getpixel((x-l1, y-t1))[3] and \ + im2.getpixel((x-l2, y-t2)): return 1 + else: + if im2.mode == "RGBA": + for y in range(t, b): + for x in range(l, r): + if im1.getpixel((x-l1, y-t1)) and \ + im2.getpixel((x-l2, y-t2))[3]: return 1 + else: + for y in range(t, b): + for x in range(l, r): + if im1.getpixel((x-l1, y-t1)) and \ + im2.getpixel((x-l2, y-t2)): return 1 + + return 0 + + def quick_collide(self, other): + l1, t1, r1, b1 = self.get_rect() + l2, t2, r2, b2 = other.get_rect() + # Pygame would be nice for this, but we don't depend on pygame + l = max(l1, l2) + t = max(t1, t2) + r = min(r1, r2) + b = min(b1, b2) + if l >= r or t >= b: return None + return l, t, r, b + + def load(self): + self.image, (self.xoffset, self.yoffset) = load_cel(self.name) + self.width, self.height = self.image.size + self.convert() + + def get_rect(self): + x, y = self.obj.get_pos() + x += self.xoffset + y += self.yoffset + return x, y, x+self.width, y+self.height + + def set_palette(self, palette): pass + + def convert(self): + """Meant to be overridden.""" + pass + + def get_pos(self): + pos = self.obj.get_pos() + if pos is not None: + return pos[0] + self.xoffset, pos[1] + self.yoffset + + def map(self): + self.mapped = 1 + + def unmap(self): + self.mapped = 0 + + def altmap(self): + self.mapped = not self.mapped + + def is_visible(self): + return self.mapped and self.visible[self.obj.kiss.set] + + +def load_cel(filename): + f = open(filename) + + header = f.read(32) + if header[:4] == 'KiSS': + bpp, reserved, width, height, xoffset, yoffset = struct.unpack('<BHHHHH', header[5:16]) + data = f.read() + else: + bpp = 4 + xoffset = yoffset = 0 + width, height = struct.unpack('<HH', header[:4]) + data = header[4:] + f.read() + + f.close() + + if bpp == 4: + im = Image.fromstring("F", (width, height), data, + "bit", 4, 8, 0).convert("P") + elif bpp == 8: + im = Image.fromstring("P", (width, height), data) + elif bpp == 32: + # The R and B are swapped, probably because they're dealing with ints + im = Image.fromstring("RGBA", (width, height), data) + r, g, b, a = im.split() + im = Image.merge("RGBA", (b, g, r, a)) + else: raise ValueError, "Unimplemented bpp %d" % bpp + + return im, (xoffset, yoffset) + + +def load_palette(filename): + f = open(filename) + header = f.read(32) + if header[:5] != 'KiSS\x10': + bits = 12 + ncols = 16 + ngroups = 10 + data = header + f.read() + else: + bits, blah, ncols, ngroups = struct.unpack('<BHHH', header[5:12]) + data = f.read() + + f.close() + pal = [] + i = 0 + if bits == 24: + for group in range(ngroups): + grp = [None] * ncols + for col in range(ncols): + r, g, b = struct.unpack('<BBB', data[i:i+3]) + grp[col] = (r, g, b) + i += 3 + + pal.append(grp) + + elif bits == 12: + for group in range(ngroups): + grp = [None] * ncols + for col in range(ncols): + r, g = struct.unpack('<BB', data[i:i+2]) + b = r & 15 + r >>= 4 + grp[col] = (r<<4, g<<4, b<<4) + i += 2 + + pal.append(grp) + + # Copy all remaining groups from group 0 + while len(pal) < 10: pal.append(pal[0]) + return pal + + +class Event: + def __init__(self, kiss, name, args): + self.kiss = kiss + self.name = name + self.args = args + self.cmds = [] + self.ifblock = 0 # State variable. 1 means we're executing a block + # 2 means we're skipping to an else + + def __repr__(self): + return '<%s %s(%s) at %x>' % (self.__class__.__name__, self.name, ', '.join(map(repr, self.args)), id(self)) + + def __call__(self): + print "@", self.name, self.args + kiss = self.kiss + for cmd, args in self.cmds: + if self.ifblock == 0: + # Need to handle ifequal, ifgreaterthan, iflessthan, ifnotequal, + # else, endif specially + if cmd == "ifequal": + if int(args[1]) == int(args[2]): self.ifblock = 1 + else: self.ifblock = 2 + continue + elif cmd == "ifgreaterthan": + if int(args[1]) > int(args[2]): self.ifblock = 1 + else: self.ifblock = 2 + continue + elif cmd == "iflessthan": + if int(args[1]) < int(args[2]): self.ifblock = 1 + else: self.ifblock = 2 + continue + elif cmd == "ifnotequal": + if int(args[1]) != int(args[2]): self.ifblock = 1 + else: self.ifblock = 2 + continue + elif self.ifblock == 1: + # Executing an if or else block + if cmd in ("else", "endif"): + self.ifblock = 0 + continue + elif self.ifblock == 2: + # Skipping the block + if cmd == "else": self.ifblock = 1 + continue + + try: func = getattr(kiss, 'cmd_' + cmd) + except AttributeError: print >> sys.stderr, "Unimplemented:", cmd, args + else: + # A true return value means to stop processing + print cmd, args + if func(*args): break + + def append_cmd(self, cmd, args): + self.cmds.append((cmd, args)) + + +class KiSS: + def __init__(self, cnf, canvas=None, cel_class=Cel): + self.objs = [None] * 1024 + # Cel names are per set + self.cels = {} + self.cellist = [[], [], [], [], [], [], [], [], [], []] + self.cel_class = cel_class + self.canvas = canvas + self.width = 448 + self.height = 320 + self.bordercol = 0 + self.handlers = {} + self.timers = [] + self.palgroup = [0] * 10 # The palette group for each set + self.palettes = [] # My palettes + self.cnfpos = {} # The configured position of each object + + parser = KissParser(self) + parser.parse(os.path.basename(cnf)) + + # methods called by the parser + def get_obj(self, num): + return self.objs[num] + + def get_var(self, name): + try: var = self.vars[name] + except KeyError: + var = Variable(name) + self.vars[name] = var + + return var + + def is_action(self, name): + return hasattr(self, 'cmd_' + name) + + def set_palette(self, set, palnum): + self.palgroup[set] = palnum + + def add_event(self, name, args, actions): + handler = Event(self, name, args) + for action, aargs in actions: + handler.append_cmd(action, aargs) + + if name in ("apart", "collide"): + # Handle a collision event + name1, name2 = args + + cel1 = self.cels[name1] + cel2 = self.cels[name2] + obj1 = cel1.obj + obj2 = cel2.obj + + # See if there is an existing handler + for collider in obj1.colliders: + if not isinstance(collider, Collider): continue + if collider.cel1 == cel1 and collider.cel2 == cel2 or \ + collider.cel2 == cel1 and collider.cel1 == cel2: break + else: + collider = Collider(cel1, cel2) + obj1.colliders.append(collider) + obj2.colliders.append(collider) + + if name == "apart": + if collider.handler2 is not None: + raise ParseError, "Two apart handlers for %s and %s" % \ + (cel1.name, cel2.name) + collider.handler2 = handler + else: + if collider.handler1 is not None: + raise ParseError, "Two collide handlers for %s and %s" % \ + (cel1.name, cel2.name) + collider.handler1 = handler + + return + + if name in ("in", "out", "stillin", "stillout"): + obj1, obj2 = args + + for inout in obj1.colliders: + if not isinstance(inout, Inout): continue + if (inout.obj1 == obj1 and inout.obj2 == obj2) or \ + (inout.obj2 == obj1 and inout.obj1 == obj2): break + else: inout = Inout(obj1, obj2) + + if name == "in": + if inout.handler1 is not None: + raise ParseError, "Two in handlers for #%d and #%d" % \ + obj1.num, obj2.num + inout.handler1 = handler + elif name == "out": + if inout.handler2 is not None: + raise ParseError, "Two out handlers for #%d and #%d" % \ + obj1.num, obj2.num + inout.handler1 = handler + elif name == "stillin": + if inout.handler3 is not None: + raise ParseError, "Two stillin handlers for #%d and #%d" % \ + obj1.num, obj2.num + inout.handler3 = handler + else: + if inout.handler0 is not None: + raise ParseError, "Two stillout handlers for #%d and #%d" % \ + obj1.num, obj2.num + inout.handler0 = handler + + return + + + if self.handlers.has_key((name, args)): + self.handlers[(name, args)].cmds.extend(actions) + else: self.handlers[(name, args)] = handler + + def set_size(self, (width, height)): + self.width = width + self.height = height + + def add_palette(self, palfile): + self.palettes.extend(load_palette(palfile)) + + def set_border(self, bordercol): + self.bordercol = bordercol + + def add_cel(self, name, objnum, fixval, palfile, groups, trans): + if groups is None: groups = range(10) + + obj = self.objs[objnum] + if obj is None: + obj = Object(self, objnum, fixval) + self.objs[objnum] = obj + + cel = self.cel_class(name, obj, palfile, groups, trans) + #print 'adding cel', cel.name + if self.cels.has_key(name): + print >> sys.stderr, "Ambiguous cel name: %s" % name + self.cels[name] = cel + obj.add_cel(cel) + + for group in groups: + self.cellist[group].append(cel) + + def set_objpos(self, objnum, set, pos): + obj = self.objs[objnum] + if obj is None: return + obj.pos[set] = pos + self.cnfpos[obj, set] = pos + + # Methods called by the GUI + def pulse(self): + """Call this at least every 100 ms""" + now = time.time() + flag = 0 + while self.timers: + t, num = self.timers[0] + if t > now: break + del self.timers[0] + self.handle("alarm", num) + flag = 1 + + if flag: + self.draw() + + def start(self): + self.set = 0 + self.dragging = None + self.change_palette(self.palgroup[self.set]) + self.handle('initialize') + self.handle('version', 1) + self.handle('version', 2) + self.handle('version', 3) + for obj in self.objs: + if obj is None: continue + for collider in obj.colliders: + collider.setup() + self.handle('begin') + self.draw() + + def end(self): + self.handle('end') + + def get_cel_at(self, (x, y)): + for cel in self.cellist[self.set]: + if not cel.mapped: continue + x1, y1 = cel.get_pos() + w, h = cel.image.size + #print cel.name, x, y, x1, y1, w, h + if x >= x1 and y >= y1 and x < x1+w and y < y1+h: + pixel = cel.image.getpixel((x-x1, y-y1)) + if type(pixel) == type(0): + if pixel: return cel + elif pixel[3]: return cel + else: return None + + def press(self, (x, y)): + """Mouse button pressed""" + + cel = self.get_cel_at((x, y)) + if cel is None: return + self.pressed = cel + obj = cel.obj + print "#%d.%d %s" % (obj.num, obj.fixval, cel.name) + if obj.fixval == 0: + x1, y1 = obj.get_pos() + self.handle("catch", cel.name) + self.handle("catch", obj) + # Store the object and the position offset + self.dragging = obj, cel, x1-x, y1-y + self.canvas.start_drag() + else: + self.handle("fixcatch", cel.name) + self.handle("fixcatch", obj) + obj.fixval -= 1 + if obj.fixval == 0: + self.handle("unfix", obj) + for cel in obj.cels: + if cel.is_visible(): + self.handle("unfix", cel.name) + + self.handle("press", cel.name) + self.handle("press", obj) + + def release(self, pos): + """Mouse button released""" + if self.dragging: + self.canvas.end_drag() + obj, cel, xoff, yoff = self.dragging + self.dragging = None + self.handle("drop", cel.name) + self.handle("drop", obj) + # Check for collide/apart events + for collider in obj.colliders: + #print collider + collider.check() + else: + # Just use the cel under the current position + cel = self.pressed + obj = cel.obj + if obj.fixval: + # Object is fixed + self.handle("fixdrop", cel.name) + self.handle("fixdrop", obj) + + # Generate events that don't care about fixval + self.handle("release", cel.name) + self.handle("release", obj) + self.pressed = None + + def drag(self, (x, y)): + """Mouse drag""" + obj, cel, xoff, yoff = self.dragging + obj.set_pos((x+xoff, y+yoff)) + self.draw() + + # Internal + def handle(self, name, *args): + #print "handling", name, args + try: handler = self.handlers[name, args] + except KeyError: return + + handler() + self.draw() + + def change_set(self, set): + self.set = set + self.change_palette(self.palgroup[set]) + self.draw() + + # Reset all colliders + for obj in self.objs: + if obj is None: continue + for collider in obj.colliders: + collider.setup() + + self.handle("set", set) + + def change_palette(self, palnum): + if not self.palettes: return + if palnum >= len(self.palettes): + print >> sys.stderr, "No such palette: %d" % palnum + return + self.palgroup[self.set] = palnum + for cel in self.cellist[self.set]: + cel.set_palette(self.palettes[cel.palfile*10+self.palgroup[palnum]]) + + def draw(self): + self.canvas.clear(self.palettes[self.palgroup[self.set]][self.bordercol]) + cellist = self.cellist[self.set] + # Loop through backwards to make sure that the top cels are drawn last + for i in range(len(cellist)-1, -1, -1): + cel = cellist[i] + if cel.mapped: self.canvas.draw(cel, cel.get_pos()) + + self.canvas.update() + + # Commands + def cmd_altmap(self, what): + """altmap(#object) / altmap("celname.cel") + + Turns any mapped cels to unmapped and vice-versa. + """ + + if type(what) == type(""): + try: self.cels[what.lower()].altmap() + except KeyError: print >> sys.stderr, "Missing cel: %s" % what + else: what.altmap() + + def cmd_map(self, what): + """map(#object) / map("celname.cel") + + Maps all cels. + """ + + if type(what) == type(""): + try: self.cels[what.lower()].map() + except KeyError: print >> sys.stderr, "Missing cel: %s" % what + else: what.map() + + + def cmd_randomtimer(self, number, minimum, maximum): + self.cmd_timer(number, random.randint(int(minimum), int(maximum))) + + + def cmd_setfix(self, obj, fixval): + """setfix(#object,fixval) + + Manually sets the fix val for the object. If the value is 0, any unfix() events linked to this object will be triggered. Note: This is a proposed FKiSS2.1 command, and is not generally supported by other viewers. + """ + + obj.fixval = int(fixval) + if fixval == 0: + self.handle("unfix", obj) + for cel in obj.cels: + if cel.is_visible(): + self.handle("unfix", cel.name) + + def cmd_timer(self, number, duration): + """timer(number,duration) + """ + + now = time.time() + timers = self.timers + for i in range(len(self.timers)): + if self.timers[i][1] == number: + del self.timers[i] + break + + if duration: bisect.insort(timers, (now + int(duration)/1000.0, number)) + + + def cmd_unmap(self, what): + """unmap(#object) / unmap("celname.cel") + + First case, unmaps all cels belonging to object #object. Second case + unmaps a specific cel. + """ + + if type(what) == type(""): + try: self.cels[what.lower()].unmap() + except KeyError: pass + else: what.unmap() + + def cmd_move(self, obj, offsetx, offsety): + """move(#object,offsetx,offsety) + + Moves an object using the offsets specified. Note: PlayFKiSS will never move an object off-screen, even when "Enforce Boundaries" is turned off. + """ + + x, y = obj.get_pos() + obj.set_pos((x+int(offsetx), y+int(offsety))) + + def cmd_movebyx(self, obj1, obj2, offsetx): + """movebyx(#object,#object,offsetx) + + Moves an object using the offset from another object. Note: This is a proposed FKiSS2 command, and is not generally supported by other viewers. + """ + + x, y = obj1.get_pos() + x1, y1 = obj2.get_pos() + x = x1 + offsetx + obj1.set_pos((x, y)) + + + def cmd_movebyy(self, obj1, obj2, offsety): + """movebyy(#object,#object,offsety) + + Moves an object using the offset from another object. Note: This is a proposed FKiSS2 command, and is not generally supported by other viewers. + """ + + x, y = obj1.get_pos() + x1, y1 = obj2.get_pos() + y = y1 + offsety + obj1.set_pos((x, y)) + + def cmd_ifmapped(self, celname, timer, value): + """ifmapped("cel", timer, value) + + Sets timer if the cel specified by "cel" is mapped. Note: This is a proposed FKiSS2.1 event and is not generally supported by other viewers. + """ + + try: + if self.cels[celname.lower()].mapped: + self.cmd_timer(timer, value) + except KeyError: pass + + def cmd_ifnotmapped(self, celname, timer, value): + """ifnotmapped("cel", timer, value) + + Sets timer if the cel specified by "cel" is not mapped. Note: This is a proposed FKiSS2.1 event and is not generally supported by other viewers. + """ + + try: + if not self.cels[celname.lower()].mapped: + self.cmd_timer(timer, value) + except KeyError: pass + + def cmd_sound(self, filename): + """sound("filename.wav") / sound("filename.au") + + Plays a sound. Not all viewers support .au format and vice-versa. If possible, include 2 versions of your .cnf file and convert all sounds to both formats. + """ + + self.canvas.sound(filename) + + def cmd_moveto(self, object, x, y): + """moveto(#object,x,y) + + Moves an object to the absolute coordinates specified. Note: PlayFKiSS will never move an object off-screen, even when "Enforce Boundaries" is turned off. Note: This is a proposed FKiSS2 command, and is not generally supported by other viewers. + """ + + object.set_pos((int(x), int(y))) + + def cmd_moverandx(self, object, min, max): + """moverandx(#object,min,max) + + Moves an object using random values. The new coordinate is relative, based on the min and max values. Ex: If min is -1 and max is 1, the new location may be either 1 pixel less, one pixel more, or the same location. Note: This is a proposed FKiSS2.1 command, and is not generally supported by other viewers. + """ + + x, y = object.get_pos() + object.set_pos((random.randint(x+int(min), x+int(max)), y)) + + def cmd_moverandy(self, object, min, max): + """moverandy(#object,min,max) + + Moves an object using random values. The new coordinate is relative, based on the min and max values. Ex: If min is -1 and max is 1, the new location may be either 1 pixel less, one pixel more, or the same location. Note: This is a proposed FKiSS2.1 command, and is not generally supported by other viewers. + """ + + x, y = object.get_pos() + object.set_pos((x, random.randint(y+int(min), y+int(max)))) + + def cmd_movetorand(self): + """movetorand(#object) + + Moves an object to random coordinates. Note: PlayFKiSS will never move an object off-screen, even when "Enforce Boundaries" is turned off. Note: This is a proposed FKiSS2.1 command, and is not generally supported by other viewers. + """ + + object.set_pos((random.randint(self.width), random.randint(self.height))) + + def cmd_transparent(self, what, reloffset): + """transparent(#object, reloffset) / transparent("celname.cel", reloffset) + + Sets the transparency level of the cels/object. reloffset is the value to add to the current transparency level. The maximum range is from 0 (totally opaque) up to 255 (totally transparent). Note: This command is fairly recent and is not supported by a ll FKiSS viewers. Also, some viewers show cels dithered and other's change to true-color mode (16 million colors). + """ + + if type(what) == type(""): + try: cel = self.cels[what.lower()] + except KeyError: pass + else: cel.set_transparent(int(reloffset)) + else: + for cel in what.cels: cel.set_transparent(int(reloffset)) + + def cmd_viewport(self, x, y): + """viewport(x,y) + + Changes the viewport offset. Note: PlayFKiSS ignores this command, but will retain it when saving. + """ + + self.canvas.viewport((int(x), int(y))) + + def cmd_windowsize(self, w, h): + """windowsize(x,y) + + Changes the window size. Note: PlayFKiSS ignores this command, but will retain it when saving. + """ + + self.canvas.windowsize((int(w), int(h))) + + def cmd_changecol(self, palette): + """changecol(palette) + + Forces another palette group to become the active one. + """ + + #self.palette[self.set] = palette + self.change_palette(palette) + self.handle("col", palette) + + changecol = cmd_changecol + + def cmd_changeset(self, set): + """changeset(set) + + Forces another set to become the active one. + """ + + self.change_set(int(set)) + + def cmd_debug(self, messagestring): + """debug("messagestring") + + Displays a message. Each viewer may have a different method to display debug information. + """ + + print >> sys.stderr, "Debug:", messagestring + + def cmd_iffixed(self, obj, timer, duration): + """iffixed(#obj, timer, value) + + Sets timer if the object specified by #obj is fixed (fix value is + greater than zero). Note: This is a proposed FKiSS2.1 event and is + not generally supported by other viewers. + """ + + if obj.fixval > 0: self.cmd_timer(int(timer), int(duration)) + + def cmd_ifmoved(self, timer, duration): + """ifmoved (#obj, timer, value) + + Sets timer if the object specified by #obj has been moved. An object is defined as moved if it's coordinates are not equal to the coordinates specified in the .CNF file. Note: This is a proposed FKiSS2.1 event and is not generally supported by other viewers. + """ + + if obj.pos[self.set] != self.cnfpos[obj, self.set]: + self.cmd_timer(int(timer), int(duration)) + + def cmd_ifnotfixed(self, obj, timer, duration): + """ifnotfixed(#obj, timer, value) + + Sets timer if the object specified by #obj is not fixed (fix value is equal to zero). Note: This is a proposed FKiSS2.1 event and is not generally supported by other viewers. + """ + + if self.fixval == 0: self.cmd_timer(int(timer), int(duration)) + + def cmd_ifnotmoved(self, obj, timer, duration): + """ifnotmoved (#obj, timer, value) + + Sets timer if the object specified by #obj has not been moved. An object is defined as moved if it's coordinates are not equal to the coordinates specified in the .CNF file. Note: This is a proposed FKiSS2.1 event and is not generally supported by o ther viewers. + """ + + if self.cnfpos[obj, self.set] == obj.pos[self.set]: + self.cmd_timer(int(timer), int(duration)) + + def cmd_music(self, filename): + """music ("filename.mid") + + Plays a MIDI song. Note: This is a proposed FKiSS2 command, and is not generally supported by other viewers. + """ + + self.canvas.music(filename) + + def cmd_nop(self, *args): + """nop() + + Does nothing. NOP is short for "No Operation" in assembly language. + """ + + return + + def cmd_notify(self, messagestring): + """notify("message string") + + Currently a synonym for debug() Note: This is a proposed FKiSS2 command, and is not generally supported by other viewers. + """ + + print >> sys.stderr, "Notify:" , messagestring + + def cmd_quit(self): + """quit() + + Forces the viewer to exit. Note: PlayFKiSS retains this command when saving, but will not quit when issued. + """ + + self.canvas.quit() + + # KiSS 3 + def cmd_goto(self, label): + self.handle('label', label) + # Stop processing of this event + return 1 + + def cmd_gosub(self, label): + self.handle('label', label) + # Don't stop processing :) + + def cmd_gotorandom(self, percent, label1, label2): + if random.randrange(100) < percent: handle("label", label1) + else: handle("label", label2) + return 1 + + def cmd_gosubrandom(self, percent, label1, label2): + if random.randrange(100) < percent: handle("label", label1) + else: handle("label", label2) + + def cmd_exitevent(self): + """Stop processing of the current event""" + return 1 + + # FKiSS 3 commands + def cmd_let(self, var, value): + var.value = int(value) + + def cmd_add(self, var1, var2, var3): + var1.value = int(var2) + int(var3) + + def cmd_sub(self, var1, var2, var3): + var1.value = int(var2) - int(var3) + + def cmd_mul(self, var1, var2, var3): + var1.value = int(var2) * int(var3) + + def cmd_div(self, var1, var2, var3): + var1.value = int(var2) / int(var3) + + def cmd_mod(self, var1, var2, var3): + var1.value = int(var2) % int(var3) + + def cmd_random(self, var, min, max): + var.value = random.randint(int(min), int(max)) + + def cmd_letobjectx(self, var, obj): + var.value = obj.get_pos()[0] + + def cmd_letobjecty(self, var, obj): + var.value = obj.get_pos()[1] + + def cmd_letfix(self, var, obj): + var.value = obj.fixval + + def cmd_letmapped(self, var, celname): + try: var.value = self.cels[celname.lower()].mapped + except KeyError: pass + + def cmd_letset(self, var): + var.value = self.set + + def cmd_letpal(self, var): + var.value = self.palgroup[self.set] + + def cmd_letmousex(self, var): + var.value = self.canvas.get_mousepos()[0] + + def cmd_letmousey(self, var): + var.value = self.canvas.get_mousepos()[1] + + def cmd_letcatch(self, var): + if self.dragging is not None: + var.value = self.dragging[0].num + else: print >> sys.stderr, "Warning: attempted to letcatch without a caught object" + + def cmd_letcollide(self, var, cel1name, cel2name): + try: + cel1 = self.cels[cel1name.lower()] + cel2 = self.cels[cel2name.lower()] + except KeyError: pass + else: var.value = cel1.collide(cel2) + + def cmd_letinside(self, var, obj1, obj2): + var.value = obj1.rectcollide(obj2) + + def cmd_lettransparent(self, var, celname): + try: cel = self.cels[celname.lower()] + except KeyError: pass + else: var.value = cel.transparent + + def cmd_shell(self, *args): pass + diff --git a/kisscompiler.py b/kisscompiler.py @@ -0,0 +1,367 @@ +# Copyright (C) 2003 Sean R. Lynch <seanl@chaosring.org> +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import string, re, random + +class ConversionError(Exception): pass + +class VarDict(dict): + """A dictionary that creates local variables automatically""" + def __init__(self, compiler): + # Need a reference to the compiler to create local variables + self.compiler = compiler + + def __getitem__(self, key): + """Return the value that's in the dictionary if it exists, + create a new entry with a local variable otherwise.""" + + if self.has_key(key): return dict.__getitem__(self, key) + + var = self.compiler.local(key) + self[key] = var + return var + + +class Action: + """A defined action.""" + def __init__(self, compiler, cmd, mode, *lines): + # Extract the name and args from the command + m = re.match("(\w+)\(([^)]*)\)", cmd) + if not m: raise ValueError, "Bad command desc: %r" % cmd + name = m.group(1) + argstr = m.group(2) + args = map(lambda a: a.split(":"), map(string.strip, argstr.split(','))) + + self.compiler = compiler + self.name = name + self.args = args + self.mode = mode + self.lines = lines + + def compile(self, args): + if len(args) != len(self.args): + raise ValueError, "Wrong number of arguments for %s: %r, %r" % (self.name, self.args, args) + # First, convert the arguments + compiler = self.compiler + vars = VarDict(compiler) + try: + for arg, (name, typechar) in zip(args, self.args): + if typechar == 'i': vars[name] = compiler.var_or_int(arg) + elif typechar == 'I': + if type(arg) != type(0): raise ConversionError + vars[name] = str(arg) + elif typechar == 'o': vars[name] = compiler.cel_or_obj(arg) + elif typechar == 'O': + if not hasattr(arg, 'num'): raise ConversionError + vars[name] = compiler.obj(arg) + elif typechar == 'c': vars[name] = compiler.cel(arg) + elif typechar == 's': + if type(arg) != type(""): raise ConversionError + else: vars[name] = arg + elif typechar == 'S': + # Lowercase string + if type(arg) != type(""): raise ConversionError + else: vars[name] = arg.lower() + elif typechar == 't': + # Timers are just ints right now + # Don't yet allow symbolic names. + vars[name] = compiler.var_or_int(arg) + elif typechar == 'l': vars[name] = compiler.label(arg) + else: raise ValueError, "Unknown type character: %r" % typechar + except ConversionError, expl: + print self.args, args + self.compiler.kiss.warn("ConversionError: %s" % expl) + return ['pass'] + try: return [l % vars for l in self.lines] + except: + import traceback + print self.lines, vars + traceback.print_exc() + raise SystemExit + + +class Event: + def __init__(self, code, f): + self.code = code + self.f = f + + def __call__(self, *args): + try: self.f(*args) + except: + print self.code + import traceback + traceback.print_exc() + raise SystemExit + + +class EventCompiler: + tr = string.maketrans(".-()'\", ", "PMOCSDA_") + def __init__(self, kiss): + self.kiss = kiss + self.cmds = {} + self.vars = {'kiss': kiss, 'random': random} + +## # Add all objects and cels to the variable list +## for obj in kiss.objs: +## if obj is None: continue +## self.vars['obj_%d' % obj.num] = obj + +## for name, cel in kiss.cels: +## # Don't use varname because that will check for existence +## # of the variable +## self.vars['cel_%s' % name.translate(self.tr)] = cel + + self.add_cmds() + + def add_label(self, num, func): + self.vars['label_%d' % num] = func + + def add_cmd(self, cmd, mode, *lines): + # Exctract the name and args from the command + action = Action(self, cmd, mode, *lines) + if self.cmds.has_key(action.name): + raise ValueError, "Action %s defined twice" % action.name + self.cmds[action.name] = action + + def compile(self, eventname, actionlist): + """Compile a list of actions into a Python function""" + eventname = intern(eventname) + #print eventname + # Initialize state + self.local_index = 0 + self.defaults = {'kiss': 'kiss'} + source = [None] + indent = 1 + funcname = 'e_%x' % id(eventname) + + for cmd, args in actionlist: + #print cmd, args + action = self.cmds[cmd] + lines = action.compile(args) + if action.mode == 2: indent -= 1 + spaces = ' ' * indent + source.append('%s# %s%r' % (spaces, cmd, args)) + source.extend([spaces + line for line in lines]) + if action.mode == 1: indent += 1 + elif action.mode == -1: indent -= 1 + + if len(source) == 1: + # Empty + return None + + source.append('') + source[0] = 'def %s(%s):' % (funcname, ','.join(map(lambda x: '%s=%s' % x, self.defaults.items()))) + + source = '\n'.join(source) + try: exec source in self.vars + except: + import traceback, sys + print >> sys.stderr, source + traceback.print_exc() + sys.exit(1) + + func = self.vars[funcname] + func.__doc__ = eventname + func.source = source + return func + + def local(self, name): + """Return a new local variable name""" + name = "x_%s_%x" % (name, self.local_index) + self.local_index += 1 + return name + + + def varname(self, prefix, name, default=None): + """Convert a string to something that's suitable for a variable name""" + var = prefix + string.translate(name, self.tr) + if default is None: + if self.vars.has_key(var): return var + else: + raise ValueError, "Nonexistent variable: %r" % var + return None + else: + self.vars[var] = default + return var + + # Type conversion methods + def cel_or_obj(self, what): + if type(what) == type(""): + return self.cel(what.lower()) + else: return self.obj(what) + + def var_or_int(self, what): + if type(what) == type(0): return str(what) + else: return self.var(name) + + def var(self, name): + """Return the translated name of a variable, creating it if necessary""" + return self.varname('var_', name, 0) + + def obj(self, obj): + name = 'obj_%d' % obj.num + if not self.defaults.has_key(name): + self.defaults[name] = 'kiss.objs[%d]' % obj.num + return name + + def cel(self, celname): + if not self.kiss.cels.has_key(celname): + raise ConversionError, "Missing cel %r" % celname + name = 'cel_%s' % celname.translate(self.tr) + if not self.defaults.has_key(name): + self.defaults[name] = 'kiss.cels["%s"]' % celname + return name + + def add_cmds(self): + self.add_cmd("altmap(what:o)", 0, "%(what)s.altmap()") + self.add_cmd("map(what:o)", 0, "%(what)s.map()") + self.add_cmd("move(obj:O, xoff:i, yoff:i)", 0, + "%(x)s, %(y)s = %(obj)s.get_pos()", + "%(obj)s.move((%(x)s+%(xoff)s, %(y)s+%(yoff)s))") + self.add_cmd("movebyx(obj1:O, obj2:O, xoff:i)", 0, + "%(x)s, %(y)s = %(obj1)s.get_pos()", + "%(x1)s, %(y1)s = %(obj2)s.get_pos()", + "%(x)s = %(x1)s + %(xoff)s", + "%(obj1)s.move((%(x)s, %(y)s))") + self.add_cmd("movebyy(obj1:O, obj2:O, yoff:i)", 0, + "%(x)s, %(y)s = %(obj1)s.get_pos()", + "%(x1)s, %(y1)s = %(obj2)s.get_pos()", + "%(y)s = %(y1)s + %(yoff)s", + "%(obj1)s.move((%(x)s, %(y)s))") + self.add_cmd("randomtimer(timer:t, min:i, max:i)", 0, + "kiss.timer(%(timer)s, random.randint(%(min)s, %(min)s+%(max)s))") + self.add_cmd("setfix(obj:O, fixval:i)", 0, + "kiss.setfix(%(obj)s, %(fixval)s)") + self.add_cmd("timer(timer:t, duration:i)", 0, + "kiss.timer(%(timer)s, %(duration)s)") + self.add_cmd("unmap(what:o)", 0, "%(what)s.unmap()") + self.add_cmd("ifmapped(cel:c, timer:t, value:i)", 0, + "if %(cel)s.mapped: kiss.timer(%(timer)s, %(value)s)") + self.add_cmd("ifnotmapped(cel:c, timer:t, value:i)", 0, + "if not %(cel)s.mapped: kiss.timer(%(timer)s, %(value)s)") + self.add_cmd("sound(filename:S)", 0, + 'kiss.canvas.sound("%(filename)s")') + self.add_cmd("moveto(obj:O, x:i, y:i)", 0, + "%(obj)s.move((%(x)s, %(y)s))") + self.add_cmd("moverandx(obj:O, min:i, max:i)", 0, + "%(x)s, %(y)s = %(obj)s.get_pos()", + "%(obj)s.move((random.randint(%(x)s+%(min)s, %(x)s+%(max)s), %(y)s))") + self.add_cmd("moverandy(obj:O, min:i, max:i)", 0, + "%(x)s, %(y)s = %(obj)s.get_pos()", + "%(obj)s.move((%(x)s, random.randint(%(y)s+%(min)s, %(y)s+%(max)s)))") + self.add_cmd("movetorand(obj:O)", 0, + "%(obj)s.move((random.randrange(kiss.width), random.randrange(kiss.height)))") + self.add_cmd("transparent(what:o, offset:i)", 0, + "%(what)s.set_transparent(%(offset)s)") + self.add_cmd("viewport(x:i, y:i)", 0, + "kiss.canvas.viewport(%(x)s, %(y)s)") + self.add_cmd("windowsize(w:i, h:i)", 0, + "kiss.canvas.windowsize(%(w)s, %(h)s)") + self.add_cmd("changecol(palette:i)", 0, + "kiss.changecol(%(palette)s)") + self.add_cmd("changeset(set:i)", 0, + "kiss.changeset(%(set)s)") + self.add_cmd("debug(message:s)", 0, + 'kiss.debug("%(message)s")') + self.add_cmd("iffixed(what:o, timer:t, duration:i)", 0, + "if %(what)s.is_fixed(): kiss.timer(%(timer)s, %(duration)s)") + self.add_cmd("ifmoved(what:o, timer:t, duration:i)", 0, + "if %(what)s.is_moved():", + " kiss.timer(%(timer)s, %(duration)s)") + self.add_cmd("ifnotfixed(what:o, timer:t, duration:i)", 0, + "if not %(what)s.is_fixed():", + " kiss.timer(%(timer)s, %(duration)s)") + self.add_cmd("ifnotmoved(what:o, timer:t, duration:i)", 0, + "if %(what)s.is_moved():", + " kiss.timer(%(timer)s, %(duration)s)") + self.add_cmd("music(filename:s)", 0, + 'kiss.canvas.music("%(filename)s")') + self.add_cmd("nop()", 0) + self.add_cmd("notify(message:s)", 0, + 'kiss.notify("%(message)s")') + self.add_cmd("quit()", 0, + "kiss.canvas.quit()") + + # FKiSS 3 + # I suppose it's possible that the stack could fill up, but + # I don't know how otherwise to handle labels + self.add_cmd("goto(label:l)", 0, + "%(label)s()", + "return") + self.add_cmd("gosub(label:l)", 0, + "%(label)s()") + self.add_cmd("gotorandom(percent:i, label1:l, label2:l)", 0, + "if random.randrange(100) < %(percent)s: %(label1)s()" + "else: %(label2)s()" + "return") + self.add_cmd("gosubrandom(percent:i, label1:l, label2:l)", 0, + "if random.randrange(100) < %(percent)s: %(label1)s()" + "else: %(label2)s()") + self.add_cmd("exitevent()", 0, + "return") + self.add_cmd("ifequal(a:i, b:i)", 1, + "if %(a)s == %(b)s:") + self.add_cmd("ifgreaterthan(a:i, b:i)", 1, + "if %(a)s > %(b)s:") + self.add_cmd("iflessthan(a:i, b:i)", 1, + "if %(a)s < %(b)s:") + self.add_cmd("ifnotequal(a:i, b:i)", 1, + "if %(a)s != %(b)s:") + self.add_cmd("else()", 2, + "else:") + self.add_cmd("endif()", -1) + self.add_cmd("let(var:v, value:i)", 0, + "%(var)s = %(value)s") + self.add_cmd("add(var:v, a:i, b:i)", 0, + "%(var)s = %(a)s + %(b)s") + self.add_cmd("sub(var:v, a:i, b:i)", 0, + "%(var)s = %(a)s - %(b)s") + self.add_cmd("mul(var:v, a:i, b:i)", 0, + "%(var)s = %(a)s * %(b)s") + self.add_cmd("div(var:v, a:i, b:i)", 0, + "%(var)s = %(a)s / %(b)s") + self.add_cmd("mod(var:v, a:i, b:i)", 0, + "%(var)s = %(a)s %% %(b)s") + self.add_cmd("random(var:v, min:i, max:i)", 0, + "%(var)s = random.randint(%(min)s, %(max)s)") + self.add_cmd("letobjectx(var:v, obj:O)", 0, + "%(var)s = %(obj)s.get_pos()[0]") + self.add_cmd("letobjecty(var:v, obj:O)", 0, + "%(var)s = %(obj)s.get_pos()[1]") + self.add_cmd("letfix(var:v, obj:O)", 0, + "%(var)s = %(obj)s.fixval") + self.add_cmd("letmapped(var:v, obj:O)", 0, + "%(var)s = %(obj)s.mapped") + self.add_cmd("letset(var:v)", 0, + "%(var)s = kiss.set") + self.add_cmd("letpal(var:v)", 0, + "%(var)s = kiss.get_palgroup()") + self.add_cmd("letmousex(var:v)", 0, + "%(var)s = kiss.canvas.get_mousepos()[0]") + self.add_cmd("letmousey(var:v)", 0, + "%(var)s = kiss.canvas.get_mousepos()[1]") + self.add_cmd("letcatch(var:v)", 0, + "%(var)s = kiss.get_catch()") + self.add_cmd("letcollide(var:v, cel1:c, cel2:c)", 0, + "%(var)s = %(cel1)s.collide(%(cel2)s)") + self.add_cmd("letinside(var:v, obj1:O, obj2:O)", 0, + "%(var)s = %(obj1)s.rectcollide(%(obj2)s)") + self.add_cmd("lettransparent(var:v, obj:O)", 0, + "%(var)s = %(obj)s.transparent") + self.add_cmd("shell(cmd:s)", 0, + 'kiss.canvas.shell("%(cmd)s")') + + diff --git a/kissparser.py b/kissparser.py @@ -0,0 +1,188 @@ +# Copyright (C) 2003 Sean R. Lynch <seanl@chaosring.org> +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import re, sys + +class ParseError(Exception): pass + +class KissParser: + def __init__(self, handler=None): + self.fkiss_started = 0 + self.event = None + self.actions = [] + self.setindex = -1 + self.handler = handler + + def parse_args(self, line): + # Line should already be stripped and only have the stuff in parentheses + args = [] + while line: + m = re.match("#([0-9]+)\s*[,\.]?\s*(.*)", line) + if m: + objnum = int(m.group(1)) + obj = self.handler.get_obj(objnum) + if obj is None: + self.handler.warn("Object #%d not found." % objnum) + return None + args.append(obj) + line = m.group(2) + continue + + m = re.match("(-?[0-9]+)\s*[,\.]?\s*(.*)", line) + if m: + args.append(int(m.group(1))) + line = m.group(2) + continue + + m = re.match('"([^"]*)"?\s*[,\.]?\s*(.*)', line) + if m: + args.append(m.group(1)) + line = m.group(2) + continue + + raise ParseError, line + + return tuple(args) + + def parse_fkiss(self, line): + line = line.strip() + while line: + m = re.match('(\w+)\s*(?:\(([^)]*)\))?', line) + if not m: return + self.parse_command(m.group(1), m.group(2)) + line = line[m.end():].lstrip() + + + def parse_command(self, name, argstr): + if name == 'EventHandler': + if self.fkiss_started: + raise ParseError, 'EventHandler called more than once!' + self.fkiss_started = 1 + return + elif not self.fkiss_started: return + + + name = name.lower() + args = self.parse_args(argstr) + if args is None: return + + if self.handler.is_action(name): + # Adding another action + if args is not None: + if self.event is None: + self.handler.warn("Attempted to add action without an event: %s%r" % + (name, args)) + else: + # Add an action to the current event + self.actions.append((name, args)) + + else: + # Starting a new event + if self.event is not None: + # Store the last event + if self.actions: + self.handler.add_event(self.event[0], self.event[1], + self.actions) + else: + self.handler.warn("Attempted to add an empty event: %s%r" % + self.event) + + # Set up the new event + self.event = name, args + self.actions = [] + + def parse_field(self, width, height): + self.handler.set_size((int(width), int(height))) + + def parse_palette(self, palfile): + palfile = palfile.lower() + self.handler.add_palette(palfile) + + def parse_border(self, bordercol): + self.handler.set_border = int(bordercol) + + def parse_cel(self, objnum, fixval, name, paloffset, groups, trans): + name = name.lower() + + if fixval is None: fixval = 0 + else: fixval = int(fixval) + + if paloffset is None: paloffset = 0 + else: paloffset = int(paloffset) + + if groups is not None: + groups = [int(group) for group in groups.split()] + + if trans is None: trans = 0 + else: trans = int(trans) + + self.handler.add_cel(name, int(objnum), fixval, paloffset, groups, trans) + + def parse_set(self, palnum, positions): + self.setindex += 1 + self.handler.set_palette(self.setindex, int(palnum)) + self.objindex = 0 + self.parse_setcont(positions) + + def parse_setcont(self, positions): + for pos in positions.split(): + if pos == '*': pos = (0, 0) + else: + try: pos = tuple([int(x) for x in pos.split(',', 1)]) + except ValueError: + self.handler.warn("Trailing garbage in set layout: %r" % pos) + return + if len(pos) != 2: pos = (pos[0], 0) + + self.handler.set_objpos(self.objindex, self.setindex, pos) + + self.objindex += 1 + + + regexps = [("\s*;?\s*@(.+)", parse_fkiss), + ("\(\s*([0-9]+)\s*,\s*([0-9]+)\s*\)", parse_field), + ("%(\S+)", parse_palette), + ("\[(\d+)", parse_border), + ("#([0-9]+)(?:\.([0-9]+))?\s+(\S+)(?:\s+\*(\d+))?(?:\s+:([\d\s]+))?(?:\s*;%t(\d+))?", parse_cel), + ("\$([0-9]+)\s+([^;]+)", parse_set), + ("\s+([^;]+)", parse_setcont)] + + def parse(self, cnf): + f = open(cnf) + for line in f.xreadlines(): + line = line.rstrip() + # Skip comments and blank lines + if not line: continue + + for r, func in self.regexps: + m = re.match(r, line) + if m: + rest = line[m.end():].strip() + if rest and rest[0] != ';': + self.handler.warn('Trailing garbage: %r' % rest) + + apply(func, (self,) + m.groups()) + break + else: + if line[0] != ';': + self.handler.warn("Homy don't parse that: %r" % line) + + # Make sure to pass the last event to the handler + if self.event: + self.handler.add_event(self.event[0], self.event[1], self.actions) + + + diff --git a/parser.py b/parser.py @@ -0,0 +1,24 @@ +# Copyright (C) 2003 Sean R. Lynch <seanl@chaosring.org> +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +if __name__ == '__main__': + import sys + + import grammar + + parser = grammar.KiSSParser() + + print parser(open(sys.argv[1]).read()) diff --git a/pykiss b/pykiss @@ -0,0 +1,251 @@ +#!/usr/bin/python +# Copyright (C) 2003 Sean R. Lynch <seanl@chaosring.org> +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import sys +import pygame #, pygame.image, pygame.font, pygame.mixer +from pygame.locals import * +from Numeric import * +import kiss + +LHACMD = "lha x" + + +class Cel(kiss.Cel): + def convert(self, mode, data): + self.mode = mode + if mode == "P": + self.palsurf = pygame.image.fromstring(data, (self.width, + self.height), 'P') + self.palsurf.set_colorkey(0, RLEACCEL) + self.surface = self.palsurf.convert_alpha() + else: + self.surface = pygame.image.fromstring(data, (self.width, + self.height), + 'RGBA').convert_alpha() + + self.alpha = pygame.surfarray.array_alpha(self.surface) + + def set_transparent(self, reloffset): + kiss.Cel.set_transparent(self, reloffset) + a = pygame.surfarray.pixels_alpha(self.surface) + a[:] = (self.alpha * (255 - self.transparent) / 255).astype(UInt8) + + def set_palette(self, palette): + kiss.Cel.set_palette(self, palette) + self.palsurf.set_palette(palette) + self.palsurf.set_alpha(255 - self.transparent) + self.surface = self.palsurf.convert_alpha() + + def pixcollide(self, other, (x1, y1), (x2, y2), (w, h)): + if self.alpha[x1:x1+w, y1:y1+h] & other.alpha[x2:x2+w, y2:y2+h]: + return 1 + else: return 0 + + def is_pixel_set(self, (x, y)): + return self.alpha[x, y] + + +class KissViewer: + def __init__(self, cnf): + self.kiss = kiss.KiSS(cnf, self, cel_class=Cel) + self.surface = pygame.display.set_mode((self.kiss.width, + self.kiss.height)) + self.viewport_pos = (0, 0) + #self.rects = [] + + # Methods called by the KiSS instance + def draw(self, cels): + l, t = self.viewport_pos + surface = self.surface + surface.fill(self.kiss.get_bordercol()) + for cel in cels: + x, y = cel.get_pos() + if cel.mapped: surface.blit(cel.surface, (x-l, y-t)) + + pygame.display.flip() + + def start_drag(self): + self.dragging = 1 + + def end_drag(self): + self.dragging = 0 + + def sound(self, filename): + sound = pygame.mixer.Sound(filename) + sound.play() + + def windowsize(self, size): + self.surface = self.display.set_mode(size) + + def viewport(self, pos): + self.viewport_pos = pos + + def get_mousepos(self): + return pygame.mouse.get_pos() + + def music(self, filename): + pygame.mixer.music.load(filename) + pygame.mixer.music.play(-1) + + # External methods + def run(self): + self.dragging = 0 + + self.kiss.start() + pygame.time.set_timer(USEREVENT, 10) + while 1: + event = pygame.event.wait() + if event.type == USEREVENT: + # Eat all timer events from the queue + pygame.event.get(USEREVENT) + self.kiss.pulse() + elif event.type == QUIT: + self.kiss.end() + return + elif event.type == MOUSEMOTION: + if self.dragging: + # Only process the last one in the queue + events = pygame.event.get(MOUSEMOTION) + if events: event = events[-1] + self.kiss.drag(event.pos) + elif event.type == MOUSEBUTTONDOWN: + self.kiss.press(event.pos) + elif event.type == MOUSEBUTTONUP: + self.kiss.release(event.pos) + elif event.type == KEYDOWN: + if event.key == K_ESCAPE: + self.kiss.end() + return + elif event.key == K_f: + pygame.display.toggle_fullscreen() + elif event.key == K_u: + self.kiss.unfix(pygame.mouse.get_pos()) + elif event.key == K_t: + self.kiss.make_transparent(pygame.mouse.get_pos()) + elif event.key == K_0: + if event.mod & KMOD_CTRL: + self.kiss.changecol(0) + else: self.kiss.changeset(0) + elif event.key == K_1: + if event.mod & KMOD_CTRL: + self.kiss.changecol(1) + else: self.kiss.changeset(1) + elif event.key == K_2: + if event.mod & KMOD_CTRL: + self.kiss.changecol(2) + else: self.kiss.changeset(2) + elif event.key == K_3: + if event.mod & KMOD_CTRL: + self.kiss.changecol(3) + else: self.kiss.changeset(3) + elif event.key == K_4: + if event.mod & KMOD_CTRL: + self.kiss.changecol(4) + else: self.kiss.changeset(4) + elif event.key == K_5: + if event.mod & KMOD_CTRL: + self.kiss.changecol(5) + else: self.kiss.changeset(5) + elif event.key == K_6: + if event.mod & KMOD_CTRL: + self.kiss.changecol(6) + else: self.kiss.changeset(6) + elif event.key == K_7: + if event.mod & KMOD_CTRL: + self.kiss.changecol(7) + else: self.kiss.changeset(7) + elif event.key == K_8: + if event.mod & KMOD_CTRL: + self.kiss.changecol(8) + else: self.kiss.changeset(8) + elif event.key == K_9: + if event.mod & KMOD_CTRL: + self.kiss.changecol(9) + else: self.kiss.changeset(9) + + +def choose(surface, items): + """Choose from a list of choices. Return the choice.""" + + choices = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + font = pygame.font.Font(None, 12) + linesize = font.get_linesize() + y = linesize + i = 0 + for item in items: + s = font.render("%s) %s" % (choices[i], item), 1, (255, 255, 255, 255)) + surface.blit(s, (0, y)) + y += linesize + i += 1 + + pygame.display.update() + + while 1: + event = pygame.event.wait() + if event.type == KEYDOWN: + if event.key == K_ESCAPE: sys.exit() + key = str(event.unicode) + choice = choices.find(key) + if choice > -1: return items[choice] + + +if __name__ == '__main__': + import os + if len(sys.argv) < 2 or len(sys.argv) > 3: + print >> sys.stderr, """ +Usage: %s (<archive.lzh> [<cnffile>] | <cnffile>) + +Use number keys to change sets, and ctrl+number to change palette. +""" % sys.argv[0] + sys.exit(1) + + filename = os.path.abspath(sys.argv[1]) + tempdir = None + + # Set up the display in case we need to ask about the cnf file + pygame.init() + surface = pygame.display.set_mode((640, 480), HWSURFACE|DOUBLEBUF) + + base, ext = os.path.splitext(filename) + try: + if ext.lower() == ".lzh": + # It's a lzh archive + import tempfile + tempdir = tempfile.mktemp() + os.mkdir(tempdir) + os.chdir(tempdir) + os.system(LHACMD + " " + filename) + if len(sys.argv) >= 3: cnf = sys.argv[2] + else: + import glob + cnffiles = glob.glob("*.cnf") + glob.glob("*.CNF") + if len(cnffiles) == 1: cnf = cnffiles[0] + else: cnf = choose(surface, cnffiles) + else: + dirname, cnf = os.path.split(filename) + if dirname: os.chdir(dirname) + + + k = KissViewer(cnf) + k.run() + finally: + if tempdir: + import shutil + shutil.rmtree(tempdir) + + + diff --git a/wxkiss.py b/wxkiss.py @@ -0,0 +1,143 @@ +#!/usr/bin/python +# Copyright (C) 2003 Sean R. Lynch <seanl@chaosring.org> +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +import os +from wxPython.wx import * +from wxPython.xrc import * +from PIL import Image +import kiss + +ID_EXIT = 110 + +class App(wxApp): + def OnInit(self): + self.res = wxXmlResource('wxkiss.xrc') + self.InitFrame() + self.InitMenu() + self.InitKiss() + return True + + def InitFrame(self): + self.frame = self.res.LoadFrame(None, "MainFrame") + self.frame.Show(1) + + def InitMenu(self): + self.menuBar = self.res.LoadMenuBar("MenuBar") + EVT_MENU(self.frame, XRCID("OpenItem"), self.OnOpen) + EVT_MENU(self.frame, XRCID("ExitItem"), self.OnExit) + EVT_CLOSE(self.frame, self.OnExit) + self.frame.SetMenuBar(self.menuBar) + + def InitKiss(self): + self.kissWindow = KissWindow(self.frame) + + def OnExit(self, event): + sys.exit() + + def OnOpen(self, event): + dialog = wxFileDialog(None, wildcard="*.cnf", style=wxOPEN) + result = dialog.ShowModal() + if result == wxID_OK: + cnf = os.path.join(dialog.GetDirectory(), dialog.GetFilename()) + self.kissWindow.set_kiss(cnf) + +class Cel(kiss.Cel): + def convert(self): + import tempfile + + print 'Converting', self.name + print self.bpp, self.width, self.height, len(self.data) + if self.bpp == 4: + im = Image.fromstring("F", (self.width, self.height), self.data, + "bit", 4, 8, 1).convert("RGBA") + elif self.bpp == 8: + im = Image.fromstring("L", (self.width, self.height), self.data).convert("RGBA") + elif self.bpp == 32: + im = Image.fromstring("RGBA", (self.width, self.height), self.data) + r, g, b, a = im.split() + im = Image.merge("RGBA", (b, g, r, a)) + else: raise ValueError, "Unimplemented bpp %d" % self.bpp + + filename = tempfile.mktemp('.png') + im.save(filename) + self.bitmap = wxBitmap(filename, wxBITMAP_TYPE_PNG) + #os.unlink(filename) + + +class KissWindow(wxScrolledWindow): + def __init__(self, *args): + wxScrolledWindow.__init__(self, *args) + EVT_TIMER(self, -1, self.OnTimer) + + def OnPaint(self, event): + dc = wxPaintDC(self) + upd = wxRegionIterator(self.GetUpdateRegion()) + self.PrepareDC(dc) + dc1 = wxMemoryDC() + dc1.SelectObject(self.bitmap) + while upd.HaveRects(): + x = upd.GetX() + y = upd.GetY() + dc.Blit(x, y, upd.GetW(), upd.GetH(), dc1, x, y) + upd.Next() + + def OnTimer(self, event): + self.kiss.pulse() + +## def OnDraw(self, dc): +## if not hasattr(self, 'kiss'): return True +## self.dc = dc +## set = 0 +## self.kiss.draw(self, set) +## return True + + def set_kiss(self, cnf): + self.kiss = kiss.KiSS(cnf, canvas=self, cel_class=Cel) + self.bitmap = wxEmptyBitmap(self.kiss.width, self.kiss.height) + #self.dc = wxMemoryDC() + #self.dc.SelectObject(self.bitmap) + self.SetScrollbars(20, 20, self.kiss.width/20, self.kiss.height/20) + self.SetBackgroundColour(wxColour(0, 0, 0)) + self.kiss.run() + self.timer = wxTimer(self) + self.timer.Start(10) + self.redraw() + EVT_PAINT(self, self.OnPaint) + + # Functions called by KiSS class + def draw(self, cel, (x, y)): + print cel.width, cel.height + dc = wxMemoryDC() + dc.SelectObject(self.bitmap) + dc.DrawBitmap(cel.bitmap, x, y, True) + self.Refresh(eraseBackground=False, + rect=wxRect(x, y, cel.width, cel.height)) + + + def redraw(self): + self.kiss.draw(self, 0) + + +if __name__ == '__main__': + wxInitAllImageHandlers() + app = App() + app.MainLoop() + + + + + diff --git a/wxkiss.xrc b/wxkiss.xrc @@ -0,0 +1,18 @@ +<?xml version="1.0" ?> +<resource> + <object class="wxMenuBar" name="MenuBar"> + <object class="wxMenu" name="FileMenu"> + <label>File</label> + <object class="wxMenuItem" name="OpenItem"> + <label>Open</label> + </object> + <object class="separator"/> + <object class="wxMenuItem" name="ExitItem"> + <label>Exit</label> + </object> + </object> + </object> + <object class="wxFrame" name="MainFrame"> + <title>wxPyKiSS</title> + </object> +</resource>+ \ No newline at end of file