├── .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 | ![Screenshot](screenshot.png) 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 | mymc icon 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 | mymc screenshot 25 | 26 | 38 | 39 |
40 | 41 |

42 | New in Version 2.6 43 |

44 | 47 | 48 |

49 | New in Version 2.5 50 |

51 | 55 | 56 |

57 | New in Version 2.4 58 |

59 | 66 | 67 |

68 | New in Version 2.1 69 |

70 | 75 | 76 |

77 | New in Version 1.6 78 |

79 |

80 |

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 | --------------------------------------------------------------------------------