Permalink
Browse files

NEW: eb.CccInterfaceModule: Allows for integration of CoilSnake and C…

…CScript

eb.EbTablesModule: CCScript labels can be used instead of pointers
Added a simple GUI Frontend that interfaces with the CCScript compiler
Fixed some of the unit tests
  • Loading branch information...
mrtenda committed Apr 2, 2012
1 parent 33b09f0 commit bca66b34f9babf890ca589538ca4a009482121c5
Showing with 331 additions and 54 deletions.
  1. +2 −0 .gitignore
  2. +53 −46 CoilSnake.py
  3. +192 −0 CoilSnakeGUI.py
  4. +9 −5 README.md
  5. +1 −0 modulelist.txt
  6. +18 −0 modules/Rom.py
  7. +41 −0 modules/eb/CccInterfaceModule.py
  8. +2 −0 modules/eb/EbModule.py
  9. +10 −0 modules/eb/EbTablesModule.py
  10. +1 −1 tests/test_EbModule.py
  11. +1 −1 tests/test_Project.py
  12. +1 −1 tests/test_Rom.py
View
@@ -4,3 +4,5 @@
roms/
*/*/build
*.so
+*.prof
+prefs.yml
View
@@ -7,86 +7,93 @@
from modules import Project
from modules import Rom
-def loadModules():
- modules = []
- with open('modulelist.txt', 'r') as f:
- for line in f:
- line = line.rstrip('\n')
- if line[0] == '#':
- continue
- mod = __import__("modules." + line)
- components = line.split('.')
- for comp in components:
- mod = getattr(mod, comp)
- modules.append((line, getattr(mod,components[-1])()))
- return modules
-def main():
- parser = argparse.ArgumentParser()
- parser.add_argument('--cleanrom', dest='cleanrom', required=False,
- type=argparse.FileType('rb'), help="a clean, unmodified ROM")
- parser.add_argument('input', metavar='INPUT', type=argparse.FileType('r'),
- help="either a ROM or a CoilSnake project file")
- parser.add_argument('output', metavar='OUTPUT',
- help="either a ROM or a CoilSnake project file")
- args = parser.parse_args()
-
- output_is_proj = os.path.splitext(args.output)[1] == ".csp"
- if (not output_is_proj) and (args.cleanrom == None):
- print >> sys.stderr, "ERROR: Need a clean ROM to export to ROM"
- return
- input_is_proj = os.path.splitext(args.input.name)[1] == ".csp"
-
- modules = loadModules()
- romtype = ""
- # Load data into modules
- if input_is_proj and not output_is_proj:
+class CoilSnake:
+ def __init__(self):
+ self.loadModules()
+ def loadModules(self):
+ self._modules = []
+ with open('modulelist.txt', 'r') as f:
+ for line in f:
+ line = line.rstrip('\n')
+ if line[0] == '#':
+ continue
+ mod = __import__("modules." + line)
+ components = line.split('.')
+ for comp in components:
+ mod = getattr(mod, comp)
+ self._modules.append((line, getattr(mod,components[-1])()))
+ def projToRom(self, inputFname, cleanRomFname, outRomFname):
# Open project
proj = Project.Project()
- proj.load(args.input)
+ proj.load(inputFname)
# Open rom
rom = Rom.Rom("romtypes.yaml")
- rom.load(args.cleanrom)
+ rom.load(cleanRomFname)
# Make sure project type matches romtype
assert(rom.type() == proj.type())
# Make list of compatible modules
- curMods = filter(lambda (x,y): y.compatibleWithRomtype(rom.type()), modules)
+ curMods = filter(lambda (x,y): y.compatibleWithRomtype(rom.type()), self._modules)
# Add the ranges from the compatible modules to the free range list
newRanges = []
- for (n,m) in curMods:
+ for (n,m) in curMods:
newRanges += m.freeRanges()
rom.addFreeRanges(newRanges)
- print "Project:", args.input.name, " -> ROM:", args.output, "(", proj.type(), ")"
+ print "Project:", inputFname, " -> ROM:", outRomFname, "(", proj.type(), ")"
for (n,m) in curMods:
print "-", m.name(), "Module\t...",
sys.stdout.flush()
m.readFromProject(lambda x,y: proj.getResource(n,x,y,'r'))
m.writeToRom(rom)
m.free()
print "DONE"
- rom.save(args.output)
- elif not input_is_proj and output_is_proj:
+ rom.save(outRomFname)
+ def romToProj(self, inputRomFname, outputFname):
# Load the ROM
rom = Rom.Rom("romtypes.yaml")
- rom.load(args.input)
+ rom.load(inputRomFname)
# Load the Project
proj = Project.Project()
- proj.load(args.output, rom.type())
+ proj.load(outputFname, rom.type())
- print "ROM:", args.input.name, "-> Project:", args.output, "(", rom.type(), ")"
+ print "ROM:", inputRomFname, "-> Project:", outputFname, "(", rom.type(), ")"
for (n,m) in filter(
- lambda (x,y): y.compatibleWithRomtype(rom.type()), modules):
+ lambda (x,y): y.compatibleWithRomtype(rom.type()), self._modules):
print "-", m.name(), "Module\t...",
sys.stdout.flush()
m.readFromRom(rom)
m.writeToProject(lambda x,y: proj.getResource(n,x,y,'w'))
m.free()
print "DONE"
- proj.write(args.output)
+ proj.write(outputFname)
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--cleanrom', dest='cleanrom', required=False,
+ type=argparse.FileType('rb'), help="a clean, unmodified ROM")
+ parser.add_argument('input', metavar='INPUT', type=argparse.FileType('r'),
+ help="either a ROM or a CoilSnake project file")
+ parser.add_argument('output', metavar='OUTPUT',
+ help="either a ROM or a CoilSnake project file")
+ args = parser.parse_args()
+
+ output_is_proj = os.path.splitext(args.output)[1] == ".csproj"
+ if (not output_is_proj) and (args.cleanrom == None):
+ print >> sys.stderr, "ERROR: Need a clean ROM to export to ROM"
+ return
+ input_is_proj = os.path.splitext(args.input.name)[1] == ".csproj"
+
+ cs = CoilSnake()
+ # Load data into modules
+ if input_is_proj and not output_is_proj:
+ cs.projToRom(args.input.name, args.cleanrom, args.output)
+ elif not input_is_proj and output_is_proj:
+ cs.romToProj(args.input.name, args.output)
#import cProfile
if (__name__ == '__main__'):
sys.exit(main())
- #cProfile.run('main()', 'mainprof')
+ #cProfile.run('main()', 'main.prof')
#sys.exit(0)
View
@@ -0,0 +1,192 @@
+#! /usr/bin/env python
+
+from subprocess import Popen
+import os
+import yaml
+import shutil
+
+from Tkinter import *
+import tkFileDialog, tkMessageBox
+
+from CoilSnake import CoilSnake
+
+_version = "0.1"
+
+class CoilSnakeFrontend:
+ PREFS_FNAME = "prefs.yml"
+ def __init__(self):
+ self._cs = CoilSnake()
+ try:
+ with open(self.PREFS_FNAME, 'r') as f:
+ self._prefs = yaml.load(f)
+ except IOError:
+ self._prefs = {
+ 'Emulator': None,
+ 'CCC': None
+ }
+ def setEmulatorExe(self):
+ self._prefs["Emulator"] = tkFileDialog.askopenfilename(
+ parent=self._root,
+ title="Select an Emulator Executable",
+ initialfile=self._prefs["Emulator"])
+ with open(self.PREFS_FNAME, "w") as f:
+ yaml.dump(self._prefs, f)
+ def setCccExe(self):
+ self._prefs["CCC"] = tkFileDialog.askopenfilename(
+ parent=self._root,
+ title="Select the CCC Executable",
+ initialfile=self._prefs["CCC"])
+ with open(self.PREFS_FNAME, "w") as f:
+ yaml.dump(self._prefs, f)
+ def browseForRom(self, entry, save=False):
+ if save:
+ fname = tkFileDialog.asksaveasfilename(
+ parent=self._root, title="Select an output ROM",
+ filetypes=[('SNES ROMs','*.smc'), ('All files','*.*')])
+ else:
+ fname = tkFileDialog.askopenfilename(
+ parent=self._root, title="Select a ROM",
+ filetypes=[('SNES ROMs','*.smc'), ('All files','*.*')])
+ entry.delete(0, END)
+ entry.insert(0, fname)
+ entry.xview(END)
+ def browseForProject(self, entry, save=False):
+ fname = tkFileDialog.askdirectory(
+ parent=self._root, title="Select a Project Directory",
+ mustexist=(not save))
+ entry.delete(0, END)
+ entry.insert(0, fname)
+ entry.xview(END)
+ def runRom(self, entry):
+ romFname = entry.get()
+ if self._prefs["Emulator"] == None:
+ tkMessageBox.showerror(parent=self._root,
+ title="Error",
+ message="""Emulator executable not specified.
+Please specify it in the Preferences menu.""")
+ elif romFname != "":
+ Popen([self._prefs["Emulator"], romFname])
+ def doImport(self, romEntry, projEntry):
+ if (romEntry.get() != "") and (projEntry.get() != ""):
+ self._cs.romToProj(romEntry.get(), projEntry.get() +
+ "/Project.csproj")
+ def doExport(self, projEntry, cleanRomEntry, romEntry):
+ if self._prefs["CCC"] == None:
+ tkMessageBox.showerror(parent=self._root,
+ title="Error",
+ message="""CCScript Compiler executable not specified.
+Please specify it in the Preferences menu.""")
+ elif ((projEntry.get() != "") and (cleanRomEntry.get() != "")
+ and (romEntry.get() != "")):
+ oldRom = cleanRomEntry.get()
+ newRom = romEntry.get()
+ projDir = projEntry.get()
+ # Copy the clean rom to the output rom
+ shutil.copyfile(oldRom, newRom)
+ # Get a list of the script filenames in projDir/ccscript
+ scriptFnames = [ projDir + "/ccscript/" + x
+ for x in os.listdir(projDir + "/ccscript")
+ if x.endswith('.ccs') ]
+ # Compile scripts using the CCC, and put the data at $F00000
+ process = Popen(
+ [self._prefs["CCC"], "-n", "-o", newRom, "-s", "F00000",
+ "--summary", projDir + "/ccscript/summary.txt"] +
+ scriptFnames)
+ process.wait()
+ # Run CoilSnake as usual
+ self._cs.projToRom(projDir + "/Project.csproj",
+ newRom, newRom)
+ def main(self):
+ self._root = Tk()
+ self._root.wm_title("CoilSnake")
+
+ menuBar = Menu(self._root)
+ # Preferences pulldown menu
+ prefMenu = Menu(menuBar, tearoff=0)
+ prefMenu.add_command(label="CCScript Compiler Executable",
+ command=self.setCccExe)
+ prefMenu.add_command(label="Emulator Executable",
+ command=self.setEmulatorExe)
+ menuBar.add_cascade(label="Preferences", menu=prefMenu)
+ self._root.config(menu=menuBar)
+
+ # Left side: Import
+ a=Label(self._root, text="ROM -> New Project",justify=CENTER).grid(
+ row=0, column=1, columnspan=1)
+ # ROM file selector
+ Label(self._root, text="Input ROM:").grid(
+ row=1, column=0, sticky=E)
+ inRom = Entry(self._root)
+ inRom.grid(row=1, column=1)
+ def browseTmp():
+ self.browseForRom(inRom)
+ def runTmp():
+ self.runRom(inRom)
+ Button(self._root, text="Browse...",
+ command=browseTmp).grid(row=1, column=2, sticky=W)
+ Button(self._root, text="Run",
+ command=runTmp).grid(row=1, column=3, sticky=W)
+ # Project dir selector
+ Label(self._root, text="Output Directory:").grid(
+ row=2, column=0, sticky=E)
+ outProj = Entry(self._root)
+ outProj.grid(row=2, column=1)
+ def browseTmp():
+ self.browseForProject(outProj, save=True)
+ Button(self._root, text="Browse...",
+ command=browseTmp).grid(row=2, column=2)
+ # Import Button
+ def importTmp():
+ self.doImport(inRom, outProj)
+ Button(self._root, text="Import", command=importTmp).grid(
+ row=4, column=1, columnspan=1, sticky=W+E)
+
+ # Right side: Export
+ Label(self._root, text="Project -> New ROM").grid(
+ row=0, column=5, columnspan=1)
+ # Base ROM file selector
+ Label(self._root, text="Base ROM:").grid(
+ row=1, column=4, sticky=E)
+ baseRom = Entry(self._root)
+ baseRom.grid(row=1, column=5)
+ def browseTmp():
+ self.browseForRom(baseRom)
+ def runTmp():
+ self.runRom(baseRom)
+ Button(self._root, text="Browse...",
+ command=browseTmp).grid(row=1, column=6)
+ Button(self._root, text="Run",
+ command=runTmp).grid(row=1, column=7, sticky=W)
+ # Project dir selector
+ Label(self._root, text="Project Directory:").grid(
+ row=2, column=4, sticky=E)
+ inProj = Entry(self._root)
+ inProj.grid(row=2, column=5)
+ def browseTmp():
+ self.browseForProject(inProj, save=False)
+ Button(self._root, text="Browse...",
+ command=browseTmp).grid(row=2, column=6)
+ # ROM file selector
+ Label(self._root, text="Output ROM:").grid(
+ row=3, column=4, sticky=E)
+ outRom = Entry(self._root)
+ outRom.grid(row=3, column=5)
+ def browseTmp():
+ self.browseForRom(outRom, save=True)
+ def runTmp():
+ self.runRom(outRom)
+ Button(self._root, text="Browse...",
+ command=browseTmp).grid(row=3, column=6)
+ Button(self._root, text="Run",
+ command=runTmp).grid(row=3, column=7, sticky=W)
+ # Export Button
+ def exportTmp():
+ self.doExport(inProj, baseRom, outRom)
+ Button(self._root, text="Export", command=exportTmp).grid(
+ row=4, column=5, columnspan=1, sticky=W+E)
+
+ self._root.mainloop()
+
+if (__name__ == '__main__'):
+ csf = CoilSnakeFrontend()
+ sys.exit(csf.main())
View
@@ -3,16 +3,20 @@ CoilSnake
CoilSnake is a modular ROM hacking tool. It provides a layer of abstraction and protection to ROM hacking by allowing the user to export a ROM's data to a CoilSnake project, using the relevant CoilSnake modules for each individual set of data depending on the game being read. That CoilSnake project may then be edited with external utilities. At the end, the user can later compile a CoilSnake project into a fresh ROM using CoilSnake. Of course, the process can also be reversed. In this way, the user avoids the all too common scenario in ROM hacking of one losing all of his work after making a single mistake or a utility failing, since all of his changes were stored in the ROM itself and can not easily be copied into another ROM. In addition, collaborative work becomes much, much easier.
-How To Run
+How To Use
----------
Examples:
-Importing from ROM to CoilSnake project:
+Use the GUI frontend with CCScript support
- ./CoilSnake.py EB.smc MyProject/MyProject.csp
+ ./CoilSnakeGUI.py
-Compiling CoilSnake project into a ROM:
+Import from ROM to CoilSnake project:
- ./CoilSnake.py --cleanrom EB-clean.smc MyProject/MyProject.csp EB-new.smc
+ ./CoilSnake.py EB.smc MyProject/MyProject.csproj
+
+Compile CoilSnake project into a ROM:
+
+ ./CoilSnake.py --cleanrom EB-clean.smc MyProject/MyProject.csproj EB-new.smc
View
@@ -1,3 +1,4 @@
+eb.CccInterfaceModule
#eb.MapModule
eb.BattleBgModule
eb.EnemyModule
View
@@ -131,6 +131,24 @@ def addFreeRanges(self, ranges):
# TODO do some check so that free ranges don't overlap
self._freeRanges += ranges
self._freeRanges.sort()
+ def markRangeAsNotFree(self, usedRange):
+ usedBegin, usedEnd = usedRange
+ for i in range(len(self._freeRanges)):
+ begin, end = self._freeRanges[i]
+ if usedBegin == begin:
+ if usedEnd < end:
+ self._freeRanges[i] = (usedEnd+1, end)
+ elif usedEnd == end:
+ del(self._freeRanges[i])
+ else: # usedEnd > end
+ del(self._freeRanges[i])
+ self._markRangeAsNotFree(end+1, usedEnd)
+ break
+ elif (usedBegin > begin) and (usedEnd <= end):
+ self._freeRanges[i] = (begin, usedBegin-1)
+ if usedEnd != end:
+ self._freeRanges.insert(i, (usedEnd+1, end))
+ break
# Find a free range starting at addr such that add & mask == 0
def getFreeLoc(self, size, mask=0):
ranges = filter(lambda (x,y): x & mask == 0, self._freeRanges)
Oops, something went wrong.

0 comments on commit bca66b3

Please sign in to comment.