├── .github
└── workflows
│ ├── python-build.yml
│ └── python-publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs
├── README-original.txt
├── README.md
├── mc4.png
├── mymc-website-original.html
├── ps2mcfs.html
└── ss7.png
├── mymcplusplus
├── __init__.py
├── __main__.py
├── gui
│ ├── __init__.py
│ ├── dirlist_control.py
│ ├── gui.py
│ ├── icon_renderer.py
│ ├── icon_window.py
│ ├── linalg.py
│ ├── resources.py
│ └── utils.py
├── mymc.py
├── ps2icon.py
├── ps2iconsys.py
├── ps2mc.py
├── ps2mc_dir.py
├── ps2mc_ecc.py
├── round.py
├── save
│ ├── __init__.py
│ ├── format_codebreaker.py
│ ├── format_ems.py
│ ├── format_max_drive.py
│ ├── format_psv.py
│ ├── format_sharkport.py
│ ├── lzari.py
│ ├── ps2save.py
│ └── utils.py
├── sjistab.py
├── utils.py
└── verbuild.py
├── screenshot.png
├── setup.py
└── test
├── conftest.py
├── data.tar.gz
├── data_lzari.py
├── test_ecc.py
├── test_iconsys.py
├── test_lzari.py
└── test_memorycard.py
/.github/workflows/python-build.yml:
--------------------------------------------------------------------------------
1 | name: Build Python Package
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "master" ]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | deploy:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v3
17 | - name: Set up Python
18 | uses: actions/setup-python@v3
19 | with:
20 | python-version: '3.x'
21 | - name: Install dependencies
22 | run: |
23 | python -m pip install --upgrade pip
24 | pip install build
25 | - name: Build package
26 | run: python -m build
27 | - name: Upload artifact
28 | uses: actions/upload-artifact@v2
29 | with:
30 | name: wheels
31 | path: dist/*.whl
32 |
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | name: Upload Python Package
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | permissions:
8 | contents: read
9 |
10 | jobs:
11 | deploy:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - name: Set up Python
16 | uses: actions/setup-python@v3
17 | with:
18 | python-version: '3.x'
19 | - name: Install dependencies
20 | run: |
21 | python -m pip install --upgrade pip
22 | pip install build
23 | - name: Build package
24 | run: python -m build
25 | - name: Publish package
26 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
27 | with:
28 | user: __token__
29 | password: ${{ secrets.PYPI_API_TOKEN }}
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.swp
3 | .idea/
4 | .pytest_cache/
5 | /test/data/
6 | build/
7 | dist/
8 | raw/
9 | *.egg-info/
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mymc++
2 |
3 | mymc++ is a PlayStation 2 memory card manager for use with .ps2 images created by PCSX2, as well as .mc2 files created by the MemCard PRO2.
4 |
5 | It is based on [mymc+](https://git.sr.ht/~thestr4ng3r/mymcplus) by Florian Märkl and the classic [mymc](http://www.csclub.uwaterloo.ca:11068/mymc/) utility created by Ross Ridge.
6 |
7 | Changes that have been made from the original code include the following:
8 |
9 | * Added support for MemCard PRO2 .mc2 files
10 | * Usability improvements
11 | * Various bug fixes
12 |
13 | Please note that mymc++ is released under the **GPLv3, not Public Domain**!
14 |
15 | Here is an overview of most features:
16 |
17 | * Read and write the PS2 memory card file system, including extracting and adding files at file system level
18 | * Import save games in MAX Drive (.max), EMS (.psu), SharkPort (.sps), X-Port (.xps), Code Breaker (.cbs) and PSV (.psv) format
19 | * Export save games in MAX Drive (.max) and EMS (.psu) format
20 | * Command line interface
21 | * Optional wxPython based GUI, also displaying the 3D icons
22 |
23 | 
24 |
25 | ## Installation
26 |
27 | mymc++ is available on [PyPI](https://pypi.org/project/mymcplusplus/).
28 | You can install it, including the GUI, using pip:
29 |
30 | ```
31 | pip install mymcplusplus[gui]
32 | ```
33 |
34 | If you only wish to install the command line interface, simply omit the
35 | gui extra:
36 |
37 | ```
38 | pip install mymcplusplus
39 | ```
40 |
41 | ## Usage
42 |
43 | If the GUI component is installed (i.e. wxPython can be found), it can
44 | simply be started using the following command:
45 |
46 | ```
47 | mymcplusplus
48 | ```
49 |
50 | ### Command Line Interface
51 |
52 | The command line interface can be used like this:
53 |
54 | ```
55 | Usage: /usr/bin/mymcplusplus [-ih] memcard.ps2 command [...]
56 |
57 | Manipulate PS2 memory card images.
58 |
59 | Supported commands:
60 | add: Add files to the memory card.
61 | check: Check for file system errors.
62 | clear: Clear mode flags on files and directories
63 | delete: Recursively delete a directory (save file).
64 | df: Display the amount free space.
65 | dir: Display save file information.
66 | export: Export save files from the memory card.
67 | extract: Extract files from the memory card.
68 | format: Creates a new memory card image.
69 | gui: Starts the graphical user interface.
70 | import: Import save files into the memory card.
71 | ls: List the contents of a directory.
72 | mkdir: Make directories.
73 | remove: Remove files and directories.
74 | set: Set mode flags on files and directories
75 |
76 | Options:
77 | --version show program's version number and exit
78 | -h, --help show this help message and exit
79 | -i, --ignore-ecc Ignore ECC errors while reading.
80 | ```
81 |
82 | It is always necessary to specify the path to a memory card image
83 | with `-i ` first. For example:
84 |
85 | ```
86 | mymcplusplus -i empty.ps2 format
87 | ```
88 |
89 | creates the file `empty.ps2` and formats it as an empty memory card.
90 |
--------------------------------------------------------------------------------
/docs/README-original.txt:
--------------------------------------------------------------------------------
1 | README.txt
2 |
3 | By Ross Ridge
4 | Pubic Domain
5 |
6 | @(#) mymc README.txt 1.6 12/10/04 19:18:08
7 |
8 |
9 | This file describes mymc, a utility for manipulating PlayStation 2
10 | memory card images as used by the emulator PCSX2. Its main purpose is
11 | to allow save games to be imported and exported to and from these
12 | images. Both MAX Drive and EMS (.psu) save files are fully supported,
13 | however save files in the SharkPort/X-Port and Code Breaker formats
14 | can only be imported and not exported. In addition to these basic
15 | functions, mymc can also perform a number of other operations, like
16 | creating new memory card images, viewing their contents, and adding
17 | and extracting individual files.
18 |
19 | A simple, hopefully easy to use, graphicial user interface (GUI) is
20 | provided, but it's limitted to only basic operations. More advanced
21 | opterations require the use of a command line tool. To install mymc,
22 | unpack the downloaded ZIP archive to a new directory on your machine.
23 | You can then run the GUI version of mymc by openning that newn
24 | directory with Windows Explorer and double clicking on the "mymc-gui"
25 | icon. To make it easier to access, you can drag the "mymc-gui" icon
26 | to either your Desktop, Start Menu or Quick Launch toolbar. Make sure
27 | if you do so, that you create a shortcut to "mymc-gui.exe". If you
28 | copy the file instead, the program won't work.
29 |
30 | The command line utility can be invoked from the Windows Command
31 | Prompt by using the "mymc" command. The executable "mymc.exe" and
32 | number of support files and these file must kept together in the same
33 | directory. To run the command you need to either add the directory
34 | where you unpacked the distribution to your PATH or type the full
35 | pathname of the executable. For example if you unpacked mymc to a
36 | directory named "c:\mymc" you need to enter "c:\mymc\mymc.exe" to run
37 | the program.
38 |
39 | The second important thing to note is that mymc is only "alpha"
40 | quality software. This means that has is been released without
41 | extensive testing and may be unreliable. While it works fine for me,
42 | the author, it might not work as well for you. For that reason you
43 | should be careful how you use it, and prepared for the eventuality of
44 | it corrupting your save game images or producing garbage save files.
45 | If you're worried about this, one make things safer is to use two
46 | memory card images. Use the first image to load and save your games
47 | with under PCSX2, and the second image to import and export saves
48 | games using mysc. Then use the PS2 browser to copy files between two
49 | card images.
50 |
51 |
52 | GUI TUTORIAL
53 | ============
54 |
55 | The GUI for mymc is should be easy to use. After starting mymc, you
56 | can select the PS2 memory card image you want to work with by
57 | selecting the "Open" command by pressing the first button on the
58 | toolbar. You can then import a save file clicking on the Import
59 | toolbar button. To export a save files, first select it and then
60 | press the Export button. You can delete a save file permanently from
61 | your memory card, by selecting the "Delete" command from the File
62 | menu.
63 |
64 | Do not try to use mymc to modify a memory card image while PCSX2 is
65 | running. Doing so will corrupt your memory card.
66 |
67 |
68 | COMMAND LINE TUTORIAL
69 | =====================
70 |
71 | The basic usage template for mysc is "mymc memcard.ps2 command". The
72 | first argument, "memcard.ps2" is the filename of the memory card image
73 | while "command" is the name of the command you wish to use on the
74 | image. So for example, assuming you've installed mymc in "c:\mymc"
75 | and you've installed PCSX2 in "c:\pcsx2" you could enter the following
76 | command to see the contents of the memory card in the emulator's slot
77 | 1:
78 |
79 | c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 dir
80 |
81 | You would see output something like this:
82 |
83 | BASLUS-20678USAGAS00 UNLIMITED SAGA
84 | 154KB Not Protected SYSTEMDATA
85 |
86 | BADATA-SYSTEM Your System
87 | 5KB Not Protected Configuration
88 |
89 | BASLUS-20488-0000D SOTET<13>060:08
90 | 173KB Not Protected Arias
91 |
92 | 7,800 KB Free
93 |
94 | This is the simple "user friendly" way to view the contents of a
95 | memory card. It displays the same information you can see using the
96 | PlayStation 2 memory card browser. On the right is name of each save,
97 | and on the left is the size and protection status of the save. Also
98 | on the left is one bit of information you won't see in the browser,
99 | the directory name of the save file. PlayStation 2 saves are actually
100 | a collection of different files all stored in a single directory on
101 | the memory card. This is important information, because you need to
102 | know it to export save files.
103 |
104 | As mentioned above, if you know the directory name of a save, you can
105 | export it. Exporting a save creates a save file in either the EMS
106 | (.psu) or MAX Drive (.max) format. You can then transfer the save to
107 | real PS2 memory using the appropriate tools. You can also send the
108 | saves to someone else to use or just keep them on your hard drive as a
109 | backup. The following command demonstrates how to export a save in
110 | the EMS format using mymc:
111 |
112 | c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 export BASLUS-20448-0000D
113 |
114 | This will create a file called "BASLUS-20448-0000D.psu" in the current
115 | directory. To create a file in the MAX format instead, use the export
116 | command's -m option:
117 |
118 | c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 export -m BASLUS-20448-0000D
119 |
120 | This creates a file named "BASLUS-20448-0000D.max". Note the "-m"
121 | option that appears after the "export" command.
122 |
123 | Importing save files is similar. The save file type is auto-detected,
124 | so you don't need use an "-m" option with MAX Drive saves. Here's a
125 | couple of examples using each format:
126 |
127 | c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 import BASLUS-20035.psu
128 | c:\mymc\mymc c:\pcsx2\memcards\Mcd001.ps2 import 20062_3583_GTA3.max
129 |
130 |
131 | ADVANCED NOTES
132 | ==============
133 |
134 | - To get general help with the command line utility use the "-h"
135 | global option (eg. "mymc -h"). To get help with a specific
136 | command use the "-h" option with that command (eg. "mymc x
137 | import -h"). In this later case, you need to specify a memory
138 | card image file, but it's ignored and so doesn't need to exist.
139 |
140 | - Both executables in the Windows version, "mymc.exe" and
141 | "mymc-gui.exe" do the same thing and support the same options.
142 | The difference is that "mymc" is console application, while
143 | "mymc-gui" is a Windows appliction. Currently, using "mymc"
144 | to start the GUI will result in a fair amount debug messages
145 | being printed that are normally not seen "mymc-gui" is used.
146 |
147 | - It's possible to use mymc create images that are bigger (or
148 | smaller) than standard PS2 memory cards. Be very careful if you
149 | do this, not all games may be compatible with such images.
150 |
151 | - The bad block list on images is ignored. Since memory card
152 | images created with either PCSX2 or mymc won't have any bad
153 | blocks, this shouldn't be a problem unless you've somehow
154 | extracted a complete image from a real memory card and expect to
155 | copy it back.
156 |
157 | - The PS2 only uses at most 8,000 KB of a memory card, but there
158 | is actually 8,135 KB of allocatable space on a standard
159 | error-free memory card. The extra 135 KB is reserved so that
160 | memory card with bad blocks don't appear to have less space than
161 | memory cards with fewer or no bad blocks. Since there are no
162 | bad blocks on memory card images, mymc uses the full capacity
163 | provided by standard memory cards.
164 |
165 |
166 | PYTHON SOURCE DISTRIBUTION
167 | ==========================
168 |
169 | The "source code" distribution of mymc is provided for users of Linux
170 | and other non-Windows operating systems. It uses the same Python code
171 | that the Windows distribution is built with (using py2exe) and
172 | supports all the same functionality. One big difference is that the
173 | Windows DLL "mymcsup.dll" is not included and as a result compressing
174 | and decompressing MAX Drive saves will be as much as 100 times slower.
175 | The GUI mode is hasn't been extensively tested on non-Windows systems,
176 | and the 3D display of save file icons requires the DLL. The Python
177 | source version should support big-endian machines, but this hasn't
178 | been tested.
179 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # docs
2 |
3 | This directory contains copies of the original README and Website for
4 | mymc, as well as the detailed description of the PS2 Memory Card File
5 | System, all created by Ross Ridge.
6 |
--------------------------------------------------------------------------------
/docs/mc4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adubbz/mymcplusplus/fcd946557b6275ae47f8082e7cd3aac54269d9e5/docs/mc4.png
--------------------------------------------------------------------------------
/docs/mymc-website-original.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | mymc, a PS2 Memory Card Image Utility
5 |
6 |
7 |
8 |
9 | mymc, a PS2 Memory Card Image Utility
10 |
11 |
12 |
13 |
14 | mymc is a public domain utility for working with PlayStation 2
15 | memory card images (.ps2) as used by the PlayStation 2 emulator
16 | PCSX2. It allows save files in
17 | the MAX Drive (.max), EMS (.psu), SharkPort (.sps), X-Port (.xps) and
18 | Code Breaker (.cbs) formats to be imported directly into these images.
19 | It can also export save files in eiter the MAX Drive and EMS formats.
20 | See the README.txt file included in the
21 | distribution below for more details.
22 |
23 |
24 |
25 |
26 |
38 |
39 |
40 |
41 |
42 | New in Version 2.6
43 |
44 |
45 | - a few minor bugs fixed
46 |
47 |
48 |
49 | New in Version 2.5
50 |
51 |
52 | - set the mode and attributes of imported saves correctly
53 | - a couple of minor typos and bugs fixed
54 |
55 |
56 |
57 | New in Version 2.4
58 |
59 |
60 | - import SharkPort/X-Port and Code Breaker saves
61 | - fixed bugs caused by installing mymc in directories with
62 | non-ASCII names
63 | - 3D icons displayed using multisample anti-aliasing (where supported)
64 | - a number of minor bugs and typos fixed
65 |
66 |
67 |
68 | New in Version 2.1
69 |
70 |
71 | - easy to use GUI
72 | - delete command for recursively removing directories.
73 | - faster directory reading
74 |
75 |
76 |
77 | New in Version 1.6
78 |
79 |
80 |
81 | - faster ECC caclulation on Windows
82 | - new long name format for exporting files
83 | - a couple of minor bug and typos fixed
84 |
85 |
86 |
87 |
88 |
89 | Requirements
90 |
91 |
92 | The Windows release of mymc requires the April 2006 DirectX update. You
93 | can download and install all of the DirectX updates using Microsoft's
94 |
95 | DirectX End-User Runtime Web Installer.
96 | Users of Windows Vista, Windows 7 or newer may need to download and
97 | install the files MSVCR71.DLL and MSVCP71.DLL into the same directory
98 | you installed mymc. Unfortunately, Microsoft doesn't make these files
99 | available for download, so I can't provide a link to them.
100 |
101 | If you're using Windows 98 or Windows ME then you may also need
102 |
103 | UNICOWS.DLL, and
104 |
105 | GDIPLUS.DLL. If necessary download and copy these files to the same
106 | directory you unpacked mymc.
107 |
108 |
109 |
110 | The Python release optionally requires
111 |
112 | wxPython. It will work without it, but the GUI mode won't be available.
113 |
114 |
115 |
116 |
117 |
118 |
Download mymc
119 |
120 | Current Windows Release:
121 | mymc-alpha-2.6.zip (~4.5M)
122 |
123 |
124 | Current Python Source Release:
125 | mymc-pysrc-2.6.zip (~48k)
126 |
127 |
128 |
129 | Related Documentation
130 |
131 |
132 | PlayStation 2 Memory Card File System
133 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/docs/ss7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adubbz/mymcplusplus/fcd946557b6275ae47f8082e7cd3aac54269d9e5/docs/ss7.png
--------------------------------------------------------------------------------
/mymcplusplus/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | __all__ = [
19 | "gui",
20 | "ps2icon",
21 | "lzari",
22 | "mymc",
23 | "ps2mc",
24 | "ps2mc_dir",
25 | "ps2mc_ecc",
26 | "ps2save",
27 | "round",
28 | "sjistab",
29 | "verbuild"
30 | ]
--------------------------------------------------------------------------------
/mymcplusplus/__main__.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | import sys
19 |
20 | from . import mymc
21 |
22 | if __name__ == "__main__":
23 | sys.exit(mymc.main(sys.argv))
--------------------------------------------------------------------------------
/mymcplusplus/gui/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | __all__ = [
19 | "dirlist_control",
20 | "gui",
21 | "icon_window",
22 | "icon_renderer",
23 | "linalg",
24 | "resources",
25 | "utils",
26 | ]
--------------------------------------------------------------------------------
/mymcplusplus/gui/dirlist_control.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | from enum import Enum, IntEnum
19 | import wx
20 |
21 | from .. import ps2mc, ps2iconsys
22 | from ..save import ps2save
23 |
24 | from . import utils
25 |
26 |
27 | def get_dialog_units(win):
28 | return win.ConvertDialogToPixels((1, 1))[0]
29 |
30 |
31 | class DirListControl(wx.ListCtrl):
32 | """Lists all the save files in a memory card image."""
33 |
34 | class Column(IntEnum):
35 | DIRECTORY = 0
36 | SIZE = 1
37 | MODIFIED = 2
38 | DESCRIPTION = 3
39 |
40 | class TableEntry:
41 | class Type(Enum):
42 | PS2 = 0
43 | PS1 = 1
44 |
45 | def __init__(self, type, dirent, icon_sys, size, title):
46 | self.type = type
47 | self.dirent = dirent
48 | self.icon_sys = icon_sys
49 | self.size = size
50 | self.title = title
51 |
52 |
53 | def __init__(self, parent, evt_focus, evt_select, config):
54 | self.config = config
55 | self.selected = set()
56 | self.dirtable = []
57 |
58 | self.evt_select = evt_select
59 | wx.ListCtrl.__init__(self, parent, wx.ID_ANY,
60 | style=wx.LC_REPORT)
61 | self.Bind(wx.EVT_LIST_COL_CLICK, self.evt_col_click)
62 | self.Bind(wx.EVT_LIST_ITEM_FOCUSED, evt_focus)
63 | self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.evt_item_selected)
64 | self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.evt_item_deselected)
65 |
66 |
67 | def _update_dirtable(self, mc, dir):
68 | self.dirtable = table = []
69 | enc = "unicode"
70 | if self.config.get_ascii():
71 | enc = "ascii"
72 | for ent in dir:
73 | if not ps2mc.mode_is_dir(ent[0]):
74 | continue
75 |
76 | dirname = ent[8].decode("ascii")
77 | dirpath = "/" + dirname
78 |
79 | if ps2mc.mode_is_psx_dir(ent[0]):
80 | type = self.TableEntry.Type.PS1
81 | title = (dirname, "")
82 | icon_sys = None
83 | else:
84 | type = self.TableEntry.Type.PS2
85 | icon_sys_data = mc.get_icon_sys(dirpath)
86 | if icon_sys_data is None:
87 | continue
88 | icon_sys = ps2iconsys.IconSys(icon_sys_data)
89 | title = icon_sys.get_title(enc)
90 |
91 | size = mc.dir_size(dirpath)
92 | table.append(self.TableEntry(type, ent, icon_sys, size, title))
93 |
94 |
95 | def update_dirtable(self, mc):
96 | self.dirtable = []
97 | if mc is None:
98 | return
99 | dir = mc.dir_open("/")
100 | try:
101 | self._update_dirtable(mc, dir)
102 | finally:
103 | dir.close()
104 |
105 |
106 | def cmp_dir_name(self, i1, i2):
107 | return self.dirtable[i1].dirent[8] > self.dirtable[i2].dirent[8]
108 |
109 |
110 | def cmp_dir_title(self, i1, i2):
111 | return self.dirtable[i1].title > self.dirtable[i2].title
112 |
113 |
114 | def cmp_dir_size(self, i1, i2):
115 | return self.dirtable[i1].size > self.dirtable[i2].size
116 |
117 |
118 | def cmp_dir_modified(self, i1, i2):
119 | m1 = list(self.dirtable[i1].dirent[6])
120 | m2 = list(self.dirtable[i2].dirent[6])
121 | m1.reverse()
122 | m2.reverse()
123 | return m1 > m2
124 |
125 |
126 | def evt_col_click(self, event):
127 | col = self.Column(event.Column)
128 |
129 | if col == self.Column.DIRECTORY:
130 | cmp = self.cmp_dir_name
131 | elif col == self.Column.SIZE:
132 | cmp = self.cmp_dir_size
133 | elif col == self.Column.MODIFIED:
134 | cmp = self.cmp_dir_modified
135 | elif col == self.Column.DESCRIPTION:
136 | cmp = self.cmp_dir_title
137 | else:
138 | return
139 | self.SortItems(cmp)
140 |
141 |
142 | def evt_item_selected(self, event):
143 | self.selected.add(event.GetData())
144 | self.evt_select(event)
145 |
146 |
147 | def evt_item_deselected(self, event):
148 | self.selected.discard(event.GetData())
149 | self.evt_select(event)
150 |
151 |
152 | def update(self, mc):
153 | """Update the ListCtrl according to the contents of the
154 | memory card image."""
155 |
156 | self.ClearAll()
157 | self.selected = set()
158 |
159 | columns = [
160 | (self.Column.DIRECTORY, "Directory", None),
161 | (self.Column.SIZE, "Size", wx.LIST_FORMAT_RIGHT),
162 | (self.Column.MODIFIED, "Modified", None),
163 | (self.Column.DESCRIPTION, "Description", None)
164 | ]
165 |
166 | for (id, title, align) in columns:
167 | index = id.value
168 | column = wx.ListItem()
169 | column.SetText(title)
170 | if align is not None:
171 | column.SetAlign(align)
172 | self.InsertColumn(index, column)
173 |
174 | self.update_dirtable(mc)
175 |
176 | empty = len(self.dirtable) == 0
177 | self.Enable(not empty)
178 | if empty:
179 | return
180 |
181 | for (i, a) in enumerate(self.dirtable):
182 | li = self.InsertItem(i, a.dirent[8])
183 | self.SetItem(li, 1, "%dK" % (a.size // 1024))
184 | m = a.dirent[6]
185 | m = ("%04d-%02d-%02d %02d:%02d"
186 | % (m[5], m[4], m[3], m[2], m[1]))
187 | self.SetItem(li, 2, m)
188 | self.SetItem(li, 3, utils.single_title(a.title))
189 | self.SetItemData(li, i)
190 |
191 | du = get_dialog_units(self)
192 | for i in range(4):
193 | self.SetColumnWidth(i, wx.LIST_AUTOSIZE)
194 | self.SetColumnWidth(i, self.GetColumnWidth(i) + du)
195 | self.SortItems(self.cmp_dir_name)
196 |
--------------------------------------------------------------------------------
/mymcplusplus/gui/gui.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | """Graphical user-interface for mymc++."""
19 |
20 | import copy
21 | import os
22 | import sys
23 | import struct
24 | import io
25 |
26 | from pathlib import Path
27 |
28 | # Windows-specific fixes
29 | if os.name == "nt":
30 | # Work around a problem with mixing wx and py2exe
31 | if hasattr(sys, "setdefaultencoding"):
32 | sys.setdefaultencoding("mbcs")
33 |
34 | # Fix DPI awareness
35 | import ctypes
36 | try: ctypes.windll.shcore.SetProcessDpiAwareness(True)
37 | except: pass
38 | import wx
39 |
40 | from .. import ps2mc, ps2iconsys
41 | from ..round import *
42 | from ..save import ps2save
43 | from .icon_window import IconWindow
44 | from .dirlist_control import DirListControl
45 | from . import utils
46 |
47 |
48 | class GuiConfig(wx.Config):
49 | """A class for holding the persistant configuration state."""
50 |
51 | memcard_dir = "Memory Card Directory"
52 | savefile_dir = "Save File Directory"
53 | ascii = "ASCII Descriptions"
54 |
55 | def __init__(self):
56 | wx.Config.__init__(self, "mymc++", style = wx.CONFIG_USE_LOCAL_FILE)
57 |
58 | def get_memcard_dir(self, default = None):
59 | return self.Read(GuiConfig.memcard_dir, default)
60 |
61 | def set_memcard_dir(self, value):
62 | return self.Write(GuiConfig.memcard_dir, value)
63 |
64 | def get_savefile_dir(self, default = None):
65 | return self.Read(GuiConfig.savefile_dir, default)
66 |
67 | def set_savefile_dir(self, value):
68 | return self.Write(GuiConfig.savefile_dir, value)
69 |
70 | def get_ascii(self, default = False):
71 | return bool(self.ReadInt(GuiConfig.ascii, int(bool(default))))
72 |
73 | def set_ascii(self, value):
74 | return self.WriteInt(GuiConfig.ascii, int(bool(value)))
75 |
76 |
77 | def add_tool(toolbar, id, label, standard_art, ico):
78 | bmp = wx.NullBitmap
79 |
80 | if standard_art is not None:
81 | bmp = wx.ArtProvider.GetBitmap(standard_art, wx.ART_TOOLBAR)
82 |
83 | if bmp == wx.NullBitmap:
84 | tbsize = toolbar.GetToolBitmapSize()
85 | bmp = utils.get_png_resource_bmp(ico, tbsize)
86 |
87 | return toolbar.AddTool(id, label, bmp, shortHelp = label)
88 |
89 |
90 | class GuiFrame(wx.Frame):
91 | """The main top level window."""
92 |
93 | ID_CMD_EXIT = wx.ID_EXIT
94 | ID_CMD_OPEN = wx.ID_OPEN
95 | ID_CMD_EXPORT = 103
96 | ID_CMD_IMPORT = 104
97 | ID_CMD_DELETE = wx.ID_DELETE
98 | ID_CMD_ASCII = 106
99 | ID_CMD_SAVEAS = 107
100 |
101 | def message_box(self, message, caption = "mymcplusplus", style = wx.OK,
102 | x = -1, y = -1):
103 | return wx.MessageBox(message, caption, style, self, x, y)
104 |
105 | def error_box(self, msg):
106 | return self.message_box(msg, "Error", wx.OK | wx.ICON_ERROR)
107 |
108 | def mc_error(self, value, filename = None):
109 | """Display a message box for EnvironmentError exeception."""
110 |
111 | if filename == None:
112 | filename = getattr(value, "filename")
113 | if filename == None:
114 | filename = self.mcname
115 | if filename == None:
116 | filename = "???"
117 |
118 | strerror = getattr(value, "strerror", None)
119 | if strerror == None:
120 | strerror = "unknown error"
121 |
122 | return self.error_box(filename + ": " + strerror)
123 |
124 | def __init__(self, parent, title, mcname = None):
125 | self.f = None
126 | self.mc = None
127 | self.mcname = None
128 | self.icon_win = None
129 |
130 | wx.Frame.__init__(self, parent, wx.ID_ANY, title)
131 | self.SetClientSize(self.FromDIP((800, 400)))
132 |
133 | self.Bind(wx.EVT_CLOSE, self.evt_close)
134 |
135 | self.config = GuiConfig()
136 | self.title = title
137 |
138 | self.SetIcon(wx.Icon(utils.get_png_resource_bmp("icon.png")))
139 |
140 | self.Bind(wx.EVT_MENU, self.evt_cmd_exit, id=self.ID_CMD_EXIT)
141 | self.Bind(wx.EVT_MENU, self.evt_cmd_open, id=self.ID_CMD_OPEN)
142 | self.Bind(wx.EVT_MENU, self.evt_cmd_saveas, id=self.ID_CMD_SAVEAS)
143 | self.Bind(wx.EVT_MENU, self.evt_cmd_export, id=self.ID_CMD_EXPORT)
144 | self.Bind(wx.EVT_MENU, self.evt_cmd_import, id=self.ID_CMD_IMPORT)
145 | self.Bind(wx.EVT_MENU, self.evt_cmd_delete, id=self.ID_CMD_DELETE)
146 | self.Bind(wx.EVT_MENU, self.evt_cmd_ascii, id=self.ID_CMD_ASCII)
147 |
148 | filemenu = wx.Menu()
149 | filemenu.Append(self.ID_CMD_OPEN, "&Open...", "Opens an existing PS2 memory card image.")
150 | filemenu.AppendSeparator()
151 | self.saveas_menu_item = filemenu.Append(self.ID_CMD_SAVEAS, "&Save As...")
152 | filemenu.AppendSeparator()
153 | self.export_menu_item = filemenu.Append(self.ID_CMD_EXPORT, "&Export...", "Export a save file from this image.")
154 | self.import_menu_item = filemenu.Append(self.ID_CMD_IMPORT, "&Import...", "Import a save file into this image.")
155 | self.delete_menu_item = filemenu.Append(self.ID_CMD_DELETE, "&Delete")
156 | filemenu.AppendSeparator()
157 | filemenu.Append(self.ID_CMD_EXIT, "E&xit")
158 |
159 | optionmenu = wx.Menu()
160 | self.ascii_menu_item = optionmenu.AppendCheckItem(self.ID_CMD_ASCII, "&ASCII Descriptions", "Show descriptions in ASCII instead of Shift-JIS")
161 |
162 |
163 | self.Bind(wx.EVT_MENU_OPEN, self.evt_menu_open)
164 |
165 | self.CreateToolBar(wx.TB_HORIZONTAL)
166 | self.toolbar = toolbar = self.GetToolBar()
167 | toolbar.SetToolBitmapSize(self.FromDIP(wx.Size(50, 50)))
168 | add_tool(toolbar, self.ID_CMD_OPEN, "Open", wx.ART_FILE_OPEN, "open.png")
169 | toolbar.AddSeparator()
170 | add_tool(toolbar, self.ID_CMD_IMPORT, "Import", None, "import.png")
171 | add_tool(toolbar, self.ID_CMD_EXPORT, "Export", None, "export.png")
172 | toolbar.Realize()
173 |
174 | self.statusbar = self.CreateStatusBar(2, style=wx.STB_SIZEGRIP)
175 | self.statusbar.SetStatusWidths([-2, -1])
176 |
177 | panel = wx.Panel(self, wx.ID_ANY, (0, 0))
178 | sizer = wx.BoxSizer(wx.HORIZONTAL)
179 | sizer.Add(panel, wx.EXPAND, wx.EXPAND)
180 | self.SetSizer(sizer)
181 |
182 | splitter_window = wx.SplitterWindow(panel, style=wx.SP_LIVE_UPDATE)
183 | splitter_window.SetSashGravity(0.5)
184 |
185 | self.dirlist = DirListControl(splitter_window,
186 | self.evt_dirlist_item_focused,
187 | self.evt_dirlist_select,
188 | self.config)
189 |
190 | if mcname is not None:
191 | self.open_mc(mcname)
192 | else:
193 | self.refresh()
194 |
195 | panel_sizer = wx.BoxSizer(wx.HORIZONTAL)
196 | panel_sizer.Add(splitter_window, wx.EXPAND, wx.EXPAND)
197 | panel.SetSizer(panel_sizer)
198 |
199 | panel.Bind(wx.EVT_CHAR_HOOK, self.evt_key_pressed)
200 |
201 | info_win = wx.Window(splitter_window)
202 | icon_win = IconWindow(info_win, self)
203 | if icon_win.failed:
204 | info_win.Destroy()
205 | info_win = None
206 | icon_win = None
207 | self.info_win = info_win
208 | self.icon_win = icon_win
209 |
210 | if icon_win is None:
211 | self.info1 = None
212 | self.info2 = None
213 | splitter_window.Initialize(self.dirlist)
214 | else:
215 | self.icon_menu = icon_menu = wx.Menu()
216 | icon_win.append_menu_options(self, icon_menu)
217 | optionmenu.AppendSubMenu(icon_menu, "Icon Window")
218 | title_style = wx.ALIGN_RIGHT | wx.ST_NO_AUTORESIZE
219 |
220 | self.info1 = wx.StaticText(info_win, -1, "", style=title_style)
221 | self.info2 = wx.StaticText(info_win, -1, "", style=title_style)
222 | # self.info3 = wx.StaticText(panel, -1, "")
223 |
224 | info_sizer = wx.BoxSizer(wx.VERTICAL)
225 | info_sizer.Add(self.info1, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, border=4)
226 | info_sizer.Add(self.info2, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, border=4)
227 | # info_sizer.Add(self.info3, 0, wx.EXPAND)
228 | info_sizer.AddSpacer(5)
229 | info_sizer.Add(icon_win, 1, wx.EXPAND)
230 | info_win.SetSizer(info_sizer)
231 |
232 | splitter_window.SplitVertically(self.dirlist, info_win, int(self.Size.Width * 0.7))
233 |
234 | menubar = wx.MenuBar()
235 | menubar.Append(filemenu, "&File")
236 | menubar.Append(optionmenu, "&Options")
237 | self.SetMenuBar(menubar)
238 |
239 | self.Show(True)
240 |
241 | if self.mc == None:
242 | self.evt_cmd_open()
243 |
244 | def _close_mc(self):
245 | if self.mc != None:
246 | try:
247 | self.mc.close()
248 | except EnvironmentError as value:
249 | self.mc_error(value)
250 | self.mc = None
251 | if self.f != None:
252 | try:
253 | self.f.close()
254 | except EnvironmentError as value:
255 | self.mc_error(value)
256 | self.f = None
257 | self.mcname = None
258 |
259 | def refresh(self):
260 | try:
261 | self.dirlist.update(self.mc)
262 | except EnvironmentError as value:
263 | self.mc_error(value)
264 | self._close_mc()
265 | self.dirlist.update(None)
266 |
267 | mc = self.mc
268 |
269 | self.toolbar.EnableTool(self.ID_CMD_IMPORT, mc != None)
270 | self.toolbar.EnableTool(self.ID_CMD_EXPORT, False)
271 |
272 | if mc == None:
273 | status = "No memory card image"
274 | else:
275 | free = mc.get_free_space() // 1024
276 | limit = mc.get_allocatable_space() // 1024
277 | status = "%dK of %dK free" % (free, limit)
278 | self.statusbar.SetStatusText(status, 1)
279 |
280 | def open_mc(self, filename):
281 | self._close_mc()
282 | self.statusbar.SetStatusText("", 1)
283 | if self.icon_win != None:
284 | self.icon_win.load_icon(None, None)
285 |
286 | f = None
287 | try:
288 | f = open(filename, "r+b")
289 | mc = ps2mc.ps2mc(f)
290 | except EnvironmentError as value:
291 | if f != None:
292 | f.close()
293 | self.mc_error(value, filename)
294 | self.SetTitle(self.title)
295 | self.refresh()
296 | return
297 |
298 | self.f = f
299 | self.mc = mc
300 | self.mcname = filename
301 | self.SetTitle(filename + " - " + self.title)
302 | self.refresh()
303 |
304 | def delete_selected(self):
305 | mc = self.mc
306 | if mc == None:
307 | return
308 |
309 | selected = self.dirlist.selected
310 | dirtable = self.dirlist.dirtable
311 |
312 | dirnames = [dirtable[i].dirent[8].decode("ascii")
313 | for i in selected]
314 | if len(selected) == 1:
315 | title = dirtable[list(selected)[0]].title
316 | s = dirnames[0] + " (" + utils.single_title(title) + ")"
317 | else:
318 | s = ", ".join(dirnames)
319 | if len(s) > 200:
320 | s = s[:200] + "..."
321 | r = self.message_box("Are you sure you want to delete "
322 | + s + "?",
323 | "Delete Save File Confirmation",
324 | wx.YES_NO)
325 | if r != wx.YES:
326 | return
327 |
328 | for dn in dirnames:
329 | try:
330 | mc.rmdir("/" + dn)
331 | except EnvironmentError as value:
332 | self.mc_error(value, dn)
333 |
334 | mc.check()
335 | self.refresh()
336 |
337 | def evt_key_pressed(self, event):
338 | keycode = event.GetUnicodeKey()
339 |
340 | if keycode == wx.WXK_DELETE:
341 | self.delete_selected()
342 |
343 | def evt_menu_open(self, event):
344 | self.import_menu_item.Enable(self.mc is not None)
345 | selected = self.mc is not None and len(self.dirlist.selected) > 0
346 | self.export_menu_item.Enable(selected)
347 | self.delete_menu_item.Enable(selected)
348 | self.ascii_menu_item.Check(self.config.get_ascii())
349 | if self.icon_win is not None:
350 | self.icon_win.update_menu(self.icon_menu)
351 |
352 |
353 | def evt_dirlist_item_focused(self, event):
354 | if self.icon_win is None:
355 | return
356 |
357 | i = event.GetData()
358 | entry = self.dirlist.dirtable[i]
359 | self.info1.SetLabel(entry.title[0])
360 | self.info2.SetLabel(entry.title[1])
361 |
362 | icon_sys = entry.icon_sys
363 | mc = self.mc
364 |
365 | if mc is None or icon_sys is None:
366 | self.icon_win.load_icon(None, None)
367 | return
368 |
369 | try:
370 | mc.chdir("/" + entry.dirent[8].decode("ascii"))
371 | f = mc.open(icon_sys.icon_file_normal, "rb")
372 | try:
373 | icon = f.read()
374 | finally:
375 | f.close()
376 | except EnvironmentError as value:
377 | print("icon failed to load", value)
378 | self.icon_win.load_icon(None, None)
379 | return
380 |
381 | self.icon_win.load_icon(icon_sys, icon)
382 |
383 |
384 | def evt_dirlist_select(self, event):
385 | self.toolbar.EnableTool(self.ID_CMD_IMPORT, self.mc != None)
386 | self.toolbar.EnableTool(self.ID_CMD_EXPORT,
387 | len(self.dirlist.selected) > 0)
388 |
389 | def evt_cmd_open(self, event = None):
390 | fn = wx.FileSelector("Open Memory Card Image",
391 | self.config.get_memcard_dir(""),
392 | "Mcd001.ps2", "ps2", "Memory Card Image (*.ps2;*.mc2)|*.ps2;*.mc2",
393 | wx.FD_FILE_MUST_EXIST | wx.FD_OPEN,
394 | self)
395 | if fn == "":
396 | return
397 | self.open_mc(fn)
398 | if self.mc != None:
399 | dirname = os.path.dirname(fn)
400 | if os.path.isabs(dirname):
401 | self.config.set_memcard_dir(dirname)
402 |
403 | def evt_cmd_saveas(self, event):
404 | mc = self.mc
405 | if mc == None:
406 | return
407 |
408 | fn = wx.FileSelector("Save As",
409 | self.config.get_memcard_dir(""), Path(self.mcname).stem + '.ps2', "ps2",
410 | "PCSX2 Image|*.ps2"
411 | "|MemCard PRO2 Image|*.mc2",
412 | (wx.FD_OVERWRITE_PROMPT
413 | | wx.FD_SAVE),
414 | self)
415 | if fn == "":
416 | return
417 | try:
418 | filename = fn.lower()
419 | if filename.endswith(".mc2"):
420 | ecc = False
421 | else:
422 | ecc = True
423 | params = (ecc,
424 | mc.page_size,
425 | mc.pages_per_erase_block,
426 | mc.clusters_per_card * mc.pages_per_cluster
427 | )
428 |
429 | with open(fn, "w+b") as f:
430 | new_mc = ps2mc.ps2mc(f, True, params)
431 | for dir_tab_entry in self.dirlist.dirtable:
432 | dirname = dir_tab_entry.dirent[8].decode("ascii")
433 | new_mc.import_save_file(mc.export_save_file("/" + dirname), False)
434 |
435 | new_mc.flush()
436 | new_mc.close()
437 | except EnvironmentError as value:
438 | self.mc_error(value, fn)
439 | return
440 |
441 | def evt_cmd_export(self, event):
442 | mc = self.mc
443 | if mc == None:
444 | return
445 |
446 | selected = self.dirlist.selected
447 | dirtable = self.dirlist.dirtable
448 | sfiles = []
449 | for i in selected:
450 | dirname = dirtable[i].dirent[8].decode("ascii")
451 | try:
452 | sf = mc.export_save_file("/" + dirname)
453 | longname = ps2save.make_longname(dirname, sf)
454 | sfiles.append((dirname, sf, longname))
455 | except EnvironmentError as value:
456 | self.mc_error(value. dirname)
457 |
458 | if len(sfiles) == 0:
459 | return
460 |
461 | dir = self.config.get_savefile_dir("")
462 | if len(selected) == 1:
463 | (dirname, sf, longname) = sfiles[0]
464 | fn = wx.FileSelector("Export " + dirname,
465 | dir, longname, "psu",
466 | "EMS save file (.psu)|*.psu"
467 | "|MAXDrive save file (.max)"
468 | "|*.max",
469 | (wx.FD_OVERWRITE_PROMPT
470 | | wx.FD_SAVE),
471 | self)
472 | if fn == "":
473 | return
474 | try:
475 | f = open(fn, "wb")
476 | try:
477 | format = ps2save.format_for_filename(fn)
478 | format.save(sf, f)
479 | finally:
480 | f.close()
481 | except EnvironmentError as value:
482 | self.mc_error(value, fn)
483 | return
484 |
485 | dir = os.path.dirname(fn)
486 | if os.path.isabs(dir):
487 | self.config.set_savefile_dir(dir)
488 |
489 | self.message_box("Exported " + fn + " successfully.")
490 | return
491 |
492 | dir = wx.DirSelector("Export Save Files", dir, parent = self)
493 | if dir == "":
494 | return
495 | count = 0
496 | for (dirname, sf, longname) in sfiles:
497 | fn = os.path.join(dir, longname) + ".psu"
498 | try:
499 | f = open(fn, "wb")
500 | sf.save_ems(f)
501 | f.close()
502 | count += 1
503 | except EnvironmentError as value:
504 | self.mc_error(value, fn)
505 | if count > 0:
506 | if os.path.isabs(dir):
507 | self.config.set_savefile_dir(dir)
508 | self.message_box("Exported %d file(s) successfully."
509 | % count)
510 |
511 |
512 | def _do_import(self, fn):
513 | sf = ps2save.PS2SaveFile()
514 | f = open(fn, "rb")
515 | try:
516 | format = ps2save.poll_format(f)
517 | f.seek(0)
518 | if format is not None:
519 | format.load(sf, f)
520 | else:
521 | self.error_box(fn + ": Save file format not recognized.")
522 | return
523 | finally:
524 | f.close()
525 |
526 | if not self.mc.import_save_file(sf, True):
527 | self.error_box(fn + ": Save file already present.")
528 |
529 | def evt_cmd_import(self, event):
530 | if self.mc == None:
531 | return
532 |
533 | dir = self.config.get_savefile_dir("")
534 | fd = wx.FileDialog(self, "Import Save File", dir,
535 | wildcard = ("PS2 save files"
536 | " (.cbs;.psu;.psv;.max;.sps;.xps)"
537 | "|*.cbs;*.psu;.psv;*.max;*.sps;*.xps"
538 | "|All files|*.*"),
539 | style = (wx.FD_OPEN | wx.FD_MULTIPLE
540 | | wx.FD_FILE_MUST_EXIST))
541 | if fd == None:
542 | return
543 | r = fd.ShowModal()
544 | if r == wx.ID_CANCEL:
545 | return
546 |
547 | success = None
548 | for fn in fd.GetPaths():
549 | try:
550 | self._do_import(fn)
551 | success = fn
552 | except EnvironmentError as value:
553 | self.mc_error(value, fn)
554 |
555 | if success != None:
556 | dir = os.path.dirname(success)
557 | if os.path.isabs(dir):
558 | self.config.set_savefile_dir(dir)
559 | self.refresh()
560 |
561 | def evt_cmd_delete(self, event):
562 | self.delete_selected()
563 |
564 | def evt_cmd_ascii(self, event):
565 | self.config.set_ascii(not self.config.get_ascii())
566 | self.refresh()
567 |
568 | def evt_cmd_exit(self, event):
569 | self.Close(True)
570 |
571 | def evt_close(self, event):
572 | self._close_mc()
573 | self.Destroy()
574 |
575 | def run(filename = None):
576 | """Display a GUI for working with memory card images."""
577 |
578 | wx_app = wx.App()
579 | frame = GuiFrame(None, "mymc++", filename)
580 | return wx_app.MainLoop()
581 |
582 | if __name__ == "__main__":
583 | import gc
584 | gc.set_debug(gc.DEBUG_LEAK)
585 |
586 | run("test.ps2")
587 |
588 | gc.collect()
589 | for o in gc.garbage:
590 | print()
591 | print(o)
592 | if type(o) == ps2mc.ps2mc_file:
593 | for m in dir(o):
594 | print(m, getattr(o, m))
595 |
--------------------------------------------------------------------------------
/mymcplusplus/gui/icon_renderer.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | from ctypes import c_void_p
19 | import time
20 | from OpenGL.GL import *
21 | from .linalg import Matrix4x4, Vector3
22 |
23 | from .. import ps2icon
24 |
25 |
26 | _LIGHTS_COUNT = 3
27 |
28 | _glsl_icon_vert = b"""
29 | #version 150
30 |
31 | uniform mat4 mvp_matrix_uni;
32 | uniform mat4 transform_matrix_uni;
33 |
34 | in vec3 vertex_attr;
35 | in vec3 normal_attr;
36 | in vec2 uv_attr;
37 | in vec4 color_attr;
38 |
39 | out vec4 color_var;
40 | out vec2 uv_var;
41 | out vec3 normal_var;
42 |
43 | void main()
44 | {
45 | vec3 pos = (vertex_attr / 4096.0) * vec3(1.0, -1.0, -1.0);
46 | pos = (transform_matrix_uni * vec4(pos, 1.0)).xyz;
47 |
48 | color_var = color_attr;
49 |
50 | uv_var = uv_attr / 4096.0;
51 |
52 | vec3 normal = normalize(normal_attr / 4096.0) * vec3(1.0, -1.0, -1.0);
53 | normal = mat3(transform_matrix_uni) * normal;
54 | normal_var = normal;
55 |
56 | gl_Position = mvp_matrix_uni * vec4(pos, 1.0);
57 | }
58 | """
59 |
60 | _glsl_icon_frag = b"""
61 | #version 150
62 |
63 | #define LIGHTS_COUNT """ + str(_LIGHTS_COUNT).encode("ascii") + b"""
64 |
65 | uniform sampler2D texture_uni;
66 |
67 | uniform vec3 light_dir_uni[LIGHTS_COUNT];
68 | uniform vec3 light_color_uni[LIGHTS_COUNT];
69 | uniform vec3 ambient_light_color_uni;
70 |
71 | in vec4 color_var;
72 | in vec2 uv_var;
73 | in vec3 normal_var;
74 |
75 | out vec4 color_out;
76 |
77 | void main()
78 | {
79 | vec3 normal = normalize(normal_var);
80 |
81 | float alpha = color_var.a;
82 |
83 | vec3 tex_color = texture(texture_uni, uv_var).rgb;
84 | vec3 base_color = color_var.rgb * tex_color;
85 |
86 | vec3 color = base_color * ambient_light_color_uni;
87 |
88 | for(int i=0; i 0.0001 else v for v in light_dir_vectors]
166 |
167 | self.light_dirs_data = (GLfloat * (3 * _LIGHTS_COUNT))(*[f for v in light_dir_vectors for f in (v.x, v.y, v.z)])
168 | self.light_colors_data = (GLfloat * (3 * _LIGHTS_COUNT))(*[f for t in self.light_colors for f in t])
169 | self.ambient_color_data = (GLfloat * 3)(*self.ambient_light_color)
170 |
171 |
172 | def __init__(self, gl_context):
173 | self.failed = False
174 |
175 | self.context = gl_context
176 |
177 | self._icon = None
178 | self._icon_sys = None
179 | self._default_lighting_config = self.LightingConfig()
180 | self.lighting_config = None
181 | self._icon_uploaded = False
182 |
183 | self.camera_rotation = (0.0, 0.0)
184 | self.camera_distance = 5.0
185 | self.camera_offset = Vector3(0.0, 0.0, 0.0)
186 |
187 | self.background_color = (0.0, 0.0, 0.0)
188 |
189 | self._program = None
190 | self._vertex_vbo = None
191 | self._vertex_data = None
192 | self._normal_uv_vbo = None
193 | self._color_vbo = None
194 | self._vao = None
195 | self._texture = None
196 |
197 | self._background_program = None
198 | self._background_vertex_vbo = None
199 | self._background_color_vbo = None
200 | self._background_vao = None
201 |
202 | self._mvp_matrix_uni = -1
203 | self._transform_matrix_uni = -1
204 | self._light_dir_uni = -1
205 | self._light_color_uni = -1
206 | self._ambient_light_color_uni = -1
207 |
208 | self._gl_initialized = False
209 |
210 |
211 | def _initialize_gl(self):
212 | self._gl_initialized = True
213 |
214 | shader_vert = glCreateShader(GL_VERTEX_SHADER)
215 | glShaderSource(shader_vert, [_glsl_icon_vert])
216 | glCompileShader(shader_vert)
217 |
218 | shader_frag = glCreateShader(GL_FRAGMENT_SHADER)
219 | glShaderSource(shader_frag, [_glsl_icon_frag])
220 | glCompileShader(shader_frag)
221 |
222 | self._program = glCreateProgram()
223 | glBindAttribLocation(self._program, _ATTRIB_ICON_VERTEX, "vertex_attr")
224 | glBindAttribLocation(self._program, _ATTRIB_ICON_NORMAL, "normal_attr")
225 | glBindAttribLocation(self._program, _ATTRIB_ICON_UV, "uv_attr")
226 | glBindAttribLocation(self._program, _ATTRIB_ICON_COLOR, "color_attr")
227 | glAttachShader(self._program, shader_vert)
228 | glAttachShader(self._program, shader_frag)
229 | glLinkProgram(self._program)
230 |
231 | log = glGetProgramInfoLog(self._program)
232 | if log:
233 | print("Failed to compile shader:")
234 | print(log.decode("utf-8"))
235 | self.failed = True
236 | return
237 |
238 | shader_vert = glCreateShader(GL_VERTEX_SHADER)
239 | glShaderSource(shader_vert, [_glsl_background_vert])
240 | glCompileShader(shader_vert)
241 |
242 | shader_frag = glCreateShader(GL_FRAGMENT_SHADER)
243 | glShaderSource(shader_frag, [_glsl_background_frag])
244 | glCompileShader(shader_frag)
245 |
246 | self._background_program = glCreateProgram()
247 | glBindAttribLocation(self._background_program, _ATTRIB_BACKGROUND_VERTEX, "vertex_attr")
248 | glBindAttribLocation(self._background_program, _ATTRIB_BACKGROUND_COLOR, "color_attr")
249 | glAttachShader(self._background_program, shader_vert)
250 | glAttachShader(self._background_program, shader_frag)
251 | glLinkProgram(self._background_program)
252 |
253 | log = glGetProgramInfoLog(self._program)
254 | if log:
255 | print("Failed to compile shader:")
256 | print(log.decode("utf-8"))
257 | self.failed = True
258 | return
259 |
260 | self._mvp_matrix_uni = glGetUniformLocation(self._program, "mvp_matrix_uni")
261 | self._transform_matrix_uni = glGetUniformLocation(self._program, "transform_matrix_uni")
262 | self._light_dir_uni = glGetUniformLocation(self._program, "light_dir_uni")
263 | self._light_color_uni = glGetUniformLocation(self._program, "light_color_uni")
264 | self._ambient_light_color_uni = glGetUniformLocation(self._program, "ambient_light_color_uni")
265 |
266 | texture_uni = glGetUniformLocation(self._program, "texture_uni")
267 | glUseProgram(self._program)
268 | glUniform1i(texture_uni, _TEX_UNIT_ICON)
269 |
270 | self._vao = glGenVertexArrays(1)
271 | glBindVertexArray(self._vao)
272 |
273 | (self._vertex_vbo, self._normal_uv_vbo, self._color_vbo) = glGenBuffers(3)
274 |
275 | glBindBuffer(GL_ARRAY_BUFFER, self._vertex_vbo)
276 | glVertexAttribPointer(_ATTRIB_ICON_VERTEX, 3, GL_SHORT, GL_FALSE, 0, c_void_p(0))
277 |
278 | glBindBuffer(GL_ARRAY_BUFFER, self._normal_uv_vbo)
279 | glVertexAttribPointer(_ATTRIB_ICON_NORMAL, 3, GL_SHORT, GL_FALSE, 5 * 2, c_void_p(0))
280 | glVertexAttribPointer(_ATTRIB_ICON_UV, 2, GL_SHORT, GL_FALSE, 5 * 2, c_void_p(3 * 2))
281 |
282 | glBindBuffer(GL_ARRAY_BUFFER, self._color_vbo)
283 | glVertexAttribPointer(_ATTRIB_ICON_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, c_void_p(0))
284 |
285 | glEnableVertexAttribArray(_ATTRIB_ICON_VERTEX)
286 | glEnableVertexAttribArray(_ATTRIB_ICON_NORMAL)
287 | glEnableVertexAttribArray(_ATTRIB_ICON_UV)
288 | glEnableVertexAttribArray(_ATTRIB_ICON_COLOR)
289 |
290 | self._texture = glGenTextures(1)
291 | glBindTexture(GL_TEXTURE_2D, self._texture)
292 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
293 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
294 | glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
295 | glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
296 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE)
297 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB5_A1, ps2icon.TEXTURE_WIDTH, ps2icon.TEXTURE_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_SHORT_1_5_5_5_REV, c_void_p(0))
298 |
299 | self._background_vao = glGenVertexArrays(1)
300 | glBindVertexArray(self._background_vao)
301 |
302 | (self._background_vertex_vbo, self._background_color_vbo) = glGenBuffers(2)
303 |
304 | glBindBuffer(GL_ARRAY_BUFFER, self._background_vertex_vbo)
305 | background_vertex_data = [
306 | -1.0, 1.0,
307 | -1.0, -1.0,
308 | 1.0, 1.0,
309 | 1.0, -1.0
310 | ]
311 | glBufferData(GL_ARRAY_BUFFER, 8*4, (GLfloat * 8)(*background_vertex_data), GL_STATIC_DRAW)
312 | glVertexAttribPointer(_ATTRIB_BACKGROUND_VERTEX, 2, GL_FLOAT, GL_FALSE, 0, c_void_p(0))
313 |
314 | glBindBuffer(GL_ARRAY_BUFFER, self._background_color_vbo)
315 | glBufferData(GL_ARRAY_BUFFER, 4*4, c_void_p(0), GL_DYNAMIC_DRAW)
316 | glVertexAttribPointer(_ATTRIB_BACKGROUND_COLOR, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, c_void_p(0))
317 |
318 | glEnableVertexAttribArray(_ATTRIB_BACKGROUND_VERTEX)
319 | glEnableVertexAttribArray(_ATTRIB_BACKGROUND_COLOR)
320 |
321 |
322 | def calculate_camera(self):
323 | camera_pos = Vector3(0.0, 0.0, 1.0)
324 | camera_pos = camera_pos * self.camera_distance
325 |
326 | return camera_pos, Vector3(0.0, 0.0, 0.0), Vector3(0.0, 1.0, 0.0)
327 |
328 |
329 | def _write_animated_vertices(self, anim_time, vertex_data):
330 | duration = float(self._icon.frame_length)
331 | if duration > 0.0:
332 | anim_time = ((time.time() - anim_time) * 8.0) % duration
333 | else:
334 | anim_time = 0.0
335 |
336 | shape_values = {}
337 |
338 | for frame in self._icon.frames:
339 | keys = frame.keys
340 | if frame.shape_id == 0:
341 | k = ps2icon.Icon.Frame.Key()
342 | k.time = 0.0
343 | k.value = 1.0
344 | keys.append(k)
345 |
346 | last = None
347 | last_time = 0.0
348 | next = None
349 | next_time = 0.0
350 |
351 | for key in keys:
352 | t = key.time if key.time <= anim_time else key.time - duration
353 | if last is None or t > last_time:
354 | last = key
355 | last_time = t
356 |
357 | t = key.time if key.time >= anim_time else key.time + duration
358 | if next is None or t < next_time:
359 | next = key
360 | next_time = t
361 |
362 | if next_time > last_time:
363 | progress = (anim_time - last_time) / (next_time - last_time)
364 | else:
365 | progress = 0.0
366 |
367 | if last is not None and next is not None:
368 | shape_values[frame.shape_id] = (1.0 - progress) * last.value + progress * next.value
369 |
370 | if shape_values == {}:
371 | shape_values = {0: 1.0}
372 |
373 | sum = 0.0
374 | for shape_id, value in shape_values.items():
375 | sum += value
376 |
377 | if sum <= 0.0:
378 | shape_values = {0: 1.0}
379 | else:
380 | for shape_id in shape_values:
381 | shape_values[shape_id] /= sum
382 |
383 | for i in range(3 * self._icon.vertex_count):
384 | acc = 0.0
385 | for shape_id, value in shape_values.items():
386 | acc += value * self._icon.vertex_data[shape_id * 3 * self._icon.vertex_count + i]
387 | vertex_data[i] = int(acc)
388 |
389 |
390 | def _update_vertex_vbo(self, anim_time):
391 | if anim_time is not None:
392 | self._write_animated_vertices(anim_time, self._vertex_data)
393 | vertex_data = self._vertex_data
394 | else:
395 | vertex_data = self._icon.vertex_data
396 |
397 | glBindBuffer(GL_ARRAY_BUFFER, self._vertex_vbo)
398 | glBufferData(GL_ARRAY_BUFFER, self._icon.vertex_count * 3 * 2, vertex_data, GL_DYNAMIC_DRAW)
399 |
400 |
401 | def _upload_icon(self):
402 | if self._icon is None or self._icon_uploaded:
403 | return
404 |
405 | glBindBuffer(GL_ARRAY_BUFFER, self._vertex_vbo)
406 | glBufferData(GL_ARRAY_BUFFER,
407 | self._icon.vertex_count * 3 * 2,
408 | c_void_p(0),
409 | GL_DYNAMIC_DRAW)
410 |
411 | self._vertex_data = (GLshort * (self._icon.vertex_count * 3))()
412 |
413 | glBindBuffer(GL_ARRAY_BUFFER, self._normal_uv_vbo)
414 | glBufferData(GL_ARRAY_BUFFER,
415 | self._icon.vertex_count * 5 * 2,
416 | self._icon.normal_uv_data,
417 | GL_STATIC_DRAW)
418 |
419 | glBindBuffer(GL_ARRAY_BUFFER, self._color_vbo)
420 | glBufferData(GL_ARRAY_BUFFER,
421 | self._icon.vertex_count * 4,
422 | self._icon.color_data,
423 | GL_STATIC_DRAW)
424 |
425 | glBindTexture(GL_TEXTURE_2D, self._texture)
426 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB5_A1, ps2icon.TEXTURE_WIDTH, ps2icon.TEXTURE_HEIGHT, 0, GL_RGBA, GL_UNSIGNED_SHORT_1_5_5_5_REV, self._icon.texture)
427 | glGenerateMipmap(GL_TEXTURE_2D)
428 |
429 | bg_colors = self._icon_sys.bg_colors
430 | bg_alpha = self._icon_sys.background_transparency
431 | colors = [int((c / 0x80) * 255) for c in [
432 | *bg_colors[0][0:3], bg_alpha,
433 | *bg_colors[2][0:3], bg_alpha,
434 | *bg_colors[1][0:3], bg_alpha,
435 | *bg_colors[3][0:3], bg_alpha]]
436 |
437 | color_data = (GLubyte * (4*4))(*colors)
438 | glBindBuffer(GL_ARRAY_BUFFER, self._background_color_vbo)
439 | glBufferData(GL_ARRAY_BUFFER, 4*4, color_data, GL_DYNAMIC_DRAW)
440 |
441 | self._icon_uploaded = True
442 |
443 |
444 | def paint(self, canvas, animation_time):
445 | self.context.SetCurrent(canvas)
446 |
447 | if not self._gl_initialized:
448 | self._initialize_gl()
449 |
450 | if self.failed:
451 | return
452 |
453 | self._upload_icon()
454 |
455 | size = canvas.Size
456 |
457 | glViewport(0, 0, size.Width, size.Height)
458 |
459 | if self.background_color is not None:
460 | glClearColor(*self.background_color, 1.0)
461 | else:
462 | glClearColor(0.0, 0.0, 0.0, 1.0)
463 |
464 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
465 |
466 | if self.background_color is None and self._icon is not None:
467 | glUseProgram(self._background_program)
468 | glDisable(GL_DEPTH_TEST)
469 | glDepthMask(GL_FALSE)
470 |
471 | glEnable(GL_BLEND)
472 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
473 |
474 | glBindVertexArray(self._background_vao)
475 | glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
476 |
477 | if self._icon is not None:
478 | glUseProgram(self._program)
479 |
480 | glEnable(GL_DEPTH_TEST)
481 | glDepthMask(GL_TRUE)
482 |
483 | glActiveTexture(GL_TEXTURE0 + _TEX_UNIT_ICON)
484 | glBindTexture(GL_TEXTURE_2D, self._texture)
485 |
486 | modelview_matrix = Matrix4x4.look_at(*self.calculate_camera())
487 | aspect = 1.0
488 | if size.Height > 0 and size.Width > 0:
489 | aspect = float(size.Width) / float(size.Height)
490 | projection_matrix = Matrix4x4.perspective(80.0, aspect, 0.1, 500.0)
491 | glUniformMatrix4fv(self._mvp_matrix_uni, 1, GL_FALSE, (projection_matrix * modelview_matrix).ctypes_array)
492 |
493 | transform_matrix = Matrix4x4.translate(self.camera_offset * -1.0 + Vector3(0.0, 2.5, 0.0)) \
494 | * Matrix4x4.rotate_x(self.camera_rotation[1]) \
495 | * Matrix4x4.rotate_y(self.camera_rotation[0]) \
496 | * Matrix4x4.translate(Vector3(0.0, -2.5, 0.0))
497 | glUniformMatrix4fv(self._transform_matrix_uni, 1, GL_FALSE, transform_matrix.ctypes_array)
498 |
499 | lighting_config = self.lighting_config if self.lighting_config is not None else self._default_lighting_config
500 | glUniform3fv(self._light_dir_uni, 3, lighting_config.light_dirs_data)
501 | glUniform3fv(self._light_color_uni, 3, lighting_config.light_colors_data)
502 | glUniform3fv(self._ambient_light_color_uni, 1, lighting_config.ambient_color_data)
503 |
504 | glEnable(GL_CULL_FACE)
505 |
506 | if self._icon.enable_alpha:
507 | glEnable(GL_BLEND)
508 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
509 | else:
510 | glDisable(GL_BLEND)
511 |
512 | self._update_vertex_vbo(animation_time)
513 |
514 | glBindVertexArray(self._vao)
515 | glDrawArrays(GL_TRIANGLES, 0, self._icon.vertex_count)
516 |
517 | canvas.SwapBuffers()
518 |
519 |
520 | def set_icon(self, icon_sys, icon):
521 | if self.failed:
522 | return
523 |
524 | self._icon = icon
525 | self._icon_sys = icon_sys
526 |
527 | self._default_lighting_config = self.LightingConfig(icon_sys=icon_sys)
528 |
529 | self._icon_uploaded = False
530 |
--------------------------------------------------------------------------------
/mymcplusplus/gui/icon_window.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | import time
19 | import wx
20 | from wx import glcanvas
21 |
22 | from .. import ps2icon
23 | from ..save import ps2save
24 | from .icon_renderer import IconRenderer
25 | from .linalg import Vector3
26 |
27 |
28 | lighting_none = IconRenderer.LightingConfig(
29 | light_dirs = ((0.0, 0.0, 0.0),
30 | (0.0, 0.0, 0.0),
31 | (0.0, 0.0, 0.0)),
32 | light_colors = ((0.0, 0.0, 0.0),
33 | (0.0, 0.0, 0.0),
34 | (0.0, 0.0, 0.0)),
35 | ambient_light_color = (1.0, 1.0, 1.0))
36 |
37 | lighting_icon = None
38 |
39 | lighting_alt1 = IconRenderer.LightingConfig(
40 | light_dirs = ((1.0, -1.0, 2.0),
41 | (-1.0, 1.0, -2.0),
42 | (0.0, 1.0, 0.0)),
43 | light_colors = ((1.0, 1.0, 1.0),
44 | (1.0, 1.0, 1.0),
45 | (0.7, 0.7, 0.7)),
46 | ambient_light_color = (0.5, 0.5, 0.5))
47 |
48 | lighting_alt2 = IconRenderer.LightingConfig(
49 | light_dirs = ((1.0, -1.0, 2.0),
50 | (-1.0, 1.0, -2.0),
51 | (0.0, 4.0, 1.0)),
52 | light_colors = ((0.7, 0.7, 0.7),
53 | (0.7, 0.7, 0.7),
54 | (0.2, 0.2, 0.2)),
55 | ambient_light_color = (0.3, 0.3, 0.3))
56 |
57 |
58 | camera_default = [0, 4, -8]
59 | camera_high = [0, 7, -6]
60 | camera_near = [0, 3, -6]
61 | camera_flat = [0, 2, -7.5]
62 |
63 |
64 | class IconWindow(wx.Window):
65 | """Displays a save file's 3D icon."""
66 |
67 | ID_CMD_ANIMATE = 201
68 | ID_CMD_LIGHT_NONE = 202
69 | ID_CMD_LIGHT_ICON = 203
70 | ID_CMD_LIGHT_ALT1 = 204
71 | ID_CMD_LIGHT_ALT2 = 205
72 | ID_CMD_BACKGROUND_ICON = 206
73 | ID_CMD_BACKGROUND_BLACK = 207
74 | ID_CMD_BACKGROUND_WHITE = 208
75 | ID_CMD_CAMERA_RESET = 209
76 |
77 | light_options = {ID_CMD_LIGHT_NONE: lighting_none,
78 | ID_CMD_LIGHT_ICON: lighting_icon,
79 | ID_CMD_LIGHT_ALT1: lighting_alt1,
80 | ID_CMD_LIGHT_ALT2: lighting_alt2}
81 |
82 | background_options = {ID_CMD_BACKGROUND_ICON: None,
83 | ID_CMD_BACKGROUND_BLACK: (0.0, 0.0, 0.0),
84 | ID_CMD_BACKGROUND_WHITE: (1.0, 1.0, 1.0)}
85 |
86 |
87 | def append_menu_options(self, win, menu):
88 | menu.AppendCheckItem(IconWindow.ID_CMD_ANIMATE, "Animate Icon")
89 | menu.AppendSeparator()
90 | menu.AppendRadioItem(IconWindow.ID_CMD_LIGHT_NONE, "Lighting Off")
91 | menu.AppendRadioItem(IconWindow.ID_CMD_LIGHT_ICON, "Icon Lighting")
92 | menu.AppendRadioItem(IconWindow.ID_CMD_LIGHT_ALT1, "Alternate Lighting")
93 | menu.AppendRadioItem(IconWindow.ID_CMD_LIGHT_ALT2, "Alternate Lighting 2")
94 | menu.AppendSeparator()
95 | menu.AppendRadioItem(IconWindow.ID_CMD_BACKGROUND_ICON, "Icon Background")
96 | menu.AppendRadioItem(IconWindow.ID_CMD_BACKGROUND_BLACK, "Black Background")
97 | menu.AppendRadioItem(IconWindow.ID_CMD_BACKGROUND_WHITE, "White Background")
98 | menu.AppendSeparator()
99 | menu.Append(IconWindow.ID_CMD_CAMERA_RESET, "Reset Camera")
100 |
101 | win.Bind(wx.EVT_MENU, self.evt_menu_animate, id=IconWindow.ID_CMD_ANIMATE)
102 | win.Bind(wx.EVT_MENU, self.evt_menu_light, id=IconWindow.ID_CMD_LIGHT_NONE)
103 | win.Bind(wx.EVT_MENU, self.evt_menu_light, id=IconWindow.ID_CMD_LIGHT_ICON)
104 | win.Bind(wx.EVT_MENU, self.evt_menu_light, id=IconWindow.ID_CMD_LIGHT_ALT1)
105 | win.Bind(wx.EVT_MENU, self.evt_menu_light, id=IconWindow.ID_CMD_LIGHT_ALT2)
106 |
107 | win.Bind(wx.EVT_MENU, self.evt_menu_background, id=IconWindow.ID_CMD_BACKGROUND_ICON)
108 | win.Bind(wx.EVT_MENU, self.evt_menu_background, id=IconWindow.ID_CMD_BACKGROUND_BLACK)
109 | win.Bind(wx.EVT_MENU, self.evt_menu_background, id=IconWindow.ID_CMD_BACKGROUND_WHITE)
110 |
111 | win.Bind(wx.EVT_MENU, self.evt_menu_camera, id=IconWindow.ID_CMD_CAMERA_RESET)
112 |
113 | def __init__(self, parent, focus):
114 | super().__init__(parent)
115 | self.failed = False
116 |
117 | def make_attrib_list(samples):
118 | return [
119 | glcanvas.WX_GL_MAJOR_VERSION, 3,
120 | glcanvas.WX_GL_MINOR_VERSION, 2,
121 | glcanvas.WX_GL_CORE_PROFILE,
122 | glcanvas.WX_GL_RGBA,
123 | glcanvas.WX_GL_DOUBLEBUFFER,
124 | glcanvas.WX_GL_DEPTH_SIZE, 24,
125 | glcanvas.WX_GL_SAMPLES, samples
126 | ]
127 |
128 | attrib_list = None
129 | samples = 16
130 | while samples >= 1:
131 | al = make_attrib_list(samples)
132 | if glcanvas.GLCanvas.IsDisplaySupported(al):
133 | attrib_list = al
134 | break
135 | samples = samples // 2
136 | if attrib_list is None:
137 | print("Failed to initialize OpenGL. 3D Icon Display will not be available.")
138 | self.failed = True
139 | return
140 |
141 | self.sizer = wx.BoxSizer(wx.VERTICAL)
142 |
143 | self.canvas = glcanvas.GLCanvas(self, attribList=attrib_list)
144 | self.context = glcanvas.GLContext(self.canvas)
145 |
146 | self._renderer = IconRenderer(self.context)
147 |
148 | self._icon = None
149 | self._icon_sys = None
150 |
151 | self.canvas.Bind(wx.EVT_PAINT, self.paint)
152 |
153 | self.sizer.Add(self.canvas, wx.EXPAND, wx.EXPAND)
154 | self.SetSizer(self.sizer)
155 |
156 | #self.config = config = mymcsup.icon_config()
157 | #config.animate = True
158 |
159 | self._camera_dragging_rotation = False
160 | self._camera_dragging_offset = False
161 | self._last_mouse_pos = None
162 |
163 | self._animate_icon = False
164 | self._timer = None
165 | self._animation_start_time = time.time()
166 |
167 | self.lighting_id = self.ID_CMD_LIGHT_ICON
168 | self.background_id = self.ID_CMD_BACKGROUND_ICON
169 |
170 | self.menu = wx.Menu()
171 | self.append_menu_options(self, self.menu)
172 | self.set_lighting(self.lighting_id)
173 | self.set_background(self.ID_CMD_BACKGROUND_ICON)
174 | self.reset_camera()
175 | self.set_animate(False)
176 |
177 | self.Bind(wx.EVT_CONTEXT_MENU, self.evt_context_menu)
178 | self.canvas.Bind(wx.EVT_LEFT_DOWN, self.evt_mouse_left_down)
179 | self.canvas.Bind(wx.EVT_LEFT_UP, self.evt_mouse_left_up)
180 | self.canvas.Bind(wx.EVT_MIDDLE_DOWN, self.evt_mouse_middle_down)
181 | self.canvas.Bind(wx.EVT_MIDDLE_UP, self.evt_mouse_middle_up)
182 | self.canvas.Bind(wx.EVT_MOTION, self.evt_mouse_motion)
183 | self.canvas.Bind(wx.EVT_MOUSEWHEEL, self.evt_mouse_wheel)
184 |
185 |
186 | def paint(self, _):
187 | if self.failed:
188 | return
189 |
190 | anim_time = None
191 |
192 | if self._animate_icon:
193 | anim_time = (time.time() - self._animation_start_time) * 8.0
194 |
195 | self._renderer.paint(self.canvas, anim_time)
196 |
197 |
198 | def update_menu(self, menu):
199 | """Update the content menu according to the current config."""
200 |
201 | menu.Check(IconWindow.ID_CMD_ANIMATE, self._animate_icon)
202 | menu.Check(self.lighting_id, True)
203 | menu.Check(self.background_id, True)
204 |
205 |
206 | def load_icon(self, icon_sys, icon_data):
207 | """Pass the raw icon data to the support DLL for display."""
208 |
209 | if self.failed:
210 | return
211 |
212 | if icon_data is None:
213 | self._icon = None
214 | self._icon_sys = None
215 | else:
216 | try:
217 | self._icon = ps2icon.Icon(icon_data)
218 | self._icon_sys = icon_sys
219 | except ps2icon.Error as e:
220 | print("Failed to load icon.", e)
221 | self._icon = None
222 | self._icon_sys = None
223 |
224 | self._renderer.set_icon(self._icon_sys, self._icon)
225 | self.canvas.Refresh(eraseBackground=False)
226 |
227 |
228 | def set_lighting(self, id):
229 | self.lighting_id = id
230 | self._renderer.lighting_config = self.light_options[id]
231 | self.canvas.Refresh(eraseBackground=False)
232 |
233 |
234 | def set_background(self, id):
235 | self.background_id = id
236 | self._renderer.background_color = self.background_options[id]
237 | self.canvas.Refresh(eraseBackground=False)
238 |
239 |
240 | def set_animate(self, animate):
241 | self._animate_icon = animate
242 |
243 | if animate:
244 | if self._timer is None:
245 | self._animation_start_time = time.time()
246 | self._timer = wx.Timer(self)
247 | self._timer.Start(16)
248 | self.Bind(wx.EVT_TIMER, self.evt_timer, self._timer)
249 | elif self._timer is not None:
250 | self._timer.Stop()
251 | self._timer.Destroy()
252 | self._timer = None
253 |
254 | self.canvas.Refresh(eraseBackground=False)
255 |
256 |
257 | def reset_camera(self):
258 | self._renderer.camera_offset = Vector3(0.0, 2.5, 0.0)
259 | self._renderer.camera_rotation = (0.0, 0.0)
260 | self._renderer.camera_distance = 5.0
261 | self.canvas.Refresh(eraseBackground=False)
262 |
263 |
264 | def evt_timer(self, _):
265 | if self._icon is None or self._icon.animation_shapes <= 1:
266 | return
267 | self.canvas.Refresh(eraseBackground=False)
268 |
269 |
270 | def evt_mouse_left_down(self, event):
271 | self._camera_dragging_rotation = True
272 | self._last_mouse_pos = event.GetPosition()
273 |
274 |
275 | def evt_mouse_left_up(self, event):
276 | self._camera_dragging_rotation = False
277 |
278 |
279 | def evt_mouse_middle_down(self, event):
280 | self._camera_dragging_offset = True
281 | self._last_mouse_pos = event.GetPosition()
282 |
283 |
284 | def evt_mouse_middle_up(self, event):
285 | self._camera_dragging_offset = False
286 |
287 |
288 | def evt_mouse_motion(self, event):
289 | import math
290 |
291 | if self._camera_dragging_rotation:
292 | speed = (0.01, 0.01)
293 |
294 | delta = event.GetPosition()
295 | delta -= self._last_mouse_pos
296 |
297 | cam_rot = self._renderer.camera_rotation
298 | cam_rot = (
299 | cam_rot[0] - speed[0] * delta.x,
300 | max(-math.pi * 0.5 + 0.01,
301 | min(math.pi * 0.5 - 0.01,
302 | cam_rot[1] - speed[1] * delta.y))
303 | )
304 | self._renderer.camera_rotation = cam_rot
305 | elif self._camera_dragging_offset:
306 | speed = 0.01
307 |
308 | delta = event.GetPosition()
309 | delta -= self._last_mouse_pos
310 |
311 | (eye, center, up) = self._renderer.calculate_camera()
312 | dir = (center - eye).normalized
313 | right = up.cross(dir)
314 | up = dir.cross(right)
315 |
316 | offset = self._renderer.camera_offset
317 | offset = offset \
318 | + right * (speed * delta.x) \
319 | + up * (speed * delta.y)
320 | self._renderer.camera_offset = offset
321 | else:
322 | return
323 |
324 | self.canvas.Refresh(eraseBackground=False)
325 | self._last_mouse_pos = event.GetPosition()
326 |
327 |
328 | def evt_mouse_wheel(self, event):
329 | speed = 0.001
330 | rot = event.GetWheelRotation()
331 | self._renderer.camera_distance = max(0.1, self._renderer.camera_distance - rot * speed)
332 | self.canvas.Refresh(eraseBackground=False)
333 |
334 |
335 | def evt_context_menu(self, event):
336 | self.update_menu(self.menu)
337 | self.PopupMenu(self.menu)
338 |
339 |
340 | def evt_menu_animate(self, event):
341 | self.set_animate(not self._animate_icon)
342 |
343 |
344 | def evt_menu_light(self, event):
345 | self.set_lighting(event.GetId())
346 |
347 |
348 | def evt_menu_background(self, event):
349 | self.set_background(event.GetId())
350 |
351 |
352 | def evt_menu_camera(self, event):
353 | self.reset_camera()
354 |
--------------------------------------------------------------------------------
/mymcplusplus/gui/linalg.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | from array import array
19 | from math import tan, radians, sqrt, sin, cos
20 | from OpenGL import GL
21 |
22 |
23 | class Vector3:
24 | def __init__(self, x, y, z):
25 | self.v = array("f", [x, y, z])
26 |
27 |
28 | @property
29 | def x(self):
30 | return self.v[0]
31 |
32 |
33 | @property
34 | def y(self):
35 | return self.v[1]
36 |
37 |
38 | @property
39 | def z(self):
40 | return self.v[2]
41 |
42 |
43 | def __getitem__(self, item):
44 | return self.v[item]
45 |
46 |
47 | @property
48 | def length_sq(self):
49 | return self.v[0]*self.v[0] \
50 | + self.v[1]*self.v[1] \
51 | + self.v[2]*self.v[2]
52 |
53 |
54 | @property
55 | def length(self):
56 | return sqrt(self.length_sq)
57 |
58 |
59 | def __add__(self, other):
60 | return Vector3(self.v[0] + other[0],
61 | self.v[1] + other[1],
62 | self.v[2] + other[2])
63 |
64 |
65 | def __sub__(self, other):
66 | return Vector3(self.v[0] - other[0],
67 | self.v[1] - other[1],
68 | self.v[2] - other[2])
69 |
70 |
71 | def __mul__(self, other):
72 | if isinstance(other, Vector3):
73 | return Vector3(self.v[0] * other[0],
74 | self.v[1] * other[1],
75 | self.v[2] * other[2])
76 | else:
77 | return Vector3(self.v[0] * other,
78 | self.v[1] * other,
79 | self.v[2] * other)
80 |
81 |
82 | def __truediv__(self, other):
83 | if other is Vector3:
84 | return Vector3(self.v[0] / other[0],
85 | self.v[1] / other[1],
86 | self.v[2] / other[2])
87 | else:
88 | return Vector3(self.v[0] / other,
89 | self.v[1] / other,
90 | self.v[2] / other)
91 |
92 |
93 | @property
94 | def normalized(self):
95 | return self / self.length
96 |
97 |
98 | def dot(self, other):
99 | return self.v[0] * other[0] \
100 | + self.v[1] * other[1] \
101 | + self.v[2] * other[2]
102 |
103 |
104 | def cross(self, other):
105 | return Vector3(self.v[1] * other[2] - self.v[2] * other[1],
106 | self.v[2] * other[0] - self.v[0] * other[2],
107 | self.v[0] * other[1] - self.v[1] * other[0])
108 |
109 |
110 | class Matrix4x4:
111 | def __init__(self, data):
112 | if len(data) != 16:
113 | raise ValueError("data must have exactly length 16.")
114 | self.m = array("f", data)
115 |
116 |
117 | @property
118 | def ctypes_array(self):
119 | return (GL.GLfloat * 16)(*self.m)
120 |
121 |
122 | def __mul__(self, other):
123 | m = self.m
124 | n = other.m
125 | return Matrix4x4([
126 | m[0] * n[0] + m[4] * n[1] + m[8] * n[2] + m[12] * n[3],
127 | m[1] * n[0] + m[5] * n[1] + m[9] * n[2] + m[13] * n[3],
128 | m[2] * n[0] + m[6] * n[1] + m[10] * n[2] + m[14] * n[3],
129 | m[3] * n[0] + m[7] * n[1] + m[11] * n[2] + m[15] * n[3],
130 | m[0] * n[4] + m[4] * n[5] + m[8] * n[6] + m[12] * n[7],
131 | m[1] * n[4] + m[5] * n[5] + m[9] * n[6] + m[13] * n[7],
132 | m[2] * n[4] + m[6] * n[5] + m[10] * n[6] + m[14] * n[7],
133 | m[3] * n[4] + m[7] * n[5] + m[11] * n[6] + m[15] * n[7],
134 | m[0] * n[8] + m[4] * n[9] + m[8] * n[10] + m[12] * n[11],
135 | m[1] * n[8] + m[5] * n[9] + m[9] * n[10] + m[13] * n[11],
136 | m[2] * n[8] + m[6] * n[9] + m[10] * n[10] + m[14] * n[11],
137 | m[3] * n[8] + m[7] * n[9] + m[11] * n[10] + m[15] * n[11],
138 | m[0] * n[12] + m[4] * n[13] + m[8] * n[14] + m[12] * n[15],
139 | m[1] * n[12] + m[5] * n[13] + m[9] * n[14] + m[13] * n[15],
140 | m[2] * n[12] + m[6] * n[13] + m[10] * n[14] + m[14] * n[15],
141 | m[3] * n[12] + m[7] * n[13] + m[11] * n[14] + m[15] * n[15]
142 | ])
143 |
144 |
145 | @classmethod
146 | def translate(cls, v):
147 | return Matrix4x4([1.0, 0.0, 0.0, 0.0,
148 | 0.0, 1.0, 0.0, 0.0,
149 | 0.0, 0.0, 1.0, 0.0,
150 | v.x, v.y, v.z, 1.0])
151 |
152 |
153 | @classmethod
154 | def rotate_x(cls, rad):
155 | s = sin(rad)
156 | c = cos(rad)
157 | return Matrix4x4([1.0, 0.0, 0.0, 0.0,
158 | 0.0, c, -s, 0.0,
159 | 0.0, s, c, 0.0,
160 | 0.0, 0.0, 0.0, 1.0])
161 |
162 |
163 | @classmethod
164 | def rotate_y(cls, rad):
165 | s = sin(rad)
166 | c = cos(rad)
167 | return Matrix4x4([ c, 0.0, s, 0.0,
168 | 0.0, 1.0, 0.0, 0.0,
169 | -s, 0.0, c, 0.0,
170 | 0.0, 0.0, 0.0, 1.0])
171 |
172 |
173 | @classmethod
174 | def rotate_z(cls, rad):
175 | s = sin(rad)
176 | c = cos(rad)
177 | return Matrix4x4([ c, -s, 0.0, 0.0,
178 | s, c, 0.0, 0.0,
179 | 0.0, 0.0, 1.0, 0.0,
180 | 0.0, 0.0, 0.0, 1.0])
181 |
182 |
183 | @classmethod
184 | def perspective(cls, fovy, aspect, near, far):
185 | f = 1.0 / tan(radians(fovy) / 2.0)
186 | d = near - far
187 | return cls([
188 | f / aspect, 0.0, 0.0, 0.0,
189 | 0.0, f, 0.0, 0.0,
190 | 0.0, 0.0, (far+near) / d, -1.0,
191 | 0.0, 0.0, 2.0*far*near / d, 0.0])
192 |
193 |
194 | @classmethod
195 | def look_at(cls, eye, center, up):
196 | f = (center - eye).normalized
197 | s = f.cross(up.normalized).normalized
198 | u = s.cross(f)
199 | return cls([
200 | s[0], u[0], -f[0], 0.0,
201 | s[1], u[1], -f[1], 0.0,
202 | s[2], u[2], -f[2], 0.0,
203 | 0.0, 0.0, 0.0, 1.0]) * cls.translate(eye * -1.0)
204 |
--------------------------------------------------------------------------------
/mymcplusplus/gui/resources.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | import base64
19 |
20 | resources = {
21 | "open.png": base64.b64decode(
22 | b"iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz"
23 | b"AAA7DgAAOw4BzLahgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAH2SURB"
24 | b"VHic7dw9ThtRFIbhF2IXIKXKAqiQgou0rICshIWkAwlIQk/LGgjeAiVR0qKUScdfAZagsKVEIDw3"
25 | b"eIbPw7yPdLvDeO75fHxNMQZJkiRJkiRJkqTHBsAX4DtwCdw1vD69yK5aoA/sAyOab7ohPNADjnj5"
26 | b"xhvCxC7Z5nc6hFXghnzzOxvCFvmmP1zbje54zpyQb3inJ+E3+Wa/2klYKKi5ApabvpEWuwLOgCFw"
27 | b"APxo4gXS7/S2rBHj/5X6z+r0E67nYGNtW0MKQ1gsKdJ/2wB2SgpLzoBrYGmm2+mmEfAB+DmtqGQC"
28 | b"SkLSYz1gs6qoJIC72e+lsz5WFTgBzVqpKujV8CJdD2jaJ8Tbqj92AsL8GhpmAGEGEGYAYR7CYU5A"
29 | b"mAGEGUCYAYR5CIc5AWEGEGYAYQYQ5iEc5gSEGUCYAYR5BoQ5AWEGEGYAYQYQ5iEc5gSEGUCYAYQZ"
30 | b"QJiHcJgTEGYAYQYQZgBhHsJhTkCYAYQZQJgBhJU8J3zB9OddfZL+aedVBSUT8KuGG+mqyt6VBDCs"
31 | b"4Ua66lsdFxmQ+bXctq9b4H1Vc99UFQB/gHfAekGt/toHDuu6WJ/xR1H6XdWWdUzNvxvH5IJfGY9W"
32 | b"eoPzum6BzzTQ/H8NgD3glPFX1PSm0+ti0otdYG2GvkqSJEmSJEmSpFfqHosQ5fEZtBzCAAAAAElF"
33 | b"TkSuQmCC"
34 | ),
35 | "import.png": base64.b64decode(
36 | b"iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz"
37 | b"AAA7DgAAOw4BzLahgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAJRSURB"
38 | b"VHic7dwxihRRFIXhXwMnNnAVowtwdAua6XY0NNJAFNyCS1DQRegKDEbQSFswGWmDQRjE6vF13ffO"
39 | b"va/PB5U11K37VzVD99BgZmZmZmaH5op6AGCrHqCznTu+OmoK+zcHEHMAMQcQcwAxBxBzALFDCvAV"
40 | b"eAjcBz6LZ0llO+B4Ddy4cM7rwKtB506v58V/AR7sOPc94LTzDOmNuuuX9H4a0ht91y/p9TSkp7jr"
41 | b"l/R4GtJT3vVLIp+G9NR3/ZKop2Enfx/Qn78PyMwBxBxAzAHEHEDMAcQcQMwBxBxAzAHEHEDMAcQc"
42 | b"QMwBxBxAzAHEHEDMAcQcQKxHgCeNr990mOGgbWmLcAJ8I/4forIcw/05sSOIA7RGuM2cEYb7e4C1"
43 | b"EbJLH2BthOxKBFgTIbsyAfaNkF2pAK0RTsKni1cuQGuE7EoGmClC2QCzROgeoGWh+xzVI5QPUD3C"
44 | b"FAEqR5gmQNUIqwJk+0Lmp3qAjEbd/Y9GXVCwKd6Cqi4fJghQefkwIED0QDMtHwoHmGH5UDTALMuH"
45 | b"ggFaln83fLp4pQK0LP8O8D18unhlAuyzfMkFNCoRYN/lO0DAQGuW7wArB1q7/BmO4bz8BAG8fGGA"
46 | b"x42v36BfkiyAfzOuP/9mXGYOIOYAYhkC/FAP0NGln2VlCPBJPUBHl15bhgBv1QN09EY9wP+4BfxC"
47 | b"//d69HEGHAfuqauX6BcWfTwP3VBnR8B79EuLOt4B10I3NMAR8ILzR1e9wH2PM87v/HLLv+gm8Az4"
48 | b"SI3PiTbAB+Aphd7zzczMzMxM5zfAhr2E0AA5NAAAAABJRU5ErkJggg=="
49 | ),
50 | "export.png": base64.b64decode(
51 | b"iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz"
52 | b"AAA7DgAAOw4BzLahgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAJTSURB"
53 | b"VHic7dwvi1RhGIbxyz9gUBCbxS9gEsymbTaDUWx2k9HZZjcpKCtGQbDYFL+EaBQxGDY6CrLjGnbD"
54 | b"Ih7nnfE855495/rBsBuG2YfnOi87Z8KAJEmSpKk50fCcefkUWWeTf/xk8o/LAHEGCDNAmAHCDBBm"
55 | b"gLBNCrAL3AJuAl/DswxmUwK8BK4e/nx9+PvT6EQDSd8J7wJ3OVj831wHHgIXC2eY7J3w0au+y+hP"
56 | b"Q+IELLvqu1SdhkmdgJarvssoT8NQJ2Ddq75Ln6dh9Cfgf676LqM5Del3QWn7wLnkAJtyH5CySA8w"
57 | b"9QBxBggzQJgBwgwQZoAwA4QZIMwAYQYIM0CYAcIMEGaAMAOEGSDMAGEGCDPAcg8qX9wAy92jMIIB"
58 | b"2pRFMEC70pPwL/Mlj7Hb/+MxeAQDhCMYIBzBAOEIBghHMEA4ggHCEQwQjjC2AKssdJ1H7xEMEI5g"
59 | b"gHAEAxRG8MO4Wj/6eBFPwHqP+30NbIDg8sEA0eXD+AKsKrp8MEB0+WCA6PLBANHlgwGiywcDRJcP"
60 | b"BoguHwxQuvzTPbzGcY6wB5xf4fkzYLvPAfwwrt2MnpcPBmg1o2D5YIAW2xQtH/zOuFX/B/TOExBm"
61 | b"gDADhLUE+FY+Rc739AAtAb6UT5HzKT1AS4A35VPkvE0P0BLgGfCrepCAPeB5eoiWAO+BJ9WDBDwG"
62 | b"PqaHaLkRAzgDvAKuFc4ypHfADeBneA5ONT5vAbwALgBXOL5vXxfAI+AOG7B8aD8BR10GbgNbwCXC"
63 | b"X//eYA585uDNxA7wITqNJEmSpMn7DU+aJ9jsfpbEAAAAAElFTkSuQmCC"
64 | ),
65 | "icon.png": base64.b64decode(
66 | b"iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAABHNCSVQICAgIfAhkiAAAAAlwSFlz"
67 | b"AAA7DgAAOw4BzLahgwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAApSSURB"
68 | b"VHic5Z3rbxTXGcZ/Z7mIrCEQMESkjVRR2RhKhGhwRQhOG+JQm9AKCgGCgaY4TRHhAx/4B/ohaoRa"
69 | b"qUR8Qb1I0CBIGkNIhbmljajJVpikiKRgYzehKq0QNikkBgKO8dMPs2uM7d2d2TlzWfJII693z7zv"
70 | b"meeZc+bc5j2GmELSWKAcmApUpD8/DJSkjwfSfwGuA1fSf68DF4A2oBU4B7QZYz4LM/9uYaLOQAaS"
71 | b"RgNzgOr0MQtIWHTxCfBO5jDGXLFouzgh6QFJP5PUJKlH4aFH0l8lvShpXNQ8hApJCUmLJP1R0hch"
72 | b"kp4NX0h6Q9IzkmyWuHhBDvHPSjoTJdt5cEbSWknDo+bLGiQNl/RTSR9HSq03/FPSCyp2ISTNk/Rh"
73 | b"tFz6wmlJj0fNo2dIGi9pq6Tb0fJnBb2SdkqaFARX1puhkpYAvwXG27YdMT4F6o0x+20atfbUl1PX"
74 | b"vwI0cO+RDzABeEvSdkkjbRm1UgIkTQFeB2bbsFcEaAZWGmPO+zXkWwBJlcABYKJfW0WG/wGLjDF/"
75 | b"82PEVxUk6Sngz8SI/FQqxeLFi7l27VrQrsYDRyXVBO1oSEhaJak7knZJFrz33nuaNGmSksmkqqur"
76 | b"1dXVFYbbW5Kei4L8WDUx+5OfOUIU4bbCEkHSU5JuhnFVbjEU+RGI0K2gqyNJlZJCuRq3yEV+BCJc"
77 | b"l/RYUORPkdQZxlW4hRvyIxChQ9I33PLqqhUkaQSwCygtUD/rSKVSLFmyxHVrx2t6H5gIvCGXnTW3"
78 | b"zdBf4sxWxQKFkhmiCJXAL9wkzNsRk7QIeNtN2jBgg8S5c+eyb98+Ro8ebTFngyBgqTFmX65EOUmV"
79 | b"NBFnYjsWYzs27+CQRPgUqDDGXM6WIF8VtIV7kPwg7GXBBOCVXAmylgA5ExFNudKEhSDJCqEkCHg8"
80 | b"25jRkOTKmYp7H5gZVK7cIow7NQQRPgK+bYzpGfhDtiroJ3xFyA/JzyPA2qF+GFQCJA0DWoCyoHLj"
81 | b"BiE2GfsQcEn4GOeBfFcpGKoEPMdXkPwQ/H4TeHbgl3eVAEkG+BCYEUQO3CAq8vsjwJJwFnjEGNOb"
82 | b"+WJgCVhIhOQ3NTVZmUyZNm0alZWVjBo1qqDzU6kUy5Yt4/r1677yMQSmA3eNmA4sAW8wRDEJAzbv"
83 | b"/JEjR7Jr1y4WLFhAe3s7p06d6jtOnz7NjRs3XNkJqCS8boxZmfmnTwA5y8EvAvfZ9OYGTU1NLF26"
84 | b"1OodN2rUKPbs2cPTTz991/c9PT20tLT0CbJ3714uX87aUaWqqoqGhgZKSkqypvGIm8BkY8zVu76V"
85 | b"s0o5dHgZUvZ6jBs3TgcOHMjpf9OmTVEMZddneO//DFhtS2K3CPqB293dTV1dHY2NjVnTzJo1K6+d"
86 | b"APK5JvMhAX3VTzAzOVkQVmsnnwhuBADr+X1c0v1wpwR8Dxhmw7IbhN3UzCXCtGnTuO8+d489i/ke"
87 | b"DlTBHQGe9GvRLaJq52cTYfjw4cyY4b7lbTH/T8IdAeb7teYGUXeysongthrKwNJ1zAdIpOv/wDtf"
88 | b"x48fz9vJqqur48EHHww0H93d3axZs4ajR4/2fedVALDSWZspaUwC5zXQQMf8U6lU3nZ+fX0927dv"
89 | b"5/Dhw0yePDnI7HDz5k2WL1/eVxIKEQB899wTQHlGgMDgprjW19ezdetWjDGUlZVx8ODBwEXoXx15"
90 | b"eRAPhM/qaGqgAnglP4OwRThy5IinB/FA+BAhOAHc1PkbNmwYRH4GZWVlNDY2hvZM8NswSKVSrF07"
91 | b"5JxLLkxNAF/z5TlLZtzU+Vu2bBmS/AzKy8tDeya0tLT4spFMJtm0aZPX0x5OAPf78jwAhVY72RBW"
92 | b"deQHyWSShoYGnnjiCa+njk4A1sZabZOfQZxF8EE+wJgEMMZGRoIiP4M4iuCTfIAxRtItwNdbf0GT"
93 | b"3x/t7e3U1tZy8eJFX3b8wgL5ALd8v6YaJvkQj5JgiXzA6Y0V3P4Km/wMohTBJvlAVwLoKuTMqMjP"
94 | b"IAoRLJMP0GUk/QP4lpezmpubWbRoUc52fkVFBSdPniSRCDYET1tbGzU1NVy6dClQPyUlJTQ0NFBV"
95 | b"VWXT7IcJ4HMvZ1y5coUVK1bkHQVsbW1l48aN9Pb25kznF+Xl5Rw6dCjQHnNA5EO6CvqvlzO2bdtG"
96 | b"R0eHq7Q7duwoehECJB/gPwmcqIKusX+/t2AhxSxCwOQDnPMswPnz3uNTFKMIIZAPhQgwYsSIgjzt"
97 | b"2LGDl156KRQR/A7gJZNJ3nzzzaDJh7QArThvcbjCzJmFvzawc+fOUEqCn6HskpIS9u7da7OpmQ29"
98 | b"QFvCGPM5zhscrrB6tb/1W3GujkKqdjI4bYzpyjTS/+L2rFWrVjF37lxfnuMoQsjkgxPmp29Zyrtu"
99 | b"zxo2bBi7d++moqLCl/ewRCgrK6O6ujpnmgjIh/6cSxorj6GDOzo6NHv2bN8LaNevX6/bt4OJfNPb"
100 | b"26vNmzfn9F9aWqpjx44F4j8HvlR6aWJ/ETznIs4ixJh8SRpc5csJZO0ZcRQh5uRL0rqhBBgr6UYh"
101 | b"1uIkQhGQ/4WyRWyX9HqhVuMgQhGQL0m7sz6WJS30YzlKEYqEfEmqzSWAkfSRH+tRiFBE5J9Rvj0K"
102 | b"JNX59RKmCEVEviStzEl+WoBhktr8egpDhCIjv11OGIj8kFRvw2OQIhQZ+ZL0vCvy+5WCUza8BiFC"
103 | b"EZL/gdze/f1E+I4sRce1KUJPT0+xkX9bOWKJ5osZ9ztgcK+tAHR0dFBbW0tra6svOxUVFTltZMbz"
104 | b"582b58uPRfzGGPNith/zCVCKM2EzwUZOOjs7WbhwIWfPnrVhbhCS9tft+MVlnBhBn2ZLkLNNmo72"
105 | b"92M8zJjlwsSJE2lsbGT69Ok2zN2FGJIv4IVc5IOLwK3GmAPAr23lKggRYkg+wK/c7Dfjas2gnNDF"
106 | b"x7AYzsBWdRRT8puBKmNMd76ErhdtyglI3YzF3TL8ihBT8juASmPMv90kdr1w0xjzL5yIWgUt5h0K"
107 | b"fqqjmJLfBdS6JR887iFjjHkfWAzc8pixrChEhJiS3w0sM8b8PXBPklbK8hYmbjtrMetkZXBb0orA"
108 | b"iR8gwo9keTvazs5OVVdXZyV/ypQpOnHihE2XNnBLbkY5AxJhvqTPbF5NT0+PXnvtNdXU1Oihhx5S"
109 | b"aWmp5syZo5dffllXr1616coGuiR93w+HNjZyexRoBALZ7DLGuAQ8Y4z5wI8R36+vpDMwG0j5tVVE"
110 | b"OAk85pd8sLSZpzHmAvBd4Oc4i07vVQh4FZhnYx9JCGY72x8Cv8fSAF6McBlYZ4z5k02j1t+gM8a8"
111 | b"jRP8+1XujdIg4A/AdNvkBw5Jj0pqjrSd4g+nJPlbCh415ExvrpOFif4QcU7S8/I6jRhnSEpI+oGc"
112 | b"+dG44iNJa+Vs4XJvQs7ir1pJe1TgWlTLuCFpt5w8Rb5hUaiQsyC4XtK7ctbMh4Uv0z7XyQnbGRli"
113 | b"o7ikEpwJn+r0MQu7rbRPgHfSx9FB4eMjQmwEGAhJY3ACCpYDFenPX8eJ8DUaGMedaF/XgKvpv9eA"
114 | b"C0AbzoKCc0CbMcbaPIZN/B/mIYMlXZD0tgAAAABJRU5ErkJggg=="
115 | )
116 | }
117 |
--------------------------------------------------------------------------------
/mymcplusplus/gui/utils.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | import io
19 | import wx
20 | import struct
21 |
22 | from . import resources
23 |
24 | def single_title(title):
25 | """Convert the two parts of an icon.sys title into one string."""
26 |
27 | title = title[0] + " " + title[1]
28 | return " ".join(title.split())
29 |
30 | def _get_icon_resource_as_images(name):
31 | ico = resources.resources[name]
32 | images = []
33 | f = io.BytesIO(ico)
34 | count = struct.unpack("= size[0] and sz[1] >= size[1]:
67 | if ((best_size[0] < size[0] or best_size[1] < size[1])
68 | or sz[0] * sz[1] < best_size[0] * best_size[1]):
69 | best = img
70 | best_size = sz
71 | elif sz[0] * sz[1] > best_size[0] * best_size[1]:
72 | best = img
73 | best_size = sz
74 | img = best.Rescale(size[0], size[1], wx.IMAGE_QUALITY_HIGH)
75 | return wx.Bitmap(img)
76 |
77 |
78 | def get_png_resource_bmp(name, size=None):
79 | img = _get_png_resource(name)
80 | if size is not None:
81 | img = img.Rescale(size[0], size[1], wx.IMAGE_QUALITY_HIGH)
82 | return wx.Bitmap(img)
83 |
84 |
--------------------------------------------------------------------------------
/mymcplusplus/ps2icon.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | """Interface for working with PS2 icons."""
19 |
20 | import struct
21 |
22 |
23 | class Error(Exception):
24 | """Base for all exceptions specific to this module."""
25 | pass
26 |
27 | class Corrupt(Error):
28 | """Corrupt icon file."""
29 |
30 | def __init__(self, msg):
31 | super().__init__(self, "Corrupt icon: " + msg)
32 |
33 | class FileTooSmall(Error):
34 | """Corrupt icon file."""
35 |
36 | def __init__(self):
37 | super().__init__(self, "Icon file too small.")
38 |
39 |
40 | _PS2_ICON_MAGIC = 0x010000
41 |
42 | _FIXED_POINT_FACTOR = 4096.0
43 |
44 |
45 | TEXTURE_WIDTH = 128
46 | TEXTURE_HEIGHT = 128
47 |
48 | _TEXTURE_SIZE = TEXTURE_WIDTH * TEXTURE_HEIGHT * 2
49 |
50 | _icon_hdr_struct = struct.Struct(" offset:
100 | print("Warning: Icon file larger than expected.")
101 |
102 |
103 | def __load_header(self, data, length, offset):
104 | if length < _icon_hdr_struct.size:
105 | raise FileTooSmall()
106 |
107 | (magic,
108 | self.animation_shapes,
109 | self.tex_type,
110 | something,
111 | self.vertex_count) = _icon_hdr_struct.unpack_from(data, offset)
112 |
113 | if magic != _PS2_ICON_MAGIC:
114 | raise Corrupt("Invalid magic.")
115 |
116 | return offset + _icon_hdr_struct.size
117 |
118 |
119 | def __load_vertex_data(self, data, length, offset):
120 | stride = _vertex_coords_struct.size * self.animation_shapes \
121 | + _normal_uv_color_struct.size
122 |
123 | if length < offset + self.vertex_count * stride:
124 | raise FileTooSmall()
125 |
126 | self.vertex_data = (int16_t * (self.animation_shapes * 3 * self.vertex_count))()
127 | self.normal_uv_data = (int16_t * (5 * self.vertex_count))()
128 | self.color_data = (uint8_t * (4 * self.vertex_count))()
129 |
130 | for i in range(self.vertex_count):
131 | for s in range(self.animation_shapes):
132 | vertex_offset = (s * self.vertex_count + i) * 3
133 | (self.vertex_data[vertex_offset],
134 | self.vertex_data[vertex_offset+1],
135 | self.vertex_data[vertex_offset+2],
136 | _) = _vertex_coords_struct.unpack_from(data, offset)
137 | offset += _vertex_coords_struct.size
138 |
139 | (self.normal_uv_data[i*5],
140 | self.normal_uv_data[i*5+1],
141 | self.normal_uv_data[i*5+2],
142 | _,
143 | self.normal_uv_data[i*5+3],
144 | self.normal_uv_data[i*5+4],
145 | self.color_data[i*4],
146 | self.color_data[i*4+1],
147 | self.color_data[i*4+2],
148 | self.color_data[i*4+3]) = _normal_uv_color_struct.unpack_from(data, offset)
149 |
150 | # This is just a hack to check if every alpha value is 0, which is the case for THPS3 for example.
151 | # Alpha will then be assumed to be 1 for all vertices when rendering, otherwise nothing will be visible.
152 | # TODO: There is probably another way to render these icons correctly.
153 | if self.color_data[i*4+3] > 0:
154 | self.enable_alpha = True
155 |
156 | offset += _normal_uv_color_struct.size
157 |
158 | return offset
159 |
160 |
161 | def __load_animation_data(self, data, length, offset):
162 | if length < offset + _anim_hdr_struct.size:
163 | raise FileTooSmall()
164 |
165 | (anim_id_tag,
166 | self.frame_length,
167 | self.anim_speed,
168 | self.play_offset,
169 | self.frame_count) = _anim_hdr_struct.unpack_from(data, offset)
170 |
171 | offset += _anim_hdr_struct.size
172 |
173 | if anim_id_tag != 0x01:
174 | raise Corrupt("Invalid ID tag in animation header: {:#x}".format(anim_id_tag))
175 |
176 | self.frames = []
177 | for i in range(self.frame_count):
178 | if length < offset + _frame_data_struct.size:
179 | raise FileTooSmall()
180 |
181 | frame = self.Frame()
182 | (frame.shape_id,
183 | key_count,
184 | _,
185 | _) = _frame_data_struct.unpack_from(data, offset)
186 |
187 | key_count -= 1
188 |
189 | offset += _frame_data_struct.size
190 |
191 | for k in range(key_count):
192 | if length < offset + _frame_key_struct.size:
193 | raise FileTooSmall()
194 |
195 | key = self.Frame.Key()
196 | (key.time,
197 | key.value) = _frame_key_struct.unpack_from(data, offset)
198 | frame.keys.append(key)
199 |
200 | offset += _frame_key_struct.size
201 |
202 | self.frames.append(frame)
203 |
204 | return offset
205 |
206 |
207 | def __load_texture(self, data, length, offset):
208 | # No textures to load. This may be the case when vertices are colored without a texture.
209 | if offset == length:
210 | self.texture = [0xFFFF]
211 | return offset
212 |
213 | if self.tex_type == 0x7:
214 | return self.__load_texture_uncompressed(data, length, offset)
215 | else:
216 | return self.__load_texture_compressed(data, length, offset)
217 |
218 |
219 | def __load_texture_uncompressed(self, data, length, offset):
220 | if length < offset + _TEXTURE_SIZE:
221 | raise FileTooSmall()
222 |
223 | self.texture = data[offset:(offset + _TEXTURE_SIZE)]
224 |
225 | return offset + _TEXTURE_SIZE
226 |
227 |
228 | def __load_texture_compressed(self, data, length, offset):
229 | if length < offset + 4:
230 | raise FileTooSmall()
231 |
232 | compressed_size = _texture_compressed_size_struct.unpack_from(data, offset)[0]
233 | offset += 4
234 |
235 | if length < offset + compressed_size:
236 | raise FileTooSmall()
237 |
238 | if compressed_size % 2 != 0:
239 | raise Corrupt("Compressed data size is odd.")
240 |
241 | texture_buf = bytearray(_TEXTURE_SIZE)
242 |
243 | tex_offset = 0
244 | rle_offset = 0
245 |
246 | while rle_offset < compressed_size:
247 | rle_code = int(data[offset + rle_offset]) | (int(data[offset + rle_offset + 1]) << 8)
248 | rle_offset += 2
249 |
250 | if rle_code & 0xff00 == 0xff00: # use the next (0xffff - rle_code) * 2 bytes as they are
251 | sublength = (0x10000 - rle_code) * 2
252 | if compressed_size < rle_offset + sublength:
253 | raise Corrupt("Compressed data is too short.")
254 | if tex_offset + sublength > _TEXTURE_SIZE:
255 | raise Corrupt("Decompressed data exceeds texture size.")
256 |
257 | for i in range(sublength):
258 | texture_buf[tex_offset] = data[offset + rle_offset]
259 | tex_offset += 1
260 | rle_offset += 1
261 |
262 | else: # repeat next 2 bytes rle_code times
263 | rep = rle_code
264 | if compressed_size < rle_offset + 2:
265 | raise Corrupt("Compressed data is too short.")
266 | if tex_offset + rep * 2 > _TEXTURE_SIZE:
267 | raise Corrupt("Decompressed data exceeds texture size.")
268 |
269 | subdata = data[(offset + rle_offset):(offset + rle_offset + 2)]
270 | rle_offset += 2
271 |
272 | for i in range(rep):
273 | texture_buf[tex_offset] = subdata[0]
274 | texture_buf[tex_offset+1] = subdata[1]
275 | tex_offset += 2
276 |
277 | assert rle_offset == compressed_size
278 |
279 | self.texture = bytes(texture_buf)
280 |
281 | return offset + compressed_size
282 |
--------------------------------------------------------------------------------
/mymcplusplus/ps2iconsys.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | """Interface for working with PS2 icon.sys files."""
19 |
20 | import struct
21 | from . import utils
22 | from .sjistab import shift_jis_normalize_table
23 | import sys
24 |
25 |
26 | class Error(Exception):
27 | """Base for all exceptions specific to this module."""
28 | pass
29 |
30 | class Corrupt(Error):
31 | """Corrupt icon file."""
32 |
33 | def __init__(self, msg):
34 | super().__init__(self, "Corrupt icon.sys: " + msg)
35 |
36 |
37 | _PS2_ICON_SYS_MAGIC = b"PS2D"
38 |
39 | _icon_sys_struct = struct.Struct("<4sHHII"
40 | "4I4I4I4I" # background colors
41 | "4f4f4f" # light dirs
42 | "4f4f4f4f" # light colors
43 | "68s64s64s64s512s")
44 |
45 | assert _icon_sys_struct.size == 964
46 |
47 | #
48 | # Table of graphically similar ASCII characters that can be used
49 | # as substitutes for Unicode characters.
50 | #
51 | char_substs = {
52 | u'\u00a2': u"c",
53 | u'\u00b4': u"'",
54 | u'\u00d7': u"x",
55 | u'\u00f7': u"/",
56 | u'\u2010': u"-",
57 | u'\u2015': u"-",
58 | u'\u2018': u"'",
59 | u'\u2019': u"'",
60 | u'\u201c': u'"',
61 | u'\u201d': u'"',
62 | u'\u2032': u"'",
63 | u'\u2212': u"-",
64 | u'\u226a': u"<<",
65 | u'\u226b': u">>",
66 | u'\u2500': u"-",
67 | u'\u2501': u"-",
68 | u'\u2502': u"|",
69 | u'\u2503': u"|",
70 | u'\u250c': u"+",
71 | u'\u250f': u"+",
72 | u'\u2510': u"+",
73 | u'\u2513': u"+",
74 | u'\u2514': u"+",
75 | u'\u2517': u"+",
76 | u'\u2518': u"+",
77 | u'\u251b': u"+",
78 | u'\u251c': u"+",
79 | u'\u251d': u"+",
80 | u'\u2520': u"+",
81 | u'\u2523': u"+",
82 | u'\u2524': u"+",
83 | u'\u2525': u"+",
84 | u'\u2528': u"+",
85 | u'\u252b': u"+",
86 | u'\u252c': u"+",
87 | u'\u252f': u"+",
88 | u'\u2530': u"+",
89 | u'\u2533': u"+",
90 | u'\u2537': u"+",
91 | u'\u2538': u"+",
92 | u'\u253b': u"+",
93 | u'\u253c': u"+",
94 | u'\u253f': u"+",
95 | u'\u2542': u"+",
96 | u'\u254b': u"+",
97 | u'\u25a0': u"#",
98 | u'\u25a1': u"#",
99 | u'\u3001': u",",
100 | u'\u3002': u".",
101 | u'\u3003': u'"',
102 | u'\u3007': u'0',
103 | u'\u3008': u'<',
104 | u'\u3009': u'>',
105 | u'\u300a': u'<<',
106 | u'\u300b': u'>>',
107 | u'\u300c': u'[',
108 | u'\u300d': u']',
109 | u'\u300e': u'[',
110 | u'\u300f': u']',
111 | u'\u3010': u'[',
112 | u'\u3011': u']',
113 | u'\u3014': u'[',
114 | u'\u3015': u']',
115 | u'\u301c': u'~',
116 | u'\u30fc': u'-',
117 | }
118 |
119 |
120 | def shift_jis_conv(src, encoding=None):
121 | """Convert Shift-JIS strings to a graphically similar representation.
122 |
123 | If encoding is "unicode" then a Unicode string is returned, otherwise
124 | a string in the encoding specified is returned. If necessary,
125 | graphically similar characters are used to replace characters not
126 | exactly representable in the desired encoding.
127 | """
128 |
129 | if encoding is None:
130 | encoding = sys.getdefaultencoding()
131 | if encoding == "shift_jis":
132 | return src
133 | u = src.decode("shift_jis", "replace")
134 | if encoding == "unicode":
135 | return u
136 | a = []
137 | for uc in u:
138 | try:
139 | uc.encode(encoding)
140 | a.append(uc)
141 | except UnicodeError:
142 | for uc2 in shift_jis_normalize_table.get(uc, uc):
143 | a.append(char_substs.get(uc2, uc2))
144 |
145 | return "".join(a)
146 |
147 |
148 | class IconSys:
149 | def __init__(self, data):
150 |
151 | if len(data) != _icon_sys_struct.size:
152 | raise Corrupt("icon.sys file has invalid size.")
153 |
154 | d = _icon_sys_struct.unpack(data)
155 |
156 | if d[0] != _PS2_ICON_SYS_MAGIC:
157 | raise Corrupt("icon.sys has incorrect magic.")
158 |
159 | self._title_line_offset = d[2]
160 | self.background_transparency = d[4]
161 |
162 | self.bg_colors = (d[5:9], d[9:13], d[13:17], d[17:21])
163 |
164 | self.light_dirs = (d[21:25], d[25:29], d[29:33])
165 | self.light_colors = (d[33:37], d[37:41], d[41:45])
166 | self.ambient_light_color = d[45:49]
167 |
168 | self._title_sjis = utils.zero_terminate(d[49])
169 | self.icon_file_normal = utils.zero_terminate(d[50]).decode("ascii")
170 | self.icon_file_copy = utils.zero_terminate(d[51]).decode("ascii")
171 | self.icon_file_delete = utils.zero_terminate(d[52]).decode("ascii")
172 |
173 | def get_title(self, encoding):
174 | title2 = shift_jis_conv(self._title_sjis[self._title_line_offset:], encoding)
175 | title1 = shift_jis_conv(self._title_sjis[:self._title_line_offset], encoding)
176 | return title1, title2
177 |
--------------------------------------------------------------------------------
/mymcplusplus/ps2mc_dir.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | """Functions for working with PS2 memory card directory entries."""
19 |
20 | import struct
21 | import time
22 | import calendar
23 |
24 | from . import utils
25 |
26 | PS2MC_DIRENT_LENGTH = 512
27 |
28 | DF_READ = 0x0001
29 | DF_WRITE = 0x0002
30 | DF_EXECUTE = 0x0004
31 | DF_RWX = DF_READ | DF_WRITE | DF_EXECUTE
32 | DF_PROTECTED = 0x0008
33 | DF_FILE = 0x0010
34 | DF_DIR = 0x0020
35 | DF_O_DCREAT = 0x0040
36 | DF_0080 = 0x0080
37 | DF_0100 = 0x0100
38 | DF_O_CREAT = 0x0200
39 | DF_0400 = 0x0400
40 | DF_POCKETSTN = 0x0800
41 | DF_PSX = 0x1000
42 | DF_HIDDEN = 0x2000
43 | DF_4000 = 0x4000
44 | DF_EXISTS = 0x8000
45 |
46 |
47 | # mode, ???, length, created,
48 | # fat_cluster, parent_entry, modified, attr,
49 | # name
50 | _dirent_fmt = ".
16 | #
17 |
18 | """
19 | Routines for calculating the Hamming codes, a simple form of error
20 | correcting codes (ECC), as used on PS2 memory cards.
21 | """
22 |
23 | import array
24 |
25 | from .round import div_round_up
26 |
27 | __ALL__ = ["ECC_CHECK_OK", "ECC_CHECK_CORRECTED", "ECC_CHECK_FAILED",
28 | "ecc_calculate", "ecc_check", "ecc_calculate_page", "ecc_check_page"]
29 |
30 | ECC_CHECK_OK = 0
31 | ECC_CHECK_CORRECTED = 1
32 | ECC_CHECK_FAILED = 2
33 |
34 |
35 | def _popcount(a):
36 | count = 0
37 | while a != 0:
38 | a &= a - 1
39 | count += 1
40 | return count
41 |
42 |
43 | def _parityb(a):
44 | a = (a ^ (a >> 1))
45 | a = (a ^ (a >> 2))
46 | a = (a ^ (a >> 4))
47 | return a & 1
48 |
49 |
50 | def _make_ecc_tables():
51 | parity_table = [_parityb(b) for b in range(256)]
52 | cpmasks = [0x55, 0x33, 0x0F, 0x00, 0xAA, 0xCC, 0xF0]
53 |
54 | column_parity_masks = [None] * 256
55 | for b in range(256):
56 | mask = 0
57 | for i in range(len(cpmasks)):
58 | mask |= parity_table[b & cpmasks[i]] << i
59 | column_parity_masks[b] = mask
60 |
61 | return parity_table, column_parity_masks
62 |
63 |
64 | _parity_table, _column_parity_masks = _make_ecc_tables()
65 |
66 |
67 | def _ecc_calculate(s):
68 | """Calculate the Hamming code for a 128 byte long string or byte array."""
69 |
70 | if not isinstance(s, array.array):
71 | a = array.array('B')
72 | a.frombytes(s)
73 | s = a
74 | column_parity = 0x77
75 | line_parity_0 = 0x7F
76 | line_parity_1 = 0x7F
77 | for i in range(len(s)):
78 | b = s[i]
79 | column_parity ^= _column_parity_masks[b]
80 | if _parity_table[b]:
81 | line_parity_0 ^= ~i
82 | line_parity_1 ^= i
83 | return [column_parity, line_parity_0 & 0x7F, line_parity_1]
84 |
85 |
86 | def _ecc_check(s, ecc):
87 | """Detect and correct any single bit errors.
88 |
89 | The parameters "s" and "ecc", the data and expected Hamming code
90 | repectively, must be modifiable sequences of integers and are
91 | updated with the corrected values if necessary."""
92 |
93 | computed = ecc_calculate(s)
94 | if computed == ecc:
95 | return ECC_CHECK_OK
96 |
97 | #print
98 | #_print_bin(0, s.tobytes())
99 | #print "computed %02x %02x %02x" % tuple(computed)
100 | #print "actual %02x %02x %02x" % tuple(ecc)
101 |
102 | # ECC mismatch
103 |
104 | cp_diff = (computed[0] ^ ecc[0]) & 0x77
105 | lp0_diff = (computed[1] ^ ecc[1]) & 0x7F
106 | lp1_diff = (computed[2] ^ ecc[2]) & 0x7F
107 | lp_comp = lp0_diff ^ lp1_diff
108 | cp_comp = (cp_diff >> 4) ^ (cp_diff & 0x07)
109 |
110 | #print "%02x %02x %02x %02x %02x" % (cp_diff, lp0_diff, lp1_diff,
111 | # lp_comp, cp_comp)
112 |
113 | if lp_comp == 0x7F and cp_comp == 0x07:
114 | print("corrected 1")
115 | # correctable 1 bit error in data
116 | s[lp1_diff] ^= 1 << (cp_diff >> 4)
117 | return ECC_CHECK_CORRECTED
118 | if ((cp_diff == 0 and lp0_diff == 0 and lp1_diff == 0)
119 | or _popcount(lp_comp) + _popcount(cp_comp) == 1):
120 | print("corrected 2")
121 | # correctable 1 bit error in ECC
122 | # (and/or one of the unused bits was set)
123 | ecc[0] = computed[0]
124 | ecc[1] = computed[1]
125 | ecc[2] = computed[2]
126 | return ECC_CHECK_CORRECTED
127 |
128 | # uncorrectable error
129 | return ECC_CHECK_FAILED
130 |
131 |
132 | def ecc_calculate_page(page):
133 | """Return a list of the ECC codes for a PS2 memory card page."""
134 | return [ecc_calculate(page[i * 128 : i * 128 + 128])
135 | for i in range(div_round_up(len(page), 128))]
136 |
137 |
138 | def ecc_check_page(page, spare):
139 | """Check and correct any single bit errors in a PS2 memory card page."""
140 |
141 | failed = False
142 | corrected = False
143 |
144 | #chunks = [(array.array('B', page[i * 128 : i * 128 + 128]),
145 | # map(ord, spare[i * 3 : i * 3 + 3]))
146 | # for i in range(div_round_up(len(page), 128))]
147 |
148 | chunks = []
149 | for i in range(div_round_up(len(page), 128)):
150 | a = array.array('B')
151 | a.frombytes(page[i * 128 : i * 128 + 128])
152 | chunks.append((a, list(spare[i * 3 : i * 3 + 3])))
153 |
154 | r = [ecc_check(s, ecc)
155 | for (s, ecc) in chunks]
156 | ret = ECC_CHECK_OK
157 | if ECC_CHECK_CORRECTED in r:
158 | # rebuild sector and spare from the corrected versions
159 | page = b"".join([a[0].tobytes() for a in chunks])
160 | spare = bytes([a[1][i]
161 | for a in chunks
162 | for i in range(3)])
163 | ret = ECC_CHECK_CORRECTED
164 | if ECC_CHECK_FAILED in r:
165 | ret = ECC_CHECK_FAILED
166 | return ret, page, spare
167 |
168 |
169 | ecc_calculate = _ecc_calculate
170 | ecc_check = _ecc_check
171 |
--------------------------------------------------------------------------------
/mymcplusplus/round.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | """Simple rounding functions."""
19 |
20 | def div_round_up(a, b):
21 | return (a + b - 1) // b
22 |
23 | def round_up(a, b):
24 | return (a + b - 1) // b * b
25 |
26 | def round_down(a, b):
27 | return a // b * b
28 |
29 |
30 |
--------------------------------------------------------------------------------
/mymcplusplus/save/__init__.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | __all__ = [
19 | "format_codebreaker",
20 | "format_ems",
21 | "format_max_drive",
22 | "format_sharkport",
23 | "format_psv",
24 | "ps2save",
25 | "lzari",
26 | "utils"
27 | ]
28 |
29 |
--------------------------------------------------------------------------------
/mymcplusplus/save/format_codebreaker.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | import array
19 | import zlib
20 |
21 | from .. import ps2mc_dir
22 | from .. import utils
23 | from .utils import *
24 |
25 |
26 | FORMAT_ID = "cbs"
27 |
28 | PS2SAVE_CBS_MAGIC = b"CFU\0"
29 |
30 | # This is the initial permutation state ("S") for the RC4 stream cipher
31 | # algorithm used to encrpyt and decrypt Codebreaker saves.
32 | PS2SAVE_CBS_RC4S = [0x5f, 0x1f, 0x85, 0x6f, 0x31, 0xaa, 0x3b, 0x18,
33 | 0x21, 0xb9, 0xce, 0x1c, 0x07, 0x4c, 0x9c, 0xb4,
34 | 0x81, 0xb8, 0xef, 0x98, 0x59, 0xae, 0xf9, 0x26,
35 | 0xe3, 0x80, 0xa3, 0x29, 0x2d, 0x73, 0x51, 0x62,
36 | 0x7c, 0x64, 0x46, 0xf4, 0x34, 0x1a, 0xf6, 0xe1,
37 | 0xba, 0x3a, 0x0d, 0x82, 0x79, 0x0a, 0x5c, 0x16,
38 | 0x71, 0x49, 0x8e, 0xac, 0x8c, 0x9f, 0x35, 0x19,
39 | 0x45, 0x94, 0x3f, 0x56, 0x0c, 0x91, 0x00, 0x0b,
40 | 0xd7, 0xb0, 0xdd, 0x39, 0x66, 0xa1, 0x76, 0x52,
41 | 0x13, 0x57, 0xf3, 0xbb, 0x4e, 0xe5, 0xdc, 0xf0,
42 | 0x65, 0x84, 0xb2, 0xd6, 0xdf, 0x15, 0x3c, 0x63,
43 | 0x1d, 0x89, 0x14, 0xbd, 0xd2, 0x36, 0xfe, 0xb1,
44 | 0xca, 0x8b, 0xa4, 0xc6, 0x9e, 0x67, 0x47, 0x37,
45 | 0x42, 0x6d, 0x6a, 0x03, 0x92, 0x70, 0x05, 0x7d,
46 | 0x96, 0x2f, 0x40, 0x90, 0xc4, 0xf1, 0x3e, 0x3d,
47 | 0x01, 0xf7, 0x68, 0x1e, 0xc3, 0xfc, 0x72, 0xb5,
48 | 0x54, 0xcf, 0xe7, 0x41, 0xe4, 0x4d, 0x83, 0x55,
49 | 0x12, 0x22, 0x09, 0x78, 0xfa, 0xde, 0xa7, 0x06,
50 | 0x08, 0x23, 0xbf, 0x0f, 0xcc, 0xc1, 0x97, 0x61,
51 | 0xc5, 0x4a, 0xe6, 0xa0, 0x11, 0xc2, 0xea, 0x74,
52 | 0x02, 0x87, 0xd5, 0xd1, 0x9d, 0xb7, 0x7e, 0x38,
53 | 0x60, 0x53, 0x95, 0x8d, 0x25, 0x77, 0x10, 0x5e,
54 | 0x9b, 0x7f, 0xd8, 0x6e, 0xda, 0xa2, 0x2e, 0x20,
55 | 0x4f, 0xcd, 0x8f, 0xcb, 0xbe, 0x5a, 0xe0, 0xed,
56 | 0x2c, 0x9a, 0xd4, 0xe2, 0xaf, 0xd0, 0xa9, 0xe8,
57 | 0xad, 0x7a, 0xbc, 0xa8, 0xf2, 0xee, 0xeb, 0xf5,
58 | 0xa6, 0x99, 0x28, 0x24, 0x6c, 0x2b, 0x75, 0x5d,
59 | 0xf8, 0xd3, 0x86, 0x17, 0xfb, 0xc0, 0x7b, 0xb3,
60 | 0x58, 0xdb, 0xc7, 0x4b, 0xff, 0x04, 0x50, 0xe9,
61 | 0x88, 0x69, 0xc9, 0x2a, 0xab, 0xfd, 0x5b, 0x1b,
62 | 0x8a, 0xd9, 0xec, 0x27, 0x44, 0x0e, 0x33, 0xc8,
63 | 0x6b, 0x93, 0x32, 0x48, 0xb6, 0x30, 0x43, 0xa5]
64 |
65 |
66 | def _rc4_crypt(s, t):
67 | """RC4 encrypt/decrypt the string t using the permutation s.
68 |
69 | Returns a byte array."""
70 |
71 | s = array.array('B', s)
72 | t = array.array('B', t)
73 | j = 0
74 | for ii in range(len(t)):
75 | i = (ii + 1) % 256
76 | j = (j + s[i]) % 256
77 | (s[i], s[j]) = (s[j], s[i])
78 | t[ii] ^= s[(s[i] + s[j]) % 256]
79 | return t
80 |
81 |
82 | def poll(hdr):
83 | return hdr.startswith(PS2SAVE_CBS_MAGIC)
84 |
85 |
86 | def load(save, f):
87 | magic = f.read(4)
88 | if magic != PS2SAVE_CBS_MAGIC:
89 | raise ps2save.Corrupt("Not a Codebreaker save file.", f)
90 | (d04, hlen) = struct.unpack(".
16 | #
17 |
18 | from . import ps2save
19 | from .. import ps2mc_dir
20 | from .utils import *
21 | from ..round import round_up
22 |
23 |
24 | FORMAT_ID = "psu"
25 |
26 |
27 | def poll(hdr):
28 | #
29 | # EMS (.psu) save files don't have a magic number. Check to
30 | # see if it looks enough like one.
31 | #
32 |
33 | if len(hdr) < ps2mc_dir.PS2MC_DIRENT_LENGTH * 3:
34 | return None
35 |
36 | dirent = ps2mc_dir.unpack_dirent(hdr[:ps2mc_dir.PS2MC_DIRENT_LENGTH])
37 | dotent = ps2mc_dir.unpack_dirent(hdr[ps2mc_dir.PS2MC_DIRENT_LENGTH : ps2mc_dir.PS2MC_DIRENT_LENGTH * 2])
38 | dotdotent = ps2mc_dir.unpack_dirent(hdr[ps2mc_dir.PS2MC_DIRENT_LENGTH * 2:])
39 |
40 | return ps2mc_dir.mode_is_dir(dirent[0]) and ps2mc_dir.mode_is_dir(dotent[0]) \
41 | and ps2mc_dir.mode_is_dir(dotdotent[0]) and dirent[2] >= 2 \
42 | and dotent[8] == b"." and dotdotent[8] == b".."
43 |
44 |
45 | def load(save, f):
46 | """Load EMS (.psu) save files."""
47 |
48 | cluster_size = 1024
49 |
50 | dirent = ps2mc_dir.unpack_dirent(read_fixed(f, ps2mc_dir.PS2MC_DIRENT_LENGTH))
51 | dotent = ps2mc_dir.unpack_dirent(read_fixed(f, ps2mc_dir.PS2MC_DIRENT_LENGTH))
52 | dotdotent = ps2save.unpack_dirent(read_fixed(f, ps2mc_dir.PS2MC_DIRENT_LENGTH))
53 | if (not ps2mc_dir.mode_is_dir(dirent[0])
54 | or not ps2save.mode_is_dir(dotent[0])
55 | or not ps2save.mode_is_dir(dotdotent[0])
56 | or dirent[2] < 2):
57 | raise ps2save.Corrupt("Not a EMS (.psu) save file.", f)
58 |
59 | dirent[2] -= 2
60 | save.set_directory(dirent)
61 |
62 | for i in range(dirent[2]):
63 | ent = ps2mc_dir.unpack_dirent(read_fixed(f, ps2mc_dir.PS2MC_DIRENT_LENGTH))
64 | if not ps2mc_dir.mode_is_file(ent[0]):
65 | raise ps2save.Subdir(f)
66 | flen = ent[2]
67 | save.set_file(i, ent, read_fixed(f, flen))
68 | read_fixed(f, round_up(flen, cluster_size) - flen)
69 |
70 |
71 | def save(save, f):
72 | cluster_size = 1024
73 |
74 | dirent = save.dirent[:]
75 | dirent[2] += 2
76 | f.write(ps2mc_dir.pack_dirent(dirent))
77 | f.write(ps2mc_dir.pack_dirent((ps2mc_dir.DF_RWX | ps2mc_dir.DF_DIR | ps2mc_dir.DF_0400 | ps2mc_dir.DF_EXISTS,
78 | 0, 0, dirent[3],
79 | 0, 0, dirent[3], 0, b".")))
80 | f.write(ps2mc_dir.pack_dirent((ps2mc_dir.DF_RWX | ps2mc_dir.DF_DIR | ps2mc_dir.DF_0400 | ps2mc_dir.DF_EXISTS,
81 | 0, 0, dirent[3],
82 | 0, 0, dirent[3], 0, b"..")))
83 |
84 | for i in range(dirent[2] - 2):
85 | (ent, data) = save.get_file(i)
86 | f.write(ps2mc_dir.pack_dirent(ent))
87 | if not ps2mc_dir.mode_is_file(ent[0]):
88 | # print ent
89 | # print hex(ent[0])
90 | raise ps2save.Error("Directory has a subdirectory.")
91 | f.write(data)
92 | f.write(b"\0" * (round_up(len(data), cluster_size) - len(data)))
93 | f.flush()
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/mymcplusplus/save/format_max_drive.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | import binascii
19 |
20 | from .. import ps2mc_dir
21 | from .. import utils
22 | from . import ps2save
23 | from . import lzari
24 | from ..round import round_up
25 | from .utils import *
26 |
27 |
28 | FORMAT_ID = "max"
29 |
30 | PS2SAVE_MAX_MAGIC = b"Ps2PowerSave"
31 |
32 |
33 | def poll(hdr):
34 | return hdr.startswith(PS2SAVE_MAX_MAGIC)
35 |
36 |
37 | def load2(save, f):
38 | (length, s) = save._compressed
39 | save._compressed = None
40 |
41 | if lzari is None:
42 | raise ps2mc_dir.Error("The lzari module is needed to decompress MAX Drive saves.")
43 | s = lzari.decode(s, length, "decompressing " + save.dirent[8].decode("ascii") + ": ")
44 | dirlen = save.dirent[2]
45 | timestamp = save.dirent[3]
46 | off = 0
47 | for i in range(dirlen):
48 | if len(s) - off < 36:
49 | raise ps2save.Eof(f)
50 | (l, name) = struct.unpack(" 0 and title[0][-1] != ' ':
99 | iconsysname = title[0] + " " + title[1].strip()
100 | else:
101 | iconsysname = title[0] + title[1].rstrip()
102 | s = b""
103 | dirent = save.dirent
104 | for i in range(dirent[2]):
105 | (ent, data) = save.get_file(i)
106 | if not ps2mc_dir.mode_is_file(ent[0]):
107 | raise ps2mc_dir.Error("Non-file in save file.")
108 | s += struct.pack(".
16 | #
17 |
18 | import struct
19 |
20 | from . import ps2save
21 | from .. import ps2mc_dir
22 | from . import utils
23 | from .. import utils as common_utils
24 |
25 |
26 | FORMAT_ID = "psv"
27 |
28 | PSV_MAGIC = b"\x00VSP"
29 |
30 |
31 | _psv_header_struct = struct.Struct("<4sI40s8xII")
32 |
33 | _ps2_header_struct = struct.Struct(".
16 | #
17 |
18 | from .. import ps2mc_dir
19 | from .. import utils
20 | from . import ps2save
21 | from .utils import *
22 |
23 |
24 | FORMAT_ID = "sps"
25 |
26 | PS2SAVE_SPS_MAGIC = b"\x0d\0\0\0SharkPortSave"
27 |
28 |
29 | def poll(hdr):
30 | return hdr.startswith(PS2SAVE_SPS_MAGIC)
31 |
32 |
33 | def load(save, f):
34 | magic = f.read(17)
35 | if magic != PS2SAVE_SPS_MAGIC:
36 | raise ps2save.Corrupt("Not a SharkPort/X-Port save file.", f)
37 | (savetype,) = struct.unpack(".
16 | #
17 |
18 | """
19 | Implementation of Haruhiko Okumura's LZARI data compression algorithm
20 | in Python. Largely based on LZARI.C, one key difference is the use of
21 | a two level dicitionary look up during compression rather than
22 | LZARI.C's binary search tree.
23 | """
24 |
25 | import sys
26 | import array
27 | import binascii
28 | import string
29 | import time
30 | from bisect import bisect_right
31 | from math import log
32 |
33 | try:
34 | import ctypes
35 | import mymcsup
36 | except ImportError:
37 | mymcsup = None
38 |
39 | hexlify = binascii.hexlify
40 |
41 | __ALL__ = ['lzari_codec', 'string_to_bit_array', 'bit_array_to_string']
42 |
43 | #
44 | # Fundamental constants of the LZARI compression alogorithm.
45 | #
46 | # Changing any of these values will create an incompatible implementation.
47 | #
48 |
49 | HIST_LEN = 4096
50 | MIN_MATCH_LEN = 3
51 | MAX_MATCH_LEN = 60
52 |
53 | ARITH_BITS = 15
54 | QUADRANT1 = 1 << ARITH_BITS
55 | QUADRANT2 = QUADRANT1 * 2
56 | QUADRANT3 = QUADRANT1 * 3
57 | QUADRANT4 = QUADRANT1 * 4
58 | MAX_CUM = QUADRANT1 - 1
59 | MAX_CHAR = (256 + MAX_MATCH_LEN - MIN_MATCH_LEN + 1)
60 |
61 | #
62 | # Other constants specific to this implementation
63 | #
64 |
65 | MAX_SUFFIX_CHAIN = 50 # limit on how many identical suffixes to try to match
66 |
67 | #def debug(value, msg):
68 | # print "@@@ %s %04x" % (msg, value)
69 | debug = lambda value, msg: None
70 |
71 | _tr_16 = bytes.maketrans(b"0123456789abcdef",
72 | b"\x00\x01\x02\x03"
73 | b"\x10\x11\x12\x13"
74 | b"\x20\x21\x22\x23"
75 | b"\x30\x31\x32\x33")
76 | _tr_4 = bytes.maketrans(b"0123",
77 | b"\x00\x01"
78 | b"\x10\x11")
79 | _tr_2 = bytes.maketrans(b"01", b"\x00\x01")
80 |
81 | def string_to_bit_array(s):
82 | """Convert a string to an array containing a sequence of bits."""
83 | s = binascii.hexlify(s).translate(_tr_16)
84 | s = binascii.hexlify(s).translate(_tr_4)
85 | s = binascii.hexlify(s).translate(_tr_2)
86 | a = array.array('B', s)
87 | return a
88 |
89 | _tr_rev_2 = bytes.maketrans(b"\x00\x01", b"01")
90 | _tr_rev_4 = bytes.maketrans(b"\x00\x01\x10\x11", b"0123")
91 | _tr_rev_16 = bytes.maketrans(b"\x00\x01\x02\x03"
92 | b"\x10\x11\x12\x13"
93 | b"\x20\x21\x22\x23"
94 | b"\x30\x31\x32\x33",
95 | b"0123456789abcdef")
96 | def bit_array_to_string(a):
97 | """Convert an array containing a sequence of bits to a string."""
98 | remainder = len(a) % 8
99 | if remainder != 0:
100 | a.fromlist([0] * (8 - remainder))
101 | s = a.tobytes()
102 | s = binascii.unhexlify(s.translate(_tr_rev_2))
103 | s = binascii.unhexlify(s.translate(_tr_rev_4))
104 | return binascii.unhexlify(s.translate(_tr_rev_16))
105 |
106 | def _match(src, pos, hpos, mlen, end):
107 | mlen += 1
108 | if not src.startswith(src[hpos : hpos + mlen], pos):
109 | return None
110 | for i in range(mlen, end):
111 | if src[pos + i] != src[hpos + i]:
112 | return i
113 | return end
114 |
115 | def _rehash_table2(src, chars, head, next, next2, hist_invalid):
116 | p = head
117 | table2 = {}
118 | l = []
119 | while p > hist_invalid:
120 | l.append(p)
121 | p = next[p % HIST_LEN]
122 | l.reverse()
123 | for p in l:
124 | p2 = p + MIN_MATCH_LEN
125 | key2 = src[p2 : p2 + chars]
126 | head2 = table2.get(key2, hist_invalid)
127 | next2[p % HIST_LEN] = head2
128 | table2[key2] = p
129 | return table2
130 |
131 | class lzari_codec(object):
132 | # despite the name this does not implement a codec compatible
133 | # with Python's codec system
134 |
135 | def init(self, decode):
136 | self.high = QUADRANT4
137 | self.low = 0
138 | if decode:
139 | self.code = 0
140 | # reverse the order of sym_cum so bisect_right() can
141 | # be used for faster searching
142 | self.sym_cum = list(range(0, MAX_CHAR + 1))
143 | else:
144 | self.shifts = 0
145 | self.char_to_symbol = list(range(1, MAX_CHAR + 1))
146 | self.sym_cum = list(range(MAX_CHAR, -1, -1))
147 | self.next_table = [None] * HIST_LEN
148 | self.next2_table = [None] * HIST_LEN
149 | self.suffix_table = {}
150 |
151 | self.symbol_to_char = [0] + list(range(MAX_CHAR))
152 | self.sym_freq = [0] + [1] * MAX_CHAR
153 | self.position_cum = [0] * (HIST_LEN + 1)
154 | a = 0
155 | for i in range(HIST_LEN, 0, -1):
156 | a = a + 10000 // (200 + i)
157 | self.position_cum[i - 1] = a
158 |
159 | def search(self, table, x):
160 | c = 1
161 | s = len(table) - 1
162 | while True:
163 | a = (s + c) // 2
164 | if table[a] <= x:
165 | s = a
166 | else:
167 | c = a + 1
168 | if c >= s:
169 | break
170 | return c
171 |
172 | def update_model_decode(self, symbol):
173 | # A compatible implemention to the one used while compressing.
174 |
175 | sym_freq = self.sym_freq
176 | sym_cum = self.sym_cum
177 |
178 | if self.sym_cum[MAX_CHAR] >= MAX_CUM:
179 | c = 0
180 | for i in range(MAX_CHAR, 0, -1):
181 | self.sym_cum[MAX_CHAR - i] = c
182 | a = (self.sym_freq[i] + 1) // 2
183 | self.sym_freq[i] = a
184 | c += a
185 | self.sym_cum[MAX_CHAR] = c
186 | freq = sym_freq[symbol]
187 | new_symbol = symbol
188 | while self.sym_freq[new_symbol - 1] == freq:
189 | new_symbol -= 1
190 | # new_symbol = sym_freq.index(freq)
191 | if new_symbol != symbol:
192 | symbol_to_char = self.symbol_to_char
193 | swap_char = symbol_to_char[new_symbol]
194 | char = symbol_to_char[symbol]
195 | symbol_to_char[new_symbol] = char
196 | symbol_to_char[symbol] = swap_char
197 | sym_freq[new_symbol] = freq + 1
198 | for i in range(MAX_CHAR - new_symbol + 1, MAX_CHAR + 1):
199 | sym_cum[i] += 1
200 |
201 | def update_model_encode(self, symbol):
202 | sym_freq = self.sym_freq
203 | sym_cum = self.sym_cum
204 |
205 | if sym_cum[0] >= MAX_CUM:
206 | c = 0
207 | for i in range(MAX_CHAR, 0, -1):
208 | sym_cum[i] = c
209 | a = (sym_freq[i] + 1) // 2
210 | sym_freq[i] = a
211 | c += a
212 | sym_cum[0] = c
213 | freq = sym_freq[symbol]
214 | new_symbol = symbol
215 | while sym_freq[new_symbol - 1] == freq:
216 | new_symbol -= 1
217 | if new_symbol != symbol:
218 | debug(new_symbol, "a")
219 | swap_char = self.symbol_to_char[new_symbol]
220 | char = self.symbol_to_char[symbol]
221 | self.symbol_to_char[new_symbol] = char
222 | self.symbol_to_char[symbol] = swap_char
223 | self.char_to_symbol[char] = new_symbol
224 | self.char_to_symbol[swap_char] = symbol
225 | sym_freq[new_symbol] += 1
226 | for i in range(new_symbol):
227 | sym_cum[i] += 1
228 |
229 | def decode_char(self):
230 | high = self.high
231 | low = self.low
232 | code = self.code
233 | sym_cum = self.sym_cum
234 |
235 | _range = high - low
236 | max_cum_freq = sym_cum[MAX_CHAR]
237 | n = ((code - low + 1) * max_cum_freq - 1) // _range
238 | i = bisect_right(sym_cum, n, 1)
239 | high = low + sym_cum[i] * _range // max_cum_freq
240 | low += sym_cum[i - 1] * _range // max_cum_freq
241 | symbol = MAX_CHAR + 1 - i
242 |
243 | while True:
244 | if low < QUADRANT2:
245 | if low < QUADRANT1 or high > QUADRANT3:
246 | if high > QUADRANT2:
247 | break
248 | else:
249 | low -= QUADRANT1
250 | code -= QUADRANT1
251 | high -= QUADRANT1
252 | else:
253 | low -= QUADRANT2
254 | code -= QUADRANT2
255 | high -= QUADRANT2
256 | low *= 2
257 | high *= 2
258 | code = code * 2 + self.in_iter()
259 |
260 | ret = self.symbol_to_char[symbol]
261 | self.high = high
262 | self.low = low
263 | self.code = code
264 | self.update_model_decode(symbol)
265 | return ret
266 |
267 | def decode_position(self):
268 | _range = self.high - self.low
269 | max_cum = self.position_cum[0]
270 | pos = self.search(self.position_cum, ((self.code - self.low + 1) * max_cum - 1) // _range) - 1
271 | self.high = (self.low + self.position_cum[pos] * _range // max_cum)
272 | self.low += self.position_cum[pos + 1] * _range // max_cum
273 | while True:
274 | if self.low < QUADRANT2:
275 | if (self.low < QUADRANT1
276 | or self.high > QUADRANT3):
277 | if self.high > QUADRANT2:
278 | return pos
279 | else:
280 | self.low -= QUADRANT1
281 | self.code -= QUADRANT1
282 | self.high -= QUADRANT1
283 | else:
284 | self.low -= QUADRANT2
285 | self.code -= QUADRANT2
286 | self.high -= QUADRANT2
287 | self.low *= 2
288 | self.high *= 2
289 | self.code = self.in_iter() + self.code * 2
290 |
291 | def add_suffix_1(self, pos, find):
292 | # naive implemention used for testing
293 |
294 | if not find:
295 | return (None, 0)
296 | src = self.src
297 | mlen = min(1000, self.max_match, len(src) - pos)
298 | hist_start = max(pos - HIST_LEN, 0)
299 | while mlen >= MIN_MATCH_LEN:
300 | i = src.rfind(src[pos : pos + mlen], hist_start, pos)
301 | if i != -1:
302 | assert (src[pos : pos + mlen]
303 | == src[i: i + mlen])
304 | return (i, mlen)
305 | mlen -= 1
306 | return (None, -1)
307 |
308 | def add_suffix_2(self, pos, find):
309 | # a two level dictionary look up that leverages Python's
310 | # built-in dicts to get something that's hopefully faster
311 | # than implementing binary trees in completely in Python.
312 |
313 | src = self.src
314 | suffix_table = self.suffix_table
315 | max_match = min(self.max_match, len(src) - pos)
316 |
317 | mlen = -1
318 | mpos = None
319 |
320 | hist_invalid = pos - HIST_LEN - 1
321 | modpos = pos % HIST_LEN
322 | pos2 = pos + MIN_MATCH_LEN
323 |
324 | key = src[pos : pos2]
325 | a = suffix_table.get(key)
326 | if a != None:
327 | next = self.next_table
328 | next2 = self.next2_table
329 |
330 | [count, head, table2, chars] = a
331 |
332 | pos3 = pos2 + chars
333 | key2 = src[pos2 : pos3]
334 | min_match2 = MIN_MATCH_LEN + chars
335 | if find:
336 | p = table2.get(key2, hist_invalid)
337 | maxmlen = max_match - min_match2
338 | while p > hist_invalid and mlen != maxmlen:
339 | p3 = p + min_match2
340 | if mpos == None and p3 <= pos:
341 | mpos = p
342 | mlen = 0
343 | if p3 >= pos:
344 | p = next2[p % HIST_LEN]
345 | continue
346 | rlen = _match(src, pos3, p3, mlen,
347 | min(maxmlen, pos - p3))
348 | if rlen != None:
349 | mpos = p
350 | mlen = rlen
351 | p = next2[p % HIST_LEN]
352 | if mpos != None:
353 | mlen += min_match2
354 | elif find:
355 | p = head
356 | maxmlen = min(chars, max_match - MIN_MATCH_LEN)
357 | i = 0
358 | while (p > hist_invalid and i < 50000
359 | and mlen < maxmlen):
360 | assert i < count
361 | i += 1
362 | p2 = p + MIN_MATCH_LEN
363 | l2 = pos - p2
364 | if mpos == None and l2 >= 0:
365 | mpos = p
366 | mlen = 0
367 | if l2 <= 0:
368 | p = next[p % HIST_LEN]
369 | continue
370 | if l2 > maxmlen:
371 | l2 = maxmlen
372 | m = mlen + 1
373 | if src.startswith(src[p2 : p2 + m],
374 | pos2):
375 | mpos = p
376 | for j in range(m, l2):
377 | if (src[pos2 + j]
378 | != src[p2 + j]):
379 | mlen = j
380 | break
381 | else:
382 | mlen = l2
383 | #rlen = _match(src, pos2, p2, mlen, l2)
384 | #if rlen != None:
385 | # mpos = p
386 | # mlen = rlen
387 | p = next[p % HIST_LEN]
388 |
389 | if mpos != None:
390 | mlen += MIN_MATCH_LEN
391 |
392 | count += 1
393 | new_chars = int(log(count, 2))
394 | # new_chars = 50
395 | new_chars = min(new_chars, max_match - MIN_MATCH_LEN)
396 | if new_chars > chars:
397 | chars = new_chars
398 | table2 = _rehash_table2(src, chars, head,
399 | next, next2,
400 | hist_invalid)
401 |
402 | next[modpos] = head
403 | head = pos
404 |
405 | key2 = src[pos2 : pos2 + chars]
406 | head2 = table2.get(key2, hist_invalid)
407 | next2[modpos] = head2
408 | table2[key2] = pos
409 |
410 | a[0] = count
411 | a[1] = head
412 | a[2] = table2
413 | a[3] = chars
414 | else:
415 | self.next_table[modpos] = hist_invalid
416 | self.next2_table[modpos] = hist_invalid
417 | key2 = b""
418 | # key2 = src[pos2 : pos2 + 1]
419 | suffix_table[key] = [1, pos, {key2: pos}, len(key2)]
420 |
421 | p = pos - HIST_LEN
422 | if p >= 0:
423 | p2 = p + MIN_MATCH_LEN
424 | key = src[p : p2]
425 | a = suffix_table[key]
426 | (count, head, table2, chars) = a
427 | count -= 1
428 | if count == 0:
429 | assert head == p
430 | del suffix_table[key]
431 | else:
432 | key2 = src[p2 : p2 + chars]
433 | if table2[key2] == p:
434 | del table2[key2]
435 | a[0] = count
436 | assert (mpos == None
437 | or src[pos : pos + mlen] == src[mpos : mpos + mlen])
438 | return (mpos, mlen)
439 |
440 | def _add_suffix(self, pos, find):
441 | r = self.add_suffix_2(pos, find)
442 | start_pos = self.start_pos
443 | if find and r[0] != None:
444 | print ("%4d %02x %4d %2d"
445 | % (pos - start_pos, ord(self.src[pos]),
446 | r[0] - start_pos, r[1]))
447 | else:
448 | print ("%4d %02x"
449 | % (pos - start_pos, ord(self.src[pos])))
450 | return r
451 |
452 | add_suffix = add_suffix_2
453 |
454 | def output_bit(self, bit):
455 | self.append_bit(bit)
456 | bit ^= 1
457 | for i in range(self.shifts):
458 | self.append_bit(bit)
459 | self.shifts = 0
460 |
461 | def encode_char(self, char):
462 | low = self.low
463 | high = self.high
464 | sym_cum = self.sym_cum
465 |
466 | symbol = self.char_to_symbol[char]
467 | range = high - low
468 |
469 | high = low + range * sym_cum[symbol - 1] // sym_cum[0]
470 | low += range * sym_cum[symbol] // sym_cum[0]
471 | debug(high, "high");
472 | debug(low, "low");
473 | while True:
474 | if high <= QUADRANT2:
475 | self.output_bit(0)
476 | elif low >= QUADRANT2:
477 | self.output_bit(1)
478 | low -= QUADRANT2
479 | high -= QUADRANT2
480 | elif low >= QUADRANT1 and high <= QUADRANT3:
481 | self.shifts += 1
482 | low -= QUADRANT1
483 | high -= QUADRANT1
484 | else:
485 | break
486 | low *= 2
487 | high *= 2
488 | self.low = low
489 | self.high = high
490 | self.update_model_encode(symbol)
491 |
492 | def encode_position(self, position):
493 | position_cum = self.position_cum
494 | low = self.low
495 | high = self.high
496 |
497 | range = high - low
498 | high = low + range * position_cum[position] // position_cum[0]
499 | low += range * position_cum[position + 1] // position_cum[0]
500 |
501 | debug(high, "high");
502 | debug(low, "low");
503 | while True:
504 | if high <= QUADRANT2:
505 | self.output_bit(0)
506 | elif low >= QUADRANT2:
507 | self.output_bit(1)
508 | low -= QUADRANT2
509 | high -= QUADRANT2
510 | elif low >= QUADRANT1 and high <= QUADRANT3:
511 | self.shifts += 1
512 | low -= QUADRANT1
513 | high -= QUADRANT1
514 | else:
515 | break
516 | low *= 2
517 | high *= 2
518 |
519 | self.low = low
520 | self.high = high
521 |
522 | def encode(self, src, progress = None):
523 | """Compress a string."""
524 |
525 | length = len(src)
526 | if length == 0:
527 | return b""
528 |
529 | out_array = array.array('B')
530 | self.out_array = out_array
531 | self.append_bit = out_array.append
532 |
533 | self.init(False)
534 |
535 | max_match = min(MAX_MATCH_LEN, length)
536 | self.max_match = max_match
537 | self.src = src = b"\x20" * max_match + src
538 |
539 | in_length = len(src)
540 |
541 | self.start_pos = max_match
542 |
543 | for in_pos in range(max_match):
544 | self.add_suffix(in_pos, False)
545 | in_pos += 1
546 | last_percent = -1
547 | while in_pos < in_length:
548 | if progress:
549 | percent = (in_pos - max_match) * 100 // length
550 | if percent != last_percent:
551 | sys.stderr.write("%s%3d%%\r"
552 | % (progress, percent))
553 | last_percent = percent
554 | debug(src[in_pos], "src")
555 | (match_pos, match_len) = self.add_suffix(in_pos, True)
556 | if match_len < MIN_MATCH_LEN:
557 | self.encode_char(src[in_pos])
558 | else:
559 | debug(in_pos - match_pos - 1, "match_pos")
560 | debug(match_len, "match_len")
561 | self.encode_char(256 - MIN_MATCH_LEN + match_len)
562 | self.encode_position(in_pos - match_pos - 1)
563 | for i in range(match_len - 1):
564 | in_pos += 1
565 | self.add_suffix(in_pos, False)
566 | in_pos += 1
567 |
568 | self.shifts += 1
569 | if self.low < QUADRANT1:
570 | self.output_bit(0)
571 | else:
572 | self.output_bit(1)
573 |
574 | #for k, v in sorted(self.suffix_table.items()):
575 | # count, head, table2, chars = v
576 | # print hexlify(k), count, head, len(table2), chars
577 |
578 | if progress:
579 | sys.stderr.write("%s100%%\n" % progress)
580 |
581 | return bit_array_to_string(out_array)
582 |
583 | def decode(self, src, out_length, progress = None):
584 | """Decompress a string."""
585 |
586 | a = string_to_bit_array(src)
587 | a.fromlist([0] * 32) # add some extra bits
588 | self.in_iter = iter(a).__next__
589 |
590 | out = array.array('B', b"\0") * out_length
591 | outpos = 0
592 |
593 | self.init(True)
594 |
595 | self.code = 0
596 | for i in range(ARITH_BITS + 2):
597 | self.code += self.code + self.in_iter()
598 |
599 | hist_pos = HIST_LEN - MAX_MATCH_LEN
600 | history = [0x20] * hist_pos + [0] * MAX_MATCH_LEN
601 |
602 | decode_char = self.decode_char
603 | last_percent = -1
604 | last_time = time.time()
605 | while outpos < out_length:
606 | if progress:
607 | percent = outpos * 100 // out_length
608 | if percent != last_percent:
609 | now = time.time()
610 | if now - last_time >= 1:
611 | sys.stderr.write("%s%3d%%\r"
612 | % (progress, percent))
613 | last_percent = percent
614 | last_time = now
615 | char = decode_char()
616 | if char >= 0x100:
617 | pos = self.decode_position()
618 | length = char - 0x100 + MIN_MATCH_LEN
619 | base = (hist_pos - pos - 1) % HIST_LEN
620 | for off in range(length):
621 | a = history[(base + off) % HIST_LEN]
622 | out[outpos] = a
623 | outpos += 1
624 | history[hist_pos] = a
625 | hist_pos = (hist_pos + 1) % HIST_LEN
626 | else:
627 | out[outpos] = char
628 | outpos += 1
629 | history[hist_pos] = char
630 | hist_pos = (hist_pos + 1) % HIST_LEN
631 |
632 | self.in_iter = None
633 | if progress:
634 | sys.stderr.write("%s100%%\n" % progress)
635 | return out.tobytes()
636 |
637 | if mymcsup == None:
638 | def decode(src, out_length, progress = None):
639 | return lzari_codec().decode(src, out_length, progress)
640 |
641 | def encode(src, progress = None):
642 | return lzari_codec().encode(src, progress)
643 | else:
644 | mylzari_decode = mymcsup.mylzari_decode
645 | mylzari_encode = mymcsup.mylzari_encode
646 | mylzari_free_encoded = mymcsup.mylzari_free_encoded
647 |
648 | def decode(src, out_length, progress = None):
649 | out = ctypes.create_string_buffer(out_length)
650 | if (mylzari_decode(src, len(src), out, out_length, progress)
651 | == -1):
652 | raise ValueError("compressed input is corrupt")
653 | return ctypes.string_at(out, out_length)
654 |
655 | def encode(src, progress = None):
656 | (r, compressed, comp_len) = mylzari_encode(src, len(src),
657 | progress)
658 | # print r, compressed.value, comp_len
659 | if r == -1:
660 | raise MemoryError("out of memory during compression")
661 | if compressed.value == None:
662 | return b""
663 | ret = ctypes.string_at(compressed.value, comp_len.value)
664 | mylzari_free_encoded(compressed)
665 | return ret;
666 |
667 | def main2(args):
668 | import struct
669 | import os
670 |
671 | src = open(args[2], "rb").read()
672 | lzari = lzari_codec()
673 | out = open(args[3], "wb")
674 | start = os.times()
675 | if args[1] == "c":
676 | dest = lzari.encode(src)
677 | now = os.times()
678 | out.write(struct.pack("L", len(src)))
679 | else:
680 | dest = lzari.decode(src[4:],
681 | struct.unpack("L", src[:4])[0])
682 | now = os.times()
683 | out.write(dest)
684 | out.close()
685 | print("time:", now[0] - start[0], now[1] - start[1], now[4] - start[4])
686 |
687 |
688 | def _get_hotshot_lineinfo(filename):
689 | import hotshot.log
690 | log = hotshot.log.LogReader(filename)
691 | timings = {}
692 | for what, loc, tdelta in log:
693 | if what == hotshot.log.LINE:
694 | a = timings.get(loc)
695 | if a == None:
696 | timings[loc] = [1, tdelta]
697 | else:
698 | a[0] += 1
699 | a[1] += tdelta
700 | return list(timings.items())
701 |
702 | def _dump_hotshot_lineinfo(log):
703 | a = sorted(_get_hotshot_lineinfo(log))
704 | total_count = sum((time[0]
705 | for (loc, time) in a))
706 | total_time = sum((time[1]
707 | for (loc, time) in a))
708 | for (loc, [count, time]) in a:
709 | print(("%8d %6.3f%% %8d %6.3f%%"
710 | % (time, time * 100.0 / total_time,
711 | count, count * 100.0 / total_count)), end=' ')
712 | print("%s:%d(%s)" % loc)
713 |
714 | def _dump_hotshot_lineinfo2(log):
715 | cur = None
716 | a = sorted(_get_hotshot_lineinfo(log))
717 | total_count = sum((time[0]
718 | for (loc, time) in a))
719 | total_time = sum((time[1]
720 | for (loc, time) in a))
721 | for ((filename, lineno, fn), [count, time]) in a:
722 | if cur != filename:
723 | if cur != None and f != None:
724 | for line in f:
725 | print(line[:-1])
726 | f.close()
727 | try:
728 | f = open(filename, "r")
729 | except OSError:
730 | f = None
731 | cur = filename
732 | l = 0
733 | print("#", filename)
734 | if f != None:
735 | while l < lineno:
736 | print(f.readline()[:-1])
737 | l += 1
738 | print ("# %8d %6.3f%% %8d %6.3f%%"
739 | % (time, time * 100.0 / total_time,
740 | count, count * 100.0 / total_count))
741 | if cur != None and f != None:
742 | for line in f:
743 | print(line[:-1])
744 | f.close()
745 |
746 | def main(args):
747 | import os
748 |
749 | if args[1] == "pc":
750 | import profile
751 | pr = profile.Profile()
752 | for i in range(5):
753 | print(pr.calibrate(100000))
754 | return
755 | elif args[1] == "p":
756 | import profile
757 | ret = 0
758 | # profile.Profile.bias = 5.26e-6
759 | profile.runctx("ret = main2(args[1:])",
760 | globals(), locals())
761 | return ret
762 | elif args[1].startswith("h"):
763 | import hotshot, hotshot.stats
764 | import warnings
765 |
766 | warnings.filterwarnings("ignore")
767 | tmp = os.tempnam()
768 | try:
769 | l = args[1].startswith("hl")
770 | p = hotshot.Profile(tmp, l)
771 | ret = p.runcall(main2, args[1:])
772 | p.close()
773 | p = None
774 | if l:
775 | if args[1] == "hl2":
776 | _dump_hotshot_lineinfo2(tmp)
777 | else:
778 | _dump_hotshot_lineinfo(tmp)
779 | else:
780 | hotshot.stats.load(tmp).print_stats()
781 | finally:
782 | try:
783 | os.remove(tmp)
784 | except OSError:
785 | pass
786 | return ret
787 |
788 | return main2(args)
789 |
790 | if __name__ == '__main__':
791 | sys.exit(main(sys.argv))
792 |
793 |
--------------------------------------------------------------------------------
/mymcplusplus/save/ps2save.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | """A simple interface for working with various PS2 save file formats."""
19 |
20 | import sys
21 | import os
22 | import binascii
23 |
24 | from ..ps2mc_dir import *
25 | from .. import ps2iconsys
26 |
27 |
28 | from . import format_codebreaker, format_ems, format_max_drive, format_sharkport, format_psv
29 |
30 |
31 | formats = [
32 | format_codebreaker,
33 | format_ems,
34 | format_max_drive,
35 | format_sharkport,
36 | format_psv
37 | ]
38 |
39 |
40 | class Error(Exception):
41 | """Base for all exceptions specific to this module."""
42 | pass
43 |
44 |
45 | class Corrupt(Error):
46 | """Corrupt save file."""
47 |
48 | def __init__(self, msg, f = None):
49 | fn = None
50 | if f != None:
51 | fn = getattr(f, "name", None)
52 | self.filename = fn
53 | Error.__init__(self, "Corrupt save file: " + msg)
54 |
55 |
56 | class Eof(Corrupt):
57 | """Save file is truncated."""
58 |
59 | def __init__(self, f = None):
60 | Corrupt.__init__(self, "Unexpected EOF", f)
61 |
62 |
63 | class Subdir(Corrupt):
64 | def __init__(self, f = None):
65 | Corrupt.__init__(self, "Non-file in save file.", f)
66 |
67 |
68 | def poll_format(f):
69 | """Detect the type of PS2 save file.
70 |
71 | The file-like object f should be positioned at the start of the file.
72 | """
73 |
74 | hdr = f.read(PS2MC_DIRENT_LENGTH * 3)
75 |
76 | for format in formats:
77 | if format.poll(hdr):
78 | return format
79 |
80 | return None
81 |
82 |
83 | def format_for_filename(filename):
84 | filename = filename.lower()
85 | if filename.endswith(".max"):
86 | return format_max_drive
87 | #elif filename.endswith(".psv"):
88 | # return format_psv
89 | else:
90 | return format_ems
91 |
92 |
93 | #
94 | # Set up tables of illegal and problematic characters in file names.
95 | #
96 | _bad_filename_chars = ("".join(map(chr, list(range(32))))
97 | + "".join(map(chr, list(range(127, 256)))))
98 | _bad_filename_repl = "_" * len(_bad_filename_chars)
99 |
100 | if os.name in ["nt", "os2", "ce"]:
101 | _bad_filename_chars += '<>:"/\\|'
102 | _bad_filename_repl += "()_'___"
103 | _bad_filename_chars2 = _bad_filename_chars + "?* "
104 | _bad_filename_repl2 = _bad_filename_repl + "___"
105 | else:
106 | _bad_filename_chars += "/"
107 | _bad_filename_repl += "_"
108 | _bad_filename_chars2 = _bad_filename_chars + "?*'&|:[<>] \\\""
109 | _bad_filename_repl2 = _bad_filename_repl + "______(())___"
110 |
111 | _filename_trans = str.maketrans(_bad_filename_chars, _bad_filename_repl)
112 | _filename_trans2 = str.maketrans(_bad_filename_chars2, _bad_filename_repl2)
113 |
114 |
115 | def fix_filename(filename):
116 | """Replace illegal or problematic characters from a filename."""
117 | return filename.translate(_filename_trans)
118 |
119 |
120 | def make_longname(dirname, sf):
121 | """Return a string containing a verbose filename for a save file."""
122 |
123 | icon_sys = sf.get_icon_sys()
124 | title = ""
125 | if icon_sys is not None:
126 | title = icon_sys.get_title("ascii")
127 | title = title[0] + " " + title[1]
128 | title = " ".join(title.split())
129 | crc = binascii.crc32(b"")
130 | for (ent, data) in sf:
131 | crc = binascii.crc32(data, crc)
132 | if len(dirname) >= 12 and (dirname[0:2] in ("BA", "BJ", "BE", "BK")):
133 | if dirname[2:6] == "DATA":
134 | title = ""
135 | else:
136 | #dirname = dirname[2:6] + dirname[7:12]
137 | dirname = dirname[2:12]
138 |
139 | return fix_filename("%s %s (%08X)" % (dirname, title, crc & 0xFFFFFFFF))
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | class PS2SaveFile(object):
148 | """The state of a PlayStation 2 save file."""
149 |
150 | def __init__(self):
151 | self.file_ents = None
152 | self.file_data = None
153 | self.dirent = None
154 | self._defer_load_max_file = None
155 | self._compressed = None
156 |
157 |
158 | def set_directory(self, ent, defer_file = None):
159 | self._defer_load_max_file = defer_file
160 | self._compressed = None
161 | self.file_ents = [None] * ent[2]
162 | self.file_data = [None] * ent[2]
163 | self.dirent = list(ent)
164 |
165 |
166 | def set_file(self, i, ent, data):
167 | self.file_ents[i] = ent
168 | self.file_data[i] = data
169 |
170 |
171 | def get_directory(self):
172 | return self.dirent
173 |
174 |
175 | def get_file(self, i):
176 | if self._defer_load_max_file is not None:
177 | f = self._defer_load_max_file
178 | self._defer_load_max_file = None
179 | format_max_drive.load2(self, f)
180 | return self.file_ents[i], self.file_data[i]
181 |
182 |
183 | def __len__(self):
184 | return self.dirent[2]
185 |
186 |
187 | def __getitem__(self, index):
188 | return self.get_file(index)
189 |
190 |
191 | def get_icon_sys(self):
192 | for i in range(self.dirent[2]):
193 | (ent, data) = self.get_file(i)
194 | if ent[8].decode("ascii") == "icon.sys" and len(data) >= 964:
195 | try:
196 | return ps2iconsys.IconSys(data[:964])
197 | except ps2iconsys.Error:
198 | pass
199 | return None
200 |
201 |
--------------------------------------------------------------------------------
/mymcplusplus/save/utils.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | import struct
19 | from . import ps2save
20 |
21 | def read_fixed(f, n):
22 | """Read a string of a fixed length from a file."""
23 |
24 | s = f.read(n)
25 | if len(s) != n:
26 | raise ps2save.Eof(f)
27 | return s
28 |
29 |
30 | def read_long_string(f):
31 | """Read a string prefixed with a 32-bit length from a file."""
32 |
33 | length = struct.unpack(".
16 | #
17 |
18 | # automatically generated
19 | 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'}
20 |
--------------------------------------------------------------------------------
/mymcplusplus/utils.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | def zero_terminate(s):
19 | """Truncate a string at the first NUL ('\0') character, if any."""
20 |
21 | i = s.find(b'\0')
22 | if i == -1:
23 | return s
24 | return s[:i]
--------------------------------------------------------------------------------
/mymcplusplus/verbuild.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | MYMC_VERSION_BUILD = r'''0'''
19 | MYMC_VERSION_MAJOR = r'''3'''
20 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adubbz/mymcplusplus/fcd946557b6275ae47f8082e7cd3aac54269d9e5/screenshot.png
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | from setuptools import setup
19 |
20 | long_description = \
21 | """mymc++ is a PlayStation 2 memory card manager for to be used with .ps2 images as created by the PCSX2 emulator for example.
22 | It is based on mymc+ by Florian Märkl and the classic mymc utility created by Ross Ridge."""
23 |
24 | setup(
25 | name="mymcplusplus",
26 | version="3.1.0",
27 | description="A PlayStation 2 memory card manager",
28 | long_description=long_description,
29 | long_description_content_type="text/plain",
30 | url="https://github.com/Adubbz/mymcplusplus",
31 | author="Adubbz",
32 | license="GPLv3",
33 | clasifiers=[
34 | "Development Status :: 5 - Production/Stable",
35 | "Environment :: Console",
36 | "Environment :: MacOS X :: Cocoa",
37 | "Environment :: Win32 (MS Windows)",
38 | "Environment :: X11 Applications :: GTK",
39 | "Intended Audience :: End Users/Desktop",
40 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
41 | "Natural Language :: English",
42 | "Operating System :: Microsoft :: Windows",
43 | "Operating System :: MacOS",
44 | "Operating System :: POSIX :: Linux",
45 | "Programming Language :: Python",
46 | "Programming Language :: Python :: 3 :: Only",
47 | "Programming Language :: Python :: 3",
48 | "Programming Language :: Python :: 3.4",
49 | "Programming Language :: Python :: 3.5",
50 | "Programming Language :: Python :: 3.6",
51 | "Programming Language :: Python :: 3.7",
52 | "Programming Language :: Python :: 3.8",
53 | "Programming Language :: Python :: 3.9",
54 | "Topic :: Games/Entertainment",
55 | "Topic :: Multimedia :: Graphics :: 3D Rendering",
56 | "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator",
57 | "Topic :: System :: Archiving",
58 | "Topic :: System :: Archiving :: Backup",
59 | "Topic :: System :: Filesystems",
60 | "Topic :: Utilities"
61 | ],
62 | keywords="playstation ps2 mymc memory card save",
63 | packages=["mymcplusplus", "mymcplusplus.gui", "mymcplusplus.save"],
64 | entry_points={
65 | "console_scripts": [
66 | "mymcplusplus = mymcplusplus.mymc:main"
67 | ]
68 | },
69 | python_requires=">=3.4",
70 | install_requires=[],
71 | extras_require={
72 | "gui": ["wxPython", "pyopengl"]
73 | }
74 | )
75 |
--------------------------------------------------------------------------------
/test/conftest.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | import sys, os
19 | import pytest
20 | import tarfile
21 | import shutil
22 |
23 | test_dir = os.path.dirname(os.path.abspath(__file__))
24 | sys.path.append(os.path.abspath(os.path.join(test_dir, "..")))
25 |
26 | @pytest.fixture(scope="session")
27 | def data(tmpdir_factory):
28 | data_dir = tmpdir_factory.mktemp("data")
29 | path = os.path.join(test_dir, "data.tar.gz")
30 | tar = tarfile.open(path)
31 | tar.extractall(data_dir.strpath)
32 | return data_dir
33 |
34 | @pytest.fixture
35 | def mc01_copy(data, tmpdir):
36 | shutil.copy(data.join("mc01.ps2").strpath, tmpdir.strpath)
37 | return tmpdir
38 |
39 | @pytest.fixture
40 | def mc02_copy(data, tmpdir):
41 | shutil.copy(data.join("mc02.ps2").strpath, tmpdir.strpath)
42 | return tmpdir
43 |
--------------------------------------------------------------------------------
/test/data.tar.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Adubbz/mymcplusplus/fcd946557b6275ae47f8082e7cd3aac54269d9e5/test/data.tar.gz
--------------------------------------------------------------------------------
/test/test_ecc.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | import array
19 | from mymcplus import ps2mc_ecc
20 | import base64
21 |
22 |
23 | _data = [0x36, 0x76, 0x11, 0x24, 0xb1, 0xd2, 0xd2, 0x81, 0xd5, 0x47, 0x15, 0x41, 0xc9, 0x47, 0xb7, 0xf2,
24 | 0x6b, 0x00, 0x25, 0x34, 0x48, 0x1b, 0xbc, 0xcd, 0x07, 0x28, 0x9a, 0x88, 0x9c, 0xd3, 0x69, 0xda,
25 | 0x25, 0xa8, 0x39, 0x62, 0xd4, 0x8c, 0xc0, 0x25, 0x43, 0x04, 0x65, 0x22, 0xa8, 0xef, 0x44, 0x5f,
26 | 0x03, 0x91, 0xda, 0x23, 0x8c, 0x17, 0x24, 0xf5, 0x14, 0xf0, 0xd6, 0x5a, 0xc2, 0xe0, 0x5a, 0xb6,
27 | 0xbe, 0x6b, 0x4d, 0xb1, 0x2e, 0x20, 0x0d, 0x8a, 0x35, 0x19, 0x28, 0x7a, 0xc1, 0x8e, 0x3f, 0x1d,
28 | 0x87, 0xa9, 0x53, 0x96, 0x13, 0xe6, 0x4c, 0x16, 0x1b, 0x49, 0x0a, 0xdb, 0x88, 0xc5, 0xe6, 0xb1,
29 | 0x44, 0xdc, 0x35, 0xbd, 0x92, 0xcf, 0x53, 0x91, 0x81, 0xed, 0x70, 0xf0, 0xc0, 0xab, 0x41, 0xa8,
30 | 0xdd, 0xf2, 0x7d, 0xa4, 0x83, 0xa3, 0xb0, 0x4f, 0x77, 0xe0, 0x80, 0xba, 0xca, 0x5e, 0xf2, 0x01]
31 |
32 | _ecc = [97, 19, 108]
33 |
34 |
35 | def test_ecc_calculate_array():
36 | s = array.array('B', _data)
37 | assert ps2mc_ecc.ecc_calculate(s) == _ecc
38 |
39 |
40 | def test_ecc_calculate_bytes():
41 | s = bytes(_data)
42 | assert ps2mc_ecc.ecc_calculate(s) == _ecc
43 |
44 |
45 | def test_ecc_check_ok():
46 | s = list(_data)
47 | s = array.array('B', s)
48 |
49 | ecc = list(_ecc)
50 |
51 | res = ps2mc_ecc.ecc_check(s, ecc)
52 |
53 | assert res == ps2mc_ecc.ECC_CHECK_OK
54 | assert s.tobytes() == bytes(_data)
55 | assert ecc == _ecc
56 |
57 |
58 | def test_ecc_check_correct_data():
59 | s = list(_data)
60 | s[42] ^= 1
61 | s = array.array('B', s)
62 |
63 | ecc = list(_ecc)
64 |
65 | res = ps2mc_ecc.ecc_check(s, ecc)
66 |
67 | assert res == ps2mc_ecc.ECC_CHECK_CORRECTED
68 | assert s.tobytes() == bytes(_data)
69 | assert ecc == _ecc
70 |
71 |
72 | def test_ecc_check_correct_ecc():
73 | s = list(_data)
74 | s = array.array('B', s)
75 |
76 | ecc = list(_ecc)
77 | ecc[0] ^= 1
78 |
79 | res = ps2mc_ecc.ecc_check(s, ecc)
80 |
81 | assert res == ps2mc_ecc.ECC_CHECK_CORRECTED
82 | assert s.tobytes() == bytes(_data)
83 | assert ecc == _ecc
84 |
85 |
86 | def test_ecc_check_fail():
87 | s = list(_data)
88 | s[42] ^= 3
89 | s = array.array('B', s)
90 |
91 | ecc = list(_ecc)
92 |
93 | res = ps2mc_ecc.ecc_check(s, ecc)
94 |
95 | assert res == ps2mc_ecc.ECC_CHECK_FAILED
96 |
--------------------------------------------------------------------------------
/test/test_iconsys.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | import base64
19 |
20 | from mymcplus import ps2iconsys
21 |
22 | _icon_sys_data = base64.b64decode(
23 | b"UFMyRAAAIAAAAAAAcwAAABQAAAAUAAAAPAAAAAAAAAAUAAAAFAAAADwAAAAAAAAAFAAAABQAAAA8"
24 | b"AAAAAAAAABQAAAAUAAAAPAAAAAAAAAAAAAA/AAAAPwAAAD8AAAAAAAAAAM3MzD4AAIC/AAAAAAAA"
25 | b"AL8AAAC/AAAAPwAAAACPwvU+j8L1PvYo3D4AAAAAuB6FPsP1qD4AAAA/AAAAAClcDz4pXA8+XI/C"
26 | b"PgAAAACPwnU+j8J1Po/CdT4AAAAAgnGChYKaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
27 | b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAByZXouaWNvAAAAAAAAAAAAAAAAAAAAAAAA"
28 | b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcmV6LmljbwAAAAAAAAAAAAAA"
29 | b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHJlei5pY28AAAAA"
30 | b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
31 | b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
32 | b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
33 | b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
34 | b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
35 | b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
36 | b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
37 | b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
38 | b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
39 | b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")
40 |
41 |
42 | def test_iconsys():
43 | icon_sys = ps2iconsys.IconSys(_icon_sys_data)
44 | assert icon_sys.get_title("ascii") == ("Rez", "")
45 | assert icon_sys.icon_file_normal == "rez.ico"
46 | assert icon_sys.icon_file_copy == "rez.ico"
47 | assert icon_sys.icon_file_delete == "rez.ico"
--------------------------------------------------------------------------------
/test/test_lzari.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | import array
19 | from mymcplus.save import lzari
20 |
21 |
22 | def bits_to_str(bits):
23 | return "".join([{0: "0", 1: "1"}[b] for b in bits])
24 |
25 |
26 | def str_to_bits(s):
27 | return array.array("B", [{"0": 0, "1": 1}[c] for c in s])
28 |
29 |
30 | def test_string_to_bit_array():
31 | bits = lzari.string_to_bit_array(b"Bufudyne\xE2\x9D\x84")
32 | assert bits_to_str(bits) == "0100001001110101011001100111010101100100011110010110111001100101111000101001110110000100"
33 |
34 |
35 | def test_bit_array_to_string():
36 | bits = str_to_bits("0100000101100111011010010110010001111001011011100110010111110000100111111001010010100101")
37 | s = lzari.bit_array_to_string(bits)
38 | assert s == b"Agidyne\xf0\x9f\x94\xa5"
39 |
40 |
41 | def test_bit_array_to_string_pad():
42 | bits = str_to_bits("00110100"
43 | "00110010"
44 | "010")
45 | s = lzari.bit_array_to_string(bits)
46 | assert s == b"42@"
47 |
48 |
49 | def test_encode():
50 | s = b"You'll never see it coming."
51 | compressed = lzari.encode(s)
52 | assert compressed == b"\xb7%\xf0\x18\xb2\x123Z\xa4\xe9\xfb3\x892\x0e\xb1nE~\xf6\xdb\x80:\xa6\x92\x11\xf8"
53 |
54 |
55 | def test_decode():
56 | original_s = b"\xF0\x9F\x8E\xB5 Every day's great at your Junes \xF0\x9F\x8E\xB5"
57 | compressed = b";\xeaJrm\xe8\xbe\xd0\xc14(:\xa9\xb4\xd4\x8b\xde\tN\xb7-\xb1D\xac\x8e\xeb{\xa5$R>\xc4\x1b\xb8\xc2:\xb2"
58 | s = lzari.decode(compressed, len(original_s))
59 | assert s == original_s
60 |
61 |
62 | def test_encode_save():
63 | from data_lzari import max_data_raw as data
64 | from data_lzari import max_data_compressed as compressed_correct
65 | compressed = lzari.encode(data)
66 | assert len(compressed) == 3964
67 | assert compressed == compressed_correct
68 |
--------------------------------------------------------------------------------
/test/test_memorycard.py:
--------------------------------------------------------------------------------
1 | #
2 | # This file is part of mymc+, based on mymc by Ross Ridge.
3 | #
4 | # mymc+ is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 | #
9 | # mymc+ is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 | #
14 | # You should have received a copy of the GNU General Public License
15 | # along with mymc+. If not, see .
16 | #
17 |
18 | from mymcplus import mymc
19 |
20 |
21 | def md5(fn):
22 | import hashlib
23 | return hashlib.md5(open(fn, "rb").read()).hexdigest()
24 |
25 |
26 | def patch_fixed_time(monkeypatch, mod):
27 | def tod_now():
28 | return 42, 37, 22, 20, 4, 2018
29 | monkeypatch.setattr(mod, "tod_now", tod_now)
30 |
31 |
32 | def patch_localtime(monkeypatch):
33 | import time
34 | def localtime(secs=None):
35 | return time.gmtime(secs)
36 | monkeypatch.setattr(time, "localtime", localtime)
37 |
38 |
39 | def test_ls(monkeypatch, capsys, data):
40 | patch_localtime(monkeypatch)
41 | mymc.main(["mymcplus",
42 | "-i", data.join("mc01.ps2").strpath,
43 | "ls"])
44 |
45 | output = capsys.readouterr()
46 | assert output.err == ""
47 | assert output.out == ("rwx--d----+---- 4 2018-04-21 14:53:07 .\n"
48 | "-wx--d----+--H- 0 2018-04-21 14:53:00 ..\n"
49 | "rwx--d-------H- 4 2018-04-21 14:53:01 BEDATA-SYSTEM\n"
50 | "rwx--d----+---- 5 2018-04-21 14:53:09 BESCES-50501REZ\n")
51 |
52 |
53 | def test_extract(capsys, data, tmpdir):
54 | out_file = tmpdir.join("BESCES-50501REZ").strpath
55 |
56 | mymc.main(["mymcplus",
57 | "-i", data.join("mc01.ps2").strpath,
58 | "extract", "-o", out_file, "BESCES-50501REZ/BESCES-50501REZ"])
59 |
60 | output = capsys.readouterr()
61 | assert output.err == ""
62 | assert output.out == ""
63 |
64 | assert md5(out_file) == "5388344a2d4bb429b9a18ff683a8a691"
65 |
66 |
67 | def test_add(monkeypatch, capsys, mc01_copy, tmpdir):
68 | from mymcplus import ps2mc
69 | patch_fixed_time(monkeypatch, ps2mc)
70 | patch_localtime(monkeypatch)
71 |
72 | file = tmpdir.join("helloworld.txt").strpath
73 | with open(file, "w") as f:
74 | f.write("Hello World!\n")
75 |
76 | mc_file = mc01_copy.join("mc01.ps2").strpath
77 |
78 | mymc.main(["mymcplus",
79 | "-i", mc_file,
80 | "add", file])
81 |
82 | output = capsys.readouterr()
83 | assert output.out == ""
84 | assert output.err == ""
85 |
86 | mymc.main(["mymcplus",
87 | "-i", mc_file,
88 | "ls"])
89 |
90 | output = capsys.readouterr()
91 | assert output.out == ("rwx--d----+---- 5 2018-04-20 13:37:42 .\n"
92 | "-wx--d----+--H- 0 2018-04-21 14:53:00 ..\n"
93 | "rwx--d-------H- 4 2018-04-21 14:53:01 BEDATA-SYSTEM\n"
94 | "rwx--d----+---- 5 2018-04-21 14:53:09 BESCES-50501REZ\n"
95 | "rwx-f-----+---- 13 2018-04-20 13:37:42 helloworld.txt\n")
96 | assert output.err == ""
97 |
98 | assert md5(mc_file) == "faa75353a97328c7d8fe38756c38fdd9"
99 |
100 |
101 | def test_check_ok(capsys, data):
102 | mymc.main(["mymcplus",
103 | "-i", data.join("mc01.ps2").strpath,
104 | "check"])
105 |
106 | output = capsys.readouterr()
107 | assert output.out == "No errors found.\n"
108 | assert output.err == ""
109 |
110 |
111 | def test_check_root_directory(capsys, mc01_copy):
112 | mc_file = mc01_copy.join("mc01.ps2").strpath
113 | with open(mc_file, "r+b") as f:
114 | f.seek(0x200)
115 | f.write(b"\x13\x37")
116 |
117 | assert md5(mc_file) == "bec7e8c3884806024b9eb9599dc4315f"
118 |
119 | mymc.main(["mymcplus",
120 | "-i", mc_file,
121 | "check"])
122 |
123 | output = capsys.readouterr()
124 | assert output.err == mc_file + ": Root directory damaged.\n"
125 | assert output.out == ""
126 |
127 | # TODO: Should probably make more tests for check
128 |
129 |
130 | def test_clear(capsys, mc01_copy):
131 | mc_file = mc01_copy.join("mc01.ps2").strpath
132 |
133 | mymc.main(["mymcplus",
134 | "-i", mc_file,
135 | "clear", "-x", "BESCES-50501REZ"])
136 |
137 | output = capsys.readouterr()
138 | assert output.err == ""
139 | assert output.out == ""
140 |
141 | assert md5(mc_file) == "defaeba9b480676e8666dd4f3ff16643"
142 |
143 |
144 | def test_set(capsys, mc01_copy):
145 | mc_file = mc01_copy.join("mc01.ps2").strpath
146 |
147 | mymc.main(["mymcplus",
148 | "-i", mc_file,
149 | "set", "-K", "BESCES-50501REZ"])
150 |
151 | output = capsys.readouterr()
152 | assert output.err == ""
153 | assert output.out == ""
154 |
155 | assert md5(mc_file) == "d235a085e75a8201bd417b127ccd8908"
156 |
157 |
158 | def test_delete(capsys, mc01_copy):
159 | mc_file = mc01_copy.join("mc01.ps2").strpath
160 |
161 | mymc.main(["mymcplus",
162 | "-i", mc_file,
163 | "delete", "BESCES-50501REZ"])
164 |
165 | output = capsys.readouterr()
166 | assert output.err == ""
167 | assert output.out == ""
168 |
169 | assert md5(mc_file) == "143e640ccf3f22e48e1d1d4b10300d57"
170 |
171 |
172 | def test_df(capsys, data):
173 | mc_file = data.join("mc01.ps2").strpath
174 |
175 | mymc.main(["mymcplus",
176 | "-i", mc_file,
177 | "df"])
178 |
179 | output = capsys.readouterr()
180 | assert output.out == mc_file + ": 8268800 bytes free.\n"
181 | assert output.err == ""
182 |
183 |
184 | def test_dir(capsys, data):
185 | mc_file = data.join("mc01.ps2").strpath
186 |
187 | mymc.main(["mymcplus",
188 | "-i", mc_file,
189 | "dir", "-a"])
190 |
191 | output = capsys.readouterr()
192 | assert output.out == ("BEDATA-SYSTEM Your System\n"
193 | " 5KB Not Protected Configuration\n"
194 | "\n"
195 | "BESCES-50501REZ Rez\n"
196 | " 53KB Not Protected \n"
197 | "\n"
198 | "8,075 KB Free\n")
199 | assert output.err == ""
200 |
201 |
202 | def test_format(monkeypatch, capsys, tmpdir):
203 | from mymcplus import ps2mc
204 | patch_fixed_time(monkeypatch, ps2mc)
205 |
206 | mc_file = tmpdir.join("mc.ps2").strpath
207 |
208 | mymc.main(["mymcplus",
209 | "-i", mc_file,
210 | "format"])
211 |
212 | output = capsys.readouterr()
213 | assert output.out == ""
214 | assert output.err == ""
215 |
216 | assert md5(mc_file) == "18ab430278362e6e70ce7cda9081888f"
217 |
218 |
219 | def test_mkdir(monkeypatch, capsys, mc01_copy):
220 | from mymcplus import ps2mc
221 | patch_fixed_time(monkeypatch, ps2mc)
222 |
223 | mc_file = mc01_copy.join("mc01.ps2").strpath
224 |
225 | mymc.main(["mymcplus",
226 | "-i", mc_file,
227 | "mkdir", "p0rn"])
228 |
229 | output = capsys.readouterr()
230 | assert output.out == ""
231 | assert output.err == ""
232 |
233 | assert md5(mc_file) == "2be30a14246f34cdb157ea68f4905b85"
234 |
235 |
236 | def test_remove(capsys, mc01_copy):
237 | mc_file = mc01_copy.join("mc01.ps2").strpath
238 |
239 | mymc.main(["mymcplus",
240 | "-i", mc_file,
241 | "remove", "BESCES-50501REZ/BESCES-50501REZ"])
242 |
243 | output = capsys.readouterr()
244 | assert output.out == ""
245 | assert output.err == ""
246 |
247 | assert md5(mc_file) == "5d0ffec85ad1dc9a371e0ead55f4932b"
248 |
249 |
250 | def test_remove_nonempty(capsys, mc01_copy):
251 | mc_file = mc01_copy.join("mc01.ps2").strpath
252 |
253 | mymc.main(["mymcplus",
254 | "-i", mc_file,
255 | "remove", "BESCES-50501REZ"])
256 |
257 | output = capsys.readouterr()
258 | assert output.out == ""
259 | assert output.err == "BESCES-50501REZ: directory not empty\n"
260 |
261 |
262 | def test_export_psu(capsys, data, tmpdir):
263 | mc_file = data.join("mc01.ps2").strpath
264 |
265 | mymc.main(["mymcplus",
266 | "-i", mc_file,
267 | "export", "-d", tmpdir.strpath, "-p", "BESCES-50501REZ"])
268 |
269 | output = capsys.readouterr()
270 | assert output.out == "Exporing BESCES-50501REZ to BESCES-50501REZ.psu\n"
271 | assert output.err == ""
272 |
273 | assert md5(tmpdir.join("BESCES-50501REZ.psu").strpath) == "d86c82e559c8250c894fbbc4405d8789"
274 |
275 |
276 | def test_export_max(capsys, data, tmpdir):
277 | mc_file = data.join("mc01.ps2").strpath
278 |
279 | mymc.main(["mymcplus",
280 | "-i", mc_file,
281 | "export", "-d", tmpdir.strpath, "-m", "BESCES-50501REZ"])
282 |
283 | output = capsys.readouterr()
284 | assert output.out == "Exporing BESCES-50501REZ to BESCES-50501REZ.max\n"
285 |
286 | assert md5(tmpdir.join("BESCES-50501REZ.max").strpath) == "3f63d38668a0a5a5fa508ab8c3bb469a"
287 |
288 |
289 | def test_import_psu(monkeypatch, capsys, data, mc02_copy):
290 | from mymcplus import ps2mc
291 | patch_fixed_time(monkeypatch, ps2mc)
292 |
293 | mc_file = mc02_copy.join("mc02.ps2").strpath
294 | psu_file = data.join("BESCES-50501REZ.psu").strpath
295 |
296 | mymc.main(["mymcplus",
297 | "-i", mc_file,
298 | "import", psu_file])
299 |
300 | output = capsys.readouterr()
301 | assert output.out == "Importing " + psu_file + " to BESCES-50501REZ\n"
302 | assert output.err == ""
303 |
304 | assert md5(mc_file) == "4085992c23fc38d6c4ece5303dc77e74"
305 |
306 |
307 | def test_import_max(monkeypatch, capsys, data, mc02_copy):
308 | from mymcplus import ps2mc
309 | from mymcplus import ps2mc_dir
310 | from mymcplus.save import ps2save
311 | patch_fixed_time(monkeypatch, ps2mc)
312 | patch_fixed_time(monkeypatch, ps2mc_dir)
313 | patch_fixed_time(monkeypatch, ps2save)
314 |
315 | mc_file = mc02_copy.join("mc02.ps2").strpath
316 | max_file = data.join("BESCES-50501REZ.max").strpath
317 |
318 | mymc.main(["mymcplus",
319 | "-i", mc_file,
320 | "import", max_file])
321 |
322 | output = capsys.readouterr()
323 | assert output.out == "Importing " + max_file + " to BESCES-50501REZ\n"
324 |
325 | assert md5(mc_file) == "0e2dd8d53f05f6debe7d93aa726fc6e6"
326 |
327 |
328 | def test_import_sps(monkeypatch, capsys, data, mc02_copy):
329 | from mymcplus import ps2mc
330 | patch_fixed_time(monkeypatch, ps2mc)
331 |
332 | mc_file = mc02_copy.join("mc02.ps2").strpath
333 | sps_file = data.join("BESCES-50501REZ.sps").strpath
334 |
335 | mymc.main(["mymcplus",
336 | "-i", mc_file,
337 | "import", sps_file])
338 |
339 | output = capsys.readouterr()
340 | assert output.out == "Importing " + sps_file + " to BESCES-50501REZ\n"
341 | assert output.err == ""
342 |
343 | assert md5(mc_file) == "9726ad34016df2586eb9663a12f9ba02"
344 |
345 |
346 | def test_import_xps(monkeypatch, capsys, data, mc02_copy):
347 | from mymcplus import ps2mc
348 | patch_fixed_time(monkeypatch, ps2mc)
349 |
350 | mc_file = mc02_copy.join("mc02.ps2").strpath
351 | xps_file = data.join("BESCES-50501REZ.xps").strpath
352 |
353 | mymc.main(["mymcplus",
354 | "-i", mc_file,
355 | "import", xps_file])
356 |
357 | output = capsys.readouterr()
358 | assert output.out == "Importing " + xps_file + " to BESCES-50501REZ\n"
359 | assert output.err == ""
360 |
361 | assert md5(mc_file) == "9726ad34016df2586eb9663a12f9ba02"
362 |
363 |
364 | def test_import_cbs(monkeypatch, capsys, data, mc02_copy):
365 | from mymcplus import ps2mc
366 | from mymcplus import ps2mc_dir
367 | from mymcplus.save import ps2save
368 | patch_fixed_time(monkeypatch, ps2mc)
369 | patch_fixed_time(monkeypatch, ps2mc_dir)
370 | patch_fixed_time(monkeypatch, ps2save)
371 |
372 | mc_file = mc02_copy.join("mc02.ps2").strpath
373 | cbs_file = data.join("BESCES-50501REZ.cbs").strpath
374 |
375 | mymc.main(["mymcplus",
376 | "-i", mc_file,
377 | "import", cbs_file])
378 |
379 | output = capsys.readouterr()
380 | assert output.out == "Importing " + cbs_file + " to BESCES-50501REZ\n"
381 | assert output.err == ""
382 |
383 | assert md5(mc_file) == "897b0fbd1965dc02a442e1723bd3df27"
384 |
385 |
386 | def test_import_psv_ps2(monkeypatch, capsys, data, mc02_copy):
387 | from mymcplus import ps2mc
388 | from mymcplus import ps2mc_dir
389 | patch_fixed_time(monkeypatch, ps2mc)
390 | patch_fixed_time(monkeypatch, ps2mc_dir)
391 |
392 | mc_file = mc02_copy.join("mc02.ps2").strpath
393 | psv_file = data.join("BESCES-5050152455A.PSV").strpath
394 |
395 | mymc.main(["mymcplus",
396 | "-i", mc_file,
397 | "import", psv_file])
398 |
399 | output = capsys.readouterr()
400 | assert output.out == "Importing " + psv_file + " to BESCES-50501REZ\n"
401 | assert output.err == ""
402 |
403 | assert md5(mc_file) == "2872c456bcb647e78aeb3ce29e718309"
404 |
405 |
406 | def test_import_psv_ps1(monkeypatch, capsys, data, mc02_copy):
407 | from mymcplus import ps2mc
408 | from mymcplus import ps2mc_dir
409 | patch_fixed_time(monkeypatch, ps2mc)
410 | patch_fixed_time(monkeypatch, ps2mc_dir)
411 |
412 | mc_file = mc02_copy.join("mc02.ps2").strpath
413 | psv_file = data.join("BASLUS-006623030303030303041.PSV").strpath
414 |
415 | mymc.main(["mymcplus",
416 | "-i", mc_file,
417 | "import", psv_file])
418 |
419 | output = capsys.readouterr()
420 | assert output.out == "Importing " + psv_file + " to BASLUS-006620000000A\n"
421 | assert output.err == ""
422 |
423 | assert md5(mc_file) == "c9f26130a5de7548248a5fec80593a4d"
424 |
--------------------------------------------------------------------------------