Initial commit from version 1e-99 master
authorChip Black <bytex64@bytex64.net>
Sun, 8 Jun 2008 09:16:20 +0000 (04:16 -0500)
committerChip Black <bytex64@bytex64.net>
Sun, 8 Jun 2008 09:16:20 +0000 (04:16 -0500)
58 files changed:
COPYING [new file with mode: 0644]
README.txt [new file with mode: 0644]
accuracygraph.py [new file with mode: 0644]
beatscape.py [new file with mode: 0755]
bmevent.py [new file with mode: 0644]
chanman.py [new file with mode: 0644]
config.py [new file with mode: 0644]
constants.py [new file with mode: 0644]
event.py [new file with mode: 0644]
fileselect.py [new file with mode: 0644]
font/Nano.ttf [new file with mode: 0644]
formats.py [new file with mode: 0644]
fx.py [new file with mode: 0644]
game.py [new file with mode: 0644]
gfx/bad.png [new file with mode: 0644]
gfx/banana0.png [new file with mode: 0644]
gfx/banana1.png [new file with mode: 0644]
gfx/banana2.png [new file with mode: 0644]
gfx/banana3.png [new file with mode: 0644]
gfx/banana4.png [new file with mode: 0644]
gfx/banana5.png [new file with mode: 0644]
gfx/banana6.png [new file with mode: 0644]
gfx/banana7.png [new file with mode: 0644]
gfx/chips_layer1.jpg [new file with mode: 0644]
gfx/chips_layer2.jpg [new file with mode: 0644]
gfx/circle.png [new file with mode: 0644]
gfx/cross.png [new file with mode: 0644]
gfx/dcircle.png [new file with mode: 0644]
gfx/fgreat.png [new file with mode: 0644]
gfx/filetile.png [new file with mode: 0644]
gfx/glyph/0.png [new file with mode: 0644]
gfx/glyph/1.png [new file with mode: 0644]
gfx/glyph/2.png [new file with mode: 0644]
gfx/glyph/3.png [new file with mode: 0644]
gfx/glyph/4.png [new file with mode: 0644]
gfx/glyph/5.png [new file with mode: 0644]
gfx/glyph/6.png [new file with mode: 0644]
gfx/glyph/7.png [new file with mode: 0644]
gfx/glyph/8.png [new file with mode: 0644]
gfx/glyph/9.png [new file with mode: 0644]
gfx/good.png [new file with mode: 0644]
gfx/great.png [new file with mode: 0644]
gfx/poor.png [new file with mode: 0644]
gfx/title.png [new file with mode: 0644]
gfx/triangle.png [new file with mode: 0644]
keyfile.py [new file with mode: 0644]
keygraph.py [new file with mode: 0644]
keyhandler.py [new file with mode: 0644]
loaders/BMloader.py [new file with mode: 0644]
loaders/SMloader.py [new file with mode: 0644]
mainmenu.py [new file with mode: 0644]
mksongcache.py [new file with mode: 0755]
rotowidget.py [new file with mode: 0644]
snd/0x00.ogg [new file with mode: 0644]
snd/0x01.ogg [new file with mode: 0644]
snd/stab.ogg [new file with mode: 0644]
songs/README.txt [new file with mode: 0644]
util.py [new file with mode: 0644]

diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..2b940a4
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,347 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+     59 Temple Place, Suite 330, Boston, MA 02111-1307, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+The Free Software Foundation has exempted Bash from the requirement of
+Paragraph 2c of the General Public License.  This is to say, there is
+no requirement for Bash to print a notice when it is started
+interactively in the usual way.  We made this exception because users
+and standards expect shells not to print such messages.  This
+exception applies to any program that serves as a shell and that is
+based primarily on Bash as opposed to other GNU software.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+\f
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+\f
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+\f
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+\f
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+       Appendix: How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) 19yy  <name of author>
+
+    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
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) 19yy name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/README.txt b/README.txt
new file mode 100644 (file)
index 0000000..492a2a9
--- /dev/null
@@ -0,0 +1,73 @@
+beatscape - the red-headed stepchild of all vertical column rhythm games
+
+beatscape is a simulator for vertical column rhythm games. Right now, it
+simulates 7-key beatmania IIDX (and, by induction, 5-key beatmania), but
+plans to support the whole line from Pop'n Music to Keyboardmania. It
+does *not* simulate any arrow games (DDR, ITG, Pump it Up, etc). If you
+want that, go use StepMania. Right now it reads BMS/BME (pretty well)
+and StepMania SM (poorly).
+
+beatscape requires:
+       pygame of a recent vintage (I'm using 1.7.1)
+       Python 2.4
+       a little bit of a clue
+
+USAGE
+
+Add your songs to the 'songs' folder. There is no restriction on the
+placement of files, other than that all files referenced by a keyfile
+(WAV,BMP,etc) must be in the same folder as that keyfile. I like to
+arrange my keyfiles logically by source, but you can do whatever you
+want.
+
+beatscape does not automatically index your music files (yet), so you
+will have to do this manually each time you add or remove songs by
+running 'mksongcache.py'.
+
+beatscape accepts a command line argument specifying a keyfile to play.
+This is handy if you're testing a keyfile and don't want to navigate the
+menus.
+
+FILES
+
+config.py
+       This lists configuration variables for various aspects of the
+       game, such as timings and button assignments. Timings are set to
+       what I think is a reasonable approximation of the console
+       versions of IIDX, and button assignments should work with your
+       standard IIDX controller hooked up via a PSX -> USB adapter.
+       Other controllers and adapters will likely require tweaking.
+
+There is not (as yet) any per-user configuration or way to install
+beatscape system-wide. Feel free to contribute. :)
+
+BUGS
+
+Oh, god, too many to mention, but these are extremely noticable:
+* bpm-change problems on some songs (notably both era mixes)
+* sometimes a song will just decide not to work after you've selected
+  it. If you select it again, it will probably work.
+* A lot of undocumented features of the BMS/BME format are still
+  unhandled, and will likely cause errors or crashes. If anyone can
+  point me to a definitive BMS/BME reference, I'd be much obliged!
+* Sometimes samples will just fail to play. I'm not yet sure if this is
+  a fault of my engine or of pygame.
+* There's no way to adjust any game options, especially speed (without
+  hacking the source).
+* It's visually abominable.
+
+LICENSE
+
+beatscape is released under the GPLv2. Details can be found in the COPYING file.
+
+OTHER
+
+beatscape might run on windows. I've only tested this a little bit, and it does
+run, but the sound is usually lagged and choppy.
+
+CONTACT
+
+I can be contacted at bytex64@bytex64.net if you have any bug reports,
+questions, fixes, or other contributions.
+
+Thanks, and have fun!
diff --git a/accuracygraph.py b/accuracygraph.py
new file mode 100644 (file)
index 0000000..0de687e
--- /dev/null
@@ -0,0 +1,36 @@
+import pygame
+from bmevent import *
+from keyhandler import *
+
+class AccuracyGraph:
+       linecolor = (255,255,0)
+       tickcolor = (200,200,200)
+       accpoints = [-90,-60,-30,-15,0,15,30,60,90]
+       
+       def __init__(self,rect):
+               self.screen = pygame.display.get_surface()
+               self.disprect = rect
+               self.datapoints = [0]
+               self.ratio = rect.height / 100.0
+               self.tickpoints = map(lambda y: (y * self.ratio) + (rect.height/2), self.accpoints)
+               self.n_hits = 0
+               self.total = 0
+
+       def addpoint(self,point):
+               self.datapoints[0] = point
+               self.total += point
+               self.n_hits += 1
+               return self.total / float(self.n_hits)
+
+       def draw(self):
+               self.screen.fill((0,0,0), self.disprect)
+               for t in self.tickpoints:
+                       y = self.disprect.top + t
+                       pygame.draw.line(self.screen, self.tickcolor,
+                               (self.disprect.left, y), (self.disprect.right-1, y))
+               if abs(self.datapoints[0]) > 100:
+                       return
+               y = self.disprect.height / 2 + self.disprect.top + (self.datapoints[0] * self.ratio)
+               pygame.draw.line(self.screen, self.linecolor,
+                       (self.disprect.left, y), (self.disprect.right, y),2)
+
diff --git a/beatscape.py b/beatscape.py
new file mode 100755 (executable)
index 0000000..c2f84e0
--- /dev/null
@@ -0,0 +1,67 @@
+#!/usr/bin/python
+# beatscape is the mother of all musical button games.
+
+import sys
+import pygame
+from keygraph import *
+from keyfile import *
+from bmevent import *
+from chanman import *
+from formats import *
+from keyhandler import *
+import game
+
+try:
+       import config
+except ImportError:
+       print "Could not parse config.py. Maybe you forgot a semicolon? :-P"
+       exit(1)
+
+song = ''
+
+if len(sys.argv) > 1:
+       song = sys.argv[1]
+
+try:
+       import psyco
+       print "w00t! going psyco!"
+       psyco.full()
+except ImportError:
+       pass
+
+pygame.mixer.pre_init(44100, -16, True, 1)
+pygame.init()
+opts = pygame.FULLSCREEN | pygame.DOUBLEBUF | pygame.HWSURFACE
+pygame.display.set_mode((640,480), opts)
+screen = pygame.display.get_surface()
+
+for n in range(0,pygame.joystick.get_count()):
+       pygame.joystick.Joystick(n).init();
+
+if song:
+       # For now, assume IIDX. We'll make this smarter later.
+       game.playgame(gametypes['IIDX'],song)
+       exit(0)
+
+from mainmenu import *
+from fileselect import *
+
+l = ['mainmenu']
+
+while 1:
+       obj = None
+
+       if l[0] == 'mainmenu':
+               obj = MainMenu()
+       elif l[0] == 'fileselect':
+               obj = FileSelect()
+       elif l[0] == 'play':
+               obj = game.Game(l[1])
+       elif l[0] == 'quit':
+               sys.exit(0)
+
+       l = obj.run()
+       # Flush events before switching to the next mode
+       pygame.event.clear()
+
+pygame.quit()
diff --git a/bmevent.py b/bmevent.py
new file mode 100644 (file)
index 0000000..b6cc994
--- /dev/null
@@ -0,0 +1,36 @@
+BME_BGM = 0x01
+BME_TEMPO = 0x02
+BME_BGA = 0x04
+BME_CHPOOR = 0x08
+BME_NOTE1 = 0x10
+BME_NOTE2 = 0x20
+BME_HIT = 0x40
+BME_STOP = 0x80
+BME_LONGMEASURE = 0x100
+
+class BMEvent:
+       def __init__(self,beat,type,key=None,dataref=None):
+               self.beat = beat
+               self.type = type
+               self.key = key
+               self.dataref = dataref
+
+       def __str__(self):
+               s = "BMEvent:\n"
+               s += "   beat=" + str(self.beat) + "\n"
+               t = { BME_BGM:          "BGM",
+                     BME_TEMPO:        "TEMPO",
+                     BME_BGA:          "BGA",
+                     BME_CHPOOR:       "CHANGE POOR",
+                     BME_NOTE1:        "NOTE P1",
+                     BME_NOTE2:        "NOTE P2"
+                   }[self.type]
+               s += "   type=" + t + "\n"
+               if self.key:
+                       s += "   key=" + str(self.key) + "\n"
+               if self.dataref:
+                       try:
+                               s += "   dataref=" + hex(self.dataref) + "\n"
+                       except TypeError:
+                               s += "   dataref=" + str(self.dataref) + "\n"
+               return s
diff --git a/chanman.py b/chanman.py
new file mode 100644 (file)
index 0000000..1323eac
--- /dev/null
@@ -0,0 +1,17 @@
+from bmevent import *
+import pygame
+
+class ChannelManager:
+       keyfile = None
+       chanmap = None
+
+       def __init__(self,keyfile):
+               self.keyfile = keyfile
+               self.chanmap = [pygame.mixer.Channel(n) for n in range(0,self.keyfile.numkeys)]
+               pygame.mixer.set_num_channels(64)
+
+       def play(self,bme):
+               if bme.type & (BME_NOTE1 | BME_NOTE2 | BME_BGM | BME_HIT):
+                       if bme.dataref > 0 and bme.dataref in self.keyfile.wavs:
+                               self.keyfile.wavs[bme.dataref].play()
+
diff --git a/config.py b/config.py
new file mode 100644 (file)
index 0000000..23f6dd4
--- /dev/null
+++ b/config.py
@@ -0,0 +1,36 @@
+# Configuration. Edit this file by hand, but beware, it is a python file
+# and the game won't start if it does not parse correctly.
+
+# These are modules necessary for certain constants used here
+from pygame.locals import *
+from constants import *
+
+# Main config, for global options
+librarypath = "/media/audio/BMS/library"
+keytimeout = 250       # milliseconds
+
+# Game config, for various game types
+
+bm = {
+       'format': 'IIDXFormat',
+       'keymap':
+               {K_SPACE:0, K_z:1, K_s:2, K_x:3, K_d:4, K_c:5, K_f:6, K_v:7},
+       'jsbuttonmap':
+               { 3: 1, 6: 2, 2: 3, 7: 4, 1: 5, 4: 6 },
+       'jsaxismap':
+               { (0,-1.0): 7, (0,0.0): 7, (1,-1.0): 0, (1,0.0): 0, (1,1.0): 0 },
+       'timings':
+               #[(15,JUDGE_FGREAT), (35,JUDGE_GREAT), (50,JUDGE_GOOD), (80,JUDGE_BAD), (100,JUDGE_POOR)]
+               [(20,JUDGE_FGREAT), (42,JUDGE_GREAT), (70,JUDGE_GOOD), (250,JUDGE_BAD), (1000,JUDGE_POOR)]
+}
+
+popn = {
+       'format': 'PopnFormat',
+}
+
+# game type mapping
+gametypes = {
+       'beatmania': bm,        # For now
+       'IIDX': bm,
+       'Popn': popn
+}
diff --git a/constants.py b/constants.py
new file mode 100644 (file)
index 0000000..8d84fef
--- /dev/null
@@ -0,0 +1,8 @@
+# Constants used everywhere
+
+JUDGE_FGREAT = 1
+JUDGE_GREAT = 2
+JUDGE_GOOD = 3
+JUDGE_BAD = 4
+JUDGE_POOR = 5
+JUDGE_NA = 6
diff --git a/event.py b/event.py
new file mode 100644 (file)
index 0000000..09b233c
--- /dev/null
+++ b/event.py
@@ -0,0 +1,49 @@
+import pygame
+from pygame.locals import *
+import config
+
+LEFT = 0
+RIGHT = 1
+UP = 2
+DOWN = 3
+OK = 4
+CANCEL = 5
+OPTION = 6
+
+keymap = {K_UP: UP, K_DOWN: DOWN, K_LEFT: LEFT, K_RIGHT: RIGHT, K_RETURN: OK, K_ESCAPE: CANCEL}
+jsamap = {(0,-1.0): OK, (1,-1.0): DOWN, (1,1.0): UP }
+jsbmap = {3: OK, 6: CANCEL, 2: OK, 7: CANCEL, 1: OK, 4: CANCEL, 10: OPTION, 9: OK}
+
+def parseevent(e):
+       try: 
+               if e.type == KEYDOWN:
+                       return keymap[e.key]
+               elif e.type == JOYBUTTONDOWN:
+                       return jsbmap[e.button]
+               elif e.type == JOYAXISMOTION:
+                       if e.value != 0.0:
+                               return jsamap[(e.axis,e.value)]
+               else:
+                       return None
+       except KeyError:
+               return None
+
+last_action = None
+last_action_time = 0
+
+def getaction():
+       global last_action, last_action_time
+
+       e = pygame.event.poll()
+       if e.type != NOEVENT:
+               last_action = parseevent(e)
+               if last_action:
+                       last_action_time = pygame.time.get_ticks()
+               return last_action
+       else:
+               if pygame.time.get_ticks() - last_action_time > config.keytimeout:
+                       last_action_time = pygame.time.get_ticks()
+                       # Only repeat for directionals
+                       if last_action in (LEFT,RIGHT,UP,DOWN):
+                               return last_action
+       return None
diff --git a/fileselect.py b/fileselect.py
new file mode 100644 (file)
index 0000000..a21997a
--- /dev/null
@@ -0,0 +1,296 @@
+import pygame
+import cPickle
+import os, os.path, re, math
+
+from keyfile import *
+from fx import *
+import event
+
+fileroot = "songs"
+songcache = 'songcache.pickle'
+possave = 0
+
+def mksongcache():
+       songs = FileNode()
+       songs.crawl(fileroot)
+       f = open(songcache,'w')
+       cPickle.dump(songs,f)
+       f.close()
+       return songs
+
+filetile = pygame.image.load("gfx/filetile.png")
+
+class SelectWidget(FX):
+       color = (240,240,255)
+       basecolor = (200,0,0)
+       fadetime = 700
+       size = (400,30)
+
+       OFF = 0
+       ON = 1
+       FADING = 2
+
+       def __init__(self,text,location=(0,0)):
+               FX.__init__(self)
+               f = pygame.font.Font('font/Nano.ttf',16)
+               self.text = f.render(text,True,self.color)
+               self.surface = pygame.Surface(self.size).convert_alpha()
+               self.surface.fill( (0,0,0,0) )
+               self.surface.blit(filetile,(0,0))
+               self.surface.blit(self.text, (9,7))
+               self.pointlist = [(10,0),(self.size[0]-2,0),(self.size[0]-2,self.size[1]-2),(0,self.size[1]-2),(0,10)]
+               self.state = self.OFF
+               self.location = location
+
+       def off(self):
+               if self.state == self.ON:
+                       self.state = self.FADING
+                       self.fade_t = pygame.time.get_ticks()
+
+       def on(self):
+               self.state = self.ON
+
+       def draw(self,t):
+               self.screen.blit(self.surface,self.location)
+               color = self.basecolor
+               if self.state == self.ON:
+                       color = self.color
+               if self.state == self.FADING:
+                       dt = pygame.time.get_ticks() - self.fade_t
+                       if dt < self.fadetime:
+                               alpha = math.exp(-0.003*dt)
+                               color = [(1.0-alpha) * self.basecolor[n] + alpha * self.color[n] for n in range(0,3)]
+                       else:
+                               self.state = self.OFF
+               pointlist = [(x[0] + self.location[0],x[1] + self.location[1]) for x in self.pointlist]
+               pygame.draw.lines(self.screen,color,True,pointlist,2)
+
+# This would look a lot less ugly if only there were something like
+# perl's cmp and <=> operators.
+# FUCK YOU PYTHON WHY DOES THIS HAVE TO BE SO COMPLICATED!?!?!?!
+def WTFsort(a,b):
+       if a.type == a.DIRECTORY and b.type == b.DIRECTORY:
+               if a.name > b.name:
+                       return 1
+               elif a.name < b.name:
+                       return -1
+               else:
+                       return 0
+       elif a.type == a.DIRECTORY and b.type == b.FILE:
+               return 1
+       elif a.type == a.FILE and b.type == b.DIRECTORY:
+               return -1
+       else:
+               if a.info['player'] > b.info['player']:
+                       return 1
+               elif a.info['player'] < b.info['player']:
+                       return -1
+               else:
+                       if not 'playlevel' in a.info:
+                               return -1
+                       if not 'playlevel' in b.info:
+                               return 1
+                       if a.info['playlevel'] > b.info['playlevel']:
+                               return 1
+                       elif a.info['playlevel'] < b.info['playlevel']:
+                               return -1
+                       else:
+                               if not 'total' in a.info:
+                                       return -1
+                               if not 'total' in b.info:
+                                       return 1
+                               if a.info['total'] > b.info['total']:
+                                       return 1
+                               elif a.info['total'] < b.info['total']:
+                                       return -1
+                               else:
+                                       return 0
+
+filematch = re.compile('\.(bms|bme|sm)$',re.I)
+
+class FileNode:
+       DIRECTORY = 0
+       FILE = 1
+
+       def __init__(self):
+               self.children = []
+               self.open = False
+               self.info = None
+               self.widget = None
+
+       def crawl(self,path):
+               print "Crawling " + path
+               self.path = path
+               if os.path.isdir(path):
+                       self.type = self.DIRECTORY
+                       self.name = os.path.basename(path)
+                       files = filter(lambda x: os.path.isdir(os.path.join(path,x)) or filematch.search(x), os.listdir(path))
+                       for f in files:
+                               c = FileNode()
+                               if c.crawl(os.path.join(path,f)):
+                                       self.children.append(c)
+                       self.children.sort(WTFsort)
+               elif os.path.isfile(path):
+                       self.type = self.FILE
+                       self.info = kf_info(path)
+                       self.name = self.info['title']
+               else:
+                       print path + " isn't a file or a directory. Ignoring."
+                       return False
+               return True
+
+       def update(self):
+               for c in self.children:
+                       if not os.path.exists(c.path):
+                               self.children.remove(c)
+                       else:
+                               c.update()
+
+       def add(self,child):
+               self.children.append(child)
+
+       def tolist(self,depth=0):
+               self.depth = depth
+               l = [self]
+               if self.open:
+                       for c in self.children:
+                               r = c.tolist(depth+1)
+                               for i in r:
+                                       l.append(i)
+               return l
+
+       def getwidget(self):
+               if not self.widget:
+                       self.widget = SelectWidget(self.name)
+               return self.widget
+
+       def nukewidgets(self):
+               self.widget = None
+               for c in self.children:
+                       c.nukewidgets()
+
+class FileSelect:
+
+       def __init__(self):
+               self.screen = pygame.display.get_surface()
+               try:
+                       f = open(songcache,'r')
+                       self.songs = cPickle.load(f)
+                       f.close()
+               except IOError:
+                       self.songs = mksongcache()
+               except cPickle.UnpicklingError:
+                       self.songs = mksongcache()
+
+               self.period = 428
+               self.songlist = self.songs.tolist()
+               try:
+                       self.position = self.songlist.index(self.songs.current)
+               except AttributeError:
+                       self.position = 0
+               except ValueError:
+                       self.position = 0
+
+               self.fslabel = Text('file select','font/Nano.ttf',30,(330,30),(255,0,0))
+               self.fadebottom = pygame.Surface((640,200)).convert_alpha()
+               for n in range(0,200):
+                       alpha = int((1.0 - math.exp(-n/30.0)) * 255.0)
+                       color = (0,0,0,alpha)
+                       pygame.draw.line(self.fadebottom,color,(0,n),(640,n))
+               self.labels = {}
+               self.olabels = {}
+               self.olabels['title'] = Text('title','font/Nano.ttf',12, (20,320),(255,130,130))
+               self.labels['title'] = Text('','font/Nano.ttf',18, (30,330),(230,230,255))
+               self.olabels['artist'] = Text('artist','font/Nano.ttf',12, (20,360),(255,130,130))
+               self.labels['artist'] = Text('','font/Nano.ttf',16, (30,370),(230,230,255))
+               self.olabels['genre'] = Text('genre','font/Nano.ttf',12, (20,390),(255,130,130))
+               self.labels['genre'] = Text('','font/Nano.ttf',16, (30,400),(230,230,255))
+               self.olabels['playlevel'] = Text('difficulty','font/Nano.ttf',12, (20,420),(255,130,130))
+               self.labels['playlevel'] = Text('','font/Nano.ttf',16, (30,430),(230,230,255))
+               self.olabels['bpm'] = Text('BPM','font/Nano.ttf',12, (20,450),(255,130,130))
+               self.labels['bpm'] = Text('','font/Nano.ttf',16, (30,460),(230,230,255))
+               self.calcsongwidgets()
+
+       def draw(self,t):
+               self.screen.fill( (30,0,0) )
+               l = len(self.songlist)
+               for nu in range(self.position-4,self.position+8):
+                       n = nu % l
+                       self.songlist[n].getwidget().draw(t)
+
+               self.screen.blit(self.fadebottom,(0,280))
+               for k in self.olabels.keys():
+                       self.olabels[k].draw(t)
+               for k in self.labels.keys():
+                       self.labels[k].draw(t)
+               self.fslabel.draw(t)
+
+       def calcsongwidgets(self):
+               l = len(self.songlist)
+               for nu in range(self.position-4,self.position+8):
+                       n = nu % l
+                       w = self.songlist[n].getwidget()
+                       w.location = (10 + self.songlist[n].depth * 20, 150 - (self.position - nu)*30)
+                       if n == self.position:
+                               w.on()
+                               if self.songlist[n].type == FileNode.DIRECTORY:
+                                       for tag in ('artist','genre','playlevel','bpm'):
+                                               self.labels[tag].settext('')
+                                       self.labels['title'].settext(self.songlist[n].name)
+                               else:
+                                       for tag in ('title','artist','genre','bpm'):
+                                               self.labels[tag].settext(self.songlist[n].info[tag])
+                                       try:
+                                               playlevel = int(self.songlist[n].info['playlevel'])
+                                               self.labels['playlevel'].settext(("*" * playlevel) + "(" + str(playlevel) + ")")
+                                       except ValueError:
+                                               self.labels['playlevel'].settext(str(self.songlist[n].info['playlevel']))
+                       else:
+                               w.off()
+
+       def run(self):
+               global possave
+               decided = 0
+               pygame.mixer.music.load('snd/0x01.ogg')
+               pygame.mixer.music.set_volume(0.75)
+               pygame.mixer.music.play(-1)
+               stab = pygame.mixer.Sound('snd/stab.ogg')
+               stab.set_volume(0.25)
+               start_t = pygame.time.get_ticks()
+
+               while decided == 0:
+                       t = pygame.time.get_ticks() - start_t
+                       act = event.getaction()
+                       while act:
+                               if act == event.CANCEL:
+                                       return ['mainmenu']
+                               elif act == event.DOWN:
+                                       self.position += 1
+                                       self.calcsongwidgets()
+                               elif act == event.UP:
+                                       self.position -= 1
+                                       self.calcsongwidgets()
+                               elif act == event.OK:
+                                       if self.songlist[self.position].type == FileNode.DIRECTORY:
+                                               self.songlist[self.position].open = not self.songlist[self.position].open
+                                               self.songlist = self.songs.tolist()
+                                               self.calcsongwidgets()
+                                       else:
+                                               decided = self.position
+                               act = event.getaction()
+
+                       self.position = self.position % len(self.songlist)
+                       self.draw(t)
+                       pygame.display.flip()
+
+               pygame.mixer.music.fadeout(1500)
+               stab.play()
+
+               self.songs.nukewidgets()
+               self.songs.current = self.songlist[self.position]
+               f = open(songcache,'w')
+               cPickle.dump(self.songs,f)
+               f.close()
+
+               #possave = self.position
+               return ['play',self.songlist[decided].path]
diff --git a/font/Nano.ttf b/font/Nano.ttf
new file mode 100644 (file)
index 0000000..0c52860
Binary files /dev/null and b/font/Nano.ttf differ
diff --git a/formats.py b/formats.py
new file mode 100644 (file)
index 0000000..7b2d221
--- /dev/null
@@ -0,0 +1,114 @@
+import pygame
+from keygraph import *
+from keyhandler import *
+from rotowidget import *
+from fx import *
+from constants import *
+
+red = (255,30,30)
+offwhite = (245,245,245)
+blue = (30,30,255)
+green = (30,255,30)
+black = (0,0,0)
+darkgray = (20,20,20)
+ggray = (200,255,200)
+
+class IIDXFormat:
+       keyfile = None
+       keystyle = [(1.75,red,black,ggray), (1,offwhite,darkgray,blue),
+                   (0.8,blue,black,green), (1,offwhite,darkgray,blue),
+                   (0.8,blue,black,green), (1,offwhite,darkgray,blue),
+                   (0.8,blue,black,green), (1,offwhite,darkgray,blue)]
+
+       def __init__(self,keyfile):
+               self.keyfile = keyfile
+               self.kg = (KeyGraph(keyfile,self.keystyle,1,pygame.Rect(0,0,192,400)),
+                          KeyGraph(keyfile,self.keystyle,2,pygame.Rect(448,0,192,400)) )
+               self.animpic = None
+               self.screen = pygame.display.get_surface()
+               self.roto = RotoWidget((30,430),25,1.0)
+               self.songlength = keyfile.length()
+               self.groove = 20.0
+               self.groovetext = GlyphText((60,415))
+               self.accuracy = 0
+               self.nacc = 0
+
+       def setproperties(self,properties):
+               if properties.has_key('judgement'):
+                       j = properties['judgement']
+                       if j == JUDGE_FGREAT:
+                               self.groove += 1.0
+                       elif j == JUDGE_GREAT:
+                               self.groove += 0.7
+                       elif j == JUDGE_GOOD:
+                               self.groove += 0.2
+                       elif j == JUDGE_BAD:
+                               self.groove -= 2.0
+                       elif j == JUDGE_POOR:
+                               self.groove -= 6.0
+                       if self.groove > 100.0:
+                               self.groove = 100.0
+                       elif self.groove < 0.0:
+                               self.groove = 0.0
+               if 'accuracy' in properties:
+                       self.accuracy += properties['accuracy']
+                       self.nacc += 1
+                       print "accuracy:", self.accuracy / float(self.nacc)
+               self.kg[0].setproperties(properties)
+
+       def draw(self, t, bmelist):
+               b = self.keyfile.eval_beatfunc(t)
+               for bme in bmelist:
+                       if bme.type == BME_BGA and bme.dataref in self.keyfile.bmps:
+                               self.animpic = self.keyfile.bmps[bme.dataref]
+               self.screen.fill( (20,20,20) )
+               for k in self.kg:
+                       k.t = t
+                       k.draw()
+               if self.animpic:
+                       self.screen.fill((0,0,0),(192,112,256,256))
+                       self.screen.blit(self.animpic,(192,112))
+               self.roto.pulsev = self.groove
+               self.roto.arcv = (float(t) / self.songlength) * 100
+               self.roto.draw(b)
+               self.groovetext.settext("%02d" % self.groove)
+               self.groovetext.draw(t)
+               pygame.display.update()
+
+lightgray = (230,230,230)
+graybg = (30,30,30)
+yellow = (230,230,0)
+yellowbg = (30,30,0)
+green = (30,255,30)
+greenbg = (0,30,0)
+blue = (0,0,230)
+bluebg = (0,0,30)
+redbg = (30,0,0)
+
+class PopnFormat:
+       keyfile = None
+       keystyle = [(1,lightgray,graybg), (0.7,yellow,yellowbg),
+                   (1,green,greenbg), (0.7,blue,bluebg), (1,red,redbg),
+                   (0.7,blue,bluebg), (1,green,greenbg), (0.7,yellow,yellowbg),
+                   (1,lightgray,graybg)]
+
+       def __init__(self,keyfile):
+               self.keyfile = keyfile
+               self.kg = KeyGraph(keyfile,self.keystyle,1,pygame.Rect(256,0,384,400))
+               self.kg.bordercolor = black
+                         
+               self.animpic = None
+               self.screen = pygame.display.get_surface()
+
+       def draw(self, t, bmelist):
+               for bme in bmelist:
+                       if bme.type == BME_BGA:
+                               self.animpic = self.keyfile.bmps[bme.dataref]
+               self.screen.fill( (20,20,20) )
+               self.kg.t = t
+               self.kg.draw()
+               if self.animpic:
+                       self.screen.fill((0,0,0),(0,112,256,256))
+                       self.screen.blit(self.animpic,(0,112))
+               pygame.display.update()
+
diff --git a/fx.py b/fx.py
new file mode 100644 (file)
index 0000000..4b608d6
--- /dev/null
+++ b/fx.py
@@ -0,0 +1,174 @@
+import pygame
+import math
+import os, os.path
+
+ALIGN_LEFT = 0
+ALIGN_RIGHT = 1
+
+# FX is the abstract base class for all "effects" objects. They have two
+# defining features:
+#
+# 1) The FX base class sets up self.screen so that you can draw to the
+#    screen. It is then important to call FX.__init__(self) in your
+#    __init__ routine.
+# 2) Each FX object has a draw(self,t) method that draws itself to the
+#    screen (possibly with appearance based on the current time).
+
+class FX:
+       def __init__(self):
+               self.screen = pygame.display.get_surface()
+
+       def draw(self,t):
+               pass
+
+class Blank(FX):
+       def draw(self,t):
+               self.screen.fill((0,0,0))
+
+class Image(FX):
+       align = ALIGN_LEFT
+
+       def __init__(self,file,location=(0,0)):
+               FX.__init__(self)
+               self.surface = pygame.image.load(file).convert_alpha()
+               self.location = location
+
+       def draw(self,t):
+               if self.align == ALIGN_RIGHT:
+                       rl = (self.location[0] - self.surface.get_width(),self.location[1])
+                       self.screen.blit(self.surface,rl)
+               else:
+                       self.screen.blit(self.surface,self.location)
+
+class Text(Image):
+       def __init__(self,text,font,size,location=(0,0),color=(255,255,255)):
+               FX.__init__(self)
+               self.font = pygame.font.Font(font,size)
+               self.location = location
+               self.color = color
+               self.settext(text)
+
+       def settext(self,text):
+               f = self.font.render(text,True,(0,0,0))
+               self.surface = pygame.surface.Surface((f.get_width()+2,f.get_height()+2)).convert_alpha()
+               self.surface.fill((0,0,0,0))
+               self.surface.blit(f,(2,2))
+               f = self.font.render(text,True,self.color)
+               self.surface.blit(f,(0,0))
+
+textglyphs = {}
+
+class GlyphText(FX):
+       def __init__(self,location=(0,0),glyphdir='gfx/glyph'):
+               FX.__init__(self)
+               if not textglyphs:
+                       l = os.listdir(glyphdir)
+                       for g in l:
+                               c = g.split('.')[0]
+                               textglyphs[c] = pygame.image.load(os.path.join(glyphdir,g)).convert_alpha()
+               self.location = location
+               self.text = ''
+
+       def settext(self,str):
+               self.text = str
+
+       def draw(self,t):
+               (x,y) = self.location
+               for c in self.text:
+                       if textglyphs.has_key(c):
+                               self.screen.blit(textglyphs[c],(x,y))
+                               x += textglyphs[c].get_width()
+
+class PulseLine(FX):
+       r = -0.02
+
+       def __init__(self, pointlist, color, period, maxwidth = 8, minwidth = 1):
+               FX.__init__(self)
+               self.period = period
+               self.color = color
+               if maxwidth:
+                       self.maxwidth = maxwidth
+               if minwidth:
+                       self.minwidth = minwidth
+               self.pointlist = pointlist
+               self.location = (0,0)
+
+       def draw(self,t):
+               w = (self.maxwidth - 1) * math.exp(self.r * (t % self.period)) + self.minwidth
+               pl = map(lambda x: (x[0] + self.location[0],x[1] + self.location[1]),self.pointlist)
+               pygame.draw.lines(self.screen,self.color, 0, pl, int(w))
+
+class ColumnPulse(FX):
+       r = -0.03
+
+       def __init__(self,rect,color):
+               FX.__init__(self)
+               self.rect = rect
+               self.color = color
+               self.st = 0
+               self.surf = []
+               t = 0
+               a = 1.0
+               while a > 0.01:
+                       s = pygame.Surface(self.rect.size).convert_alpha()
+                       s.lock()
+                       s.fill( (0,0,0,0) )
+                       for n in range(0,self.rect.height):
+                               c = (self.color[0],self.color[1],self.color[2],float(n) / self.rect.height * a * 255.0)
+                               w = (self.rect.width / 2) * (1.0 - a)
+                               pygame.draw.line(s,c,(w,n),(self.rect.width-w,n))
+                               #s.fill(c,(0,n,self.rect.width,1))
+                       s.unlock()
+                       self.surf.append(s)
+                       t += 16
+                       a = math.exp(self.r * t)
+
+               self.hit = 0
+
+       def down(self):
+               self.hit = 1
+
+       def up(self):
+               self.st = pygame.time.get_ticks()
+               self.hit = 0
+
+       def draw(self):
+               dt = pygame.time.get_ticks() - self.st
+               if self.hit:
+                       self.screen.blit(self.surf[0],self.rect)
+               else:
+                       n = dt / 16
+                       if n > len(self.surf) - 1:
+                               return
+                       self.screen.blit(self.surf[n],self.rect)
+
+class PlaneScroll(FX):
+       surface = None
+
+       def __init__(self,file,direction,zoom=1.0,alpha=1.0):
+               FX.__init__(self)
+               ts = pygame.image.load(file)
+               if zoom != 1.0:
+                       ts = pygame.transform.rotozoom(ts, 0.0, zoom)
+               self.surface = ts.convert()
+               del ts
+               if alpha < 1.0 and alpha >= 0.0:
+                       self.surface.set_alpha(int(alpha * 255))
+               self.direction = direction
+
+       def draw(self,t):
+               w = self.surface.get_width()
+               h = self.surface.get_height()
+               location = self.surface.get_rect()
+               location.size = (640,480)
+               location.move_ip((t * self.direction[0]) % w, (t * self.direction[1]) % h)
+               self.screen.blit(self.surface,(0,0),location)
+               if location.right > w:
+                       x = 640 - (location.right - w)
+                       self.screen.blit(self.surface,(x,0),(0,location.top,640,480))
+               if location.bottom > h:
+                       y = 480 - (location.bottom - h)
+                       self.screen.blit(self.surface,(0,y),(location.left,0,640,480))
+               if location.bottom > h and location.right > w:
+                       # x and y must be calculated from above two tests
+                       self.screen.blit(self.surface,(x,y),(0,0,640,480))
diff --git a/game.py b/game.py
new file mode 100644 (file)
index 0000000..9ee7961
--- /dev/null
+++ b/game.py
@@ -0,0 +1,132 @@
+import pygame
+import thread
+
+from keygraph import *
+from keyfile import *
+from bmevent import *
+from formats import *
+from chanman import *
+from keyhandler import *
+import config
+
+events = []
+evlock = thread.allocate_lock()
+evrun = 1
+t_orig = 0
+
+def event_thread():
+       global events,evlock,t_orig
+       while evrun:
+               e = pygame.event.wait()
+               t = pygame.time.get_ticks() - t_orig
+               evlock.acquire()
+               events.append((t,e))
+               evlock.release()
+
+def event_thread_start():
+       global evrun
+       evrun = 1
+       thread.start_new_thread(event_thread,())
+
+def event_thread_stop():
+       global evrun
+       evrun = 0
+       pygame.event.post(pygame.event.Event(pygame.NOEVENT,{}))
+
+def get_events():
+       global events,evlock
+       evlock.acquire()
+       eold = events
+       events = []
+       evlock.release()
+       return eold
+
+def playgame(gameconfig,song):
+       global t_orig
+       k = kf_load(song)
+
+       dm = eval(gameconfig['format'] + '(k)');
+       cm = ChannelManager(k);
+       kh = KeyHandler(gameconfig,k);
+
+       # No background anims? No problem!
+       if not k.bmps:
+               print "IT'S PEANUT BUTTER JELLY TIME!"
+               for x in range(0,8):
+                       k.bmps[x] = pygame.transform.scale(pygame.image.load("gfx/banana%d.png" % x).convert(), (256,256))
+               lastbeat = k.bmelist[-1].beat
+               i = 0.0
+               j = 0
+               while i < lastbeat:
+                       k.add(BMEvent(i,BME_BGA,None,j))
+                       j = (j+1) % 8
+                       i += 0.25
+               k.sort()
+       
+       li = BMEListIter(k.bmelist)
+       reaper_li = BMEListIter(k.bmelist)
+       t_finish = k.length() + 5000
+
+       t_orig = pygame.time.get_ticks()
+       try:
+               if k.offset < 0:
+                       t_orig -= int(k.offset * 1000)
+       except NameError:
+               pass
+       b = b_l = k.eval_beatfunc(t_orig)
+
+       event_thread_start()
+
+       animpic = None
+       while 1:
+               t = pygame.time.get_ticks() - t_orig
+               b = k.eval_beatfunc(t)
+               print t,b,b_l
+               db = b - b_l
+               b_l = b
+               li.goto(b)
+               bmelist = li.window(db)
+               dm.draw(t + 45, bmelist)
+               for bme in bmelist:
+                       if bme.type == BME_BGM:
+                               cm.play(bme);
+                       if bme.type == BME_TEMPO:
+                               bpm = bme.dataref
+                               kh.setproperties({'bpm': bme.dataref})
+
+               for (t,e) in get_events():
+                       r = kh.handle(e,t)
+                       if r:
+                               (hitbme, properties) = r
+                               if hitbme:
+                                       cm.play(hitbme)
+                               dm.setproperties(properties)
+
+                       if e.type == pygame.KEYDOWN:
+                               if e.key == pygame.K_ESCAPE:
+                                       event_thread_stop()
+                                       pygame.mixer.stop()
+                                       return False
+                               elif e.key == pygame.K_PRINT:
+                                       pygame.image.save(screen,"screenshot.bmp")
+
+               reaper_li.goto(b - 2.0)
+               bmelist = reaper_li.window(1.0, BME_NOTE1 | BME_NOTE2)
+               for bme in bmelist:
+                       dm.setproperties({'judgement': JUDGE_POOR})
+                       bme.type = BME_HIT
+
+               if t > t_finish:
+                       event_thread_stop()
+                       return True
+
+# Hmm. This is sort of braindead, isn't it?
+class Game:
+       def __init__(self,song):
+               self.song = song
+
+       def run(self):
+               print "Playing",self.song
+               # IIDX is hardcoded for now
+               playgame(config.gametypes['IIDX'], self.song)
+               return ['fileselect']
diff --git a/gfx/bad.png b/gfx/bad.png
new file mode 100644 (file)
index 0000000..9d852f6
Binary files /dev/null and b/gfx/bad.png differ
diff --git a/gfx/banana0.png b/gfx/banana0.png
new file mode 100644 (file)
index 0000000..1fc979a
Binary files /dev/null and b/gfx/banana0.png differ
diff --git a/gfx/banana1.png b/gfx/banana1.png
new file mode 100644 (file)
index 0000000..fef450e
Binary files /dev/null and b/gfx/banana1.png differ
diff --git a/gfx/banana2.png b/gfx/banana2.png
new file mode 100644 (file)
index 0000000..00ec334
Binary files /dev/null and b/gfx/banana2.png differ
diff --git a/gfx/banana3.png b/gfx/banana3.png
new file mode 100644 (file)
index 0000000..59636d6
Binary files /dev/null and b/gfx/banana3.png differ
diff --git a/gfx/banana4.png b/gfx/banana4.png
new file mode 100644 (file)
index 0000000..94c407c
Binary files /dev/null and b/gfx/banana4.png differ
diff --git a/gfx/banana5.png b/gfx/banana5.png
new file mode 100644 (file)
index 0000000..7ca5579
Binary files /dev/null and b/gfx/banana5.png differ
diff --git a/gfx/banana6.png b/gfx/banana6.png
new file mode 100644 (file)
index 0000000..de4c2dc
Binary files /dev/null and b/gfx/banana6.png differ
diff --git a/gfx/banana7.png b/gfx/banana7.png
new file mode 100644 (file)
index 0000000..da45870
Binary files /dev/null and b/gfx/banana7.png differ
diff --git a/gfx/chips_layer1.jpg b/gfx/chips_layer1.jpg
new file mode 100644 (file)
index 0000000..198d5ef
Binary files /dev/null and b/gfx/chips_layer1.jpg differ
diff --git a/gfx/chips_layer2.jpg b/gfx/chips_layer2.jpg
new file mode 100644 (file)
index 0000000..abcd3dd
Binary files /dev/null and b/gfx/chips_layer2.jpg differ
diff --git a/gfx/circle.png b/gfx/circle.png
new file mode 100644 (file)
index 0000000..28b0d0a
Binary files /dev/null and b/gfx/circle.png differ
diff --git a/gfx/cross.png b/gfx/cross.png
new file mode 100644 (file)
index 0000000..5c73448
Binary files /dev/null and b/gfx/cross.png differ
diff --git a/gfx/dcircle.png b/gfx/dcircle.png
new file mode 100644 (file)
index 0000000..70921b6
Binary files /dev/null and b/gfx/dcircle.png differ
diff --git a/gfx/fgreat.png b/gfx/fgreat.png
new file mode 100644 (file)
index 0000000..ca8e3a3
Binary files /dev/null and b/gfx/fgreat.png differ
diff --git a/gfx/filetile.png b/gfx/filetile.png
new file mode 100644 (file)
index 0000000..77e140c
Binary files /dev/null and b/gfx/filetile.png differ
diff --git a/gfx/glyph/0.png b/gfx/glyph/0.png
new file mode 100644 (file)
index 0000000..993fe0d
Binary files /dev/null and b/gfx/glyph/0.png differ
diff --git a/gfx/glyph/1.png b/gfx/glyph/1.png
new file mode 100644 (file)
index 0000000..40fb9da
Binary files /dev/null and b/gfx/glyph/1.png differ
diff --git a/gfx/glyph/2.png b/gfx/glyph/2.png
new file mode 100644 (file)
index 0000000..e5be3d2
Binary files /dev/null and b/gfx/glyph/2.png differ
diff --git a/gfx/glyph/3.png b/gfx/glyph/3.png
new file mode 100644 (file)
index 0000000..6b32519
Binary files /dev/null and b/gfx/glyph/3.png differ
diff --git a/gfx/glyph/4.png b/gfx/glyph/4.png
new file mode 100644 (file)
index 0000000..e8fe6a4
Binary files /dev/null and b/gfx/glyph/4.png differ
diff --git a/gfx/glyph/5.png b/gfx/glyph/5.png
new file mode 100644 (file)
index 0000000..f3948b4
Binary files /dev/null and b/gfx/glyph/5.png differ
diff --git a/gfx/glyph/6.png b/gfx/glyph/6.png
new file mode 100644 (file)
index 0000000..fa5e1a1
Binary files /dev/null and b/gfx/glyph/6.png differ
diff --git a/gfx/glyph/7.png b/gfx/glyph/7.png
new file mode 100644 (file)
index 0000000..f515801
Binary files /dev/null and b/gfx/glyph/7.png differ
diff --git a/gfx/glyph/8.png b/gfx/glyph/8.png
new file mode 100644 (file)
index 0000000..56e877a
Binary files /dev/null and b/gfx/glyph/8.png differ
diff --git a/gfx/glyph/9.png b/gfx/glyph/9.png
new file mode 100644 (file)
index 0000000..507f024
Binary files /dev/null and b/gfx/glyph/9.png differ
diff --git a/gfx/good.png b/gfx/good.png
new file mode 100644 (file)
index 0000000..f48ad0c
Binary files /dev/null and b/gfx/good.png differ
diff --git a/gfx/great.png b/gfx/great.png
new file mode 100644 (file)
index 0000000..455899f
Binary files /dev/null and b/gfx/great.png differ
diff --git a/gfx/poor.png b/gfx/poor.png
new file mode 100644 (file)
index 0000000..d453669
Binary files /dev/null and b/gfx/poor.png differ
diff --git a/gfx/title.png b/gfx/title.png
new file mode 100644 (file)
index 0000000..222d3cb
Binary files /dev/null and b/gfx/title.png differ
diff --git a/gfx/triangle.png b/gfx/triangle.png
new file mode 100644 (file)
index 0000000..0df4d05
Binary files /dev/null and b/gfx/triangle.png differ
diff --git a/keyfile.py b/keyfile.py
new file mode 100644 (file)
index 0000000..3375e65
--- /dev/null
@@ -0,0 +1,214 @@
+import pygame
+import imp
+import os.path
+from bmevent import *
+
+class KeyFile:
+       bmelist = None
+       numkeys = 0
+
+       # Loaded from a file
+       player = None
+       genre = None
+       title = None
+       artist = None
+       stagefile = None
+       playlevel = -1
+       rank = 3
+       stagefile = None
+       volwav = 1.0
+       wavs = None
+       bmps = None
+       offset = 0
+
+       # Allows us to continuously map time to the current beat
+       beatfunc = None
+
+       # Keymapping is a sequence of keycodes, mapping the index to the
+       # key
+       def __init__(self):
+               self.bmelist = []
+               self.wavs = {}
+               self.bmps = {}
+
+       def add(self,bme):
+               self.bmelist.append(bme)
+
+       def remove(self,bme):
+               return self.bmelist.remove(bme)
+
+       def sort(self):
+               def sortfun(a,b):
+                       if a.beat == b.beat:
+                               return 0
+                       elif a.beat > b.beat:
+                               return 1
+                       elif a.beat < b.beat:
+                               return -1
+               self.bmelist.sort(sortfun)
+
+       def dump(self):
+               for b in self.bmelist:
+                       print str(b)
+
+       # AWFUL DIRTY NO GOOD HACK
+       # (but does find the end of the song within 10ms)
+       def length(self):
+               if len(self.beatfunc) >= 2:
+                       t = self.beatfunc[-2][0]
+               else:
+                       t = 0
+               last_beat = self.bmelist[-1].beat
+               b = self.eval_beatfunc(t)
+               while b < last_beat:
+                       t+= 10
+                       b = self.eval_beatfunc(t)
+               return t
+
+       # It's time to get func-y
+       def generate_beatfunc(self):
+               self.beatfunc = []
+               bpms = filter(lambda x: x.type & (BME_TEMPO | BME_LONGMEASURE | BME_STOP), self.bmelist)
+               self.beatfunc = self.generate_beatfunc_r(bpms)
+
+       def generate_beatfunc_r(self, bpms, ct=0):
+               if len(bpms) == 0:
+                       return [(3600000.0,0)]
+               beat = bpms[0].beat
+               type = bpms[0].type
+
+               if type == BME_TEMPO:
+                       ms_per_beat = 60000.0 / bpms[0].dataref
+                       func = lambda t: (t - ct) / ms_per_beat + beat
+                       if len(bpms) == 1:
+                               next = 4000
+                       else:
+                               next = bpms[1].beat
+                       duration = (next - beat) * ms_per_beat
+                       self.lastbpm = bpms[0].dataref
+               elif type == BME_LONGMEASURE:
+                       # Long measures only last one measure, so we do
+                       # the slow measure just like a tempo change,
+                       # then add a tempo change back at the end.
+                       ms_per_beat = (60000.0 / self.lastbpm) * bpms[0].dataref
+                       func = lambda t: (t - ct) / ms_per_beat + beat
+
+                       print "Last BPM:",self.lastbpm
+                       duration = 4 * ms_per_beat
+                       bpms.insert(1,BMEvent(beat+4,BME_TEMPO,0,self.lastbpm))
+               elif type == BME_STOP:
+                       func = lambda t: beat
+                       duration = bpms[0].dataref
+               else:
+                       raise Exception("WTF? Invalid type in generate_beatfunc_r")
+               l = self.generate_beatfunc_r(bpms[1:], ct + duration)
+               l.insert(0,(ct,func))
+               return l
+
+       def eval_beatfunc(self,t):
+               for n in range(0,len(self.beatfunc)-1):
+                       if self.beatfunc[n][0] <= t and self.beatfunc[n+1][0] > t:
+                               return self.beatfunc[n][1](t)
+
+       def show_beatfunc(self,surface):
+               end = self.length()
+               xscale = end / surface.get_width()
+               yscale = self.bmelist[-1].beat / surface.get_height()
+
+               for t in range(0,end,100):
+                       beat = self.eval_beatfunc(t)
+                       if beat:
+                               pygame.draw.circle(surface,(255,255,255),(t/xscale,480 - beat / yscale),1)
+                       else:
+                               pygame.draw.line(surface,(255,0,0), (t/140.625,0), (t/140.625,480))
+
+
+class BMEListIter:
+       bmelist = None
+
+       def __init__(self,bmelist):
+               self.bmelist = bmelist 
+               self.b = 0.0
+               self.i = 0
+
+       def goto(self,b):
+               l = len(self.bmelist) - 1
+               self.b = b
+
+               while self.i > 0 and b < self.bmelist[self.i].beat:
+                       self.i -= 1
+               while self.i < l and b > self.bmelist[self.i].beat:
+                       self.i += 1
+
+       def window(self,db,type=None):
+               l = len(self.bmelist)
+               eb = self.b + db
+               ei = self.i
+
+               while ei < l and eb >= self.bmelist[ei].beat:
+                       ei += 1
+
+               if type:
+                       return filter(lambda x: x.type & type, self.bmelist[self.i:ei])
+               else:
+                       return self.bmelist[self.i:ei]
+
+
+screen = None
+font = None
+
+loaders = []
+for x in ["BMloader","SMloader"]:
+       f = open("loaders/" + x + ".py")
+       loaders.append(imp.load_module(x,f,x + ".py",(".py",'r',imp.PY_SOURCE)))
+       #f.close()
+
+def vmessage(message):
+       fs = font.render(message, 0, (255,255,255),(0,0,0))
+       screen.fill((0,0,0),(0,450,640,30))
+       screen.blit(fs,(0,450))
+
+def vstatus(type,arg):
+       if type == "STAGEFILE":
+               screen.blit(arg,(0,0))
+       elif type == "WAV":
+               vmessage("Loaded WAV " + arg)
+       elif type == "BMP":
+               vmessage("Loaded BMP " + arg)
+       elif type == "TRACK":
+               vmessage("Parsing track " + str(arg))
+       elif type == "ERROR":
+               vmessage("ERROR: " + arg)
+       pygame.display.flip()
+
+def likelihood(file):
+       likelihoods = map(lambda l: l.detect(file), loaders)
+       gl = 0.0
+       gn = None
+       for n in range(0,len(likelihoods)):
+               if likelihoods[n] > gl:
+                       gl = likelihoods[n]
+                       gn = n
+       return gn
+
+def kf_load(file):
+       global screen,font
+       screen = pygame.display.get_surface()
+       font = pygame.font.SysFont("Helvetica Normal",30)
+       gn = likelihood(file)
+       if gn != None:
+               print "Load..."
+               kf = loaders[gn].load(file,vstatus)
+               kf.generate_beatfunc()
+               return kf
+       else:
+               return None
+       font = None
+
+def kf_info(file):
+       gn = likelihood(file)
+       d = loaders[gn].info(file)
+       d['loader.name'] = loaders[gn].name
+       d['loader.version'] = loaders[gn].version
+
+       return d
diff --git a/keygraph.py b/keygraph.py
new file mode 100644 (file)
index 0000000..0468075
--- /dev/null
@@ -0,0 +1,189 @@
+import pygame
+from bmevent import *
+from keyhandler import *
+from keyfile import BMEListIter
+from fx import ColumnPulse
+from constants import *
+
+red = (255,30,30)
+offwhite = (245,245,245)
+blue = (30,30,255)
+black = (0,0,0)
+darkgray = (20,20,20)
+
+class KeyGraph:
+       keyfile = None
+       t = 0
+       b = 0
+       velocity = 150.0
+       li = None
+       bordercolor = (255,255,255)
+       judgemapping = {JUDGE_FGREAT: ["gfx/great.png","gfx/fgreat.png"],
+                       JUDGE_GREAT: ["gfx/great.png"],
+                       JUDGE_GOOD: ["gfx/good.png"],
+                       JUDGE_BAD: ["gfx/bad.png"],
+                       JUDGE_POOR: ["gfx/poor.png"],
+                       JUDGE_NA: []}
+       colmarkmap = {JUDGE_FGREAT: "gfx/dcircle.png",
+                     JUDGE_GREAT: "gfx/circle.png",
+                     JUDGE_GOOD: "gfx/triangle.png",
+                     JUDGE_BAD: "gfx/cross.png",
+                     JUDGE_POOR: "gfx/cross.png",
+                     JUDGE_NA: None}
+       numberfiles = ["gfx/glyph/0.png","gfx/glyph/1.png","gfx/glyph/2.png","gfx/glyph/3.png","gfx/glyph/4.png","gfx/glyph/5.png","gfx/glyph/6.png","gfx/glyph/7.png","gfx/glyph/8.png","gfx/glyph/9.png"]
+       
+       def __init__(self, kf, keystyle, player, disprect):
+               self.numkeys = len(keystyle)
+               self.keyfile = kf
+               self.screen = pygame.display.get_surface()
+               self.disprect = disprect
+               self.vheight = disprect.height - 5;
+               self.barrect = pygame.Rect(disprect.left,
+                                          disprect.bottom - 4,
+                                          disprect.width,
+                                          5)
+               if player == 1:
+                       self.notefilter = BME_NOTE1
+               elif player == 2:
+                       self.notefilter = BME_NOTE2
+               else:
+                       print "Invalid player given to keygraph: " + kf
+                       exit(1)
+               self.t = 0
+               self.b = 0
+               self.li = BMEListIter(self.keyfile.bmelist)
+               self.keystyle = []
+
+               w = 0
+               tw = sum(map(lambda x: x[0],keystyle))
+               for x in keystyle:
+                       self.keystyle.append((int(w),x[1],x[2]))
+                       w += (x[0] / tw) * (disprect.width - 1)
+               self.keystyle.append((int(w),None,None))
+
+               # Load gfx
+               self.judgeimg = {}
+               for k in self.judgemapping.keys():
+                       self.judgeimg[k] = map(lambda y: pygame.image.load(y).convert_alpha(),self.judgemapping[k])
+               self.colmarkimg = {}
+               for k in self.colmarkmap.keys():
+                       if self.colmarkmap[k]:
+                               self.colmarkimg[k] = pygame.image.load(self.colmarkmap[k]).convert_alpha()
+                       else:
+                               self.colmarkimg[k] = None
+               self.colmarkimg[0] = None
+               self.numberimg = map(lambda y: pygame.image.load(y).convert_alpha(),self.numberfiles)
+               self.judgement = 0
+               self.kjudgement = [0 for x in range(0,self.numkeys)]
+               self.judgetime = 0
+               self.kjudgetime = [0 for x in range(0,self.numkeys)]
+               self.combo = 0
+
+               # Initialize keypulses (they are neat)
+               self.pulse = []
+               for n in range(0,len(keystyle)):
+                       x = self.keystyle[n][0] + 1
+                       y = self.disprect.top + 0.3 * self.disprect.height
+                       w = self.keystyle[n+1][0] - self.keystyle[n][0] - 1
+                       h = 0.7 * self.disprect.height - 4
+                       self.pulse.append(ColumnPulse(pygame.Rect(x,y,w,h),keystyle[n][3]))
+               self.accuracy = 0
+               self.key = 0
+
+       def setproperties(self,properties):
+               try:
+                       ud = properties['ud']
+                       k = properties['key']
+                       if ud == 1:
+                               self.pulse[k].down()
+                       else:
+                               self.pulse[k].up()
+                       self.key = k
+               except KeyError:
+                       pass
+
+               try:
+                       self.judgement = properties['judgement']
+                       self.kjudgement[self.key] = properties['judgement']
+                       self.judgetime = self.t
+                       self.kjudgetime[self.key] = self.t
+                       if self.judgement <= JUDGE_GOOD:
+                               self.combo += 1
+                       else:
+                               self.combo = 0
+               except KeyError:
+                       pass
+
+               try:
+                       self.accuracy = properties['accuracy']
+               except KeyError:
+                       pass
+
+       def numimgs(self,n):
+               imgs = []
+               while n:
+                       imgs.append(self.numberimg[n % 10])
+                       n = n / 10
+               imgs.reverse()
+               return imgs
+
+       def draw(self):
+               self.b = self.keyfile.eval_beatfunc(self.t)
+               for n in range(0,self.numkeys):
+                       x = self.disprect.left + self.keystyle[n][0]
+                       w = self.keystyle[n+1][0] - self.keystyle[n][0]
+                       self.screen.fill(self.keystyle[n][2],
+                               (x,self.disprect.top,w,self.disprect.height))
+               for n in range(0,self.numkeys+1):
+                       pygame.draw.line(self.screen, self.bordercolor,
+                               (self.disprect.left + self.keystyle[n][0], self.disprect.top),
+                               (self.disprect.left + self.keystyle[n][0], self.disprect.bottom - 1) )
+
+               self.li.goto(self.b)
+               keylist = self.li.window(self.vheight / self.velocity, self.notefilter)
+               pygame.draw.rect(self.screen, (255,0,0), self.barrect)
+               distortion = 1.0
+               for k in keylist:
+                       y = self.disprect.top + self.vheight - int((k.beat - self.b) * (self.velocity / distortion))
+                       x = self.disprect.left + self.keystyle[k.key][0] + 1
+                       w = self.keystyle[k.key+1][0] - self.keystyle[k.key][0] - 1
+                       pygame.draw.rect(self.screen, self.keystyle[k.key][1], (x,y,w,5) )
+               for n in self.pulse:
+                       n.draw()
+
+               if self.judgement:
+                       if self.t - self.judgetime > 500:
+                               self.judgement = 0
+                       elif len(self.judgeimg[self.judgement]) == 0:
+                               pass
+                       else:
+                               z = (self.t / 60) % len(self.judgeimg[self.judgement])
+                               w = self.judgeimg[self.judgement][z].get_width()
+                               x = self.disprect.left + (self.disprect.width / 2) - (w / 2)
+                               y = self.disprect.top + self.disprect.height * 0.7
+                               if self.combo >= 2:
+                                       imgs = self.numimgs(self.combo)
+                                       x -= 2
+                                       w += 4
+                                       for i in imgs:
+                                               x -= i.get_width() / 2
+                                       for i in imgs:
+                                               self.screen.blit(i,(x + w,y))
+                                               w += i.get_width()
+                               self.screen.blit(self.judgeimg[self.judgement][z],(x,y))
+                               # Draw accuracy bar
+                               x = self.disprect.left + self.disprect.width / 2
+                               y += 50
+                               dx = self.accuracy
+                               if dx < 0:
+                                       x += dx
+                                       dx = -dx
+                               self.screen.fill((255,0,0), (x,y,dx,3))
+               for i in range(0,self.numkeys):
+                       if self.t - self.kjudgetime[i] > 300:
+                               self.kjudgement[i] = 0
+                       else:
+                               x = self.disprect.left + self.keystyle[i][0] + (self.keystyle[i+1][0] - self.keystyle[i][0] - 24) / 2
+                               y = self.disprect.bottom - 30
+                               if self.colmarkimg[self.kjudgement[i]]:
+                                       self.screen.blit(self.colmarkimg[self.kjudgement[i]],(x,y))
diff --git a/keyhandler.py b/keyhandler.py
new file mode 100644 (file)
index 0000000..f06e344
--- /dev/null
@@ -0,0 +1,100 @@
+from bmevent import *
+from keyfile import *
+from pygame.locals import *
+from constants import *
+
+class KeyHandler:
+       keyfile = None
+       li = None
+
+       keymap = None
+       jsbuttonmap = None
+       jsaxismap = None
+       timings = None
+
+       def __init__(self,gameconfig,keyfile):
+               self.keyfile = keyfile
+               self.li = BMEListIter(keyfile.bmelist)
+               self.lastdataref = [0,0,0,0,0,0,0,0]
+               self.li.goto(0)
+               for b in self.li.window(1.0):
+                       if b.type == BME_TEMPO:
+                               self.bpm = b.dataref
+                               self.adjust_window()
+                               break
+               self.keymap = gameconfig['keymap']
+               self.jsbuttonmap = gameconfig['jsbuttonmap']
+               self.jsaxismap = gameconfig['jsaxismap']
+               self.timings = gameconfig['timings']
+
+       def decodekey(self,event):
+               try: 
+                       if event.type == KEYDOWN:
+                               return (self.keymap[event.key],1)
+                       elif event.type == KEYUP:
+                               return (self.keymap[event.key],0)
+                       elif event.type == JOYBUTTONDOWN:
+                               return (self.jsbuttonmap[event.button],1)
+                       elif event.type == JOYBUTTONUP:
+                               return (self.jsbuttonmap[event.button],0)
+                       elif event.type == JOYAXISMOTION:
+                               if event.value == 0.0:
+                                       return (self.jsaxismap[(event.axis,event.value)],0)
+                               else:
+                                       return (self.jsaxismap[(event.axis,event.value)],1)
+                       else:
+                               return (None,-1)
+               except KeyError:
+                       return (None,-1)
+
+       def adjust_window(self):
+               self.ms_per_beat = 60000.0 / self.bpm
+               self.tstart = 200.0 / self.ms_per_beat
+               self.tlength = 2000.0 / self.ms_per_beat
+
+       def setproperties(self,properties):
+               try:
+                       self.bpm = properties['bpm']
+                       self.adjust_window()
+               except KeyError:
+                       pass
+
+       def handle(self,event,t):
+               b = self.keyfile.eval_beatfunc(t)
+               properties = {}
+               (k,ud) = self.decodekey(event)
+               if k == None:
+                       return
+               properties['key'] = k
+               properties['ud'] = ud
+               if ud == 0:
+                       return (None, properties)
+               self.li.goto(b - self.tstart)
+               bmelist = filter(lambda x: x.key == k, self.li.window(self.tlength,BME_NOTE1))
+               if not bmelist:
+                       properties['judgement'] = JUDGE_NA
+                       return (BMEvent(b,BME_HIT,k,self.lastdataref[k]), properties)
+
+               # Find the closest bme to our event
+               sd = self.tstart
+               cbme = None
+               for bme in bmelist:
+                       d = abs(b - bme.beat)
+                       if d < sd:
+                               sd = d
+                               cbme = bme
+               # Convert back to ms
+               sd = sd * self.ms_per_beat
+
+               if cbme:
+                       for bt in self.timings:
+                               if sd < bt[0]:
+                                       properties['judgement'] = bt[1]
+                                       cbme.type = BME_HIT
+                                       break
+                       self.lastdataref[k] = cbme.dataref
+               else:
+                       properties['judgement'] = JUDGE_NA
+                       return (BMEvent(b,BME_HIT,k,self.lastdataref[k]),properties)
+               properties['accuracy'] = (b - cbme.beat) * self.ms_per_beat
+               return (cbme, properties)
diff --git a/loaders/BMloader.py b/loaders/BMloader.py
new file mode 100644 (file)
index 0000000..591bd01
--- /dev/null
@@ -0,0 +1,175 @@
+import pygame
+import os.path
+import re
+from bmevent import *
+from keyfile import *
+from util import *
+
+# Required information for a loader module. If this isn't here, the game 
+# will crash and burn and it will be ALL YOUR FAULT.
+name = "BeMusic BMS/BME"
+version = 0.0
+
+def detect(file):
+       res = [re.compile("^#PLAYLEVEL"), re.compile("^#WAV"), re.compile("^#BMP")]
+       match = [0,0,0]
+
+       f = open(file)
+       for line in f:
+               for n in range(0,len(res)):
+                       if res[n].match(line):
+                               match[n] = 1
+       return sum(match) / float(len(match))
+
+def info(file):
+       d = {}
+       f = open(file,'r')
+       r = re.compile('(WAV|BMP|\d{5}:)')
+       for line in f:
+               if len(line) == 0 or line[0] != "#":
+                       continue
+               line = line[1:]
+               l = line.split(' ')
+               arg = ''
+               cmd = l[0]
+               if len(l) >= 2:
+                       arg = ' '.join(l[1:])
+               if not r.match(cmd):
+                       d[cmd.lower()] = arg.strip()
+       return d
+
+def load(file,status=lambda x,y:None):
+       keymapping = {0:1, 1:2, 2:3, 3:4, 4:5, 5:0, 7:6, 8:7}
+       kf = KeyFile()
+       kf.numkeys = len(keymapping)
+       lastbpm = 0
+       dir = os.path.dirname(file)
+       f = open(file,'r')
+       kf.track = -1
+       for line in f:
+               line = line.strip()
+               if len(line) == 0 or line[0] != "#":
+                       continue
+               line = line[1:]
+               l = line.split(' ')
+               arg = ''
+               cmd = l[0]
+               if len(l) >= 2:
+                       arg = ' '.join(l[1:])
+               if cmd == "PLAYER":
+                       kf.player = arg
+               elif cmd == "GENRE":
+                       kf.genre = arg
+               elif cmd == "TITLE":
+                       kf.title = arg
+               elif cmd == "ARTIST":
+                       kf.artist = arg
+               elif cmd == "BPM":
+                       try:
+                               lastbpm = float(arg)
+                               kf.add(BMEvent(0, BME_TEMPO, None, lastbpm))
+                       except ValueError:
+                               print "Invalid value for BPM"
+                       #kf.ms_per_measure = 240000.0 / kf.bpm
+               elif cmd == "PLAYLEVEL":
+                       try:
+                               kf.playlevel = int(arg)
+                       except ValueError:
+                               print "Invalid value for PLAYLEVEL"
+               elif cmd == "RANK":
+                       try:
+                               kf.rank = int(arg)
+                       except ValueError:
+                               print "Invalid value for RANK"
+               elif cmd == "STAGEFILE":
+                       # This will stay here for now. Eventually, this
+                       # will go into the "status" routine or somewhere
+                       # further up.
+                       if arg:
+                               kf.stagefile = loadBMP(os.path.join(dir,arg))
+                               status("STAGEFILE",kf.stagefile)
+               elif cmd == "VOLWAV":
+                       kf.volwav = float(arg) / 100.0
+               elif cmd[0:3] == "WAV":
+                       slot = int(cmd[3:5],36)
+                       if arg[-3:].lower() == 'mp3':
+                               wav = loadMP3(os.path.join(dir,arg))
+                       else:
+                               wav = loadWAV(os.path.join(dir,arg))
+                               if wav:
+                                       wav.set_volume(kf.volwav)
+                       if wav:
+                               kf.wavs[slot] = wav
+                               status("WAV",arg)
+                       else:
+                               status("ERROR","Could not load " + arg)
+                               pygame.time.wait(1500)
+               elif cmd[0:3] == "BMP":
+                       slot = int(cmd[3:5],36)
+                       bmp = loadBMP(os.path.join(dir,arg))
+                       if bmp:
+                               kf.bmps[slot] = bmp
+                               status("BMP",arg)
+                       else:
+                               status("ERROR","Could not load " + arg)
+               elif len(cmd) > 5 and cmd[5] == ":":
+                       # Hmm. Should "track" really be "measure"?
+                       track = int(cmd[0:3])
+                       channel = int(cmd[3:5])
+                       message = cmd[6:]
+
+                       if channel == 2:
+                               # Channel 2 is a floating-point
+                               # multiplier that changes the length of
+                               # the measure.
+                               kf.add(BMEvent(track*4, BME_LONGMEASURE, None, float(message)))
+                               continue
+                       if track != kf.track:
+                               status("TRACK",track)
+                       kf.track = track
+
+                       v = [int(message[n*2:n*2+2],36) for n in range(0,len(message)/2)]
+                       l = float(len(v)) / 4.0
+
+                       bme = None
+                       if channel == 1:
+                               for n in range(0,len(v)):
+                                       if v[n] == 0:
+                                               continue
+                                       bme = BMEvent(track*4 + n / l, BME_BGM, None, v[n])
+                                       kf.add(bme)
+                       if channel == 3:
+                               v = [int(message[n*2:n*2+2],16) for n in range(0,len(message)/2)]
+                               for n in range(0,len(v)):
+                                       # WTF is up with the low tempos?
+                                       if v[n] == 0 or v[n] < 30:
+                                               continue
+                                       print "new BPM: " + str(v[n])
+                                       bme = BMEvent(track*4 + n / l, BME_TEMPO, None, v[n])
+                                       kf.add(bme)
+                                       lastbpm = v[n]
+                       if channel == 4:
+                               for n in range(0,len(v)):
+                                       if v[n] == 0:
+                                               continue
+                                       bme = BMEvent(track*4 + n / l, BME_BGA, None, v[n])
+                                       kf.add(bme)
+                       elif (channel >= 11 and channel <= 19) or (channel >= 21 and channel <= 29):
+                               if channel >= 21 and channel <= 29:
+                                       type = BME_NOTE2
+                                       b = 21
+                               else:
+                                       type = BME_NOTE1
+                                       b = 11
+                               for n in range(0,len(v)):
+                                       if v[n] == 0 or not (channel - b) in keymapping:
+                                               continue
+                                       bme = BMEvent(track*4 + n / l, type, keymapping[channel - b], v[n])
+                                       kf.add(bme)
+               else:
+                       print "Unknown command:",cmd
+       kf.sort()
+       f.close()
+
+       return kf
+
diff --git a/loaders/SMloader.py b/loaders/SMloader.py
new file mode 100644 (file)
index 0000000..1918aa6
--- /dev/null
@@ -0,0 +1,114 @@
+import pygame
+import os.path
+import re
+from bmevent import *
+from keyfile import *
+
+# Required information for a loader module. If this isn't here, the game
+# will crash and burn and it will be ALL YOUR FAULT.
+name = "StepMania"
+version = 0.0
+
+def detect(file):
+       res = [re.compile("^#SUBTITLE"), re.compile("^#BANNER"),
+              re.compile("^#BACKGROUND"),re.compile("^#NOTES")]
+       match = [0,0,0,0]
+
+       f = open(file)
+       for line in f:
+               for n in range(0,len(res)):
+                       if res[n].match(line):
+                               match[n] = 1
+       return sum(match) / float(len(match))
+
+def info(file):
+       return {}
+
+def load(file,status=lambda x,y:None):
+       keymapping = {0:1, 1:2, 2:3, 3:4, 4:5, 5:0, 7:6, 8:7}
+       kf = KeyFile()
+       kf.numkeys = len(keymapping)
+       dir = os.path.dirname(file)
+       f = open(file,'r')
+       kf.track = -1
+       matcher = re.compile("#([A-Z]+):(.*);",re.DOTALL)
+       kf.stoplist = []
+       buf = ''
+       for line in f:
+               line = line.split('//')[0];
+               line = line.strip()
+               if len(line) == 0:
+                       continue
+               buf += line
+               m = matcher.match(buf)
+               if m:
+                       buf = '';
+                       cmd = m.group(1)
+                       arg = m.group(2).split(':')
+               else:
+                       continue
+
+               if cmd == "TITLE":
+                       kf.title = arg[0]
+               elif cmd == "SUBTITLE":
+                       kf.subtitle = arg[0]
+               elif cmd == "ARTIST":
+                       kf.artist = arg[0]
+               elif cmd == "BPMS":
+                       bpmlist = map(lambda x: x.split('='),arg[0].split(','))
+                       kf.bpm = bpmlist[0][1]
+                       for b in bpmlist:
+                               kf.add(BMEvent(float(b[0]), BME_TEMPO, None, float(b[1])))
+               elif cmd == "STOPS":
+                       if arg[0]:
+                               stoplist = map(lambda x: x.split('='),arg[0].split(','))
+                               for s in stoplist:
+                                       print s[0]
+                                       kf.add(BMEvent(float(s[0]), BME_STOP, None, int(float(s[1])*1000)))
+               elif cmd == "BACKGROUND":
+                       if arg[0]:
+                               try:
+                                       kf.stagefile = pygame.image.load(os.path.join(dir,arg[0]))
+                                       kf.stagefile = kf.stagefile.convert()
+                                       status("STAGEFILE",kf.stagefile)
+                               except pygame.error:
+                                       pass
+               elif cmd == "OFFSET":
+                       kf.offset = float(arg[0])
+                       kf.add(BMEvent(int(kf.offset * 1000), BME_BGM, None, 255))
+               elif cmd == "MUSIC":
+                       print os.path.join(dir,arg[0])
+                       try:
+                               pygame.mixer.music.load(os.path.join(dir,arg[0]))
+                        except pygame.error:
+                               pygame.mixer.music.load(os.path.join(dir,arg[0].lower()))
+                       kf.wavs[255] = pygame.mixer.music
+               elif cmd == "NOTES":
+                       if arg[0] == 'dance-single' and arg[2].lower() == 'challenge':
+                               parse_notes(arg[5],kf)
+               else:
+                       print "Unknown command:",cmd
+       kf.sort()
+       f.close()
+
+       return kf
+
+# BUGS! This assumes a four-column chart.
+def parse_notes(str,kf):
+       tracks = str.split(',')
+       track = 0
+
+       for t in tracks:
+               l = len(t) / 4
+               for n in range(0,l):
+                       for m in range(0,4):
+                               k = t[n*4+m]
+                               if k == 'M':    # Freaking mines...
+                                       continue
+                               k = int(k)
+                               if k > 0:
+                                       beat = 4 * (track + (float(n)/l))
+                                       # Add note filter here?
+                                       kf.add(BMEvent(beat, BME_NOTE1, m*2+1, 1))
+               track += 1
+
diff --git a/mainmenu.py b/mainmenu.py
new file mode 100644 (file)
index 0000000..5fce8d2
--- /dev/null
@@ -0,0 +1,130 @@
+import pygame
+import thread
+from fx import *
+import event
+
+class MainMenu:
+       fx = None
+       fxlock = None
+       period = 428
+       circuitlist = [[(590,0), (550,40), (200,40), (170,70), (170,310)],
+                      [(570,0), (540,30), (190,30), (160,60), (20,60), (20,220), (50,250), (50,420), (90,460), (610,460)],
+                      [(0,220), (40,260), (40,480)],
+                      [(180,0), (180,20), (150,50), (20,50), (20,0)]]
+       # (label, message)
+       menuitems = [("play","fileselect"), ("quit","quit")]
+       subtitle = "holy shit parallax!!!11!1"
+       version = 1e-99
+
+       def __init__(self):
+               self.fxlock = thread.allocate_lock()
+               self.fx = []
+               self.menufx = []
+               self.position = 0
+               self.bar = PulseLine([(0,0),(130,0)],(200,200,255),self.period,5)
+               thread.start_new_thread(self.fxloader,())
+
+       def fxloader(self):
+               self.fxlock.acquire()
+               self.fx.append(Blank())
+               for l in self.circuitlist:
+                       o = PulseLine(l,(255,30,30),self.period)
+                       self.fx.append(o)
+               self.fxlock.release()
+
+               o = Image('gfx/title.png',(200,40))
+               self.fxlock.acquire()
+               self.fx.append(o)
+               self.fxlock.release()
+
+               n = 0
+               for l in self.menuitems:
+                       o = Text(l[0],'font/Nano.ttf',18,(30,70 + 20*n), (255,30,30))
+                       self.fxlock.acquire()
+                       self.fx.append(o)
+                       self.menufx.append(o)
+                       self.fxlock.release()
+                       n += 1
+
+               o = Text(self.subtitle,'font/Nano.ttf',15,(600,100),(255,30,30))
+               o.align = ALIGN_RIGHT
+               self.fxlock.acquire()
+               self.fx.append(o)
+               self.fxlock.release()
+
+               o = Text("version " + str(self.version),'font/Nano.ttf',14,(610,440),(255,30,30))
+               o.align = ALIGN_RIGHT
+               self.fxlock.acquire()
+               self.fx.append(o)
+               self.fxlock.release()
+
+               o = PlaneScroll('gfx/chips_layer1.jpg',(-0.12,0.06),2.0,0.75)
+               self.fxlock.acquire()
+               self.fx.insert(1,o)
+               self.fxlock.release()
+
+               o = PlaneScroll('gfx/chips_layer2.jpg',(-0.06,0.03))
+               self.fxlock.acquire()
+               self.fx.remove(self.fx[0])
+               self.fx.insert(0,o)
+               self.fxlock.release()
+                       
+       def draw(self,t):
+               self.fxlock.acquire()
+               for f in self.fx:
+                       f.draw(t)
+               self.fxlock.release()
+               self.bar.location = (30, 68 + 20*self.position)
+               self.bar.draw(t)
+               self.bar.location = (30, 68 + 20*self.position + 20)
+               self.bar.draw(t)
+
+       def run(self):
+               screen = pygame.display.get_surface()
+               decided = 0
+               pygame.mixer.music.load('snd/0x00.ogg')
+               pygame.mixer.music.set_volume(0.75)
+               pygame.mixer.music.play(-1)
+               stab = pygame.mixer.Sound('snd/stab.ogg')
+               stab.set_volume(0.25)
+               start_t = pygame.time.get_ticks()
+
+               while decided == 0:
+                       t = pygame.time.get_ticks() - start_t
+                       for e in pygame.event.get():
+                               act = event.parseevent(e)
+                               if act == event.CANCEL:
+                                       decided = 'quit'
+                               elif act == event.DOWN:
+                                       self.position += 1
+                               elif act == event.UP:
+                                       self.position -= 1
+                               elif act == event.OK:
+                                       decided = self.menuitems[self.position][1]
+
+                       self.position = self.position % len(self.menuitems)
+                       self.draw(t)
+                       pygame.display.flip()
+
+               pygame.mixer.music.fadeout(1500)
+               stab.play()
+
+               start_t2 = pygame.time.get_ticks()
+               dt = 0
+               blanksurf = pygame.surface.Surface((640,480)).convert()
+               blanksurf.fill( (0,0,0) )
+               while dt < 1500:
+                       dt = pygame.time.get_ticks() - start_t2
+                       if dt < 1000:
+                               self.draw(pygame.time.get_ticks() - start_t)
+                               blanksurf.set_alpha(int(dt / 1000.0 * 255.0))
+                               screen.blit(blanksurf,(0,0))
+                       else:
+                               screen.fill( (0,0,0) )
+                       self.menufx[self.position].draw(0)
+                       if dt >= 1000:
+                               blanksurf.set_alpha(int((dt-1000) / 500.0 * 255.0))
+                               screen.blit(blanksurf,(0,0))
+                       pygame.display.flip()
+
+               return [decided]
diff --git a/mksongcache.py b/mksongcache.py
new file mode 100755 (executable)
index 0000000..03549c3
--- /dev/null
@@ -0,0 +1,6 @@
+#!/usr/bin/python
+
+import fileselect
+
+fileselect.mksongcache()
+
diff --git a/rotowidget.py b/rotowidget.py
new file mode 100644 (file)
index 0000000..6b9fef3
--- /dev/null
@@ -0,0 +1,44 @@
+import pygame
+from bmevent import *
+from keyhandler import *
+from math import exp,pi,sqrt
+
+# A rotowidget displays two axes of information in a rotary fashion.  A
+# central "pulser" is designed to show the score, and an arc around the
+# edge shows the progress through the song.
+class RotoWidget:
+       # Color format: (level, color)
+       # If the first item's level is greater than zero, the first
+       # color is black
+       pulsecolors = [(0,(180,0,0)), (80,(50,255,0))]
+       arccolors = [(0,(200,0,200))]
+       r = -0.2
+       
+       def __init__(self,center,radius,period):
+               self.screen = pygame.display.get_surface()
+               self.center = center
+               self.radius = radius
+               self.period = period
+               self.pulsev = 0
+               self.arcv = 0
+               self.t = 0
+
+       def draw(self,t):
+               rad = self.radius * (self.pulsev / 100.0) + (0.1 * self.radius) * exp(self.r * (t % self.period))
+               if rad > self.radius:
+                       rad = self.radius
+
+               pc = (0,0,0)
+               for n in range(len(self.pulsecolors)-1,-1,-1):
+                       if self.pulsecolors[n][0] <= self.pulsev:
+                               pc = self.pulsecolors[n][1]
+                               break
+               pygame.draw.circle(self.screen,pc,self.center,rad,0)
+
+               ac = (0,0,0)
+               for n in range(len(self.arccolors)-1,-1,-1):
+                       if self.arccolors[n][0] <= self.arcv:
+                               ac = self.arccolors[n][1]
+                               break
+               pygame.draw.arc(self.screen,ac,(self.center[0]-self.radius, self.center[1]-self.radius, self.radius*2, self.radius*2),0,2*pi*(self.arcv / 100.0),3)
+
diff --git a/snd/0x00.ogg b/snd/0x00.ogg
new file mode 100644 (file)
index 0000000..2b9ce02
Binary files /dev/null and b/snd/0x00.ogg differ
diff --git a/snd/0x01.ogg b/snd/0x01.ogg
new file mode 100644 (file)
index 0000000..9f7c3b3
Binary files /dev/null and b/snd/0x01.ogg differ
diff --git a/snd/stab.ogg b/snd/stab.ogg
new file mode 100644 (file)
index 0000000..3b814cc
Binary files /dev/null and b/snd/stab.ogg differ
diff --git a/songs/README.txt b/songs/README.txt
new file mode 100644 (file)
index 0000000..d716355
--- /dev/null
@@ -0,0 +1 @@
+Put your songs here.
diff --git a/util.py b/util.py
new file mode 100644 (file)
index 0000000..230a765
--- /dev/null
+++ b/util.py
@@ -0,0 +1,33 @@
+import pygame
+import os.path
+
+import config
+
+def loadWAV(path,library=False):
+       try:
+               return pygame.mixer.Sound(path)
+       except pygame.error:
+               (dir,file) = os.path.split(path)
+               try:
+                       return pygame.mixer.Sound(os.path.join(dir,file.lower()))
+               except pygame.error:
+                       if library:
+                               return None
+                       else:
+                               # Ok, try the system sample library...
+                               return loadWAV(os.path.join(config.librarypath,file),True)
+
+def loadMP3(path):
+       pygame.mixer.music.load(path)
+       return pygame.mixer.music
+
+def loadBMP(path):
+       try:
+               return pygame.image.load(path).convert()
+       except pygame.error:
+               (dir,file) = os.path.split(path)
+               try:
+                       return pygame.image.load(os.path.join(dir,file.lower())).convert()
+               except pygame.error:
+                       return None
+