~thestr4ng3r/mymcplus

ea206c56c3ec4c56ffb7a00a5a13197854bc2eb4 — Florian Märkl 3 years ago 82dcc71
Basic Lighting, New icon.sys Parsing
M mymcplus/gui/dirlist_control.py => mymcplus/gui/dirlist_control.py +4 -4
@@ 17,7 17,7 @@

import wx

from .. import ps2mc
from .. import ps2mc, ps2iconsys
from ..save import ps2save

from . import utils


@@ 51,11 51,11 @@ class DirListControl(wx.ListCtrl):
                continue
            dirname = "/" + ent[8].decode("ascii")
            s = mc.get_icon_sys(dirname)
            if s == None:
            if s is None:
                continue
            a = ps2save.unpack_icon_sys(s)
            size = mc.dir_size(dirname)
            title = ps2save.icon_sys_title(a, encoding=enc)
            icon_sys = ps2iconsys.IconSys(s)
            title = icon_sys.get_title(enc)
            table.append((ent, s, size, title))

    def update_dirtable(self, mc):

M mymcplus/gui/gui.py => mymcplus/gui/gui.py +8 -8
@@ 27,7 27,7 @@ if os.name == "nt" and hasattr(sys, "setdefaultencoding"):
    sys.setdefaultencoding("mbcs")
import wx

from .. import ps2mc
from .. import ps2mc, ps2iconsys
from ..save import ps2save
from .icon_window import IconWindow
from .dirlist_control import DirListControl


@@ 287,24 287,24 @@ class GuiFrame(wx.Frame):
        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.import_menu_item.Enable(self.mc is not None)
        selected = self.mc is not 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:
        if self.icon_win is not None:
            self.icon_win.update_menu(self.icon_menu)

    def evt_dirlist_item_focused(self, event):
        if self.icon_win == None:
        if self.icon_win is None:
            return

        i = event.GetData()
        (ent, icon_sys, size, title) = self.dirlist.dirtable[i]
        (ent, icon_sys_data, size, title) = self.dirlist.dirtable[i]
        self.info1.SetLabel(title[0])
        self.info2.SetLabel(title[1])

        a = ps2save.unpack_icon_sys(icon_sys)
        icon_sys = ps2iconsys.IconSys(icon_sys_data)

        mc = self.mc
        if mc is None:


@@ 313,7 313,7 @@ class GuiFrame(wx.Frame):

        try:
            mc.chdir("/" + ent[8].decode("ascii"))
            f = mc.open(a[15].decode("ascii"), "rb")
            f = mc.open(icon_sys.icon_file_normal, "rb")
            try:
                icon = f.read()
            finally:

M mymcplus/gui/icon_renderer.py => mymcplus/gui/icon_renderer.py +61 -15
@@ 22,6 22,8 @@ from .linalg import Matrix4x4, Vector3
from ..ps2icon import TEXTURE_WIDTH, TEXTURE_HEIGHT


_LIGHTS_COUNT = 3

_glsl_vert = b"""
#version 150



@@ 34,12 36,14 @@ in vec4 color_attr;

out vec4 color_var;
out vec2 uv_var;
out vec3 normal_var;

void main()
{
    vec3 pos = vertex_attr / 4096.0;
    color_var = color_attr;
    uv_var = uv_attr / 4096.0;
    normal_var = normal_attr / 4096.0;
    gl_Position = mvp_matrix_uni * vec4(pos, 1.0);
}
"""


@@ 47,17 51,34 @@ void main()
_glsl_frag = b"""
#version 150

#define LIGHTS_COUNT """ + str(_LIGHTS_COUNT).encode("ascii") + b"""

uniform sampler2D texture_uni;

uniform vec3 light_dir_uni[LIGHTS_COUNT];
uniform vec3 light_color_uni[LIGHTS_COUNT];
uniform vec3 ambient_light_color_uni;

in vec4 color_var;
in vec2 uv_var;
in vec3 normal_var;

out vec4 color_out;

void main()
{
    vec3 tex_color = texture(texture_uni, uv_var).rgb;
    color_out = color_var * vec4(tex_color, 1.0);
    vec4 base_color = color_var * vec4(tex_color, 1.0);
    
    vec4 color = base_color * vec4(ambient_light_color_uni, 1.0);
    
    for(int i=0; i<LIGHTS_COUNT; i++)
    {
        float lambert = dot(light_dir_uni[i], normalize(normal_var));
        color += vec4(lambert * light_color_uni[i], 1.0) * base_color;
    }
    
    color_out = color;
}
"""



