Import mymc 2.5 Python source code

Source: http://www.csclub.uwaterloo.ca:11068/mymc/mymc-pysrc-2.5.zip
This commit is contained in:
Mathias Lafeldt 2012-07-30 11:50:16 +02:00
commit 7806741c2a
12 changed files with 5730 additions and 0 deletions

178
README.txt Normal file
View File

@ -0,0 +1,178 @@
README.txt
By Ross Ridge
Pubic Domain
@(#) mymc README.txt 1.5 08/02/06 13:26:26
This file describes mymc, a utility for manipulating PlayStation 2
memory card images as used by the emulator PCSX2. Its main purpose is
to allow save games to be imported and exported to and from these
images. Both MAX Drive and EMS (.psu) save files are fully supported,
however save files in the SharkPort/X-Port and Code Breaker formats
can only be imported and not exported. In addition to these basic
functions, mymc can also perform a number of other operations, like
creating new memory card images, viewing their contents, and adding
and extracting individual files.
A simple, hopefully easy to use, graphicial user interface (GUI) is
provided, but it's limitted to only basic operations. More advanced
opterations require the use of a command line tool. To install mymc,
unpack the downloaded ZIP archive to a new directory on your machine.
You can then run the GUI version of mymc by openning that newn
directory with Windows Explorer and double clicking on the "mymc-gui"
icon. To make it easier to access, you can drag the "mymc-gui" icon
to either your Desktop, Start Menu or Quick Launch toolbar. Make sure
if you do so, that you create a shortcut to "mymc-gui.exe". If you
copy the file instead, the program won't work.
The command line utility can be invoked from the Windows Command
Prompt by using the "mymc" command. The executable "mymc.exe" and
number of support files and these file must kept together in the same
directory. To run the command you need to either add the directory
where you unpacked the distribution to your PATH or type the full
pathname of the executable. For example if you unpacked mymc to a
directory named "c:\mymc" you need to enter "c:\mymc\mymc.exe" to run
the program.
The second important thing to note is that mymc is only "alpha"
quality software. This means that has is been released without
extensive testing and may be unreliable. While it works fine for me,
the author, it might not work as well for you. For that reason you
should be careful how you use it, and prepared for the eventuality of
it corrupting your save game images or producing garbage save files.
If you're worried about this, one make things safer is to use two
memory card images. Use the first image to load and save your games
with under PCSX2, and the second image to import and export saves
games using mysc. Then use the PS2 browser to copy files between two
card images.
GUI TUTORIAL
============
The GUI for mymc is should be easy to use. After starting mymc, you
can select the PS2 memory card image you want to work with by
selecting the "Open" command by pressing the first button on the
toolbar. You can then import a save file clicking on the Import
toolbar button. To export a save files, first select it and then
press the Export button. You can delete a save file permanently from
your memory card, by selecting the "Delete" command from the File
menu.
Do not try to use mymc to modify a memory card image while PCSX2 is
running. Doing so will corrupt your memory card.
COMMAND LINE TUTORIAL
=====================
The basic usage template for mysc is "mymc memcard.ps2 command". The
first argument, "memcard.ps2" is the filename of the memory card image
while "command" is the name of the command you wish to use on the
image. So for example, assuming you've installed mymc in "c:\mymc"
and you've installed PCSX2 in "c:\pcsx2" you could enter the following
command to see the contents of the memory card in the emulator's slot
1:
c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 dir
You would see output something like this:
BASLUS-20678USAGAS00 UNLIMITED SAGA
154KB Not Protected SYSTEMDATA
BADATA-SYSTEM Your System
5KB Not Protected Configuration
BASLUS-20488-0000D SOTET<13>060:08
173KB Not Protected Arias
7,800 KB Free
This is the simple "user friendly" way to view the contents of a
memory card. It displays the same information you can see using the
PlayStation 2 memory card browser. On the right is name of each save,
and on the left is the size and protection status of the save. Also
on the left is one bit of information you won't see in the browser,
the directory name of the save file. PlayStation 2 saves are actually
a collection of different files all stored in a single directory on
the memory card. This is important information, because you need to
know it to export save files.
As mentioned above, if you know the directory name of a save, you can
export it. Exporting a save creates a save file in either the EMS
(.psu) or MAX Drive (.max) format. You can then transfer the save to
real PS2 memory using the appropriate tools. You can also send the
saves to someone else to use or just keep them on your hard drive as a
backup. The following command demonstrates how to export a save in
the EMS format using mymc:
c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 export BASLUS-20448-000D
This will create a file called "BASLUS-20448-000D.psu" in the current
directory. To create a file in the MAX format instead, use the export
command's -m option:
c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 export -m BASLUS-20448-000D
This creates a file named "BASLUS-20448-000D.max". Note the "-m"
option that appears after the "export" command.
Importing save files is similar. The save file type is auto-detected,
so you don't need use an "-m" option with MAX Drive saves. Here's a
couple of examples using each format:
c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 import BASLUS-20035.psu
c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 import 20062_3583_GTA3.max
ADVANCED NOTES
==============
- To get general help with the command line utility use the "-h"
global option (eg. "mymc -h"). To get help with a specific
command use the "-h" option with that command (eg. "mymc x
import -h"). In this later case, you need to specify a memory
card image file, but it's ignored and so doesn't need to exist.
- Both executables in the Windows version, "mymc.exe" and
"mymc-gui.exe" do the same thing and support the same options.
The difference is that "mymc" is console application, while
"mymc-gui" is a Windows appliction. Currently, using "mymc"
to start the GUI will result in a fair amount debug messages
being printed that are normally not seen "mymc-gui" is used.
- It's possible to use mymc create images that are bigger (or
smaller) than standard PS2 memory cards. Be very careful if you
do this, not all games may be compatible with such images.
- The bad block list on images is ignored. Since memory card
images created with either PCSX2 or mymc won't have any bad
blocks, this shouldn't be a problem unless you've somehow
extracted a complete image from a real memory card and expect to
copy it back.
- The PS2 only uses at most 8,000 KB of a memory card, but there
is actually 8,135 KB of allocatable space on a standard
error-free memory card. The extra 135 KB is reserved so that
memory card with bad blocks don't appear to have less space than
memory cards with fewer or no bad blocks. Since there are no
bad blocks on memory card images, mymc uses the full capacity
provided by standard memory cards.
PYTHON SOURCE DISTRIBUTION
==========================
The "source code" distribution of mymc is provided for users of Linux
and other non-Windows operating systems. It uses the same Python code
that the Windows distribution is built with (using py2exe) and
supports all the same functionality. One big difference is that the
Windows DLL "mymcsup.dll" is not included and as a result compressing
and decompressing MAX Drive saves will be as much as 100 times slower.
The GUI mode is hasn't been extensively tested on non-Windows systems,
and the 3D display of save file icons requires the DLL. The Python
source version should support big-endian machines, but this hasn't
been tested.

948
gui.py Normal file
View File

@ -0,0 +1,948 @@
#
# gui.py
#
# By Ross Ridge
# Public Domain
#
"""Graphical user-interface for mymc."""
_SCCS_ID = "@(#) mymc gui.py 1.3 08/02/06 13:15:41\n"
import os
import sys
import struct
import cStringIO
import time
# Work around a problem with mixing wx and py2exe
if os.name == "nt" and hasattr(sys, "setdefaultencoding"):
sys.setdefaultencoding("mbcs")
import wx
import ps2mc
import ps2save
import guires
try:
import ctypes
import mymcsup
D3DXVECTOR3 = mymcsup.D3DXVECTOR3
D3DXVECTOR4 = mymcsup.D3DXVECTOR4
D3DXVECTOR4_ARRAY3 = mymcsup.D3DXVECTOR4_ARRAY3
def mkvec4arr3(l):
return D3DXVECTOR4_ARRAY3(*[D3DXVECTOR4(*vec)
for vec in l])
except ImportError:
mymcsup = None
lighting_none = {"lighting": False,
"vertex_diffuse": False,
"alt_lighting": False,
"light_dirs": [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
"light_colours": [[0, 0, 0, 0], [0, 0, 0, 0],
[0, 0, 0, 0]],
"ambient": [0, 0, 0, 0]}
lighting_diffuse = {"lighting": False,
"vertex_diffuse": True,
"alt_lighting": False,
"light_dirs": [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
"light_colours": [[0, 0, 0, 0], [0, 0, 0, 0],
[0, 0, 0, 0]],
"ambient": [0, 0, 0, 0]}
lighting_icon = {"lighting": True,
"vertex_diffuse": True,
"alt_lighting": False,
"light_dirs": [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]],
"light_colours": [[0, 0, 0, 0], [0, 0, 0, 0],
[0, 0, 0, 0]],
"ambient": [0, 0, 0, 0]}
lighting_alternate = {"lighting": True,
"vertex_diffuse": True,
"alt_lighting": True,
"light_dirs": [[1, -1, 2, 0],
[-1, 1, -2, 0],
[0, 1, 0, 0]],
"light_colours": [[1, 1, 1, 1],
[1, 1, 1, 1],
[0.7, 0.7, 0.7, 1]],
"ambient": [0.5, 0.5, 0.5, 1]}
lighting_alternate2 = {"lighting": True,
"vertex_diffuse": False,
"alt_lighting": True,
"light_dirs": [[1, -1, 2, 0],
[-1, 1, -2, 0],
[0, 4, 1, 0]],
"light_colours": [[0.7, 0.7, 0.7, 1],
[0.7, 0.7, 0.7, 1],
[0.2, 0.2, 0.2, 1]],
"ambient": [0.3, 0.3, 0.3, 1]}
camera_default = [0, 4, -8]
camera_high = [0, 7, -6]
camera_near = [0, 3, -6]
camera_flat = [0, 2, -7.5]
def get_dialog_units(win):
return win.ConvertDialogPointToPixels((1, 1))[0]
def single_title(title):
"""Convert the two parts of an icon.sys title into one string."""
title = title[0] + " " + title[1]
return u" ".join(title.split())
def _get_icon_resource_as_images(name):
ico = guires.resources[name]
images = []
f = cStringIO.StringIO(ico)
count = struct.unpack("<HHH", ico[0:6])[2]
# count = wx.Image_GetImageCount(f, wx.BITMAP_TYPE_ICO)
for i in range(count):
f.seek(0)
images.append(wx.ImageFromStream(f, wx.BITMAP_TYPE_ICO, i))
return images
def get_icon_resource(name):
"""Convert a Window ICO contained in a string to an IconBundle."""
bundle = wx.IconBundle()
for img in _get_icon_resource_as_images(name):
bmp = wx.BitmapFromImage(img)
icon = wx.IconFromBitmap(bmp)
bundle.AddIcon(icon)
return bundle
def get_icon_resource_bmp(name, size):
"""Get an icon resource as a Bitmap.
Tries to find the closest matching size if no exact match exists."""
best = None
best_size = (0, 0)
for img in _get_icon_resource_as_images(name):
sz = (img.GetWidth(), img.GetHeight())
if sz == size:
return wx.BitmapFromImage(img)
if sz[0] >= size[0] and sz[1] >= size[1]:
if ((best_size[0] < size[0] or best_size[1] < size[1])
or sz[0] * sz[1] < best_size[0] * best_size[1]):
best = img
best_size = sz
elif sz[0] * sz[1] > best_size[0] * best_size[1]:
best = img
best_size = sz
img = best.Rescale(size[0], size[1], wx.IMAGE_QUALITY_HIGH)
return wx.BitmapFromImage(img)
class dirlist_control(wx.ListCtrl):
"""Lists all the save files in a memory card image."""
def __init__(self, parent, evt_focus, evt_select, config):
self.config = config
self.selected = set()
self.evt_select = evt_select
wx.ListCtrl.__init__(self, parent, wx.ID_ANY,
style = wx.LC_REPORT)
wx.EVT_LIST_COL_CLICK(self, -1, self.evt_col_click)
wx.EVT_LIST_ITEM_FOCUSED(self, -1, evt_focus)
wx.EVT_LIST_ITEM_SELECTED(self, -1, self.evt_item_selected)
wx.EVT_LIST_ITEM_DESELECTED(self, -1, self.evt_item_deselected)
def _update_dirtable(self, mc, dir):
self.dirtable = table = []
enc = "unicode"
if self.config.get_ascii():
enc = "ascii"
for ent in dir:
if not ps2mc.mode_is_dir(ent[0]):
continue
dirname = "/" + ent[8]
s = mc.get_icon_sys(dirname)
if s == None:
continue
a = ps2save.unpack_icon_sys(s)
size = mc.dir_size(dirname)
title = ps2save.icon_sys_title(a, encoding = enc)
table.append((ent, s, size, title))
def update_dirtable(self, mc):
self.dirtable = []
if mc == None:
return
dir = mc.dir_open("/")
try:
self._update_dirtable(mc, dir)
finally:
dir.close()
def cmp_dir_name(self, i1, i2):
return self.dirtable[i1][0][8] > self.dirtable[i2][0][8]
def cmp_dir_title(self, i1, i2):
return self.dirtable[i1][3] > self.dirtable[i2][3]
def cmp_dir_size(self, i1, i2):
return self.dirtable[i1][2] > self.dirtable[i2][2]
def cmp_dir_modified(self, i1, i2):
m1 = list(self.dirtable[i1][0][6])
m2 = list(self.dirtable[i2][0][6])
m1.reverse()
m2.reverse()
return m1 > m2
def evt_col_click(self, event):
col = event.m_col
if col == 0:
cmp = self.cmp_dir_name
elif col == 1:
cmp = self.cmp_dir_size
elif col == 2:
cmp = self.cmp_dir_modified
elif col == 3:
cmp = self.cmp_dir_title
self.SortItems(cmp)
return
def evt_item_selected(self, event):
self.selected.add(event.GetData())
self.evt_select(event)
def evt_item_deselected(self, event):
self.selected.discard(event.GetData())
self.evt_select(event)
def update(self, mc):
"""Update the ListCtrl according to the contents of the
memory card image."""
self.ClearAll()
self.selected = set()
self.InsertColumn(0, "Directory")
self.InsertColumn(1, "Size")
self.InsertColumn(2, "Modified")
self.InsertColumn(3, "Description")
li = self.GetColumn(1)
li.SetAlign(wx.LIST_FORMAT_RIGHT)
li.SetText("Size")
self.SetColumn(1, li)
self.update_dirtable(mc)
empty = len(self.dirtable) == 0
self.Enable(not empty)
if empty:
return
for (i, a) in enumerate(self.dirtable):
(ent, icon_sys, size, title) = a
li = self.InsertStringItem(i, ent[8])
self.SetStringItem(li, 1, "%dK" % (size / 1024))
m = ent[6]
m = ("%04d-%02d-%02d %02d:%02d"
% (m[5], m[4], m[3], m[2], m[1]))
self.SetStringItem(li, 2, m)
self.SetStringItem(li, 3, single_title(title))
self.SetItemData(li, i)
du = get_dialog_units(self)
for i in range(4):
self.SetColumnWidth(i, wx.LIST_AUTOSIZE)
self.SetColumnWidth(i, self.GetColumnWidth(i) + du)
self.SortItems(self.cmp_dir_name)
class icon_window(wx.Window):
"""Displays a save file's 3D icon. Windows only.
The rendering of the 3D icon is handled by C++ code in the
mymcsup DLL which subclasses this window. This class mainly
handles configuration options that affect how the 3D icon is
displayed.
"""
ID_CMD_ANIMATE = 201
ID_CMD_LIGHT_NONE = 202
ID_CMD_LIGHT_ICON = 203
ID_CMD_LIGHT_ALT1 = 204
ID_CMD_LIGHT_ALT2 = 205
ID_CMD_CAMERA_FLAT = 206
ID_CMD_CAMERA_DEFAULT = 207
ID_CMD_CAMERA_NEAR = 209
ID_CMD_CAMERA_HIGH = 210
light_options = {ID_CMD_LIGHT_NONE: lighting_none,
ID_CMD_LIGHT_ICON: lighting_icon,
ID_CMD_LIGHT_ALT1: lighting_alternate,
ID_CMD_LIGHT_ALT2: lighting_alternate2}
camera_options = {ID_CMD_CAMERA_FLAT: camera_flat,
ID_CMD_CAMERA_DEFAULT: camera_default,
ID_CMD_CAMERA_NEAR: camera_near,
ID_CMD_CAMERA_HIGH: camera_high}
def append_menu_options(self, win, menu):
menu.AppendCheckItem(icon_window.ID_CMD_ANIMATE,
"Animate Icons")
menu.AppendSeparator()
menu.AppendRadioItem(icon_window.ID_CMD_LIGHT_NONE,
"Lighting Off")
menu.AppendRadioItem(icon_window.ID_CMD_LIGHT_ICON,
"Icon Lighting")
menu.AppendRadioItem(icon_window.ID_CMD_LIGHT_ALT1,
"Alternate Lighting")
menu.AppendRadioItem(icon_window.ID_CMD_LIGHT_ALT2,
"Alternate Lighting 2")
menu.AppendSeparator()
menu.AppendRadioItem(icon_window.ID_CMD_CAMERA_FLAT,
"Camera Flat")
menu.AppendRadioItem(icon_window.ID_CMD_CAMERA_DEFAULT,
"Camera Default")
menu.AppendRadioItem(icon_window.ID_CMD_CAMERA_NEAR,
"Camera Near")
menu.AppendRadioItem(icon_window.ID_CMD_CAMERA_HIGH,
"Camera High")
wx.EVT_MENU(win, icon_window.ID_CMD_ANIMATE,
self.evt_menu_animate)
wx.EVT_MENU(win, icon_window.ID_CMD_LIGHT_NONE,
self.evt_menu_light)
wx.EVT_MENU(win, icon_window.ID_CMD_LIGHT_ICON,
self.evt_menu_light)
wx.EVT_MENU(win, icon_window.ID_CMD_LIGHT_ALT1,
self.evt_menu_light)
wx.EVT_MENU(win, icon_window.ID_CMD_LIGHT_ALT2,
self.evt_menu_light)
wx.EVT_MENU(win, icon_window.ID_CMD_CAMERA_FLAT,
self.evt_menu_camera)
wx.EVT_MENU(win, icon_window.ID_CMD_CAMERA_DEFAULT,
self.evt_menu_camera)
wx.EVT_MENU(win, icon_window.ID_CMD_CAMERA_NEAR,
self.evt_menu_camera)
wx.EVT_MENU(win, icon_window.ID_CMD_CAMERA_HIGH,
self.evt_menu_camera)
def __init__(self, parent, focus):
self.failed = False
wx.Window.__init__(self, parent)
if mymcsup == None:
self.failed = True
return
r = mymcsup.init_icon_renderer(focus.GetHandle(),
self.GetHandle())
if r == -1:
print "init_icon_renderer failed"
self.failed = True
return
self.config = config = mymcsup.icon_config()
config.animate = True
self.menu = wx.Menu()
self.append_menu_options(self, self.menu)
self.set_lighting(self.ID_CMD_LIGHT_ALT2)
self.set_camera(self.ID_CMD_CAMERA_DEFAULT)
wx.EVT_CONTEXT_MENU(self, self.evt_context_menu)
def __del__(self):
if mymcsup != None:
mymcsup.delete_icon_renderer()
def update_menu(self, menu):
"""Update the content menu according to the current config."""
menu.Check(icon_window.ID_CMD_ANIMATE, self.config.animate)
menu.Check(self.lighting_id, True)
menu.Check(self.camera_id, True)
def load_icon(self, icon_sys, icon):
"""Pass the raw icon data to the support DLL for display."""
if self.failed:
return
if icon_sys == None or icon == None:
r = mymcsup.load_icon(None, 0, None, 0)
else:
r = mymcsup.load_icon(icon_sys, len(icon_sys),
icon, len(icon))
if r != 0:
print "load_icon", r
self.failed = True
def _set_lighting(self, lighting, vertex_diffuse, alt_lighting,
light_dirs, light_colours, ambient):
if self.failed:
return
config = self.config
config.lighting = lighting
config.vertex_diffuse = vertex_diffuse
config.alt_lighting = alt_lighting
config.light_dirs = mkvec4arr3(light_dirs)
config.light_colours = mkvec4arr3(light_colours)
config.ambient = D3DXVECTOR4(*ambient)
if mymcsup.set_config(config) == -1:
self.failed = True
def set_lighting(self, id):
self.lighting_id = id
self._set_lighting(**self.light_options[id])
def set_animate(self, animate):
if self.failed:
return
self.config.animate = animate
if mymcsup.set_config(self.config) == -1:
self.failed = True
def _set_camera(self, camera):
if self.failed:
return
self.config.camera = mymcsup.D3DXVECTOR3(*camera)
if mymcsup.set_config(self.config) == -1:
self.failed = True
def set_camera(self, id):
self.camera_id = id
self._set_camera(self.camera_options[id])
def evt_context_menu(self, event):
self.update_menu(self.menu)
self.PopupMenu(self.menu)
def evt_menu_animate(self, event):
self.set_animate(not self.config.animate)
def evt_menu_light(self, event):
self.set_lighting(event.GetId())
def evt_menu_camera(self, event):
self.set_camera(event.GetId())
class gui_config(wx.Config):
"""A class for holding the persistant configuration state."""
memcard_dir = "Memory Card Directory"
savefile_dir = "Save File Directory"
ascii = "ASCII Descriptions"
def __init__(self):
wx.Config.__init__(self, "mymc", "Ross Ridge",
style = wx.CONFIG_USE_LOCAL_FILE)
def get_memcard_dir(self, default = None):
return self.Read(gui_config.memcard_dir, default)
def set_memcard_dir(self, value):
return self.Write(gui_config.memcard_dir, value)
def get_savefile_dir(self, default = None):
return self.Read(gui_config.savefile_dir, default)
def set_savefile_dir(self, value):
return self.Write(gui_config.savefile_dir, value)
def get_ascii(self, default = False):
return bool(self.ReadInt(gui_config.ascii, int(bool(default))))
def set_ascii(self, value):
return self.WriteInt(gui_config.ascii, int(bool(value)))
def add_tool(toolbar, id, label, ico):
tbsize = toolbar.GetToolBitmapSize()
bmp = get_icon_resource_bmp(ico, tbsize)
return toolbar.AddLabelTool(id, label, bmp, shortHelp = label)
class gui_frame(wx.Frame):
"""The main top level window."""
ID_CMD_EXIT = wx.ID_EXIT
ID_CMD_OPEN = wx.ID_OPEN
ID_CMD_EXPORT = 103
ID_CMD_IMPORT = 104
ID_CMD_DELETE = wx.ID_DELETE
ID_CMD_ASCII = 106
def message_box(self, message, caption = "mymc", style = wx.OK,
x = -1, y = -1):
return wx.MessageBox(message, caption, style, self, x, y)
def error_box(self, msg):
return self.message_box(msg, "Error", wx.OK | wx.ICON_ERROR)
def mc_error(self, value, filename = None):
"""Display a message box for EnvironmentError exeception."""
if filename == None:
filename = self.mcname
if filename == None:
filename = "???"
filename = getattr(value, "filename", filename)
strerror = getattr(value, "strerror", "unknown error")
return self.error_box(filename + ": " + strerror)
def __init__(self, parent, title, mcname = None):
self.f = None
self.mc = None
self.mcname = None
self.icon_win = None
size = (750, 350)
if mymcsup == None:
size = (500, 350)
wx.Frame.__init__(self, parent, wx.ID_ANY, title, size = size)
wx.EVT_CLOSE(self, self.evt_close)
self.config = gui_config()
self.title = title
self.SetIcons(get_icon_resource("mc4.ico"))
wx.EVT_MENU(self, self.ID_CMD_EXIT, self.evt_cmd_exit)
wx.EVT_MENU(self, self.ID_CMD_OPEN, self.evt_cmd_open)
wx.EVT_MENU(self, self.ID_CMD_EXPORT, self.evt_cmd_export)
wx.EVT_MENU(self, self.ID_CMD_IMPORT, self.evt_cmd_import)
wx.EVT_MENU(self, self.ID_CMD_DELETE, self.evt_cmd_delete)
wx.EVT_MENU(self, self.ID_CMD_ASCII, self.evt_cmd_ascii)
filemenu = wx.Menu()
filemenu.Append(self.ID_CMD_OPEN, "&Open...",
"Opens an existing PS2 memory card image.")
filemenu.AppendSeparator()
self.export_menu_item = filemenu.Append(
self.ID_CMD_EXPORT, "&Export...",
"Export a save file from this image.")
self.import_menu_item = filemenu.Append(
self.ID_CMD_IMPORT, "&Import...",
"Import a save file into this image.")
self.delete_menu_item = filemenu.Append(
self.ID_CMD_DELETE, "&Delete")
filemenu.AppendSeparator()
filemenu.Append(self.ID_CMD_EXIT, "E&xit")
optionmenu = wx.Menu()
self.ascii_menu_item = optionmenu.AppendCheckItem(
self.ID_CMD_ASCII, "&ASCII Descriptions",
"Show descriptions in ASCII instead of Shift-JIS")
wx.EVT_MENU_OPEN(self, self.evt_menu_open);
self.CreateToolBar(wx.TB_HORIZONTAL)
self.toolbar = toolbar = self.GetToolBar()
tbsize = (32, 32)
toolbar.SetToolBitmapSize(tbsize)
add_tool(toolbar, self.ID_CMD_OPEN, "Open", "mc2.ico")
toolbar.AddSeparator()
add_tool(toolbar, self.ID_CMD_IMPORT, "Import", "mc5b.ico")
add_tool(toolbar, self.ID_CMD_EXPORT, "Export", "mc6a.ico")
toolbar.Realize()
self.statusbar = self.CreateStatusBar(2,
style = wx.ST_SIZEGRIP)
self.statusbar.SetStatusWidths([-2, -1])
panel = wx.Panel(self, wx.ID_ANY, (0, 0))
self.dirlist = dirlist_control(panel,
self.evt_dirlist_item_focused,
self.evt_dirlist_select,
self.config)
if mcname != None:
self.open_mc(mcname)
else:
self.refresh()
sizer = wx.BoxSizer(wx.HORIZONTAL)
sizer.Add(self.dirlist, 2, wx.EXPAND)
sizer.AddSpacer(5)
icon_win = None
if mymcsup != None:
icon_win = icon_window(panel, self)
if icon_win.failed:
icon_win.Destroy()
icon_win = None
self.icon_win = icon_win
if icon_win == None:
self.info1 = None
self.info2 = None
else:
self.icon_menu = icon_menu = wx.Menu()
icon_win.append_menu_options(self, icon_menu)
optionmenu.AppendSubMenu(icon_menu, "Icon Window")
title_style = wx.ALIGN_RIGHT | wx.ST_NO_AUTORESIZE
self.info1 = wx.StaticText(panel, -1, "",
style = title_style)
self.info2 = wx.StaticText(panel, -1, "",
style = title_style)
# self.info3 = wx.StaticText(panel, -1, "")
info_sizer = wx.BoxSizer(wx.VERTICAL)
info_sizer.Add(self.info1, 0, wx.EXPAND)
info_sizer.Add(self.info2, 0, wx.EXPAND)
# info_sizer.Add(self.info3, 0, wx.EXPAND)
info_sizer.AddSpacer(5)
info_sizer.Add(icon_win, 1, wx.EXPAND)
sizer.Add(info_sizer, 1, wx.EXPAND | wx.ALL,
border = 5)
menubar = wx.MenuBar()
menubar.Append(filemenu, "&File")
menubar.Append(optionmenu, "&Options")
self.SetMenuBar(menubar)
panel.SetSizer(sizer)
panel.SetAutoLayout(True)
sizer.Fit(panel)
self.Show(True)
if self.mc == None:
self.evt_cmd_open()
def _close_mc(self):
if self.mc != None:
try:
self.mc.close()
except EnvironmentError, value:
self.mc_error(value)
self.mc = None
if self.f != None:
try:
self.f.close()
except EnvironmentError, value:
self.mc_error(value)
self.f = None
self.mcname = None
def refresh(self):
try:
self.dirlist.update(self.mc)
except EnvironmentError, value:
self.mc_error(value)
self._close_mc()
self.dirlist.update(None)
mc = self.mc
self.toolbar.EnableTool(self.ID_CMD_IMPORT, mc != None)
self.toolbar.EnableTool(self.ID_CMD_EXPORT, False)
if mc == None:
status = "No memory card image"
else:
free = mc.get_free_space() / 1024
limit = mc.get_allocatable_space() / 1024
status = "%dK of %dK free" % (free, limit)
self.statusbar.SetStatusText(status, 1)
def open_mc(self, filename):
self._close_mc()
self.statusbar.SetStatusText("", 1)
if self.icon_win != None:
self.icon_win.load_icon(None, None)
f = None
try:
f = file(filename, "r+b")
mc = ps2mc.ps2mc(f)
except EnvironmentError, value:
if f != None:
f.close()
self.mc_error(value, filename)
self.SetTitle(self.title)
self.refresh()
return
self.f = f
self.mc = mc
self.mcname = filename
self.SetTitle(filename + " - " + self.title)
self.refresh()
def evt_menu_open(self, event):
self.import_menu_item.Enable(self.mc != None)
selected = self.mc != None and len(self.dirlist.selected) > 0
self.export_menu_item.Enable(selected)
self.delete_menu_item.Enable(selected)
self.ascii_menu_item.Check(self.config.get_ascii())
if self.icon_win != None:
self.icon_win.update_menu(self.icon_menu)
def evt_dirlist_item_focused(self, event):
if self.icon_win == None:
return
mc = self.mc
i = event.GetData()
(ent, icon_sys, size, title) = self.dirlist.dirtable[i]
self.info1.SetLabel(title[0])
self.info2.SetLabel(title[1])
a = ps2save.unpack_icon_sys(icon_sys)
try:
mc.chdir("/" + ent[8])
f = mc.open(a[15], "rb")
try:
icon = f.read()
finally:
f.close()
except EnvironmentError, value:
print "icon failed to load", value
self.icon_win.load_icon(None, None)
return
self.icon_win.load_icon(icon_sys, icon)
def evt_dirlist_select(self, event):
self.toolbar.EnableTool(self.ID_CMD_IMPORT, self.mc != None)
self.toolbar.EnableTool(self.ID_CMD_EXPORT,
len(self.dirlist.selected) > 0)
def evt_cmd_open(self, event = None):
fn = wx.FileSelector("Open Memory Card Image",
self.config.get_memcard_dir(""),
"Mcd001.ps2", "ps2", "*.ps2",
wx.FD_FILE_MUST_EXIST | wx.FD_OPEN,
self)
if fn == "":
return
self.open_mc(fn)
if self.mc != None:
dirname = os.path.dirname(fn)
if os.path.isabs(dirname):
self.config.set_memcard_dir(dirname)
def evt_cmd_export(self, event):
mc = self.mc
if mc == None:
return
selected = self.dirlist.selected
dirtable = self.dirlist.dirtable
sfiles = []
for i in selected:
dirname = dirtable[i][0][8]
try:
sf = mc.export_save_file("/" + dirname)
longname = ps2save.make_longname(dirname, sf)
sfiles.append((dirname, sf, longname))
except EnvironmentError, value:
self.mc_error(value. dirname)
if len(sfiles) == 0:
return
dir = self.config.get_savefile_dir("")
if len(selected) == 1:
(dirname, sf, longname) = sfiles[0]
fn = wx.FileSelector("Export " + dirname,
dir, longname, "psu",
"EMS save file (.psu)|*.psu"
"|MAXDrive save file (.max)"
"|*.max",
(wx.FD_OVERWRITE_PROMPT
| wx.FD_SAVE),
self)
if fn == "":
return
try:
f = file(fn, "wb")
try:
if fn.endswith(".max"):
sf.save_max_drive(f)
else:
sf.save_ems(f)
finally:
f.close()
except EnvironmentError, value:
self.mc_error(value, fn)
return
dir = os.path.dirname(fn)
if os.path.isabs(dir):
self.config.set_savefile_dir(dir)
self.message_box("Exported " + fn + " successfully.")
return
dir = wx.DirSelector("Export Save Files", dir, parent = self)
if dir == "":
return
count = 0
for (dirname, sf, longname) in sfiles:
fn = os.path.join(dir, longname) + ".psu"
try:
f = file(fn, "wb")
sf.save_ems(f)
f.close()
count += 1
except EnvironmentError, value:
self.mc_error(value, fn)
if count > 0:
if os.path.isabs(dir):
self.config.set_savefile_dir(dir)
self.message_box("Exported %d file(s) successfully."
% count)
def _do_import(self, fn):
sf = ps2save.ps2_save_file()
f = file(fn, "rb")
try:
ft = ps2save.detect_file_type(f)
f.seek(0)
if ft == "max":
sf.load_max_drive(f)
elif ft == "psu":
sf.load_ems(f)
elif ft == "cbs":
sf.load_codebreaker(f)
elif ft == "sps":
sf.load_sharkport(f)
elif ft == "npo":
self.error_box(fn + ": nPort saves"
" are not supported.")
return
else:
self.error_box(fn + ": Save file format not"
" recognized.")
return
finally:
f.close()
if not self.mc.import_save_file(sf, True):
self.error_box(fn + ": Save file already present.")
def evt_cmd_import(self, event):
if self.mc == None:
return
dir = self.config.get_savefile_dir("")
fd = wx.FileDialog(self, "Import Save File", dir,
wildcard = ("PS2 save files"
" (.cbs;.psu;.max;.sps;.xps)"
"|*.cbs;*.psu;*.max;*.sps;*.xps"
"|All files|*.*"),
style = (wx.FD_OPEN | wx.FD_MULTIPLE
| wx.FD_FILE_MUST_EXIST))
if fd == None:
return
r = fd.ShowModal()
if r == wx.ID_CANCEL:
return
success = None
for fn in fd.GetPaths():
try:
self._do_import(fn)
success = fn
except EnvironmentError, value:
self.mc_error(value, fn)
if success != None:
dir = os.path.dirname(success)
if os.path.isabs(dir):
self.config.set_savefile_dir(dir)
self.refresh()
def evt_cmd_delete(self, event):
mc = self.mc
if mc == None:
return
selected = self.dirlist.selected
dirtable = self.dirlist.dirtable
dirnames = [dirtable[i][0][8]
for i in selected]
if len(selected) == 1:
title = dirtable[list(selected)[0]][3]
s = dirnames[0] + " (" + single_title(title) + ")"
else:
s = ", ".join(dirnames)
if len(s) > 200:
s = s[:200] + "..."
r = self.message_box("Are you sure you want to delete "
+ s + "?",
"Delete Save File Confirmation",
wx.YES_NO)
if r != wx.YES:
return
for dn in dirnames:
try:
mc.rmdir("/" + dn)
except EnvironmentError, value:
self.mc_error(value, dn)
mc.check()
self.refresh()
def evt_cmd_ascii(self, event):
self.config.set_ascii(not self.config.get_ascii())
self.refresh()
def evt_cmd_exit(self, event):
self.Close(True)
def evt_close(self, event):
self._close_mc()
self.Destroy()
def run(filename = None):
"""Display a GUI for working with memory card images."""
wx_app = wx.PySimpleApp()
frame = gui_frame(None, "mymc", filename)
return wx_app.MainLoop()
if __name__ == "__main__":
import gc
gc.set_debug(gc.DEBUG_LEAK)
run("test.ps2")
gc.collect()
for o in gc.garbage:
print
print o
if type(o) == ps2mc.ps2mc_file:
for m in dir(o):
print m, getattr(o, m)
# while True:
# for o in gc.garbage:
# if type(o) == ps2mc.ps2mc_file:
# for m in dir(o):
# if getattr(o, m) == None:
# continue
# if (m == "__del__"
# or m == "__class__"
# or m == "__dict__"
# or m == "__weakref__"):
# continue
# print m
# setattr(o, m, None)
# o = None
# break
# break
# del gc.garbage[:]
# gc.collect()

185
guires.py Normal file
View File

@ -0,0 +1,185 @@
resources = {
"mc4.ico": (
"AAABAAIAICAQAAAAAADoAgAAJgAAADAwAAEAAAAAqA4AAA4DAAAoAAAAIAAAAEAAAAABAAQAAAAA\n"
"AIACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAICAgADAwMAA\n"
"AAD/AAD/AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAcABwAAAAAAAAAAAAAAAABwAABwAAAAAAAAAAAAAAAHAAYAB3d3d3dwAAAAAAAAcABm\n"
"YAB3d3dwAAAAAAAABwAGZmYAB3d3AAAAAAAAAHAAZmZmYAB4iIiIiIiAAAcABmZmZgAAB/+P+q/4\n"
"gABwAGZmZmAAAABu7u7u6IAHAAZmZmYAAAAABu7u7u6AAABmZmZgBwcAAABu7u7ugAAGZmZmAHBw\n"
"AAAABu7u7oAAAGZmYAcHBwAAAABu7u6ABwAGZgBwcHBwAAAABu7ugAAAAGAABwcHAABAAADu7oAA\n"
"AAAAAHB3cAAEAAAG7u6AAAAAAAAAB3AAwAAAbu7ugAAABwAAAAAABAAABu7u7oAAAAhgAAAAAMAH\n"
"AG7u7u6AAAAI5gAAAAAAdwbu7u7ugAAACO5gAAxAAABu7u7u7oAAAAju5gAEzAAG7u7u7u6AAAAI\n"
"7u5gAAwAbu7u7u7ugAAACO7u5gAABu7u7u7u7oAAAAju7u5gAG7u7u7u7u6AAAAI7u7u5gbu7u7u\n"
"7u7ugAAACI7u7u7u7u7u7u7u6IAAAAiIiIiIiIiIiIiIiIiAAAAIiIiIiIiIiIiIiIiIgAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////////g////wH///4AAH/8AAH/+AAD//AA\n"
"AAHgAAABwAAAAYAAAAGAAAABgAAAAYAAAAGAAAAB4AAAAfAAAAH4AAAB+AAAAfgAAAH4AAAB+AAA\n"
"AfgAAAH4AAAB+AAAAfgAAAH4AAAB+AAAAfgAAAH4AAAB//////////8oAAAAMAAAAGAAAAABAAgA\n"
"AAAAAIAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAgAAAAICAAIAAAACAAIAAgIAAAMDAwADA\n"
"3MAA8MqmAAAgQAAAIGAAACCAAAAgoAAAIMAAACDgAABAAAAAQCAAAEBAAABAYAAAQIAAAECgAABA\n"
"wAAAQOAAAGAAAABgIAAAYEAAAGBgAABggAAAYKAAAGDAAABg4AAAgAAAAIAgAACAQAAAgGAAAICA\n"
"AACAoAAAgMAAAIDgAACgAAAAoCAAAKBAAACgYAAAoIAAAKCgAACgwAAAoOAAAMAAAADAIAAAwEAA\n"
"AMBgAADAgAAAwKAAAMDAAADA4AAA4AAAAOAgAADgQAAA4GAAAOCAAADgoAAA4MAAAODgAEAAAABA\n"
"ACAAQABAAEAAYABAAIAAQACgAEAAwABAAOAAQCAAAEAgIABAIEAAQCBgAEAggABAIKAAQCDAAEAg\n"
"4ABAQAAAQEAgAEBAQABAQGAAQECAAEBAoABAQMAAQEDgAEBgAABAYCAAQGBAAEBgYABAYIAAQGCg\n"
"AEBgwABAYOAAQIAAAECAIABAgEAAQIBgAECAgABAgKAAQIDAAECA4ABAoAAAQKAgAECgQABAoGAA\n"
"QKCAAECgoABAoMAAQKDgAEDAAABAwCAAQMBAAEDAYABAwIAAQMCgAEDAwABAwOAAQOAAAEDgIABA\n"
"4EAAQOBgAEDggABA4KAAQODAAEDg4ACAAAAAgAAgAIAAQACAAGAAgACAAIAAoACAAMAAgADgAIAg\n"
"AACAICAAgCBAAIAgYACAIIAAgCCgAIAgwACAIOAAgEAAAIBAIACAQEAAgEBgAIBAgACAQKAAgEDA\n"
"AIBA4ACAYAAAgGAgAIBgQACAYGAAgGCAAIBgoACAYMAAgGDgAICAAACAgCAAgIBAAICAYACAgIAA\n"
"gICgAICAwACAgOAAgKAAAICgIACAoEAAgKBgAICggACAoKAAgKDAAICg4ACAwAAAgMAgAIDAQACA\n"
"wGAAgMCAAIDAoACAwMAAgMDgAIDgAACA4CAAgOBAAIDgYACA4IAAgOCgAIDgwACA4OAAwAAAAMAA\n"
"IADAAEAAwABgAMAAgADAAKAAwADAAMAA4ADAIAAAwCAgAMAgQADAIGAAwCCAAMAgoADAIMAAwCDg\n"
"AMBAAADAQCAAwEBAAMBAYADAQIAAwECgAMBAwADAQOAAwGAAAMBgIADAYEAAwGBgAMBggADAYKAA\n"
"wGDAAMBg4ADAgAAAwIAgAMCAQADAgGAAwICAAMCAoADAgMAAwIDgAMCgAADAoCAAwKBAAMCgYADA\n"
"oIAAwKCgAMCgwADAoOAAwMAAAMDAIADAwEAAwMBgAMDAgADAwKAA8Pv/AKSgoACAgIAAAAD/AAD/\n"
"AAAA//8A/wAAAP8A/wD//wAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAKQAAKQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAApAAAAACkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACk\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKQAAAAAAAAApAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApAAAAAAACQAAAACkpKSkpKSkpKSk\n"
"pKSkpAAAAAAAAAAAAAAAAAAAAAAAAAAAAACkAAAAAAAJCQkAAACkpKSkpKSkpKSkpKQAAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAKQAAAAAAAkJCQkJAAAAAKSkpKSkpKQAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAApAAAAAAACQkJCQkJCQAAAACkpKSkpKQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACkAAAA\n"
"AAAJCQkJCQkJCQkAAAAApPf39/f39/f39/f39/f39/f39/cAAAAAAAAAAKQAAAAAAAkJCQkJCQkJ\n"
"CQkJAAAAAKQHBwf3BwcH9/f39wcHB/f39/cAAAAAAAAApAAAAAAACQkJCQkJCQkJCQkAAAAAAACk\n"
"9vb39vb29/r69/b29vf39/cAAAAAAACkAAAAAAAJCQkJCQkJCQkJCQAAAAAAAAAApPf39/f39/f3\n"
"9/f39/f39/cAAAAAAKQAAAAAAAkJCQkJCQkJCQkJAAAAAAAAAAAAAKT39/f39/f39/f39/f39/cA\n"
"AAAApAAAAAAACQkJCQkJCQkJCQkAAAAAAAAAAAAAAAD3/v7+/v7+/v7+/v739/cAAACkAAAAAAAJ\n"
"CQkJCQkJCQkJCQAApAAApAAAAAAAAAAAB/7+/v7+/v7+/v7+9/cAAAAAAAAAAAkJCQkJCQkJCQkJ\n"
"AACkAACkAAAAAAAAAAAAAAf+/v7+/v7+/v7+9/cAAAAAAAAACQkJCQkJCQkJCQkAAKQAAKQAAAAA\n"
"AAAAAAAAAAAH/v7+/v7+/v7+9/cAAACkAAAAAAkJCQkJCQkJCQAApAAApAAAAKQAAAAAAAAAAAAA\n"
"B/7+/v7+/v7+9/cAAAAApAAAAAAJCQkJCQkJAACkAACkAAAApACkAAAAAAAAAAAAAAf+/v7+/v7+\n"
"9/cAAAAAAACkAAAACQkJCQkAAKQAAKQAAACkAKQAAAAAAAAAAAAAAAAH/v7+/v7+9/cAAAAAAAAA\n"
"AAAAAAkJCQAAAAAApAAAAAAApAAAAAAAAADAAAAAAAAAB/7+/v7+9/cAAAAAAAAAAKQAAAAJAAAA\n"
"AACkAAAKpKQAAAAAAAAAAMAAAAAAAAAAAP7+/v7+9/cAAAAAAAAAAAAAAAAAAAAAAKQAAACkAKSk\n"
"AAAAAAAAwAAAAAAAAAAAB/7+/v7+9/cAAAAAAAAAAACkAAAAAAAAAAAAAACkpACkAAAAAADAAAAA\n"
"AAAAAAAH/v7+/v7+9/cAAAAAAAAAAAD3pAAAAAAAAAAAAAAApKQKAAAAAMDAAAAAAAAAAAf+/v7+\n"
"/v7+9/cAAAAAAAAAAAD39wcAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAB/7+/v7+/v7+9/cAAAAA\n"
"AAAAAAD39/4HAAAAAAAAAAAAAAAAAADAAAAACqQAAAAH/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+\n"
"BwAAAAAAAAAAAAAAAMDAAAAKpAoAAAf+/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/gcAAAAAAAAA\n"
"AAAAAAAAAAqkpAAAB/7+/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/v4HAAAAAAAAAAAAAAAAAKQK\n"
"AAAH/v7+/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/v7+BwAAAAAAwADAAAAAAAAAAAf+/v7+/v7+\n"
"/v7+/v7+9/cAAAAAAAAAAAD39/7+/v7+/gcAAAAAAMDAwAAAAAAAB/7+/v7+/v7+/v7+/v7+9/cA\n"
"AAAAAAAAAAD39/7+/v7+/v4HAAAAwAAAwAAAAAAH/v7+/v7+/v7+/v7+/v7+9/cAAAAAAAAAAAD3\n"
"9/7+/v7+/v7+BwAAAAAAwAAAAAf+/v7+/v7+/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/v7+/v7+\n"
"/gcAAAAAAAAAB/7+/v7+/v7+/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/v7+/v7+/v4HAAAAAAAH\n"
"/v7+/v7+/v7+/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/v7+/v7+/v7+BwAAAAf+/v7+/v7+/v7+\n"
"/v7+/v7+/v7+9/cAAAAAAAAAAAD39/7+/v7+/v7+/v7+/gcAB/7+/v7+/v7+/v7+/v7+/v7+/v7+\n"
"9/cAAAAAAAAAAAD39/7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+9/cAAAAAAAAA\n"
"AAD39/f+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v739/cAAAAAAAAAAAD3+/v79/f3\n"
"9/f39/f39/f39/f39/f39/f39/f39/f39/f39/f39/cAAAAAAAAAAAD30NDQ9/f39/f39/f39/f3\n"
"9/f39/f39/f39/f39/f396SkpKSk9/cAAAAAAAAAAAD3+ff59/f39/f39/f39/f39/f39/f39/f3\n"
"9/f39/f39/f39/f39/cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///////wAA////\n"
"////AAD//h////8AAP/8D////wAA//gP////AAD/8Af///8AAP/gAAAP/wAA/8AAAD//AAD/gAAB\n"
"//8AAP8AAAH//wAA/gAAAAADAAD8AAAAAAMAAPgAAAAAAwAA8AAAAAADAADgAAAAAAMAAMAAAAAA\n"
"AwAAgAAAAAADAACAAAAAAAMAAIAAAAAAAwAAgAAAAAADAADAAAAAAAMAAPAAAAAAAwAA+AAAAAAD\n"
"AAD8AAAAAAMAAP4AAAAAAwAA/gAAAAADAAD+AAAAAAMAAP4AAAAAAwAA/gAAAAADAAD+AAAAAAMA\n"
"AP4AAAAAAwAA/gAAAAADAAD+AAAAAAMAAP4AAAAAAwAA/gAAAAADAAD+AAAAAAMAAP4AAAAAAwAA\n"
"/gAAAAADAAD+AAAAAAMAAP4AAAAAAwAA/gAAAAADAAD+AAAAAAMAAP4AAAAAAwAA/gAAAAADAAD+\n"
"AAAAAAMAAP///////wAA////////AAD///////8AAA==\n"
).decode("base64_codec"),
"mc5b.ico": (
"AAABAAEAICAQAAAAAADoAgAAFgAAACgAAAAgAAAAQAAAAAEABAAAAAAAgAIAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA\n"
"/wD/AP//AAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAAAAAAAAAAAAAAAABwAAAAAAAAAAAAAAAAAA\n"
"AHAAYAAAAAAAAAAAAAAAAAcABmYAAAAAAAAAAAAAAKBwAGZmYABwAAAAAAAAAACqAAZmZmYABwAA\n"
"AAAAAAAAqqBmZmZgAABwAAAAAAAAB6qqZmZmAAAABwAACqqqqqqiqqZmYAAAAABwAAqqqqqqoiqq\n"
"ZgBwcAAABwAKoiIiIiIiqqAHBwAAAABwCqIiIiIiIiqqcHBwAAAABwqiIiIiIiIqqgcHBwAAAAAK\n"
"oiIiIiIiqqBwcHAABAAACqqqqqqiKqoHB3cAAEAAAAqqqqqqoqqgAAB3AAwAAAcAAAAAAKqqAAAA\n"
"AABAAABwAAAAAACqpwAAAAAMAHAHAAAAAAAAqgBwAAAAAAdwcAAAAAAAAKAABwAAxAAABwAAAAAA\n"
"AAAAAABwAEzAAHAAAAAAAAAAAAAABwAAwAcAAAAAAAAAAAAAAABwAABwAAAAAAAAAAAAAAAABwAH\n"
"AAAAAAAAAAAAAAAAAABwcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAD///////////////////////8H///+B////AP///gB///QAH//wAA//8AAH/+A\n"
"AA+AAAAHgAAAA4AAAAGAAAAAgAAAAIAAAACAAAAAgAAAAP/AAAH/wAAD/8wAB//eAA///wAf//+A\n"
"P///wH///+D////x/////////////////w==\n"
).decode("base64_codec"),
"mc2.ico": (
"AAABAAEAMDAAAQAAAACoDgAAFgAAACgAAAAwAAAAYAAAAAEACAAAAAAAgAoAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAwMDAAMDcwADwyqYAACBAAAAgYAAAIIAA\n"
"ACCgAAAgwAAAIOAAAEAAAABAIAAAQEAAAEBgAABAgAAAQKAAAEDAAABA4AAAYAAAAGAgAABgQAAA\n"
"YGAAAGCAAABgoAAAYMAAAGDgAACAAAAAgCAAAIBAAACAYAAAgIAAAICgAACAwAAAgOAAAKAAAACg\n"
"IAAAoEAAAKBgAACggAAAoKAAAKDAAACg4AAAwAAAAMAgAADAQAAAwGAAAMCAAADAoAAAwMAAAMDg\n"
"AADgAAAA4CAAAOBAAADgYAAA4IAAAOCgAADgwAAA4OAAQAAAAEAAIABAAEAAQABgAEAAgABAAKAA\n"
"QADAAEAA4ABAIAAAQCAgAEAgQABAIGAAQCCAAEAgoABAIMAAQCDgAEBAAABAQCAAQEBAAEBAYABA\n"
"QIAAQECgAEBAwABAQOAAQGAAAEBgIABAYEAAQGBgAEBggABAYKAAQGDAAEBg4ABAgAAAQIAgAECA\n"
"QABAgGAAQICAAECAoABAgMAAQIDgAECgAABAoCAAQKBAAECgYABAoIAAQKCgAECgwABAoOAAQMAA\n"
"AEDAIABAwEAAQMBgAEDAgABAwKAAQMDAAEDA4ABA4AAAQOAgAEDgQABA4GAAQOCAAEDgoABA4MAA\n"
"QODgAIAAAACAACAAgABAAIAAYACAAIAAgACgAIAAwACAAOAAgCAAAIAgIACAIEAAgCBgAIAggACA\n"
"IKAAgCDAAIAg4ACAQAAAgEAgAIBAQACAQGAAgECAAIBAoACAQMAAgEDgAIBgAACAYCAAgGBAAIBg\n"
"YACAYIAAgGCgAIBgwACAYOAAgIAAAICAIACAgEAAgIBgAICAgACAgKAAgIDAAICA4ACAoAAAgKAg\n"
"AICgQACAoGAAgKCAAICgoACAoMAAgKDgAIDAAACAwCAAgMBAAIDAYACAwIAAgMCgAIDAwACAwOAA\n"
"gOAAAIDgIACA4EAAgOBgAIDggACA4KAAgODAAIDg4ADAAAAAwAAgAMAAQADAAGAAwACAAMAAoADA\n"
"AMAAwADgAMAgAADAICAAwCBAAMAgYADAIIAAwCCgAMAgwADAIOAAwEAAAMBAIADAQEAAwEBgAMBA\n"
"gADAQKAAwEDAAMBA4ADAYAAAwGAgAMBgQADAYGAAwGCAAMBgoADAYMAAwGDgAMCAAADAgCAAwIBA\n"
"AMCAYADAgIAAwICgAMCAwADAgOAAwKAAAMCgIADAoEAAwKBgAMCggADAoKAAwKDAAMCg4ADAwAAA\n"
"wMAgAMDAQADAwGAAwMCAAMDAoADw+/8ApKCgAICAgAAAAP8AAP8AAAD//wD/AAAA/wD/AP//AAD/\n"
"//8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKQAAKQAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAACkpKSkpKSkpAAAAACkpKSkpKSkpKSkpKSkpKSkpKSkpKSk\n"
"pKQAAAAAAAAAAAAAAKSkpKSkpKQAAAAAAAAApKSkpKSkpKSkpKSkpKSkpKSkpKSkpKSkAAAAAAAA\n"
"AAAAAKSkpKSkpAcAAAAAAAAAB6SkpKSkpKSkpKSkpKSkpKSkpKSkpKSkAAAAAAAAAAAAAKSkpKSk\n"
"BwAAAAAACQAAAACkpKSkpKSkpKSkpKSkpKSkpKQAAACkAAAAAAAAAAAAAKSkpKQHAAAAAAAJCQkA\n"
"AAAHpKSkpKSkpKSkpKSkpKSkpKQAAACkAAAAAAAAAAAAAKSkpAcAAAAAAAkJCQkJAAAAAAekpKSk\n"
"pKSkpKSkpKSkpKQAAACkAAAAAAAAAAAAAKSkBwAAAAAACQkJCQkJCQAAAAAHpKSkpKSkpKSkpKSk\n"
"pKSkpKSkAAAAAAAAAAAAAKQAAAAAAAAJCQkJCQkJCQkAAAAAB6SkpKSkpKSkpKSkpKSkpKSkAAAA\n"
"AAAAAAAAAKQAAAAAAAkJCQkJCQkJCQkJAAAAAAekpKSkpKSkpKSkpKSkpKSkAAAAAAAAAAAApAAA\n"
"AAAACQkJCQkJCQkJCQkAAAAAAAAHpKSkpKSkpKSkpKSkpKSkAAAAAAAAAACkAAAAAAAJCQkJCQkJ\n"
"CQkJCQAAAAAAAAAAB6SkpKSkpKSkpKSkpKSkAAAAAAAAAKQAAAAAAAkJCQkJCQkJCQkJAAAAAAAA\n"
"AAAAAAekpKSkpKSkpKSkpKSkAAAAAAAApAAAAAAACQkJCQkJCQkJCQkAAAAAAAAAAAAAAAAHpKSk\n"
"pKSkpKSkpKSkAAAAAACkAAAAAAAJCQkJCQkJCQkJCQAApAAApAAAAAAAAAAAB6SkpKSkpKSkpKSk\n"
"AAAAAAAAAAAAAAkJCQkJCQkJCQkJAACkAACkAAAAAAAAAAAAAAekpKSkpKSkpKSkAAAAAAAAAAAA\n"
"CQkJCQkJCQkJCQkAAKQAAKQAAAAAAAAAAAAAAAAHpKSkpKSkpKSkAAAAAACkAAAAAAkJCQkJCQkJ\n"
"CQAApAAApAAAAKQAAAAAAAAAAAAAB6SkpKSkpKSkAAAAAAAApAAAAAAJCQkJCQkJAACkAACkAAAA\n"
"pACkAAAAAAAAAAAAAAekpKSkpKSkAAAAAAAAAACkAAAACQkJCQkAAKQAAKQAAACkAKQAAAAAAAAA\n"
"AAAAAAAHpKSkpKSkAAAAAAAAAAAAAAAAAAkJCQAAAAAApAAAAAAApAAAAAAAAADAAAAAAAAAB6Sk\n"
"pKSkAAAAAAAAAAAAAAAAAAAJAAAAAACkAAAKpKQAAAAAAAAAAMAAAAAAAAAAAKSkpKSkAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAKQAAACkAKSkAAAAAAAAwAAAAAAAAAAAB6SkpKSkAAAAAAAAAAAAAKQAAAAA\n"
"AAAAAAAAAACkpACkAAAAAADAAAAAAAAAAAAHpKSkpKSkAAAAAAAAAAAAAKSkBwAAAAAAAAAAAAAA\n"
"pKQKAAAAAMDAAAAAAAAAAAekpKSkpKSkAAAAAAAAAAAAAKSkpAcAAAAAAAAAAAAAAAAAAAAAwAAA\n"
"AAAAAAAAB6SkpKSkpKSkAAAAAAAAAAAAAKSkpKQHAAAAAAAAAAAAAAAAAADAAAAACqQAAAAHpKSk\n"
"pKSkpKSkAAAAAAAAAAAAAKSkpKSkBwAAAAAAAAAAAAAAAMDAAAAKpAoAAAekpKSkpKSkpKSkAAAA\n"
"AAAAAAAAAKSkpKSkpAcAAAAAAAAAAAAAAAAAAAqkpAAAB6SkpKSkpKSkpKSkAAAAAAAAAAAAAKSk\n"
"pKSkpKQHAAAAAAAAAAAAAAAAAKQKAAD3pKSkpKSkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9wAA\n"
"AAAAwADAAAAAAAAAAPf39/f396SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9/cAAAAAAMDAwAAA\n"
"AAAAB6SkpPf396SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9/f3AAAAwAAAwAAAAACkpKSkpPf3\n"
"96SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9/f39wAAAAAAwAAAAPekpKSkpPf396SkpKSkpKSk\n"
"AAAAAAAAAAAAAKSkpKSkpKSk9/f39/cAAAAAAAAA9/ekpKSkpPf396SkpKSkpKSkAAAAAAAAAAAA\n"
"AKSkpKSkpKSk9/f39/f3AAAAAAD39/ekpKSkpPf396SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk\n"
"9/f39/f39wAAAPf39/ekpKSkpPf396SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9/f39/f39/cA\n"
"9/f39/ekpKSkpPf396SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9/f39/f39/f39/f39/ekpKSk\n"
"pPf396SkpKSkpKSkAAAAAAAAAAAAAKSkpKSkpKSk9/f39/f39/f39/f39/ekpKSkpPf396SkpKSk\n"
"pKQAAAAAAAAAAAAAAKSkpKSkpKSk9/f39/f39/f39/f39/ekpKSkpPf396SkpKSkpAAA9gAAAAAA\n"
"AAAAAACkpKSkpKSk9/f39/f39/f39/f39/f39/f39/f396SkpKSkAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////////AAD///////8AAP///////wAA////////\n"
"AAD//w////8AAP8AAAAADwAA/gAAAAAHAAD+AAAAAAcAAP4AAAAABwAA/gAAAAAHAAD+AAAAAAcA\n"
"AP4AAAAABwAA/gAAAAAHAAD+AAAAAAcAAPwAAAAABwAA+AAAAAAHAADwAAAAAAcAAOAAAAAABwAA\n"
"wAAAAAAHAADAAAAAAAcAAMAAAAAABwAAwAAAAAAHAADgAAAAAAcAAPgAAAAABwAA/AAAAAAHAAD+\n"
"AAAAAAcAAP4AAAAABwAA/gAAAAAHAAD+AAAAAAcAAP4AAAAABwAA/gAAAAAHAAD+AAAAAAcAAP4A\n"
"AAAABwAA/gAAAAAHAAD+AAAAAAcAAP4AAAAABwAA/gAAAAAHAAD+AAAAAAcAAP4AAAAABwAA/gAA\n"
"AAAHAAD+AAAAAAcAAP4AAAAABwAA/gAAAAAHAAD+AAAAAA8AAP4AAAAAGwAA/wAAAAA/AAD/////\n"
"//sAAP///////wAA\n"
).decode("base64_codec"),
"mc6a.ico": (
"AAABAAEAICAQAAAAAADoAgAAFgAAACgAAAAgAAAAQAAAAAEABAAAAAAAgAIAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAACAAACAAAAAgIAAgAAAAIAAgACAgAAAgICAAMDAwAAAAP8AAP8AAAD//wD/AAAA\n"
"/wD/AP//AAD///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAAAAAAAAAAAAAAAAHAAAAAAAAAAAAAAAAAAAAcABg\n"
"AAAAAAAAAAAAAAAABwAGZgAAAAAAAAAAAAAAAHAAZmZgAHAAAACgAAAAAAcABmZmZgAHAAAAqgAA\n"
"AABwAGZmZmAAAHAAAKqgAAAHAAZmZmYAAAAHAACqqgAAcABmZmZgAAqqqqqqoqqgAAAGZmZmAHB6\n"
"qqqqqqIqqgAAZmZmYAcHCqIiIiIiIqqgAAZmZgBwcHqiIiIiIiIqqnAAZmAHBwcKoiIiIiIiKqoA\n"
"AAYAAHBweqIiIiIiIqqgAAAAAAcHdwqqqqqqoiqqAAAAAAAAAHcKqqqqqqKqoAAAAHAAAAAAAEAA\n"
"AHCqqgAAAAAHAAAAAAwAcAcAqqAAAAAAAHAAAAAAB3BwAKoAAAAAAAAHAADEAAAHAACgAAAAAAAA\n"
"AHAATMAAcAAAAAAAAAAAAAAHAADABwAAAAAAAAAAAAAAAHAAAHAAAAAAAAAAAAAAAAAHAAcAAAAA\n"
"AAAAAAAAAAAAAHBwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n"
"AAAAAAAAAAAAAAD//////////////////////wf///4H///8A///+AH///AAf3/gAD8/wAAfH4AA\n"
"Dw8AAAAHAAAAAwAAAAEAAAAAAAAAAMAAAAHgAAAD8AAAB/AAAQ/4AAMf/AAHP/4AD3//AB///4A/\n"
"///Af///4P////H//////////////////w==\n"
).decode("base64_codec"),
}

787
lzari.py Normal file
View File

@ -0,0 +1,787 @@
#
# lzari.py
#
# By Ross Ridge
#
"""
Implementation of Haruhiko Okumura's LZARI data compression algorithm
in Python. Largely based on LZARI.C, one key difference is the use of
a two level dicitionary look up during compression rather than
LZARI.C's binary search tree.
"""
_SCCS_ID = "@(#) mysc lzari.py 1.5 08/02/05 16:00:34\n"
import sys
import array
import binascii
import string
import time
from bisect import bisect_right
from math import log
try:
import ctypes
import mymcsup
except ImportError:
mymcsup = None
hexlify = binascii.hexlify
__ALL__ = ['lzari_codec', 'string_to_bit_array', 'bit_array_to_string']
#
# Fundamental constants of the LZARI compression alogorithm.
#
# Changing any of these values will create an incompatible implementation.
#
HIST_LEN = 4096
MIN_MATCH_LEN = 3
MAX_MATCH_LEN = 60
ARITH_BITS = 15
QUADRANT1 = 1 << ARITH_BITS
QUADRANT2 = QUADRANT1 * 2
QUADRANT3 = QUADRANT1 * 3
QUADRANT4 = QUADRANT1 * 4
MAX_CUM = QUADRANT1 - 1
MAX_CHAR = (256 + MAX_MATCH_LEN - MIN_MATCH_LEN + 1)
#
# Other constants specific to this implementation
#
MAX_SUFFIX_CHAIN = 50 # limit on how many identical suffixes to try to match
#def debug(value, msg):
# print "@@@ %s %04x" % (msg, value)
debug = lambda value, msg: None
_tr_16 = string.maketrans("0123456789abcdef",
"\x00\x01\x02\x03"
"\x10\x11\x12\x13"
"\x20\x21\x22\x23"
"\x30\x31\x32\x33")
_tr_4 = string.maketrans("0123",
"\x00\x01"
"\x10\x11")
_tr_2 = string.maketrans("01", "\x00\x01")
def string_to_bit_array(s):
"""Convert a string to an array containing a sequence of bits."""
s = binascii.hexlify(s).translate(_tr_16)
s = binascii.hexlify(s).translate(_tr_4)
s = binascii.hexlify(s).translate(_tr_2)
a = array.array('B', s)
return a
_tr_rev_2 = string.maketrans("\x00\x01", "01")
_tr_rev_4 = string.maketrans("\x00\x01"
"\x10\x11",
"0123")
_tr_rev_16 = string.maketrans("\x00\x01\x02\x03"
"\x10\x11\x12\x13"
"\x20\x21\x22\x23"
"\x30\x31\x32\x33",
"0123456789abcdef")
def bit_array_to_string(a):
"""Convert an array containing a sequence of bits to a string."""
remainder = len(a) % 8
if remainder != 0:
a.fromlist([0] * (8 - remainder))
s = a.tostring()
s = binascii.unhexlify(s.translate(_tr_rev_2))
s = binascii.unhexlify(s.translate(_tr_rev_4))
return binascii.unhexlify(s.translate(_tr_rev_16))
def _match(src, pos, hpos, mlen, end):
mlen += 1
if not src.startswith(src[hpos : hpos + mlen], pos):
return None
for i in range(mlen, end):
if src[pos + i] != src[hpos + i]:
return i
return end
def _rehash_table2(src, chars, head, next, next2, hist_invalid):
p = head
table2 = {}
l = []
while p > hist_invalid:
l.append(p)
p = next[p % HIST_LEN]
l.reverse()
for p in l:
p2 = p + MIN_MATCH_LEN
key2 = src[p2 : p2 + chars]
head2 = table2.get(key2, hist_invalid)
next2[p % HIST_LEN] = head2
table2[key2] = p
return table2
class lzari_codec(object):
# despite the name this does not implement a codec compatible
# with Python's codec system
def init(self, decode):
self.high = QUADRANT4
self.low = 0
if decode:
self.code = 0
# reverse the order of sym_cum so bisect_right() can
# be used for faster searching
self.sym_cum = range(0, MAX_CHAR + 1)
else:
self.shifts = 0
self.char_to_symbol = range(1, MAX_CHAR + 1)
self.sym_cum = range(MAX_CHAR, -1, -1)
self.next_table = [None] * HIST_LEN
self.next2_table = [None] * HIST_LEN
self.suffix_table = {}
self.symbol_to_char = [0] + range(MAX_CHAR)
self.sym_freq = [0] + [1] * MAX_CHAR
self.position_cum = [0] * (HIST_LEN + 1)
a = 0
for i in range(HIST_LEN, 0, -1):
a = a + 10000 / (200 + i)
self.position_cum[i - 1] = a
def search(self, table, x):
c = 1
s = len(table) - 1
while True:
a = (s + c) / 2
if table[a] <= x:
s = a
else:
c = a + 1
if c >= s:
break
return c
def update_model_decode(self, symbol):
# A compatible implemention to the one used while compressing.
sym_freq = self.sym_freq
sym_cum = self.sym_cum
if self.sym_cum[MAX_CHAR] >= MAX_CUM:
c = 0
for i in range(MAX_CHAR, 0, -1):
self.sym_cum[MAX_CHAR - i] = c
a = (self.sym_freq[i] + 1) / 2
self.sym_freq[i] = a
c += a
self.sym_cum[MAX_CHAR] = c
freq = sym_freq[symbol]
new_symbol = symbol
while self.sym_freq[new_symbol - 1] == freq:
new_symbol -= 1
# new_symbol = sym_freq.index(freq)
if new_symbol != symbol:
symbol_to_char = self.symbol_to_char
swap_char = symbol_to_char[new_symbol]
char = symbol_to_char[symbol]
symbol_to_char[new_symbol] = char
symbol_to_char[symbol] = swap_char
sym_freq[new_symbol] = freq + 1
for i in range(MAX_CHAR - new_symbol + 1, MAX_CHAR + 1):
sym_cum[i] += 1
def update_model_encode(self, symbol):
sym_freq = self.sym_freq
sym_cum = self.sym_cum
if sym_cum[0] >= MAX_CUM:
c = 0
for i in range(MAX_CHAR, 0, -1):
sym_cum[i] = c
a = (sym_freq[i] + 1) / 2
sym_freq[i] = a
c += a
sym_cum[0] = c
freq = sym_freq[symbol]
new_symbol = symbol
while sym_freq[new_symbol - 1] == freq:
new_symbol -= 1
if new_symbol != symbol:
debug(new_symbol, "a")
swap_char = self.symbol_to_char[new_symbol]
char = self.symbol_to_char[symbol]
self.symbol_to_char[new_symbol] = char
self.symbol_to_char[symbol] = swap_char
self.char_to_symbol[char] = new_symbol
self.char_to_symbol[swap_char] = symbol
sym_freq[new_symbol] += 1
for i in range(new_symbol):
sym_cum[i] += 1
def decode_char(self):
high = self.high
low = self.low
code = self.code
sym_cum = self.sym_cum
_range = high - low
max_cum_freq = sym_cum[MAX_CHAR]
n = ((code - low + 1) * max_cum_freq - 1) / _range
i = bisect_right(sym_cum, n, 1)
high = low + sym_cum[i] * _range / max_cum_freq
low += sym_cum[i - 1] * _range / max_cum_freq
symbol = MAX_CHAR + 1 - i
while True:
if low < QUADRANT2:
if low < QUADRANT1 or high > QUADRANT3:
if high > QUADRANT2:
break
else:
low -= QUADRANT1
code -= QUADRANT1
high -= QUADRANT1
else:
low -= QUADRANT2
code -= QUADRANT2
high -= QUADRANT2
low *= 2
high *= 2
code = code * 2 + self.in_iter()
ret = self.symbol_to_char[symbol]
self.high = high
self.low = low
self.code = code
self.update_model_decode(symbol)
return ret
def decode_position(self):
_range = self.high - self.low
max_cum = self.position_cum[0]
pos = self.search(self.position_cum,
((self.code - self.low + 1)
* max_cum - 1) / _range) - 1
self.high = (self.low +
self.position_cum[pos] * _range / max_cum)
self.low += self.position_cum[pos + 1] * _range / max_cum
while True:
if self.low < QUADRANT2:
if (self.low < QUADRANT1
or self.high > QUADRANT3):
if self.high > QUADRANT2:
return pos
else:
self.low -= QUADRANT1
self.code -= QUADRANT1
self.high -= QUADRANT1
else:
self.low -= QUADRANT2
self.code -= QUADRANT2
self.high -= QUADRANT2
self.low *= 2
self.high *= 2
self.code = self.in_iter() + self.code * 2
def add_suffix_1(self, pos, find):
# naive implemention used for testing
if not find:
return (None, 0)
src = self.src
mlen = min(1000, self.max_match, len(src) - pos)
hist_start = max(pos - HIST_LEN, 0)
while mlen >= MIN_MATCH_LEN:
i = src.rfind(src[pos : pos + mlen], hist_start, pos)
if i != -1:
assert (src[pos : pos + mlen]
== src[i: i + mlen])
return (i, mlen)
mlen -= 1
return (None, -1)
def add_suffix_2(self, pos, find):
# a two level dictionary look up that leverages Python's
# built-in dicts to get something that's hopefully faster
# than implementing binary trees in completely in Python.
src = self.src
suffix_table = self.suffix_table
max_match = min(self.max_match, len(src) - pos)
mlen = -1
mpos = None
hist_invalid = pos - HIST_LEN - 1
modpos = pos % HIST_LEN
pos2 = pos + MIN_MATCH_LEN
key = src[pos : pos2]
a = suffix_table.get(key)
if a != None:
next = self.next_table
next2 = self.next2_table
[count, head, table2, chars] = a
pos3 = pos2 + chars
key2 = src[pos2 : pos3]
min_match2 = MIN_MATCH_LEN + chars
if find:
p = table2.get(key2, hist_invalid)
maxmlen = max_match - min_match2
while p > hist_invalid and mlen != maxmlen:
p3 = p + min_match2
if mpos == None and p3 <= pos:
mpos = p
mlen = 0
if p3 >= pos:
p = next2[p % HIST_LEN]
continue
rlen = _match(src, pos3, p3, mlen,
min(maxmlen, pos - p3))
if rlen != None:
mpos = p
mlen = rlen
p = next2[p % HIST_LEN]
if mpos != None:
mlen += min_match2
elif find:
p = head
maxmlen = min(chars, max_match - MIN_MATCH_LEN)
i = 0
while (p > hist_invalid and i < 50000
and mlen < maxmlen):
assert i < count
i += 1
p2 = p + MIN_MATCH_LEN
l2 = pos - p2
if mpos == None and l2 >= 0:
mpos = p
mlen = 0
if l2 <= 0:
p = next[p % HIST_LEN]
continue
if l2 > maxmlen:
l2 = maxmlen
m = mlen + 1
if src.startswith(src[p2 : p2 + m],
pos2):
mpos = p
for j in range(m, l2):
if (src[pos2 + j]
!= src[p2 + j]):
mlen = j
break
else:
mlen = l2
#rlen = _match(src, pos2, p2, mlen, l2)
#if rlen != None:
# mpos = p
# mlen = rlen
p = next[p % HIST_LEN]
if mpos != None:
mlen += MIN_MATCH_LEN
count += 1
new_chars = int(log(count, 2))
# new_chars = 50
new_chars = min(new_chars, max_match - MIN_MATCH_LEN)
if new_chars > chars:
chars = new_chars
table2 = _rehash_table2(src, chars, head,
next, next2,
hist_invalid)
next[modpos] = head
head = pos
key2 = src[pos2 : pos2 + chars]
head2 = table2.get(key2, hist_invalid)
next2[modpos] = head2
table2[key2] = pos
a[0] = count
a[1] = head
a[2] = table2
a[3] = chars
else:
self.next_table[modpos] = hist_invalid
self.next2_table[modpos] = hist_invalid
key2 = ""
# key2 = src[pos2 : pos2 + 1]
suffix_table[key] = [1, pos, {key2: pos}, len(key2)]
p = pos - HIST_LEN
if p >= 0:
p2 = p + MIN_MATCH_LEN
key = src[p : p2]
a = suffix_table[key]
(count, head, table2, chars) = a
count -= 1
if count == 0:
assert head == p
del suffix_table[key]
else:
key2 = src[p2 : p2 + chars]
if table2[key2] == p:
del table2[key2]
a[0] = count
assert (mpos == None
or src[pos : pos + mlen] == src[mpos : mpos + mlen])
return (mpos, mlen)
def _add_suffix(self, pos, find):
r = self.add_suffix_2(pos, find)
start_pos = self.start_pos
if find and r[0] != None:
print ("%4d %02x %4d %2d"
% (pos - start_pos, ord(self.src[pos]),
r[0] - start_pos, r[1]))
else:
print ("%4d %02x"
% (pos - start_pos, ord(self.src[pos])))
return r
add_suffix = add_suffix_2
def output_bit(self, bit):
self.append_bit(bit)
bit ^= 1
for i in range(self.shifts):
self.append_bit(bit)
self.shifts = 0
def encode_char(self, char):
low = self.low
high = self.high
sym_cum = self.sym_cum
symbol = self.char_to_symbol[char]
range = high - low
high = low + range * sym_cum[symbol - 1] / sym_cum[0]
low += range * sym_cum[symbol] / sym_cum[0]
debug(high, "high");
debug(low, "low");
while True:
if high <= QUADRANT2:
self.output_bit(0)
elif low >= QUADRANT2:
self.output_bit(1)
low -= QUADRANT2
high -= QUADRANT2
elif low >= QUADRANT1 and high <= QUADRANT3:
self.shifts += 1
low -= QUADRANT1
high -= QUADRANT1
else:
break
low *= 2
high *= 2
self.low = low
self.high = high
self.update_model_encode(symbol)
def encode_position(self, position):
position_cum = self.position_cum
low = self.low
high = self.high
range = high - low
high = low + range * position_cum[position] / position_cum[0]
low += range * position_cum[position + 1] / position_cum[0]
debug(high, "high");
debug(low, "low");
while True:
if high <= QUADRANT2:
self.output_bit(0)
elif low >= QUADRANT2:
self.output_bit(1)
low -= QUADRANT2
high -= QUADRANT2
elif low >= QUADRANT1 and high <= QUADRANT3:
self.shifts += 1
low -= QUADRANT1
high -= QUADRANT1
else:
break
low *= 2
high *= 2
self.low = low
self.high = high
def encode(self, src, progress = None):
"""Compress a string."""
length = len(src)
if length == 0:
return ""
out_array = array.array('B')
self.out_array = out_array
self.append_bit = out_array.append
self.init(False)
max_match = min(MAX_MATCH_LEN, length)
self.max_match = max_match
self.src = src = "\x20" * max_match + src
in_length = len(src)
self.start_pos = max_match
for in_pos in range(max_match):
self.add_suffix(in_pos, False)
in_pos += 1
last_percent = -1
while in_pos < in_length:
if progress:
percent = (in_pos - max_match) * 100 / length
if percent != last_percent:
sys.stderr.write("%s%3d%%\r"
% (progress, percent))
last_percent = percent
debug(ord(src[in_pos]), "src")
(match_pos, match_len) = self.add_suffix(in_pos, True)
if match_len < MIN_MATCH_LEN:
self.encode_char(ord(src[in_pos]))
else:
debug(in_pos - match_pos - 1, "match_pos")
debug(match_len, "match_len")
self.encode_char(256 - MIN_MATCH_LEN
+ match_len)
self.encode_position(in_pos - match_pos - 1)
for i in range(match_len - 1):
in_pos += 1
self.add_suffix(in_pos, False)
in_pos += 1
self.shifts += 1
if self.low < QUADRANT1:
self.output_bit(0)
else:
self.output_bit(1)
#for k, v in sorted(self.suffix_table.items()):
# count, head, table2, chars = v
# print hexlify(k), count, head, len(table2), chars
if progress:
sys.stderr.write("%s100%%\n" % progress)
return bit_array_to_string(out_array)
def decode(self, src, out_length, progress = None):
"""Decompress a string."""
a = string_to_bit_array(src)
a.fromlist([0] * 32) # add some extra bits
self.in_iter = iter(a).next
out = array.array('B', "\0") * out_length
outpos = 0
self.init(True)
self.code = 0
for i in range(ARITH_BITS + 2):
self.code += self.code + self.in_iter()
hist_pos = HIST_LEN - MAX_MATCH_LEN
history = [0x20] * hist_pos + [0] * MAX_MATCH_LEN
decode_char = self.decode_char
last_percent = -1
last_time = time.time()
while outpos < out_length:
if progress:
percent = outpos * 100 / out_length
if percent != last_percent:
now = time.time()
if now - last_time >= 1:
sys.stderr.write("%s%3d%%\r"
% (progress, percent))
last_percent = percent
last_time = now
char = decode_char()
if char >= 0x100:
pos = self.decode_position()
length = char - 0x100 + MIN_MATCH_LEN
base = (hist_pos - pos - 1) % HIST_LEN
for off in range(length):
a = history[(base + off) % HIST_LEN]
out[outpos] = a
outpos += 1
history[hist_pos] = a
hist_pos = (hist_pos + 1) % HIST_LEN
else:
out[outpos] = char
outpos += 1
history[hist_pos] = char
hist_pos = (hist_pos + 1) % HIST_LEN
self.in_iter = None
if progress:
sys.stderr.write("%s100%%\n" % progress)
return out.tostring()
if mymcsup == None:
def decode(src, out_length, progress = None):
return lzari_codec().decode(src, out_length, progress)
def encode(src, progress = None):
return lzari_codec().encode(src, progress)
else:
mylzari_decode = mymcsup.mylzari_decode
mylzari_encode = mymcsup.mylzari_encode
mylzari_free_encoded = mymcsup.mylzari_free_encoded
def decode(src, out_length, progress = None):
out = ctypes.create_string_buffer(out_length)
if (mylzari_decode(src, len(src), out, out_length, progress)
== -1):
raise ValueError, "compressed input is corrupt"
return ctypes.string_at(out, out_length)
def encode(src, progress = None):
(r, compressed, comp_len) = mylzari_encode(src, len(src),
progress)
# print r, compressed.value, comp_len
if r == -1:
raise MemoryError, "out of memory during compression"
ret = ctypes.string_at(compressed.value, comp_len.value)
mylzari_free_encoded(compressed)
return ret;
def main2(args):
import struct
import os
src = file(args[2], "rb").read()
lzari = lzari_codec()
out = file(args[3], "wb")
start = os.times()
if args[1] == "c":
dest = lzari.encode(src)
now = os.times()
out.write(struct.pack("L", len(src)))
else:
dest = lzari.decode(src[4:],
struct.unpack("L", src[:4])[0])
now = os.times()
out.write(dest)
out.close()
print "time:", now[0] - start[0], now[1] - start[1], now[4] - start[4]
def _get_hotshot_lineinfo(filename):
import hotshot.log
log = hotshot.log.LogReader(filename)
timings = {}
for what, loc, tdelta in log:
if what == hotshot.log.LINE:
a = timings.get(loc)
if a == None:
timings[loc] = [1, tdelta]
else:
a[0] += 1
a[1] += tdelta
return timings.items()
def _dump_hotshot_lineinfo(log):
a = sorted(_get_hotshot_lineinfo(log))
total_count = sum((time[0]
for (loc, time) in a))
total_time = sum((time[1]
for (loc, time) in a))
for (loc, [count, time]) in a:
print ("%8d %6.3f%% %8d %6.3f%%"
% (time, time * 100.0 / total_time,
count, count * 100.0 / total_count)),
print "%s:%d(%s)" % loc
def _dump_hotshot_lineinfo2(log):
cur = None
a = sorted(_get_hotshot_lineinfo(log))
total_count = sum((time[0]
for (loc, time) in a))
total_time = sum((time[1]
for (loc, time) in a))
for ((filename, lineno, fn), [count, time]) in a:
if cur != filename:
if cur != None and f != None:
for line in f:
print line[:-1]
f.close()
try:
f = file(filename, "r")
except OSError:
f = None
cur = filename
l = 0
print "#", filename
if f != None:
while l < lineno:
print f.readline()[:-1]
l += 1
print ("# %8d %6.3f%% %8d %6.3f%%"
% (time, time * 100.0 / total_time,
count, count * 100.0 / total_count))
if cur != None and f != None:
for line in f:
print line[:-1]
f.close()
def main(args):
import os
if args[1] == "pc":
import profile
pr = profile.Profile()
for i in range(5):
print pr.calibrate(100000)
return
elif args[1] == "p":
import profile
ret = 0
# profile.Profile.bias = 5.26e-6
profile.runctx("ret = main2(args[1:])",
globals(), locals())
return ret
elif args[1].startswith("h"):
import hotshot, hotshot.stats
import warnings
warnings.filterwarnings("ignore")
tmp = os.tempnam()
try:
l = args[1].startswith("hl")
p = hotshot.Profile(tmp, l)
ret = p.runcall(main2, args[1:])
p.close()
p = None
if l:
if args[1] == "hl2":
_dump_hotshot_lineinfo2(tmp)
else:
_dump_hotshot_lineinfo(tmp)
else:
hotshot.stats.load(tmp).print_stats()
finally:
try:
os.remove(tmp)
except OSError:
pass
return ret
return main2(args)
if __name__ == '__main__':
sys.exit(main(sys.argv))

810
mymc.py Normal file
View File

@ -0,0 +1,810 @@
#
# mymc.py
#
# By Ross Ridge
# Public Domain
#
"""A utility for manipulating PS2 memory card images."""
_SCCS_ID = "@(#) mysc mymc.py 1.11 08/08/11 14:12:26\n"[:-1]
import sys
import os
import time
import optparse
import textwrap
import binascii
import string
from errno import EEXIST, EIO
import ps2mc
import ps2save
from ps2mc_dir import *
from round import *
import verbuild
class subopt_error(Exception):
pass
io_error = ps2mc.io_error
if os.name == "nt":
import codecs
class file_wrap(object):
""" wrap a file-like object with a new encoding attribute. """
def __init__(self, f, encoding):
object.__setattr__(self, "_f", f)
object.__setattr__(self, "encoding", encoding)
def __getattribute__(self, name):
if name == "encoding":
return object.__getattribute__(self, name)
return getattr(object.__getattribute__(self, "_f"),
name)
def __setattr__(self, name, value):
if name == "encoding":
raise TypeError, "readonly attribute"
return setattr(object.__getattribute__(self, "_f"),
name, value)
for name in ["stdin", "stdout", "stderr"]:
f = getattr(sys, name)
cur = getattr(f, "encoding", None)
if cur == "ascii" or cur == None:
f = file_wrap(f, "mbcs")
else:
try:
codecs.lookup(cur)
except LookupError:
f = file_wrap(f, "mbcs")
setattr(sys, name, f)
if os.name in ["nt", "os2", "ce"]:
from glob import glob
else:
# assume globing is done by the shell
glob = lambda pattern: [pattern]
def glob_args(args, globfn):
ret = []
for arg in args:
match = globfn(arg)
if len(match) == 0:
ret.append(arg)
else:
ret += match
return ret
def _copy(fout, fin):
"""copy the contents of one file to another"""
while True:
s = fin.read(1024)
if s == "":
break
fout.write(s)
def do_ls(cmd, mc, opts, args, opterr):
mode_bits = "rwxpfdD81C+KPH4"
if len(args) == 0:
args = ["/"]
out = sys.stdout
args = glob_args(args, mc.glob)
for dirname in args:
dir = mc.dir_open(dirname)
try:
if len(args) > 1:
sys.stdout.write("\n" + dirname + ":\n")
for ent in dir:
mode = ent[0]
if (mode & DF_EXISTS) == 0:
continue
for bit in range(0, 15):
if mode & (1 << bit):
out.write(mode_bits[bit])
else:
out.write("-")
if opts.creation_time:
tod = ent[3]
else:
tod = ent[6]
tm = time.localtime(tod_to_time(tod))
out.write(" %7d %04d-%02d-%02d"
" %02d:%02d:%02d %s\n"
% (ent[2],
tm.tm_year, tm.tm_mon, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec,
ent[8]))
finally:
dir.close()
def do_add(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Filename required.")
if opts.directory != None:
mc.chdir(opts.directory)
for src in glob_args(args, glob):
f = open(src, "rb")
dest = os.path.basename(src)
out = mc.open(dest, "wb")
_copy(out, f)
out.close()
f.close()
def do_extract(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Filename required.")
if opts.directory != None:
mc.chdir(opts.directory)
close_out = False
out = None
if opts.output != None:
if opts.use_stdout:
opterr("The -o and -p options are mutually exclusive.")
dont_close_out = True
out = file(opts.output, "wb")
elif opts.use_stdout:
out = sys.stdout
try:
for filename in glob_args(args, mc.glob):
f = mc.open(filename, "rb")
try:
if out != None:
_copy(out, f)
continue
a = filename.split("/")
o = file(a[-1], "wb")
try:
_copy(o, f)
finally:
o.close()
finally:
f.close()
finally:
if close_out:
out.close()
def do_mkdir(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Directory required.")
for filename in args:
mc.mkdir(filename)
def do_remove(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Filename required.")
for filename in args:
mc.remove(filename)
def do_import(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Filename required.")
args = glob_args(args, glob)
if opts.directory != None and len(args) > 1:
opterr("The -d option can only be used with a"
"single savefile.")
for filename in args:
sf = ps2save.ps2_save_file()
f = file(filename, "rb")
try:
ftype = ps2save.detect_file_type(f)
f.seek(0)
if ftype == "max":
sf.load_max_drive(f)
elif ftype == "psu":
sf.load_ems(f)
elif ftype == "cbs":
sf.load_codebreaker(f)
elif ftype == "sps":
sf.load_sharkport(f)
elif ftype == "npo":
raise io_error, (EIO, "nPort saves"
" are not supported.",
filename)
else:
raise io_error, (EIO, "Save file format not"
" recognized", filename)
finally:
f.close()
dirname = opts.directory
if dirname == None:
dirname = sf.get_directory()[8]
print "Importing", filename, "to", dirname
if not mc.import_save_file(sf, opts.ignore_existing,
opts.directory):
print (filename + ": already in memory card image,"
" ignored.")
#re_num = re.compile("[0-9]+")
def do_export(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Directory name required")
if opts.overwrite_existing and opts.ignore_existing:
opterr("The -i and -f options are mutually exclusive.")
args = glob_args(args, mc.glob)
if opts.output_file != None:
if len(args) > 1:
opterr("Only one directory can be exported"
" when the -o option is used.")
if opts.longnames:
opterr("The -o and -l options are mutually exclusive.")
if opts.directory != None:
os.chdir(opts.directory)
for dirname in args:
sf = mc.export_save_file(dirname)
filename = opts.output_file
if opts.longnames:
filename = (ps2save.make_longname(dirname, sf)
+ "." + opts.type)
if filename == None:
filename = dirname + "." + opts.type
if not opts.overwrite_existing:
exists = True
try:
open(filename, "rb").close()
except EnvironmentError:
exists = False
if exists:
if opts.ignore_existing:
continue
raise io_error(EEXIST, "File exists", filename)
f = file(filename, "wb")
try:
print "Exporing", dirname, "to", filename
if opts.type == "max":
sf.save_max_drive(f)
else:
sf.save_ems(f)
finally:
f.close()
def do_delete(cmd, mc, opts, args, opterr):
if len(args) < 1:
opterr("Directory required.")
for dirname in args:
mc.rmdir(dirname)
def do_setmode(cmd, mc, opts, args, opterr):
set_mask = 0
clear_mask = ~0
for (opt, bit) in [(opts.read, DF_READ),
(opts.write, DF_WRITE),
(opts.execute, DF_EXECUTE),
(opts.protected, DF_PROTECTED),
(opts.psx, DF_PSX),
(opts.pocketstation, DF_POCKETSTN),
(opts.hidden, DF_HIDDEN)]:
if opt != None:
if opt:
set_mask |= bit
else:
clear_mask ^= bit
value = opts.hex_value
if set_mask == 0 and clear_mask == ~0:
if value == None:
opterr("At least one option must be given.")
if value.startswith("0x") or value.startswith("0X"):
value = int(value[2:], 16)
else:
value = int(value, 16)
else:
if value != None:
opterr("The -X option can't be combined with"
" other options.")
for arg in glob_args(args, mc.glob):
ent = mc.get_dirent(arg)
if value == None:
ent[0] = (ent[0] & clear_mask) | set_mask
print "new %04x" % ent[0]
else:
ent[0] = value
mc.set_dirent(arg, ent)
def _get_ps2_title(mc, enc):
s = mc.get_icon_sys(".");
if s == None:
return None
a = ps2save.unpack_icon_sys(s)
return ps2save.icon_sys_title(a, enc)
def _get_psx_title(mc, savename, enc):
mode = mc.get_mode(savename)
if mode == None or not mode_is_file(mode):
return None
f = mc.open(savename)
s = f.read(128)
if len(s) != 128:
return None
(magic, icon, blocks, title) = struct.unpack("<2sBB64s28x32x", s)
if magic != "SC":
return None
return [ps2save.shift_jis_conv(zero_terminate(title), enc), ""]
def do_dir(cmd, mc, opts, args, opterr):
if len(args) != 0:
opterr("Incorrect number of arguments.")
f = None
dir = mc.dir_open("/")
try:
for ent in list(dir)[2:]:
dirmode = ent[0]
if not mode_is_dir(dirmode):
continue
dirname = "/" + ent[8]
mc.chdir(dirname)
length = mc.dir_size(".");
enc = getattr(sys.stdout, "encoding", None)
if dirmode & DF_PSX:
title = _get_psx_title(mc, ent[8], enc)
else:
title = _get_ps2_title(mc, enc)
if title == None:
title = ["Corrupt", ""]
protection = dirmode & (DF_PROTECTED | DF_WRITE)
if protection == 0:
protection = "Delete Protected"
elif protection == DF_WRITE:
protection = "Not Protected"
elif protection == DF_PROTECTED:
protection = "Copy & Delete Protected"
else:
protection = "Copy Protected"
type = None
if dirmode & DF_PSX:
type = "PlayStation"
if dirmode & DF_POCKETSTN:
type = "PocketStation"
if type != None:
protection = type
print "%-32s %s" % (ent[8], title[0])
print ("%4dKB %-25s %s"
% (length / 1024, protection, title[1]))
print
finally:
if f != None:
f.close()
dir.close()
free = mc.get_free_space() / 1024
if free > 999999:
free = "%d,%03d,%03d" % (free / 1000000, free / 1000 % 1000,
free % 1000)
elif free > 999:
free = "%d,%03d" % (free / 1000, free % 1000)
else:
free = "%d" % free
print free + " KB Free"
def do_df(cmd, mc, opts, args, opterr):
if len(args) != 0:
opterr("Incorrect number of arguments.")
print mc.f.name + ":", mc.get_free_space(), "bytes free."
def do_check(cmd, mc, opts, args, opterr):
if len(args) != 0:
opterr("Incorrect number of arguments.")
if mc.check():
print "No errors found."
return 0
return 1
def do_format(cmd, mcname, opts, args, opterr):
if len(args) != 0:
opterr("Incorrect number of arguments.")
pages_per_card = ps2mc.PS2MC_STANDARD_PAGES_PER_CARD
if opts.clusters != None:
pages_per_cluster = (ps2mc.PS2MC_CLUSTER_SIZE
/ ps2mc.PS2MC_STANDARD_PAGE_SIZE)
pages_per_card = opts.clusters * pages_per_cluster
params = (True,
ps2mc.PS2MC_STANDARD_PAGE_SIZE,
ps2mc.PS2MC_STANDARD_PAGES_PER_ERASE_BLOCK,
pages_per_card)
if not opts.overwrite_existing:
exists = True
try:
file(mcname, "rb").close()
except EnvironmentError:
exists = False
if exists:
raise io_error, (EEXIST, "file exists", mcname)
f = file(mcname, "w+b")
try:
ps2mc.ps2mc(f, True, params).close()
finally:
f.close()
def do_gui(cmd, mcname, opts, args, opterr):
if len(args) != 0:
opterr("Incorrect number of arguments.")
try:
import gui
except ImportError:
write_error(None, "GUI not available")
return 1
gui.run(mcname)
return 0
def do_create_pad(cmd, mc, opts, args, opterr):
length = mc.clusters_per_card
if len(args) > 1:
length = int(args[1])
pad = "\0" * mc.cluster_size
f = mc.open(args[0], "wb")
try:
for i in xrange(length):
f.write(pad)
finally:
f.close()
def do_frob(cmd, mc, opts, args, opterr):
mc.write_superblock()
_trans = string.maketrans("".join(map(chr, range(32))), " " * 32)
def _print_bin(base, s):
for off in range(0, len(s), 16):
print "%04X" % (base + off),
a = s[off : off + 16]
for b in a:
print "%02X" % ord(b),
print "", a.translate(_trans)
def _print_erase_block(mc, n):
ppb = mc.pages_per_erase_block
base = n * ppb
for i in range(ppb):
s = mc.read_page(base + i)
_print_bin(i * mc.page_size, s)
print
def do_print_good_blocks(cmd, mc, opts, args, opterr):
print "good_block2:"
_print_erase_block(mc, mc.good_block2)
print "good_block1:"
_print_erase_block(mc, mc.good_block1)
def do_ecc_check(cmd, mc, opts, args, opterr):
for i in range(mc.clusters_per_card * mc.pages_per_cluster):
try:
mc.read_page(i)
except ps2mc.ecc_error:
print "bad: %05x" % i
opt = optparse.make_option
#
# Each value in the dictionary is a tuple consisting of:
# - function implementing the command
# - mode to use to open the ps2 save file
# - help description of the command
# - list of options supported by the command
#
cmd_table = {
"ls": (do_ls, "rb",
"[directory ...]",
"List the contents of a directory.",
[opt("-c", "--creation-time", action="store_true",
help = "Display creation times.")]),
"extract": (do_extract, "rb",
"filename ...",
"Extract files from the memory card.",
[opt("-o", "--output", metavar = "FILE",
help = 'Extract file to "FILE".'),
opt("-d", "--directory",
help = 'Extract files from "DIRECTORY".'),
opt("-p", "--use-stdout", action="store_true",
help = "Extract files to standard output.")]),
"add": (do_add, "r+b",
"filename ...",
"Add files to the memory card.",
[opt("-d", "--directory",
help = 'Add files to "directory".')]),
"mkdir": (do_mkdir, "r+b",
"directory ...",
"Make directories.",
[]),
"remove": (do_remove, "r+b",
"filename ...",
"Remove files and directories.",
[]),
"import": (do_import, "r+b",
"savefile ...",
"Import save files into the memory card.",
[opt("-i", "--ignore-existing", action="store_true",
help = ("Ignore files that already exist"
"on the image.")),
opt("-d", "--directory", metavar="DEST",
help = 'Import to "DEST".')]),
"export": (do_export, "rb",
"directory ...",
"Export save files from the memory card.",
[opt("-f", "--overwrite-existing", action = "store_true",
help = "Overwrite any save files already exported."),
opt("-i", "--ignore-existing", action = "store_true",
help = "Ingore any save files already exported."),
opt("-o", "--output-file", metavar = "filename",
help = 'Use "filename" as the name of the save file.'),
opt("-d", "--directory", metavar = "directory",
help = 'Export save files to "directory".'),
opt("-l", "--longnames", action = "store_true",
help = ("Generate longer, more descriptive,"
" filenames.")),
opt("-p", "--ems", action = "store_const",
dest = "type", const = "psu", default = "psu",
help = "Use the EMS .psu save file format. [default]"),
opt("-m", "--max-drive", action = "store_const",
dest = "type", const = "max",
help = "Use the MAX Drive save file format.")]),
"delete": (do_delete, "r+b",
"dirname ...",
"Recursively delete a directory (save file).",
[]),
"set": (do_setmode, "r+b",
"filename ...",
"Set mode flags on files and directories",
[opt("-p", "--protected", action="store_true",
help = "Set copy protected flag"),
opt("-P", "--psx", action="store_true",
help = "Set PSX flag"),
opt("-K", "--pocketstation", action="store_true",
help = "Set PocketStation flag"),
opt("-H", "--hidden", action="store_true",
help = "Set hidden flag"),
opt("-r", "--read", action="store_true",
help = "Set read allowed flag"),
opt("-w", "--write", action="store_true",
help = "Set write allowed flag"),
opt("-x", "--execute", action="store_true",
help = "Set executable flag"),
opt("-X", "--hex-value", metavar="mode",
help = 'Set mode to "mode".')]),
"clear": (do_setmode, "r+b",
"filename ...",
"Clear mode flags on files and directories",
[opt("-p", "--protected", action="store_false",
help = "Clear copy protected flag"),
opt("-P", "--psx", action="store_false",
help = "Clear PSX flag"),
opt("-K", "--pocketstation", action="store_false",
help = "Clear PocketStation flag"),
opt("-H", "--hidden", action="store_false",
help = "Clear hidden flag"),
opt("-r", "--read", action="store_false",
help = "Clear read allowed flag"),
opt("-w", "--write", action="store_false",
help = "Clear write allowed flag"),
opt("-x", "--execute", action="store_false",
help = "Clear executable flag"),
opt("-X", dest="hex_value", default=None,
help = optparse.SUPPRESS_HELP)]),
"dir": (do_dir, "rb",
None,
"Display save file information.",
[]),
"df": (do_df, "rb",
None,
"Display the amount free space.",
[]),
"check": (do_check, "rb",
"",
"Check for file system errors.",
[]),
"format": (do_format, None,
"",
"Creates a new memory card image.",
[opt("-c", "--clusters", type="int",
help = "Size in clusters of the memory card."),
opt("-f", "--overwrite-existing", action="store_true",
help = "Overwrite any existing file")]),
"gui": (do_gui, None,
"",
"Starts the graphical user interface.",
[]),
}
#
# secret commands for debugging purposes.
#
debug_cmd_table = {
"frob": (do_frob, "r+b",
"",
None,
[]),
"print_good_blocks": (do_print_good_blocks, "rb",
"",
None,
[]),
"ecc_check": (do_ecc_check, "rb",
"",
None,
[]),
"create_pad": (do_create_pad, "r+b",
"",
None,
[])
}
del opt # clean up name space
def write_error(filename, msg):
if filename == None:
sys.stderr.write(msg + "\n")
else:
sys.stderr.write(filename + ": " + msg + "\n")
class suboption_parser(optparse.OptionParser):
def exit(self, status = 0, msg = None):
if msg:
sys.stderr.write(msg)
raise subopt_error, status
class my_help_formatter(optparse.IndentedHelpFormatter):
"""A better formatter for optparser's help message"""
def format_description(self, description):
if not description:
return ""
desc_width = self.width - self.current_indent
indent = " " * self.current_indent
lines = []
for line in description.split('\n'):
ii = indent
si = indent
if line.startswith("\t"):
line = line[1:]
ii = indent + " " * 4
si = ii + " " * line.find(":") + 2
line = textwrap.fill(line, desc_width,
initial_indent = ii,
subsequent_indent = si)
lines.append(line)
return "\n".join(lines) + "\n"
def main():
prog = sys.argv[0].decode(sys.getdefaultencoding(), "replace")
usage = "usage: %prog [-ih] memcard.ps2 command [...]"
description = ("Manipulate PS2 memory card images.\n\n"
"Supported commands: ")
for cmd in sorted(cmd_table.keys()):
description += "\n " + cmd + ": " + cmd_table[cmd][3]
version = ("mymc "
+ verbuild.MYMC_VERSION_MAJOR
+ "." + verbuild.MYMC_VERSION_BUILD
+ " (" + _SCCS_ID + ")")
optparser = optparse.OptionParser(prog = prog, usage = usage,
description = description,
version = version,
formatter = my_help_formatter())
optparser.add_option("-D", dest = "debug", action = "store_true",
default = False, help = optparse.SUPPRESS_HELP)
optparser.add_option("-i", "--ignore-ecc", action = "store_true",
help = "Ignore ECC errors while reading.")
optparser.disable_interspersed_args()
(opts, args) = optparser.parse_args()
if len(args) == 0:
try:
import gui
except ImportError:
gui = None
if gui != None:
gui.run()
sys.exit(0)
if len(args) < 2:
optparser.error("Incorrect number of arguments.")
if opts.debug:
cmd_table.update(debug_cmd_table)
cmd = args[1]
if cmd not in cmd_table:
optparser.error('Command "%s" not recognized.' % cmd)
(fn, mode, usage_args, description, optlist) = cmd_table[cmd]
usage = "%prog"
if len(optlist) > 0:
usage += " [options]"
if usage_args != None:
usage += " " + usage_args
subprog = prog + " memcard.ps2 " + cmd
subopt_parser = suboption_parser(prog = subprog, usage = usage,
description = description,
option_list = optlist)
subopt_parser.disable_interspersed_args()
f = None
mc = None
ret = 0
mcname = args[0]
try:
(subopts, subargs) = subopt_parser.parse_args(args[2:])
try:
if mode == None:
ret = fn(cmd, mcname, subopts, subargs,
subopt_parser.error)
else:
f = file(mcname, mode)
mc = ps2mc.ps2mc(f, opts.ignore_ecc)
ret = fn(cmd, mc, subopts, subargs,
subopt_parser.error)
finally:
if mc != None:
mc.close()
if f != None:
# print "f.close()"
f.close()
except EnvironmentError, value:
if getattr(value, "filename", None) != None:
write_error(value.filename, value.strerror)
ret = 1
elif getattr(value, "strerror", None) != None:
write_error(mcname, value.strerror)
ret = 1
else:
# something weird
raise
if opts.debug:
raise
except subopt_error, (ret,):
pass
except (ps2mc.error, ps2save.error), value:
fn = getattr(value, "filename", None)
if fn == None:
fn = mcname
write_error(fn, str(value))
if opts.debug:
raise
ret = 1
if ret == None:
ret = 0
return ret
sys.exit(main())

1859
ps2mc.py Normal file

File diff suppressed because it is too large Load Diff

131
ps2mc_dir.py Normal file
View File

@ -0,0 +1,131 @@
#
# ps2mc_dir.py
#
# By Ross Ridge
# Public Domain
#
"""Functions for working with PS2 memory card directory entries."""
_SCCS_ID = "@(#) mysc ps2mc_dir.py 1.3 08/02/05 15:51:58\n"
import struct
import time
import calendar
PS2MC_DIRENT_LENGTH = 512
DF_READ = 0x0001
DF_WRITE = 0x0002
DF_EXECUTE = 0x0004
DF_RWX = DF_READ | DF_WRITE | DF_EXECUTE
DF_PROTECTED = 0x0008
DF_FILE = 0x0010
DF_DIR = 0x0020
DF_O_DCREAT = 0x0040
DF_0080 = 0x0080
DF_0100 = 0x0100
DF_O_CREAT = 0x0200
DF_0400 = 0x0400
DF_POCKETSTN = 0x0800
DF_PSX = 0x1000
DF_HIDDEN = 0x2000
DF_4000 = 0x4000
DF_EXISTS = 0x8000
def zero_terminate(s):
"""Truncate a string at the first NUL ('\0') character, if any."""
i = s.find('\0')
if i == -1:
return s
return s[:i]
# mode, ???, length, created,
# fat_cluster, parent_entry, modified, attr,
# name
_dirent_fmt = "<HHL8sLL8sL28x448s"
# secs, mins, hours, mday, month, year
_tod_fmt = "<xBBBBBH"
#
# Use the new Struct object if available
#
if hasattr(struct, "Struct"):
_dirent_struct = struct.Struct(_dirent_fmt)
_tod_struct = struct.Struct(_tod_fmt)
def unpack_tod(s):
return _tod_struct.unpack(s)
def pack_tod(tod):
return _tod_struct.pack(tod)
def unpack_dirent(s):
ent = _dirent_struct.unpack(s)
ent = list(ent)
ent[3] = _tod_struct.unpack(ent[3])
ent[6] = _tod_struct.unpack(ent[6])
ent[8] = zero_terminate(ent[8])
return ent
def pack_dirent(ent):
ent = list(ent)
ent[3] = _tod_struct.pack(*ent[3])
ent[6] = _tod_struct.pack(*ent[6])
return _dirent_struct.pack(*ent)
else:
def unpack_tod(s):
return struct.unpack(_tod_fmt, s)
def pack_tod(tod):
return struct.pack(_tod_fmt, tod)
def unpack_dirent(s):
# mode, ???, length, created,
# fat_cluster, parent_entry, modified, attr,
# name
ent = struct.unpack(_dirent_fmt, s)
ent = list(ent)
ent[3] = struct.unpack(_tod_fmt, ent[3])
ent[6] = struct.unpack(_tod_fmt, ent[6])
ent[8] = zero_terminate(ent[8])
return ent
def pack_dirent(ent):
ent = list(ent)
ent[3] = struct.pack(_tod_fmt, *ent[3])
ent[6] = struct.pack(_tod_fmt, *ent[6])
return struct.pack(_dirent_fmt, *ent)
def time_to_tod(when):
"""Convert a Python time value to a ToD tuple"""
tm = time.gmtime(when + 9 * 3600)
return (tm.tm_sec, tm.tm_min, tm.tm_hour,
tm.tm_mday, tm.tm_mon, tm.tm_year)
def tod_to_time(tod):
"""Convert a ToD tuple to a Python time value."""
try:
month = tod[4]
if month == 0:
month = 1
return calendar.timegm((tod[5], month, tod[3],
tod[2], tod[1], tod[0],
None, None, 0)) - 9 * 3600
except ValueError:
return 0
def tod_now():
"""Get the current time as a ToD tuple."""
return time_to_tod(time.time())
def mode_is_file(mode):
return (mode & (DF_FILE | DF_DIR | DF_EXISTS)) == (DF_FILE | DF_EXISTS)
def mode_is_dir(mode):
return (mode & (DF_FILE | DF_DIR | DF_EXISTS)) == (DF_DIR | DF_EXISTS)

182
ps2mc_ecc.py Normal file
View File

@ -0,0 +1,182 @@
#
# ps2mc_ecc.py
#
# By Ross Ridge
# Public Domain
#
"""
Routines for calculating the Hamming codes, a simple form of error
correcting codes (ECC), as used on PS2 memory cards.
"""
_SCCS_ID = "@(#) mysc ps2mc_ecc.py 1.4 07/12/17 02:34:04\n"
import array
from round import div_round_up
try:
import ctypes
import mymcsup
except ImportError:
mymcsup = None
__ALL__ = ["ECC_CHECK_OK", "ECC_CHECK_CORRECTED", "ECC_CHECK_FAILED",
"ecc_calculate", "ecc_check",
"ecc_calculate_page", "ecc_check_page"]
ECC_CHECK_OK = 0
ECC_CHECK_CORRECTED = 1
ECC_CHECK_FAILED = 2
def _popcount(a):
count = 0
while a != 0:
a &= a - 1
count += 1
return count
def _parityb(a):
a = (a ^ (a >> 1))
a = (a ^ (a >> 2))
a = (a ^ (a >> 4))
return a & 1
def _make_ecc_tables():
parity_table = [_parityb(b)
for b in range(256)]
cpmasks = [0x55, 0x33, 0x0F, 0x00, 0xAA, 0xCC, 0xF0]
column_parity_masks = [None] * 256
for b in range(256):
mask = 0
for i in range(len(cpmasks)):
mask |= parity_table[b & cpmasks[i]] << i
column_parity_masks[b] = mask
return parity_table, column_parity_masks
_parity_table, _column_parity_masks = _make_ecc_tables()
def _ecc_calculate(s):
"Calculate the Hamming code for a 128 byte long string or byte array."
if not isinstance(s, array.array):
a = array.array('B')
a.fromstring(s)
s = a
column_parity = 0x77
line_parity_0 = 0x7F
line_parity_1 = 0x7F
for i in range(len(s)):
b = s[i]
column_parity ^= _column_parity_masks[b]
if _parity_table[b]:
line_parity_0 ^= ~i
line_parity_1 ^= i
return [column_parity, line_parity_0 & 0x7F, line_parity_1]
def _ecc_check(s, ecc):
"""Detect and correct any single bit errors.
The parameters "s" and "ecc", the data and expected Hamming code
repectively, must be modifiable sequences of integers and are
updated with the corrected values if necessary."""
computed = ecc_calculate(s)
if computed == ecc:
return ECC_CHECK_OK
#print
#_print_bin(0, s.tostring())
#print "computed %02x %02x %02x" % tuple(computed)
#print "actual %02x %02x %02x" % tuple(ecc)
# ECC mismatch
cp_diff = (computed[0] ^ ecc[0]) & 0x77
lp0_diff = (computed[1] ^ ecc[1]) & 0x7F
lp1_diff = (computed[2] ^ ecc[2]) & 0x7F
lp_comp = lp0_diff ^ lp1_diff
cp_comp = (cp_diff >> 4) ^ (cp_diff & 0x07)
#print "%02x %02x %02x %02x %02x" % (cp_diff, lp0_diff, lp1_diff,
# lp_comp, cp_comp)
if lp_comp == 0x7F and cp_comp == 0x07:
print "corrected 1"
# correctable 1 bit error in data
s[lp1_diff] ^= 1 << (cp_diff >> 4)
return ECC_CHECK_CORRECTED
if ((cp_diff == 0 and lp0_diff == 0 and lp1_diff == 0)
or _popcount(lp_comp) + _popcount(cp_comp) == 1):
print "corrected 2"
# correctable 1 bit error in ECC
# (and/or one of the unused bits was set)
ecc[0] = computed[0]
ecc[1] = computed[1]
ecc[2] = computed[2]
return ECC_CHECK_CORRECTED
# uncorrectable error
return ECC_CHECK_FAILED
def ecc_calculate_page(page):
"""Return a list of the ECC codes for a PS2 memory card page."""
return [ecc_calculate(page[i * 128 : i * 128 + 128])
for i in range(div_round_up(len(page), 128))]
def ecc_check_page(page, spare):
"Check and correct any single bit errors in a PS2 memory card page."
failed = False
corrected = False
#chunks = [(array.array('B', page[i * 128 : i * 128 + 128]),
# map(ord, spare[i * 3 : i * 3 + 3]))
# for i in range(div_round_up(len(page), 128))]
chunks = []
for i in range(div_round_up(len(page), 128)):
a = array.array('B')
a.fromstring(page[i * 128 : i * 128 + 128])
chunks.append((a, map(ord, spare[i * 3 : i * 3 + 3])))
r = [ecc_check(s, ecc)
for (s, ecc) in chunks]
ret = ECC_CHECK_OK
if ECC_CHECK_CORRECTED in r:
# rebuild sector and spare from the corrected versions
page = "".join([a[0].tostring()
for a in chunks])
spare = "".join([chr(a[1][i])
for a in chunks
for i in range(3)])
ret = ECC_CHECK_CORRECTED
if ECC_CHECK_FAILED in r:
ret = ECC_CHECK_FAILED
return (ret, page, spare)
if mymcsup == None:
ecc_calculate = _ecc_calculate
ecc_check = _ecc_check
else:
# _c_ubyte_p = ctypes.POINTER(ctypes.c_ubyte)
def ecc_calculate(s):
aecc = array.array('B', "\0\0\0")
cecc = ctypes.c_ubyte.from_address(aecc.buffer_info()[0])
mymcsup.ecc_calculate(s, len(s), cecc)
return list(aecc)
def ecc_check(s, ecc):
cs = ctypes.c_ubyte.from_address(s.buffer_info()[0])
# print "%08X" % s.buffer_info()[0]
aecc = array.array('B', ecc)
cecc = ctypes.c_ubyte.from_address(aecc.buffer_info()[0])
ret = mymcsup.ecc_check(cs, len(s), cecc)
ecc[0] = aecc[0]
ecc[1] = aecc[1]
ecc[2] = aecc[2]
return ret

625
ps2save.py Normal file
View File

@ -0,0 +1,625 @@
#
# ps2save.py
#
# By Ross Ridge
# Public Domain
#
# A simple interface for working with various PS2 save file formats.
#
_SCCS_ID = "@(#) mysc ps2save.py 1.6 08/08/11 14:11:54\n"
import sys
import os
import string
import struct
import binascii
import array
import zlib
from round import div_round_up, round_up
from ps2mc_dir import *
from sjistab import shift_jis_normalize_table
try:
import lzari
except ImportError:
lzari = None
PS2SAVE_MAX_MAGIC = "Ps2PowerSave"
PS2SAVE_SPS_MAGIC = "\x0d\0\0\0SharkPortSave"
PS2SAVE_CBS_MAGIC = "CFU\0"
PS2SAVE_NPO_MAGIC = "nPort"
# This is the initial permutation state ("S") for the RC4 stream cipher
# algorithm used to encrpyt and decrypt Codebreaker saves.
PS2SAVE_CBS_RC4S = [0x5f, 0x1f, 0x85, 0x6f, 0x31, 0xaa, 0x3b, 0x18,
0x21, 0xb9, 0xce, 0x1c, 0x07, 0x4c, 0x9c, 0xb4,
0x81, 0xb8, 0xef, 0x98, 0x59, 0xae, 0xf9, 0x26,
0xe3, 0x80, 0xa3, 0x29, 0x2d, 0x73, 0x51, 0x62,
0x7c, 0x64, 0x46, 0xf4, 0x34, 0x1a, 0xf6, 0xe1,
0xba, 0x3a, 0x0d, 0x82, 0x79, 0x0a, 0x5c, 0x16,
0x71, 0x49, 0x8e, 0xac, 0x8c, 0x9f, 0x35, 0x19,
0x45, 0x94, 0x3f, 0x56, 0x0c, 0x91, 0x00, 0x0b,
0xd7, 0xb0, 0xdd, 0x39, 0x66, 0xa1, 0x76, 0x52,
0x13, 0x57, 0xf3, 0xbb, 0x4e, 0xe5, 0xdc, 0xf0,
0x65, 0x84, 0xb2, 0xd6, 0xdf, 0x15, 0x3c, 0x63,
0x1d, 0x89, 0x14, 0xbd, 0xd2, 0x36, 0xfe, 0xb1,
0xca, 0x8b, 0xa4, 0xc6, 0x9e, 0x67, 0x47, 0x37,
0x42, 0x6d, 0x6a, 0x03, 0x92, 0x70, 0x05, 0x7d,
0x96, 0x2f, 0x40, 0x90, 0xc4, 0xf1, 0x3e, 0x3d,
0x01, 0xf7, 0x68, 0x1e, 0xc3, 0xfc, 0x72, 0xb5,
0x54, 0xcf, 0xe7, 0x41, 0xe4, 0x4d, 0x83, 0x55,
0x12, 0x22, 0x09, 0x78, 0xfa, 0xde, 0xa7, 0x06,
0x08, 0x23, 0xbf, 0x0f, 0xcc, 0xc1, 0x97, 0x61,
0xc5, 0x4a, 0xe6, 0xa0, 0x11, 0xc2, 0xea, 0x74,
0x02, 0x87, 0xd5, 0xd1, 0x9d, 0xb7, 0x7e, 0x38,
0x60, 0x53, 0x95, 0x8d, 0x25, 0x77, 0x10, 0x5e,
0x9b, 0x7f, 0xd8, 0x6e, 0xda, 0xa2, 0x2e, 0x20,
0x4f, 0xcd, 0x8f, 0xcb, 0xbe, 0x5a, 0xe0, 0xed,
0x2c, 0x9a, 0xd4, 0xe2, 0xaf, 0xd0, 0xa9, 0xe8,
0xad, 0x7a, 0xbc, 0xa8, 0xf2, 0xee, 0xeb, 0xf5,
0xa6, 0x99, 0x28, 0x24, 0x6c, 0x2b, 0x75, 0x5d,
0xf8, 0xd3, 0x86, 0x17, 0xfb, 0xc0, 0x7b, 0xb3,
0x58, 0xdb, 0xc7, 0x4b, 0xff, 0x04, 0x50, 0xe9,
0x88, 0x69, 0xc9, 0x2a, 0xab, 0xfd, 0x5b, 0x1b,
0x8a, 0xd9, 0xec, 0x27, 0x44, 0x0e, 0x33, 0xc8,
0x6b, 0x93, 0x32, 0x48, 0xb6, 0x30, 0x43, 0xa5]
class error(Exception):
"""Base for all exceptions specific to this module."""
pass
class corrupt(error):
"""Corrupt save file."""
def __init__(self, msg, f = None):
fn = None
if f != None:
fn = getattr(f, "name", None)
self.filename = fn
error.__init__(self, "Corrupt save file: " + msg)
class eof(corrupt):
"""Save file is truncated."""
def __init__(self, f = None):
corrupt.__init__(self, "Unexpected EOF", f)
class subdir(corrupt):
def __init__(self, f = None):
corrupt.__init__(self, "Non-file in save file.", f)
#
# Table of graphically similar ASCII characters that can be used
# as substitutes for Unicode characters.
#
char_substs = {
u'\u00a2': u"c",
u'\u00b4': u"'",
u'\u00d7': u"x",
u'\u00f7': u"/",
u'\u2010': u"-",
u'\u2015': u"-",
u'\u2018': u"'",
u'\u2019': u"'",
u'\u201c': u'"',
u'\u201d': u'"',
u'\u2032': u"'",
u'\u2212': u"-",
u'\u226a': u"<<",
u'\u226b': u">>",
u'\u2500': u"-",
u'\u2501': u"-",
u'\u2502': u"|",
u'\u2503': u"|",
u'\u250c': u"+",
u'\u250f': u"+",
u'\u2510': u"+",
u'\u2513': u"+",
u'\u2514': u"+",
u'\u2517': u"+",
u'\u2518': u"+",
u'\u251b': u"+",
u'\u251c': u"+",
u'\u251d': u"+",
u'\u2520': u"+",
u'\u2523': u"+",
u'\u2524': u"+",
u'\u2525': u"+",
u'\u2528': u"+",
u'\u252b': u"+",
u'\u252c': u"+",
u'\u252f': u"+",
u'\u2530': u"+",
u'\u2533': u"+",
u'\u2537': u"+",
u'\u2538': u"+",
u'\u253b': u"+",
u'\u253c': u"+",
u'\u253f': u"+",
u'\u2542': u"+",
u'\u254b': u"+",
u'\u25a0': u"#",
u'\u25a1': u"#",
u'\u3001': u",",
u'\u3002': u".",
u'\u3003': u'"',
u'\u3007': u'0',
u'\u3008': u'<',
u'\u3009': u'>',
u'\u300a': u'<<',
u'\u300b': u'>>',
u'\u300a': u'<<',
u'\u300b': u'>>',
u'\u300c': u'[',
u'\u300d': u']',
u'\u300e': u'[',
u'\u300f': u']',
u'\u3010': u'[',
u'\u3011': u']',
u'\u3014': u'[',
u'\u3015': u']',
u'\u301c': u'~',
u'\u30fc': u'-',
}
def shift_jis_conv(src, encoding = None):
"""Convert Shift-JIS strings to a graphically similar representation.
If encoding is "unicode" then a Unicode string is returned,
otherwise a string in encoding specified is returned. If necessary,
graphically similar characters are used to replace characters not
exactly representable in the desired encoding.
"""
if encoding == None:
encoding = sys.getdefaultencoding()
if encoding == "shift_jis":
return src
u = src.decode("shift_jis", "replace")
if encoding == "unicode":
return u
a = []
for uc in u:
try:
uc.encode(encoding)
a.append(uc)
except UnicodeError:
for uc2 in shift_jis_normalize_table.get(uc, uc):
a.append(char_substs.get(uc2, uc2))
return u"".join(a).encode(encoding, "replace")
def rc4_crypt(s, t):
"""RC4 encrypt/decrypt the string t using the permutation s.
Returns a byte array."""
s = array.array('B', s)
t = array.array('B', t)
j = 0
for ii in range(len(t)):
i = (ii + 1) % 256
j = (j + s[i]) % 256
(s[i], s[j]) = (s[j], s[i])
t[ii] ^= s[(s[i] + s[j]) % 256]
return t
# def sps_check(s):
# """Calculate the checksum for a SharkPort save."""
#
# h = 0
# for c in array.array('B', s):
# h += c << (h % 24)
# h &= 0xFFFFFFFF
# return h
def unpack_icon_sys(s):
"""Unpack an icon.sys file into a tuple."""
# magic, title offset, ...
# [14] title, normal icon, copy icon, del icon
a = struct.unpack("<4s2xH4x"
"L" "16s16s16s16s" "16s16s16s" "16s16s16s" "16s"
"68s64s64s64s512x", s)
a = list(a)
for i in range(3, 7):
a[i] = struct.unpack("<4L", a[i])
a[i] = map(hex, a[i])
for i in range(7, 14):
a[i] = struct.unpack("<4f", a[i])
a[14] = zero_terminate(a[14])
a[15] = zero_terminate(a[15])
a[16] = zero_terminate(a[16])
a[17] = zero_terminate(a[17])
return a
def icon_sys_title(icon_sys, encoding = None):
"""Extract the two lines of the title stored in an icon.sys tuple."""
offset = icon_sys[1]
title = icon_sys[14]
title2 = shift_jis_conv(title[offset:], encoding)
title1 = shift_jis_conv(title[:offset], encoding)
return (title1, title2)
def _read_fixed(f, n):
"""Read a string of a fixed length from a file."""
s = f.read(n)
if len(s) != n:
raise eof, f
return s
def _read_long_string(f):
"""Read a string prefixed with a 32-bit length from a file."""
length = struct.unpack("<L", _read_fixed(f, 4))[0]
return _read_fixed(f, length)
class ps2_save_file(object):
"""The state of a PlayStation 2 save file."""
def __init__(self):
self.file_ents = None
self.file_data = None
self.dirent = None
self._defer_load_max = False
def set_directory(self, ent, defer = False):
self._defer_load_max = defer
self._compressed = None
self.file_ents = [None] * ent[2]
self.file_data = [None] * ent[2]
self.dirent = ent
def set_file(self, i, ent, data):
self.file_ents[i] = ent
self.file_data[i] = data
def get_directory(self):
return self.dirent
def get_file(self, i):
if self._defer_load_max:
self._defer_load_max = False
self._load_max_drive_2()
return (self.file_ents[i], self.file_data[i])
def __len__(self):
return self.dirent[2]
def __getitem__(self, index):
return self.get_file(index)
def get_icon_sys(self):
for i in range(self.dirent[2]):
(ent, data) = self.get_file(i)
if ent[8] == "icon.sys" and len(data) >= 964:
return unpack_icon_sys(data[:964])
return None
def load_ems(self, f):
"""Load EMS (.psu) save files."""
cluster_size = 1024
dirent = unpack_dirent(_read_fixed(f, PS2MC_DIRENT_LENGTH))
dotent = unpack_dirent(_read_fixed(f, PS2MC_DIRENT_LENGTH))
dotdotent = unpack_dirent(_read_fixed(f, PS2MC_DIRENT_LENGTH))
if (not mode_is_dir(dirent[0])
or not mode_is_dir(dotent[0])
or not mode_is_dir(dotdotent[0])
or dirent[2] < 2):
raise corrupt, ("Not a EMS (.psu) save file.", f)
dirent[2] -= 2
self.set_directory(dirent)
for i in range(dirent[2]):
ent = unpack_dirent(_read_fixed(f,
PS2MC_DIRENT_LENGTH))
if not mode_is_file(ent[0]):
raise subdir, f
flen = ent[2]
self.set_file(i, ent, _read_fixed(f, flen))
_read_fixed(f, round_up(flen, cluster_size) - flen)
def save_ems(self, f):
cluster_size = 1024
dirent = self.dirent[:]
dirent[2] += 2
f.write(pack_dirent(dirent))
f.write(pack_dirent((DF_RWX | DF_DIR | DF_0400 | DF_EXISTS,
0, 0, dirent[3],
0, 0, dirent[3], 0, ".")))
f.write(pack_dirent((DF_RWX | DF_DIR | DF_0400 | DF_EXISTS,
0, 0, dirent[3],
0, 0, dirent[3], 0, "..")))
for i in range(dirent[2] - 2):
(ent, data) = self.get_file(i)
f.write(pack_dirent(ent))
if not mode_is_file(ent[0]):
raise error, "Directory has a subdirectory."
f.write(data)
f.write("\0" * (round_up(len(data), cluster_size)
- len(data)))
f.flush()
def _load_max_drive_2(self):
(length, s) = self._compressed
self._compressed = None
if lzari == None:
raise error, ("The lzari module is needed to "
" decompress MAX Drive saves.")
s = lzari.decode(s, length,
"decompressing " + self.dirent[8] + ": ")
dirlen = self.dirent[2]
now = tod_now()
off = 0
for i in range(dirlen):
if len(s) - off < 36:
raise eof, f
(l, name) = struct.unpack("<L32s", s[off : off + 36])
name = zero_terminate(name)
# print "%08x %08x %s" % (off, l, name)
off += 36
data = s[off : off + l]
if len(data) != l:
raise eof, f
self.set_file(i,
(DF_RWX | DF_FILE | DF_0400, 0, l,
now, 0, 0, now, 0, name),
data)
off += l
off = round_up(off + 8, 16) - 8
def load_max_drive(self, f):
s = f.read(0x5C)
magic = None
if len(s) == 0x5C:
(magic, crc, dirname, iconsysname, clen, dirlen,
length) = struct.unpack("<12sL32s32sLLL", s)
if magic != PS2SAVE_MAX_MAGIC:
raise corrupt, ("Not a MAX Drive save file", f)
if clen == length:
# some saves have the uncompressed size here
# instead of the compressed size
s = f.read()
else:
s = _read_fixed(f, clen - 4)
dirname = zero_terminate(dirname)
now = tod_now()
self.set_directory((DF_RWX | DF_DIR | DF_0400, 0,
dirlen, now, 0, 0, now, 0, dirname),
True)
self._compressed = (length, s)
def save_max_drive(self, f):
if lzari == None:
raise error, ("The lzari module is needed to "
" decompress MAX Drive saves.")
iconsysname = ""
icon_sys = self.get_icon_sys()
if icon_sys != None:
title = icon_sys_title(icon_sys, "ascii")
if len(title[0]) > 0 and title[0][-1] != ' ':
iconsysname = title[0] + " " + title[1].strip()
else:
iconsysname = title[0] + title[1].rstrip()
s = ""
dirent = self.dirent
for i in range(dirent[2]):
(ent, data) = self.get_file(i)
if not mode_is_file(ent[0]):
raise error, "Non-file in save file."
s += struct.pack("<L32s", ent[2], ent[8])
s += data
s += "\0" * (round_up(len(s) + 8, 16) - 8 - len(s))
length = len(s)
progress = "compressing " + dirent[8] + ": "
compressed = lzari.encode(s, progress)
hdr = struct.pack("<12sL32s32sLLL", PS2SAVE_MAX_MAGIC,
0, dirent[8], iconsysname,
len(compressed) + 4, dirent[2], length)
crc = binascii.crc32(hdr)
crc = binascii.crc32(compressed, crc)
f.write(struct.pack("<12sL32s32sLLL", PS2SAVE_MAX_MAGIC,
crc & 0xFFFFFFFF, dirent[8], iconsysname,
len(compressed) + 4, dirent[2], length))
f.write(compressed)
f.flush()
def load_codebreaker(self, f):
magic = f.read(4)
if magic != PS2SAVE_CBS_MAGIC:
raise corrupt, ("Not a Codebreaker save file.", f)
(d04, hlen) = struct.unpack("<LL", _read_fixed(f, 8))
if hlen < 92 + 32:
raise corrupt, ("Header lengh too short.", f)
(dlen, flen, dirname, created, modified, d44, d48, dirmode,
d50, d54, d58, title) \
= struct.unpack("<LL32s8s8sLLLLLL%ds" % (hlen - 92),
_read_fixed(f, hlen - 12))
dirname = zero_terminate(dirname)
created = unpack_tod(created)
modified = unpack_tod(modified)
title = zero_terminate(title)
# These fields don't always seem to be set correctly.
if not mode_is_dir(dirmode):
dirmode = DF_RWX | DF_DIR | DF_0400
if tod_to_time(created) == 0:
created = tod_now()
if tod_to_time(modified) == 0:
modified = tod_now()
# flen can either be the total length of the file,
# or the length of compressed body of the file
body = f.read(flen)
clen = len(body)
if clen != flen and clen != flen - hlen:
raise eof, f
body = rc4_crypt(PS2SAVE_CBS_RC4S, body)
dcobj = zlib.decompressobj()
body = dcobj.decompress(body, dlen)
files = []
while body != "":
if len(body) < 64:
raise eof, f
header = struct.unpack("<8s8sLHHLL32s", body[:64])
size = header[2]
data = body[64 : 64 + size]
if len(data) != size:
raise eof, f
body = body[64 + size:]
files.append((header, data))
self.set_directory((dirmode, 0, len(files), created, 0, 0,
modified, 0, dirname))
for i in range(len(files)):
(header, data) = files[i]
(created, modified, size, mode, h06, h08, h0C, name) \
= header
name = zero_terminate(name)
created = unpack_tod(created)
modified = unpack_tod(modified)
if not mode_is_file(mode):
raise subdir, f
if tod_to_time(created) == 0:
created = tod_now()
if tod_to_time(modified) == 0:
modified = tod_now()
self.set_file(i, (mode, 0, size, created, 0, 0,
modified, 0, name), data)
def load_sharkport(self, f):
magic = f.read(17)
if magic != PS2SAVE_SPS_MAGIC:
raise corrupt, ("Not a SharkPort/X-Port save file.", f)
(savetype,) = struct.unpack("<L", _read_fixed(f, 4))
dirname = _read_long_string(f)
datestamp = _read_long_string(f)
comment = _read_long_string(f)
(flen,) = struct.unpack("<L", _read_fixed(f, 4))
(hlen, dirname, dirlen, dirmode, created, modified) \
= struct.unpack("<H64sL8xH2x8s8s", _read_fixed(f, 98))
_read_fixed(f, hlen - 98)
dirname = zero_terminate(dirname)
created = unpack_tod(created)
modified = unpack_tod(modified)
# mode values are byte swapped
dirmode = dirmode / 256 % 256 + dirmode % 256 * 256
dirlen -= 2
if not mode_is_dir(dirmode) or dirlen < 0:
raise corrupt, ("Bad values in directory entry.", f)
self.set_directory((dirmode, 0, dirlen, created, 0, 0,
modified, 0, dirname))
for i in range(dirlen):
(hlen, name, flen, mode, created, modified) \
= struct.unpack("<H64sL8xH2x8s8s",
_read_fixed(f, 98))
if hlen < 98:
raise corrupt, ("Header length too short.", f)
_read_fixed(f, hlen - 98)
name = zero_terminate(name)
created = unpack_tod(created)
modified = unpack_tod(modified)
mode = mode / 256 % 256 + mode % 256 * 256
if not mode_is_file(mode):
raise subdir, f
self.set_file(i, (mode, 0, flen, created, 0, 0,
modified, 0, name),
_read_fixed(f, flen))
# ignore 4 byte checksum at the end
def detect_file_type(f):
"""Detect the type of PS2 save file.
The file-like object f should be positioned at the start of the file.
"""
hdr = f.read(PS2MC_DIRENT_LENGTH * 3)
if hdr[:12] == PS2SAVE_MAX_MAGIC:
return "max"
if hdr[:17] == PS2SAVE_SPS_MAGIC:
return "sps"
if hdr[:4] == PS2SAVE_CBS_MAGIC:
return "cbs"
if hdr[:5] == PS2SAVE_NPO_MAGIC:
return "npo"
#
# EMS (.psu) save files don't have a magic number. Check to
# see if it looks enough like one.
#
if len(hdr) != PS2MC_DIRENT_LENGTH * 3:
return None
dirent = unpack_dirent(hdr[:PS2MC_DIRENT_LENGTH])
dotent = unpack_dirent(hdr[PS2MC_DIRENT_LENGTH
: PS2MC_DIRENT_LENGTH * 2])
dotdotent = unpack_dirent(hdr[PS2MC_DIRENT_LENGTH * 2:])
if (mode_is_dir(dirent[0]) and mode_is_dir(dotent[0])
and mode_is_dir(dotdotent[0]) and dirent[2] >= 2
and dotent[8] == "." and dotdotent[8] == ".."):
return "psu"
return None
#
# Set up tables of illegal and problematic characters in file names.
#
_bad_filename_chars = ("".join(map(chr, range(32)))
+ "".join(map(chr, range(127, 256))))
_bad_filename_repl = "_" * len(_bad_filename_chars)
if os.name in ["nt", "os2", "ce"]:
_bad_filename_chars += '<>:"/\\|'
_bad_filename_repl += "()_'___"
_bad_filename_chars2 = _bad_filename_chars + "?* "
_bad_filename_repl2 = _bad_filename_repl + "___"
else:
_bad_filename_chars += "/"
_bad_filename_repl += "_"
_bad_filename_chars2 = _bad_filename_chars + "?*'&|:[<>] \\\""
_bad_filename_repl2 = _bad_filename_repl + "______(())___"
_filename_trans = string.maketrans(_bad_filename_chars, _bad_filename_repl);
_filename_trans2 = string.maketrans(_bad_filename_chars2, _bad_filename_repl2);
def fix_filename(filename):
"""Replace illegal or problematic characters from a filename."""
return filename.translate(_filename_trans)
def make_longname(dirname, sf):
"""Return a string containing a verbose filename for a save file."""
icon_sys = sf.get_icon_sys()
title = ""
if icon_sys != None:
title = icon_sys_title(icon_sys, "ascii")
title = title[0] + " " + title[1]
title = " ".join(title.split())
crc = binascii.crc32("")
for (ent, data) in sf:
crc = binascii.crc32(data, crc)
if len(dirname) >= 12 and (dirname[0:2] in ("BA", "BJ", "BE", "BK")):
if dirname[2:6] == "DATA":
title = ""
else:
#dirname = dirname[2:6] + dirname[7:12]
dirname = dirname[2:12]
return fix_filename("%s %s (%08X)"
% (dirname, title, crc & 0xFFFFFFFF))

21
round.py Normal file
View File

@ -0,0 +1,21 @@
#
# round.py
#
# By Ross Ridge
# Public Domain
#
# Simple rounding functions.
#
_SCCS_ID = "@(#) mysc round.py 1.3 07/04/17 02:10:27\n"
def div_round_up(a, b):
return (a + b - 1) / b
def round_up(a, b):
return (a + b - 1) / b * b
def round_down(a, b):
return a / b * b

2
sjistab.py Normal file
View File

@ -0,0 +1,2 @@
# automatically generated
shift_jis_normalize_table = {u'\uff81': u'\u30c1', u'\u3000': u' ', u'\uff85': u'\u30ca', u'\uff06': u'&', u'\uff89': u'\u30ce', u'\uff0a': u'*', u'\uff8d': u'\u30d8', u'\uff0e': u'.', u'\uff91': u'\u30e0', u'\uff12': u'2', u'\uff95': u'\u30e6', u'\uff16': u'6', u'\uff99': u'\u30eb', u'\u309b': u' \u3099', u'\uff1a': u':', u'\uff9d': u'\u30f3', u'\uff03': u'#', u'\uff1e': u'>', u'\uff22': u'B', u'\uff26': u'F', u'\uff2a': u'J', u'\u222c': u'\u222b\u222b', u'\uff2e': u'N', u'\uff32': u'R', u'\uff36': u'V', u'\uff3a': u'Z', u'\uff3e': u'^', u'\uff42': u'b', u'\uff46': u'f', u'\uff4a': u'j', u'\uff4e': u'n', u'\uff52': u'r', u'\uff56': u'v', u'\uff5a': u'z', u'\uff62': u'\u300c', u'\uffe5': u'\xa5', u'\uff66': u'\u30f2', u'\uff6a': u'\u30a7', u'\uff6e': u'\u30e7', u'\uff72': u'\u30a4', u'\uff76': u'\u30ab', u'\uff7a': u'\u30b3', u'\uff7e': u'\u30bb', u'\uff01': u'!', u'\uff82': u'\u30c4', u'\uff05': u'%', u'\uff86': u'\u30cb', u'\uff09': u')', u'\uff8a': u'\u30cf', u'\uff8e': u'\u30db', u'\uff11': u'1', u'\uff92': u'\u30e1', u'\uff15': u'5', u'\uff96': u'\u30e8', u'\uff19': u'9', u'\uff9a': u'\u30ec', u'\uff1d': u'=', u'\u309c': u' \u309a', u'\uff9e': u'\u3099', u'\uff21': u'A', u'\uff25': u'E', u'\uff29': u'I', u'\xa8': u' \u0308', u'\uff2d': u'M', u'\uff31': u'Q', u'\u2033': u'\u2032\u2032', u'\uff35': u'U', u'\xb4': u' \u0301', u'\uff39': u'Y', u'\uff3d': u']', u'\uff41': u'a', u'\uff45': u'e', u'\uff49': u'i', u'\uff4d': u'm', u'\uff51': u'q', u'\uff55': u'u', u'\uff59': u'y', u'\uff5d': u'}', u'\uff61': u'\u3002', u'\uff65': u'\u30fb', u'\uff69': u'\u30a5', u'\uff6d': u'\u30e5', u'\uff71': u'\u30a2', u'\uff75': u'\u30aa', u'\uff79': u'\u30b1', u'\uff7d': u'\u30b9', u'\uff83': u'\u30c6', u'\uff04': u'$', u'\uff87': u'\u30cc', u'\uff08': u'(', u'\uff8b': u'\u30d2', u'\uff0c': u',', u'\uff8f': u'\u30de', u'\uff10': u'0', u'\uff93': u'\u30e2', u'\uff14': u'4', u'\uff97': u'\u30e9', u'\uff18': u'8', u'\uff9b': u'\u30ed', u'\uff1c': u'<', u'\uff9f': u'\u309a', u'\uff20': u'@', u'\uff24': u'D', u'\u2026': u'...', u'\uff28': u'H', u'\uff2c': u'L', u'\uff30': u'P', u'\uff34': u'T', u'\uff38': u'X', u'\uff3c': u'\\', u'\uff40': u'`', u'\uff44': u'd', u'\uff48': u'h', u'\uff4c': u'l', u'\uff50': u'p', u'\uff54': u't', u'\uff58': u'x', u'\uff5c': u'|', u'\uffe3': u' \u0304', u'\uff64': u'\u3001', u'\uff68': u'\u30a3', u'\uff6c': u'\u30e3', u'\uff70': u'\u30fc', u'\uff74': u'\u30a8', u'\uff78': u'\u30af', u'\uff7c': u'\u30b7', u'\uff80': u'\u30bf', u'\u2103': u'\xb0C', u'\uff84': u'\u30c8', u'\uff88': u'\u30cd', u'\uff0b': u'+', u'\uff8c': u'\u30d5', u'\uff0f': u'/', u'\uff90': u'\u30df', u'\uff13': u'3', u'\uff94': u'\u30e4', u'\uff17': u'7', u'\uff98': u'\u30ea', u'\uff1b': u';', u'\uff9c': u'\u30ef', u'\uff1f': u'?', u'\uff23': u'C', u'\u2025': u'..', u'\uff27': u'G', u'\u212b': u'\xc5', u'\uff2f': u'O', u'\uff33': u'S', u'\uff37': u'W', u'\uff3b': u'[', u'\uff3f': u'_', u'\uff43': u'c', u'\uff47': u'g', u'\uff4b': u'k', u'\uff4f': u'o', u'\uff53': u's', u'\uff57': u'w', u'\uff5b': u'{', u'\uff63': u'\u300d', u'\uff67': u'\u30a1', u'\uff6b': u'\u30a9', u'\uff6f': u'\u30c3', u'\uff73': u'\u30a6', u'\uff77': u'\u30ad', u'\uff7b': u'\u30b5', u'\uff2b': u'K', u'\uff7f': u'\u30bd'}

2
verbuild.py Normal file
View File

@ -0,0 +1,2 @@
MYMC_VERSION_BUILD = r'''5'''
MYMC_VERSION_MAJOR = r'''2'''