@@ 68,16 89,40 @@ _ATTRIB_COLOR =     3

_TEX_UNIT =         0


class IconRenderer:
    """Render a save file's 3D icon with OpenGL."""

    class LightingConfig:
        def __init__(self,
                     light_dirs = ((0.0, 0.0, 0.0),
                                   (0.0, 0.0, 0.0),
                                   (0.0, 0.0, 0.0)),
                     light_colors = ((0.0, 0.0, 0.0),
                                     (0.0, 0.0, 0.0),
                                     (0.0, 0.0, 0.0)),
                     ambient_light_color = (0.0, 0.0, 0.0)):
            self.light_dirs = light_dirs
            self.light_colors = light_colors
            self.ambient_light_color = ambient_light_color

            self.light_dirs_data = (GLfloat * (3 * _LIGHTS_COUNT))(*[-f for t in light_dirs for f in t])
            self.light_colors_data = (GLfloat * (3 * _LIGHTS_COUNT))(*[f for t in light_colors for f in t])
            self.ambient_color_data = (GLfloat * 3)(*ambient_light_color)


    def __init__(self, gl_context):
        self.failed = False

        self.context = gl_context

        self._icon = None
        self.lighting_config = self.LightingConfig(light_dirs = ((0.0, -1.0, 0.0),
                                                                 (0.0, 0.0, 0.0),
                                                                 (0.0, 0.0, 0.0)),
                                                   light_colors = ((1.0, 1.0, 1.0),
                                                                   (0.0, 0.0, 0.0),
                                                                   (0.0, 0.0, 0.0)),
                                                   ambient_light_color = (0.0, 0.0, 0.0))

        self._program = None
        self._vertex_vbo = None


@@ 85,7 130,12 @@ class IconRenderer:
        self._color_vbo = None
        self._vao = None
        self._texture = None

        self._mvp_matrix_uni = -1
        self._light_dir_uni = -1
        self._light_color_uni = -1
        self._ambient_light_color_uni = -1

        self._gl_initialized = False




@@ 119,6 169,9 @@ class IconRenderer:
            return

        self._mvp_matrix_uni = glGetUniformLocation(self._program, "mvp_matrix_uni")
        self._light_dir_uni = glGetUniformLocation(self._program, "light_dir_uni")
        self._light_color_uni = glGetUniformLocation(self._program, "light_color_uni")
        self._ambient_light_color_uni = glGetUniformLocation(self._program, "ambient_light_color_uni")

        texture_uni = glGetUniformLocation(self._program, "texture_uni")
        glUseProgram(self._program)


@@ 182,6 235,10 @@ class IconRenderer:
            projection_matrix = Matrix4x4.perspective(80.0, float(size.Width) / float(size.Height), 0.1, 500.0)
            glUniformMatrix4fv(self._mvp_matrix_uni, 1, GL_FALSE, (projection_matrix * modelview_matrix).ctypes_array)

            glUniform3fv(self._light_dir_uni, 3, self.lighting_config.light_dirs_data)
            glUniform3fv(self._light_color_uni, 3, self.lighting_config.light_colors_data)
            glUniform3fv(self._ambient_light_color_uni, 1, self.lighting_config.ambient_color_data)

            glDisable(GL_CULL_FACE)

            glBindVertexArray(self._vao)


@@ 221,21 278,10 @@ class IconRenderer:
        glGenerateMipmap(GL_TEXTURE_2D)


    def set_default_lighting(self, icon_sys):
        pass


    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
        pass

    def set_animate(self, animate):
        #if self.failed:

M mymcplus/gui/icon_window.py => mymcplus/gui/icon_window.py +4 -1
@@ 19,6 19,7 @@ import wx
from wx import glcanvas

from .. import ps2icon
from ..save import ps2save
from .icon_renderer import IconRenderer




@@ 180,19 181,21 @@ class IconWindow(wx.Window):

        if icon_data is None:
            self._icon = None
            self._icon_sys = None
        else:
            try:
                self._icon = ps2icon.Icon(icon_data)
            except ps2icon.Error as e:
                print("Failed to load icon.", e)
                self._icon = None
                self._icon_sys = None

        self._renderer.set_icon(self._icon)
        self.canvas.Refresh(eraseBackground=False)

    def set_lighting(self, id):
        self.lighting_id = id
        self._renderer.set_lighting(**self.light_options[id])
        #self._renderer.set_lighting(**self.light_options[id])

    def set_animate(self, animate):
        #if self.failed:

M mymcplus/mymc.py => mymcplus/mymc.py +10 -5
@@ 28,6 28,7 @@ from .save import ps2save
from .ps2mc_dir import *
from .save import codebreaker, ems, max_drive, sharkport
from . import verbuild
from . import ps2iconsys

class subopt_error(Exception):
    pass


@@ 329,12 330,16 @@ def do_setmode(cmd, mc, opts, args, opterr):
            ent[0] = value
        mc.set_dirent(arg, ent)

def _get_ps2_title(mc, enc):
    s = mc.get_icon_sys(".");
    if s == None:
def _get_ps2_title(mc, encoding):
    s = mc.get_icon_sys(".")
    if s is None:
        return None
    a = ps2save.unpack_icon_sys(s)
    return ps2save.icon_sys_title(a, enc)
    try:
        icon_sys = ps2iconsys.IconSys(s)
    except ps2iconsys.Error:
        return None

    return icon_sys.get_title(encoding)

def _get_psx_title(mc, savename, enc):
    mode = mc.get_mode(savename)

A mymcplus/ps2iconsys.py => mymcplus/ps2iconsys.py +177 -0
@@ 0,0 1,177 @@
#
# This file is part of mymc+, based on mymc by Ross Ridge.
#
# mymc+ is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# mymc+ is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with mymc+.  If not, see <http://www.gnu.org/licenses/>.
#

"""Interface for working with PS2 icon.sys files."""

import struct
from . import utils
from .sjistab import shift_jis_normalize_table


class Error(Exception):
    """Base for all exceptions specific to this module."""
    pass

class Corrupt(Error):
    """Corrupt icon file."""

    def __init__(self, msg):
        super().__init__(self, "Corrupt icon.sys: " + msg)


_PS2_ICON_SYS_MAGIC = b"PS2D"

_icon_sys_struct = struct.Struct("<4sHHII"
                                 "4I4I4I4I" # background colors 
                                 "4f4f4f" # light dirs
                                 "4f4f4f4f" # light colors
                                 "68s64s64s64s512s")

assert _icon_sys_struct.size == 964

#
# 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 the 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 "".join(a)


class IconSys:
    def __init__(self, data):

        if len(data) != _icon_sys_struct.size:
            raise Corrupt("icon.sys file has invalid size.")

        d = _icon_sys_struct.unpack(data)

        if d[0] != _PS2_ICON_SYS_MAGIC:
            raise Corrupt("icon.sys has incorrect magic.")

        self._title_line_offset = d[2]
        self.background_transparency = d[4]

        self.bg_colors = (d[5:21], d[5:9], d[9:13], d[17:21])

        self.light_dirs = (d[21:25], d[25:29], d[29:33], d[33:37])
        self.light_colors = (d[33:37], d[37:41], d[41:45])
        self.ambient_light_color = d[45:49]

        self._title_sjis = utils.zero_terminate(d[49])
        self.icon_file_normal = utils.zero_terminate(d[50]).decode("ascii")
        self.icon_file_copy = utils.zero_terminate(d[51]).decode("ascii")
        self.icon_file_delete = utils.zero_terminate(d[52]).decode("ascii")

    def get_title(self, encoding):
        title2 = shift_jis_conv(self._title_sjis[self._title_line_offset:], encoding)
        title1 = shift_jis_conv(self._title_sjis[:self._title_line_offset], encoding)
        return title1, title2

M mymcplus/ps2mc_dir.py => mymcplus/ps2mc_dir.py +3 -8
@@ 21,6 21,8 @@ import struct
import time
import calendar

from . import utils

PS2MC_DIRENT_LENGTH = 512

DF_READ        = 0x0001


@@ 41,13 43,6 @@ 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(b'\0')
    if i == -1:
        return s
    return s[:i]

# mode, ???, length, created,
# fat_cluster, parent_entry, modified, attr,


@@ 71,7 66,7 @@ def unpack_dirent(s):
    ent = list(ent)
    ent[3] = _tod_struct.unpack(ent[3])
    ent[6] = _tod_struct.unpack(ent[6])
    ent[8] = zero_terminate(ent[8])
    ent[8] = utils.zero_terminate(ent[8])
    return ent

def pack_dirent(ent):

M mymcplus/save/codebreaker.py => mymcplus/save/codebreaker.py +4 -3
@@ 3,6 3,7 @@ import array
import zlib

from .. import ps2mc_dir
from .. import utils
from .utils import *




@@ 69,10 70,10 @@ def load(save, f):
        raise ps2save.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 = ps2mc_dir.zero_terminate(dirname)
    dirname = utils.zero_terminate(dirname)
    created = ps2mc_dir.unpack_tod(created)
    modified = ps2mc_dir.unpack_tod(modified)
    title = ps2mc_dir.zero_terminate(title)
    title = utils.zero_terminate(title)

    # These fields don't always seem to be set correctly.
    if not ps2mc_dir.mode_is_dir(dirmode):


@@ 108,7 109,7 @@ def load(save, f):
    for i in range(len(files)):
        (header, data) = files[i]
        (created, modified, size, mode, h06, h08, h0C, name) = header
        name = ps2mc_dir.zero_terminate(name)
        name = utils.zero_terminate(name)
        created = ps2mc_dir.unpack_tod(created)
        modified = ps2mc_dir.unpack_tod(modified)
        if not ps2mc_dir.mode_is_file(mode):

M mymcplus/save/max_drive.py => mymcplus/save/max_drive.py +4 -3
@@ 2,6 2,7 @@
import binascii

from .. import ps2mc_dir
from .. import utils
from . import ps2save
from . import lzari
from ..round import round_up


@@ 25,7 26,7 @@ def load2(save, f):
        if len(s) - off < 36:
            raise ps2save.Eof(f)
        (l, name) = struct.unpack("<L32s", s[off: off + 36])
        name = ps2mc_dir.zero_terminate(name)
        name = utils.zero_terminate(name)
        # print "%08x %08x %s" % (off, l, name)
        off += 36
        data = s[off: off + l]


@@ 54,7 55,7 @@ def load(save, f, timestamp=None):
        s = f.read()
    else:
        s = read_fixed(f, clen - 4)
    dirname = ps2mc_dir.zero_terminate(dirname)
    dirname = utils.zero_terminate(dirname)
    if timestamp == None:
        timestamp = ps2mc_dir.tod_now()
    save.set_directory((ps2mc_dir.DF_RWX | ps2mc_dir.DF_DIR | ps2mc_dir.DF_0400 | ps2mc_dir.DF_EXISTS,


@@ 71,7 72,7 @@ def save(save, f):
    iconsysname = ""
    icon_sys = save.get_icon_sys()
    if icon_sys != None:
        title = ps2save.icon_sys_title(icon_sys, "ascii")
        title = icon_sys.get_title("ascii")
        if len(title[0]) > 0 and title[0][-1] != ' ':
            iconsysname = title[0] + " " + title[1].strip()
        else:

M mymcplus/save/ps2save.py => mymcplus/save/ps2save.py +7 -138
@@ 21,8 21,8 @@ import sys
import os
import binascii

from mymcplus.ps2mc_dir import *
from mymcplus.sjistab import shift_jis_normalize_table
from ..ps2mc_dir import *
from .. import ps2iconsys


from . import codebreaker, ems, max_drive, sharkport


@@ 56,81 56,6 @@ 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'-',
}



PS2SAVE_NPO_MAGIC = b"nPort"



@@ 167,39 92,6 @@ def detect_file_type(f):

    return None


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] = list(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)



#
# Set up tables of illegal and problematic characters in file names.
#


@@ 233,7 125,7 @@ def make_longname(dirname, sf):
    icon_sys = sf.get_icon_sys()
    title = ""
    if icon_sys is not None:
        title = icon_sys_title(icon_sys, "ascii")
        title = icon_sys.get_title("ascii")
        title = title[0] + " " + title[1]
        title = " ".join(title.split())
    crc = binascii.crc32(b"")


@@ 250,32 142,6 @@ def make_longname(dirname, sf):



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 the 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 "".join(a)





@@ 320,6 186,9 @@ class ps2_save_file(object):
        for i in range(self.dirent[2]):
            (ent, data) = self.get_file(i)
            if ent[8].decode("ascii") == "icon.sys" and len(data) >= 964:
                return unpack_icon_sys(data[:964])
                try:
                    return ps2iconsys.IconSys(data[:964])
                except ps2iconsys.Error:
                    pass
        return None


M mymcplus/save/sharkport.py => mymcplus/save/sharkport.py +3 -2
@@ 1,5 1,6 @@

from .. import ps2mc_dir
from .. import utils
from . import ps2save
from .utils import *



@@ 20,7 21,7 @@ def load(save, f):
        = struct.unpack("<H64sL8xH2x8s8s", read_fixed(f, 98))
    read_fixed(f, hlen - 98)

    dirname = ps2mc_dir.zero_terminate(dirname)
    dirname = utils.zero_terminate(dirname)
    created = ps2mc_dir.unpack_tod(created)
    modified = ps2mc_dir.unpack_tod(modified)



@@ 38,7 39,7 @@ def load(save, f):
        if hlen < 98:
            raise ps2save.Corrupt("Header length too short.", f)
        read_fixed(f, hlen - 98)
        name = ps2mc_dir.zero_terminate(name)
        name = utils.zero_terminate(name)
        created = ps2mc_dir.unpack_tod(created)
        modified = ps2mc_dir.unpack_tod(modified)
        mode = mode // 256 % 256 + mode % 256 * 256

M mymcplus/save/utils.py => mymcplus/save/utils.py +1 -1
@@ 15,4 15,4 @@ 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)
\ No newline at end of file
    return read_fixed(f, length)

A mymcplus/utils.py => mymcplus/utils.py +8 -0
@@ 0,0 1,8 @@

def zero_terminate(s):
    """Truncate a string at the first NUL ('\0') character, if any."""

    i = s.find(b'\0')
    if i == -1:
        return s
    return s[:i]
\ No newline at end of file

A test/test_iconsys.py => test/test_iconsys.py +31 -0
@@ 0,0 1,31 @@

import base64

from mymcplus import ps2iconsys

_icon_sys_data = base64.b64decode(
    b"UFMyRAAAIAAAAAAAcwAAABQAAAAUAAAAPAAAAAAAAAAUAAAAFAAAADwAAAAAAAAAFAAAABQAAAA8"
    b"AAAAAAAAABQAAAAUAAAAPAAAAAAAAAAAAAA/AAAAPwAAAD8AAAAAAAAAAM3MzD4AAIC/AAAAAAAA"
    b"AL8AAAC/AAAAPwAAAACPwvU+j8L1PvYo3D4AAAAAuB6FPsP1qD4AAAA/AAAAAClcDz4pXA8+XI/C"
    b"PgAAAACPwnU+j8J1Po/CdT4AAAAAgnGChYKaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAByZXouaWNvAAAAAAAAAAAAAAAAAAAAAAAA"
    b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcmV6LmljbwAAAAAAAAAAAAAA"
    b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHJlei5pY28AAAAA"
    b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")


def test_iconsys():
    icon_sys = ps2iconsys.IconSys(_icon_sys_data)
    assert icon_sys.get_title("ascii") == ("Rez", "")
    assert icon_sys.icon_file_normal == "rez.ico"
    assert icon_sys.icon_file_copy == "rez.ico"
    assert icon_sys.icon_file_delete == "rez.ico"
\ No newline at end of file