commit c3e146e2ee5ee70a45fb398e0d3cf367aa110293 Author: Richard Jones Date: Wed Jul 20 17:09:57 2022 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed4a7e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,112 @@ +mame.json +mame/* +mfe.ini +mame.dat +mess.dat +mame_exclude.txt + +.vscode/tags + +# Mac Files +.DS_Store + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..2ab7d6a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,60 @@ +{ + "version": "0.2.0", + "configurations": [{ + "name": "Python", + "type": "python", + "request": "launch", + "stopOnEntry": false, + "pythonPath": "${config:python.pythonPath}", + "program": "${workspaceRoot}/mfe.py", + "cwd": "${workspaceRoot}", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "RedirectOutput" + ] + }, + { + "name": "Integrated Terminal/Console", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "program": "${file}", + "cwd": "", + "console": "integratedTerminal", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit" + ] + }, + { + "name": "External Terminal/Console", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "pythonPath": "${config:python.pythonPath}", + "program": "${file}", + "cwd": "", + "console": "externalTerminal", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit" + ] + }, + { + "name": "Attach (Remote Debug)", + "type": "python", + "request": "attach", + "localRoot": "${workspaceRoot}", + "remoteRoot": "${workspaceRoot}", + "port": 3000, + "secret": "my_secret", + "host": "localhost" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..44cdbf3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "python.formatting.provider": "yapf", + "python.linting.flake8Enabled": true, + "python.linting.flake8Args": ["--ignore", "E501,E722,C0103"], + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "python.pythonPath": "/Users/rich/.local/share/virtualenvs/mfe-K8QhrIKQ/bin/python", +} \ No newline at end of file diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..ee43616 --- /dev/null +++ b/Pipfile @@ -0,0 +1,22 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +configobj = ">=5.0.6" +appdirs = ">=1.4.3" +lxml = ">=3.8.0" +titlecase = ">=0.11.0" +#pygame = {path = "./../pygame/dist/pygame-1.9.5.dev0-py3.7-macosx-10.14-x86_64.egg"} +#Pygame = ">=1.9.3" +pygame = {file = "file:///Users/rich/git/pygame/dist/pygame-1.9.5.dev0-cp37-cp37m-macosx_10_14_x86_64.whl"} + +[dev-packages] +pylint = "*" +flake8 = "*" +cx-freeze = {editable = true,git = "https://github.com/anthony-tuininga/cx_Freeze.git"} +yapf = "*" + +[requires] +python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..52d216a --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,210 @@ +{ + "_meta": { + "hash": { + "sha256": "d7ff85d4507b7eaaf2f2f6433e8450ffcf52bdd74a889e3051a38acd4cd3c62a" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "index": "pypi", + "version": "==1.4.3" + }, + "configobj": { + "hashes": [ + "sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902" + ], + "index": "pypi", + "version": "==5.0.6" + }, + "lxml": { + "hashes": [ + "sha256:02bc220d61f46e9b9d5a53c361ef95e9f5e1d27171cd461dddb17677ae2289a5", + "sha256:22f253b542a342755f6cfc047fe4d3a296515cf9b542bc6e261af45a80b8caf6", + "sha256:2f31145c7ff665b330919bfa44aacd3a0211a76ca7e7b441039d2a0b0451e415", + "sha256:36720698c29e7a9626a0dc802ef8885f8f0239bfd1689628ecd459a061f2807f", + "sha256:438a1b0203545521f6616132bfe0f4bca86f8a401364008b30e2b26ec408ce85", + "sha256:4815892904c336bbaf73dafd54f45f69f4021c22b5bad7332176bbf4fb830568", + "sha256:5be031b0f15ad63910d8e5038b489d95a79929513b3634ad4babf77100602588", + "sha256:5c93ae37c3c588e829b037fdfbd64a6e40c901d3f93f7beed6d724c44829a3ad", + "sha256:60842230678674cdac4a1cf0f707ef12d75b9a4fc4a565add4f710b5fcf185d5", + "sha256:62939a8bb6758d1bf923aa1c13f0bcfa9bf5b2fc0f5fa917a6e25db5fe0cfa4e", + "sha256:75830c06a62fe7b8fe3bbb5f269f0b308f19f3949ac81cfd40062f47c1455faf", + "sha256:81992565b74332c7c1aff6a913a3e906771aa81c9d0c68c68113cffcae45bc53", + "sha256:8c892fb0ee52c594d9a7751c7d7356056a9682674b92cc1c4dc968ff0f30c52f", + "sha256:9d862e3cf4fc1f2837dedce9c42269c8c76d027e49820a548ac89fdcee1e361f", + "sha256:a623965c086a6e91bb703d4da62dabe59fe88888e82c4117d544e11fd74835d6", + "sha256:a7783ab7f6a508b0510490cef9f857b763d796ba7476d9703f89722928d1e113", + "sha256:aab09fbe8abfa3b9ce62aaf45aca2d28726b1b9ee44871dbe644050a2fff4940", + "sha256:abf181934ac3ef193832fb973fd7f6149b5c531903c2ec0f1220941d73eee601", + "sha256:ae07fa0c115733fce1e9da96a3ac3fa24801742ca17e917e0c79d63a01eeb843", + "sha256:b9c78242219f674ab645ec571c9a95d70f381319a23911941cd2358a8e0521cf", + "sha256:bccb267678b870d9782c3b44d0cefe3ba0e329f9af8c946d32bf3778e7a4f271", + "sha256:c4df4d27f4c93b2cef74579f00b1d3a31a929c7d8023f870c4b476f03a274db4", + "sha256:caf0e50b546bb60dfa99bb18dfa6748458a83131ecdceaf5c071d74907e7e78a", + "sha256:d3266bd3ac59ac4edcd5fa75165dee80b94a3e5c91049df5f7c057ccf097551c", + "sha256:db0d213987bcd4e6d41710fb4532b22315b0d8fb439ff901782234456556aed1", + "sha256:dbbd5cf7690a40a9f0a9325ab480d0fccf46d16b378eefc08e195d84299bfae1", + "sha256:e16e07a0ec3a75b5ee61f2b1003c35696738f937dc8148fbda9fe2147ccb6e61", + "sha256:e175a006725c7faadbe69e791877d09936c0ef2cf49d01b60a6c1efcb0e8be6f", + "sha256:edd9c13a97f6550f9da2236126bb51c092b3b1ce6187f2bd966533ad794bbb5e", + "sha256:fa39ea60d527fbdd94215b5e5552f1c6a912624521093f1384a491a8ad89ad8b" + ], + "index": "pypi", + "version": "==4.2.5" + }, + "pygame": { + "file": "file:///Users/rich/git/pygame/dist/pygame-1.9.5.dev0-cp37-cp37m-macosx_10_14_x86_64.whl", + "hashes": [ + "sha256:9b811036eebebca68f166683f21427d1682f62159cb6de2b6724dec2be322c2a", + "sha256:9fba43308b121628a6c70edc7fac4b07ae5e30ccfe5bf36b914b3a2130e1671a", + "sha256:a8e217e5c39c5ea2900f450f2e8acdb97487053a13447f680608200a956e8fe3", + "sha256:b48da4cd1b70487791767fcae7e9e3ce8eaf4d340667b5eea14d49a4104e23d1" + ], + "version": "==1.9.5.dev0" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "titlecase": { + "hashes": [ + "sha256:84de7a97fb702c400e5ba11c6b30849944b39db12e20fbf4515a23c7538a0611", + "sha256:95d643a0c08097c02933aced707adfe1c275c335019e8e514dea782a465c5b84" + ], + "index": "pypi", + "version": "==0.12.0" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:35b032003d6a863f5dcd7ec11abd5cd5893428beaa31ab164982403bcb311f22", + "sha256:6a5d668d7dc69110de01cdf7aeec69a679ef486862a0850cc0fd5571505b6b7e" + ], + "version": "==2.1.0" + }, + "cx-freeze": { + "editable": true, + "git": "https://github.com/anthony-tuininga/cx_Freeze.git", + "ref": "9e06b761740a9e93431ee7ea8d0b10f786446a6a" + }, + "flake8": { + "hashes": [ + "sha256:6a35f5b8761f45c5513e3405f110a86bea57982c3b75b766ce7b65217abe1670", + "sha256:c01f8a3963b3571a8e6bd7a4063359aff90749e160778e03817cd9b71c9e07d2" + ], + "index": "pypi", + "version": "==3.6.0" + }, + "isort": { + "hashes": [ + "sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", + "sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", + "sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497" + ], + "version": "==4.3.4" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "pycodestyle": { + "hashes": [ + "sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83", + "sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a" + ], + "version": "==2.4.0" + }, + "pyflakes": { + "hashes": [ + "sha256:9a7662ec724d0120012f6e29d6248ae3727d821bba522a0e6b356eff19126a49", + "sha256:f661252913bc1dbe7fcfcbf0af0db3f42ab65aabd1a6ca68fe5d466bace94dae" + ], + "version": "==2.0.0" + }, + "pylint": { + "hashes": [ + "sha256:689de29ae747642ab230c6d37be2b969bf75663176658851f456619aacf27492", + "sha256:771467c434d0d9f081741fec1d64dfb011ed26e65e12a28fe06ca2f61c4d556c" + ], + "index": "pypi", + "version": "==2.2.2" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "wrapt": { + "hashes": [ + "sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6" + ], + "version": "==1.10.11" + }, + "yapf": { + "hashes": [ + "sha256:8aa7f9abdb97b4da4d3227306b88477982daafef0a96cc41639754ca31f46d55", + "sha256:f2df5891481f94ddadfbf8ae8ae499080752cfb06005a31bbb102f3012f8b944" + ], + "index": "pypi", + "version": "==0.25.0" + } + } +} diff --git a/app.py b/app.py new file mode 100644 index 0000000..ed3b53d --- /dev/null +++ b/app.py @@ -0,0 +1,305 @@ +import sys +# from pprint import pprint + +import pygame + +import csvfile +import gui as guimodule +import mame +import mess +from configfile import cfg, config_write, get_emu, get_emulators, set_emu +from utils import addscanlines, image_from_data, run_emulator + +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +GREY = (70, 70, 70) +DARKGREY = (50, 50, 50) + + +class app(object): + def __init__(self): + if get_emu('EmulatorType') == 'MAME': + self.roms = mame.MameROMs(cfg['datadir'], cfg['HideMature'], + get_emu()) + if get_emu('EmulatorType') == 'MESS': + self.roms = mess.MessROMs(cfg['datadir'], get_emu()) + if get_emu('EmulatorType') == 'CSV': + self.roms = csvfile.csvROMs(cfg['datadir'], get_emu()) + + self.update_menus() + + pygame.init() + pygame.font.init() + + iconsurface = pygame.image.load("arcade.png") + pygame.display.set_icon(iconsurface) + + if cfg['FullScreen']: + self.screen = pygame.display.set_mode( + (cfg['ResolutionX'], cfg['ResolutionY']), pygame.FULLSCREEN) + else: + self.screen = pygame.display.set_mode((cfg['ResolutionX'], + cfg['ResolutionY'])) + + self.font = pygame.font.Font( + pygame.font.match_font(cfg['Font']), cfg['FontSize']) + + self.status_surface = pygame.Surface(( # pylint: disable=E1121 + cfg['ResolutionX'], self.font.get_height())) + + self.oldmouse = pygame.mouse.set_visible(False) + + pygame.key.set_repeat(100, 30) + + self.listwidth = int(cfg['ResolutionX'] / 2) + + self.gui = guimodule.gui(self.screen) + + self.gui.add_menu( + 0, + 0, + self.listwidth, + cfg['ResolutionY'] - self.font.get_height(), + border=False, + data=self.roms, + fg=(255, 255, 255), + bg=(0, 0, 0), + name='MainList', + startitem=get_emu('CurrentGame'), + itemattop=get_emu('GameAtTop')) + + self.gui.render() + + def update_menus(self): + rootmenu = [ + 'Show Artwork', 'Categories', 'Sort', + 'Hide Clones' if get_emu('ShowClones') else 'Show Clones', + 'Change Emulator', 'Quit' + ] + sortmenu = ['Name Asc', 'Name Dec', 'Year Asc', 'Year Dec'] + emulatormenu = get_emulators() + self.menus = { + 'rootmenu': rootmenu, + 'sortmenu': sortmenu, + 'emulatormenu': emulatormenu, + 'categorymenu': self.roms.categories + } + + def run(self): + keyup = False + quitcount = 0 + + while True: + event = self.gui.waitevent() + if event: + if event.type == pygame.QUIT: + break + if event.type == pygame.KEYDOWN: + keyup = False + if event.key == pygame.K_ESCAPE and self.gui.currentobject == 1: # Main List + quitcount += 1 + if quitcount == cfg['QuitKeyPresses']: + break + elif event.key == pygame.K_ESCAPE: + quitcount = 0 + if self.gui.currentobject > 1: + self.gui.deletelastobject() + else: + quitcount = 0 + if event.key in cfg[ + 'KeySelect'] and self.gui.currentobject == 1: + if sys.platform == 'darwin': + print('Running %s' % + self.roms[self.gui.getcurrentitem()].name) + run_emulator( + get_emu('EmulatorType'), get_emu('EXE'), + self.roms[self.gui.getcurrentitem()].name) + elif event.key in cfg[ + 'KeyGameHistory'] and self.gui.currentobject == 1: + history = self.roms.get_history( + self.roms[self.gui.getcurrentitem()]) + if history: + self.gui.add_notepad(-1, -1, 800, 600, history, + True, WHITE, DARKGREY, + 'history', 'Game History') + elif event.key in cfg[ + 'KeyGameInfo'] and self.gui.currentobject == 1: + info = self.roms.get_info( + self.roms[self.gui.getcurrentitem()]) + if info: + self.gui.add_notepad(-1, -1, 800, 600, info, True, + WHITE, DARKGREY, 'info', + 'Game Information') + elif event.key in cfg[ + 'KeyPopup'] and self.gui.currentobject == 1: + self.gui.add_menu(40, 40, -1, -1, + self.menus['rootmenu'], True, WHITE, + GREY, 'rootmenu', 0) + elif event.key in cfg[ + 'KeyPopup'] and self.gui.currentobject > 1: + self.gui.deletelastobject() + elif event.key in cfg['KeyShowArtwork']: + pass + # TODO: copy from below + elif event.key in cfg['KeySelect']: + currentitem = self.gui.getcurrentitem() + menuname = self.gui.getmenuname() + item = self.menus[menuname][currentitem] + if menuname == 'rootmenu': + if item == 'Quit': + break + elif item == 'Show Artwork': + print('hello') + aw = self.roms.get_artwork( + self.roms[self.gui.getcurrentitem(0)]) + if aw != []: + self.gui.add_image_notepad( + -1, -1, 800, 600, aw, True, WHITE, + DARKGREY, 'artwork', 'Artwork') + elif item == 'Categories': + self.gui.add_menu( + 80, 45, -1, + -(self.screen.get_height() - 45 - + (self.font.get_height() * 2) - 8), + self.menus['categorymenu'], True, WHITE, + GREY, 'categorymenu', 0) + elif item == 'Sort': + self.gui.add_menu(80, 45, -1, -1, + self.menus['sortmenu'], True, + WHITE, GREY, 'sortmenu', 0) + elif item == 'Show Clones': + set_emu('ShowClones', True) + self.update_menus() + self.roms.filter() + self.gui.deleteallmenus() + self.gui.setcurrentitem(0) + elif item == 'Hide Clones': + set_emu('ShowClones', False) + self.update_menus() + self.roms.filter() + self.gui.deleteallmenus() + self.gui.setcurrentitem(0) + elif item == 'Change Emulator': + self.gui.add_menu( + 80, 45, -1, + -(self.screen.get_height() - 45 - + (self.font.get_height() * 2) - 8), + self.menus['emulatormenu'], True, WHITE, + GREY, 'emulatormenu', 0) + elif menuname == 'sortmenu': + set_emu('Sort', item) + self.roms.filter() + self.gui.deleteallmenus() + elif menuname == 'categorymenu': + set_emu('Category', item) + self.roms.filter() + self.gui.deleteallmenus() + self.gui.setcurrentitem(0) + elif menuname == 'emulatormenu': + cfg['Emulator'] = item + if get_emu('EmulatorType') == 'MAME': + self.roms = mame.MameROMs( + cfg['datadir'], cfg['HideMature'], + get_emu()) + if get_emu('EmulatorType') == 'MESS': + self.roms = mess.MessROMs( + cfg['datadir'], get_emu()) + if get_emu('EmulatorType') == 'CSV': + self.roms = csvfile.csvROMs( + cfg['datadir'], get_emu()) + + self.update_menus() + self.gui.deleteallmenus() + self.gui.deletelastobject() + self.gui.add_menu( + 0, + 0, + self.listwidth, + cfg['ResolutionY'] - self.font.get_height(), + border=False, + data=self.roms, + fg=(255, 255, 255), + bg=(0, 0, 0), + name='MainList', + startitem=get_emu('CurrentGame'), + itemattop=get_emu('GameAtTop')) + + if event.type == pygame.KEYUP: + keyup = True + + if self.gui.currentobject == 1 and self.roms: + currentitem = self.gui.getcurrentitem() + + self.screen.fill(BLACK) + self.status_surface.fill((128, 128, 128)) + + if keyup: + pass + if cfg['AlwaysChangeSnap'] or keyup: + data = self.roms.get_snap(self.roms[currentitem], 'snap') + screenshot_surface = image_from_data(data, (508, 480)) + + if cfg['ScanLines']: + screenshot_surface = addscanlines(screenshot_surface) + + self.screen.blit( + screenshot_surface, + (self.listwidth + 2 + ( + (self.screen.get_width() - self.listwidth + 2) / 2) + - (screenshot_surface.get_width() / 2), 102)) + + data = self.roms.get_snap(self.roms[currentitem], + 'marquee') + marquee_surface = image_from_data(data, (640, 100)) + + self.screen.blit( + marquee_surface, + (self.listwidth + 2 + ( + (self.screen.get_width() - self.listwidth + 2) / 2) + - (marquee_surface.get_width() / 2), 0)) + + data = self.roms.get_snap(self.roms[currentitem], 'title') + title_surface = image_from_data( + data, (640, cfg['ResolutionY'] - + (screenshot_surface.get_height() + + marquee_surface.get_height() + 4 + + self.status_surface.get_height()))) + + self.screen.blit( + title_surface, (self.listwidth + 2 + ( + (self.screen.get_width() - self.listwidth + 2) / 2) + - (title_surface.get_width() / 2), + (screenshot_surface.get_height() + + marquee_surface.get_height() + 4))) + + size = self.font.size( + '%d / %d %s' % (currentitem + 1, len(self.roms), + get_emu('Sort')))[0] + + self.status_surface.blit( + self.font.render( + '%d / %d %s' % (currentitem + 1, len(self.roms), + get_emu('Sort')), True, BLACK, + (128, 128, 128)), (cfg['ResolutionX'] - size - 5, 0)) + self.status_surface.blit( + self.font.render( + '%s %s' % (self.roms[currentitem].year, + self.roms[currentitem].category), True, + BLACK, (128, 128, 128)), (5, 0)) + + try: + self.screen.blit( + self.status_surface, + (0, cfg['ResolutionY'] - self.font.get_height())) + except: + pass + + self.gui.render() + + pygame.mouse.set_visible(self.oldmouse) + pygame.key.set_repeat() + + set_emu('CurrentGame', self.gui.getcurrentitem()) + set_emu('GameAtTop', self.gui.getitemattop()) + + config_write() diff --git a/arcade.png b/arcade.png new file mode 100644 index 0000000..322bbd6 Binary files /dev/null and b/arcade.png differ diff --git a/cfg/___empty.cfg b/cfg/___empty.cfg new file mode 100644 index 0000000..bf38827 --- /dev/null +++ b/cfg/___empty.cfg @@ -0,0 +1,5 @@ + + + + + diff --git a/cfg/default.cfg b/cfg/default.cfg new file mode 100644 index 0000000..02f0986 --- /dev/null +++ b/cfg/default.cfg @@ -0,0 +1,5 @@ + + + + + diff --git a/configfile.py b/configfile.py new file mode 100644 index 0000000..a186ce2 --- /dev/null +++ b/configfile.py @@ -0,0 +1,137 @@ +from __future__ import print_function + +import os + +from appdirs import user_data_dir +from configobj import ConfigObj, flatten_errors, Section +from validate import Validator + +from exclude import MAME_EXCLUDE +from utils import get_pygame_keydict, get_keys + +CFG = '''ResolutionX = integer(default=1024) +ResolutionY = integer(default=768) +FullScreen = boolean(default=False) +Font = string(default='microsoftsansserif') +FontSize = integer(default=13) +QuitKeyPresses = integer(min=1,default=3) +ScanLines = boolean(default=True) +Key_Up = list(default=list('K_UP')) +Key_Down = list(default=list('K_DOWN')) +Key_PgUp = list(default=list('K_LEFT','K_PAGEUP')) +Key_PgDn = list(default=list('K_RIGHT','K_PAGEDOWN')) +Key_Home = list(default=list('K_HOME')) +Key_End = list(default=list('K_END')) +Key_Select = list(default=list('K_RETURN','K_1')) +Key_GameInfo = list(default=list('K_5')) +Key_GameHistory = list(default=list('K_6')) +Key_Popup = list(default=list('K_2')) +Key_ShowArtwork = list(default=list()) +AlwaysChangeSnap = boolean(default=True) +HideMature = boolean(default=True) +Emulator = string() +[__many__] +EXE = string() +Version = string(default=None) +ShowClones = boolean(default=True) +Category = string(default='All Games') +GameAtTop = integer(min=0,default=0) +CurrentGame = integer(min=0,default=0) +EmulatorType = option('MAME','MESS','CSV',default='MAME') +StatusFilter = list(default=list()) +Sort = option('Name Asc','Name Dec','Year Asc','Year Dec',default='Name Asc') +SnapDir = string(default=None) +CSVFile = string(default=None) +ArtworkDirs = list(default=list()) +''' + +appname = 'MFE' +appauthor = 'RMJ' + +datadir = user_data_dir(appname, appauthor) +datadir = os.getcwd() + +if not os.path.isdir(datadir): + os.makedirs(datadir) + +spec = CFG.split('\n') +cfg = ConfigObj( + os.path.join(datadir, 'mfe.ini'), configspec=spec, encoding='UTF8') + +validator = Validator() +result = cfg.validate(validator, copy=True, preserve_errors=True) + +stop = False +for entry in flatten_errors(cfg, result): + section_list, key, error = entry + if key is not None: + section_list.append(key) + else: + section_list.append('[missing section]') + section_string = ', '.join(section_list) + if error is False: + error = 'Missing value or section.' + print(section_string, ' = ', error) + stop = True + +cfg.write() + +if stop: + exit() + +cfg["datadir"] = datadir + +if not os.path.isfile(os.path.join(datadir, 'mame_exclude.txt')): + with open(os.path.join(datadir, "mame_exclude.txt"), "wt") as f: + f.write(MAME_EXCLUDE) + +keys = get_pygame_keydict() +cfg['KeyUp'] = get_keys(keys, cfg['Key_Up']) +cfg['KeyDown'] = get_keys(keys, cfg['Key_Down']) +cfg['KeyPgUp'] = get_keys(keys, cfg['Key_PgUp']) +cfg['KeyPgDn'] = get_keys(keys, cfg['Key_PgDn']) +cfg['KeyHome'] = get_keys(keys, cfg['Key_Home']) +cfg['KeyEnd'] = get_keys(keys, cfg['Key_End']) +cfg['KeySelect'] = get_keys(keys, cfg['Key_Select']) +cfg['KeyGameInfo'] = get_keys(keys, cfg['Key_GameInfo']) +cfg['KeyGameHistory'] = get_keys(keys, cfg['Key_GameHistory']) +cfg['KeyPopup'] = get_keys(keys, cfg['Key_Popup']) +cfg['KeyShowArtwork'] = get_keys(keys, cfg['Key_ShowArtwork']) + + +def config_write(): + Exclude = [ + 'KeyUp', 'KeyDown', 'KeyPgUp', 'KeyPgDn', 'KeySelect', 'KeyGameInfo', + 'KeyGameHistory', 'KeyPopup', 'KeyHome', 'KeyEnd', 'KeyShowArtwork', + 'datadir' + ] + + t = {} + for item in Exclude: + t[item] = cfg[item] + del cfg[item] + + cfg.write() + + for item in t: + cfg[item] = t[item] + + +def get_emu(option=None): + if option: + r = cfg[cfg['Emulator']][option] + else: + r = cfg[cfg['Emulator']] + return r + + +def set_emu(option, data): + cfg[cfg['Emulator']][option] = data + + +def get_emulators(): + r = [] + for k in cfg.keys(): + if isinstance(cfg[k], Section): + r.append(k) + return r diff --git a/csvfile.py b/csvfile.py new file mode 100644 index 0000000..8b08712 --- /dev/null +++ b/csvfile.py @@ -0,0 +1,143 @@ +import csv +import os +import zipfile + + +class csvROM(object): + ''' One CSV ROM ''' + + def __init__(self): + self.name = '' + self.cloneof = None + self.description = '' + self.year = '' + self.manufacturer = '' + self.status = '' + self.category = '' + self.snap = None + + def __str__(self): + return self.description + + def __repr__(self): + return '%s("%s", "%s", "%s", "%s", "%s", "%s", "%s")' % ( + self.__class__.__name__, self.name, self.cloneof, self.description, + self.year, self.manufacturer, self.status, self.category) + + +class csvROMs(object): + ''' A Collection of CSV Roms ''' + + def __init__(self, data_dir, cfg): + self.all_roms = [] + self.roms = [] + self.all_categories = [] + self.categories = [] + self.catdict = {} + self.len = 0 + self.data_dir = data_dir + self.cfg = cfg + self.emulator_dir = os.path.split(cfg['EXE'])[0] + + self.parse() + self.filter() + + self.snapdir = cfg['SnapDir'] + if os.path.isfile(os.path.join(self.snapdir, 'snap.zip')): + self.snapfile = zipfile.ZipFile( + os.path.join(self.snapdir, "snap.zip")) + else: + self.snapfile = None + + def __len__(self): + return self.len + + def __getitem__(self, item): + return self.roms[item] + + def filter(self): + self.roms = [] + + for rom in self.all_roms: + if self.cfg["Category"] == "All Games" or self.cfg["Category"] == rom.category: + self.roms.append(rom) + + if self.cfg["Sort"] == "Name Asc": + self.roms.sort(key=lambda x: x.description.lower(), reverse=False) + elif self.cfg["Sort"] == "Name Dec": + self.roms.sort(key=lambda x: x.description.lower(), reverse=True) + elif self.cfg["Sort"] == "Year Asc": + self.roms.sort(key=lambda x: x.year.lower(), reverse=False) + elif self.cfg["Sort"] == "Year Dec": + self.roms.sort(key=lambda x: x.year.lower(), reverse=True) + + self.len = len(self.roms) + + self.categories = self.all_categories + + def parse(self): + ''' Parse csv file ''' + if os.path.isfile(self.cfg['CSVFile']): + with open(self.cfg['CSVFile']) as f: + reader = csv.DictReader(f) + + for game in reader: + rom = csvROM() + rom.name = game['romname'] + rom.description = game[ + 'description'] if 'description' in game.keys( + ) else game['romname'] + rom.snap = game['snap'] if 'snap' in game.keys() else game[ + 'romname'] + rom.year = game[ + 'year'] if 'year' in game.keys() else 'Unknown Year' + rom.category = game[ + 'category'] if 'category' in game.keys() else 'Unknown' + self.all_roms.append(rom) + + self.all_categories.insert(0, 'All Games') + self.all_categories.append('Unknown') + + return True + + def get_snap(self, rom, type_of_image): # pylint : disable=R0912 + data = None + image_file = None + image_dir = None + + if type_of_image == 'snap': + image_dir = self.snapdir + image_file = self.snapfile + + if image_file: + try: + data = image_file.read(rom.name + ".png") + except: + try: + data = image_file.read(rom.cloneof + '.png') + except: + data = None + elif image_dir: + fn = os.path.join(image_dir, rom.name + '.png') + if os.path.isfile(fn): + with open(fn, 'rb') as f: + data = f.read() + elif rom.cloneof: + fn = os.path.join(image_dir, rom.cloneof + '.png') + if os.path.isfile(fn): + with open(fn, 'rb') as f: + data = f.read() + else: + data = None + else: + data = None + else: + data = None + + return data + + def get_info(self, rom): + return + + def get_history(self, rom): + return diff --git a/exclude.py b/exclude.py new file mode 100644 index 0000000..1ab7d39 --- /dev/null +++ b/exclude.py @@ -0,0 +1,99 @@ +MAME_EXCLUDE = """3D Printer +Astrological Computer +Audio Sequencer +Bank-teller Terminal +Barcode Printer +Bridge Machine +Business Computer / Terminal +Calculator / Pocket Computer +Cash Counter +Chess Machine +Clock +Credit Card Terminal +DVD Player +DVD Reader/Writer +Dame Machine +Development Computer +Devices +Document Processors +Dot-Matrix Display +Drum Machine +EPROM Programmer +Educational Game +Electromechanical / Change Money +Electromechanical / Coin Pusher +Electromechanical / Misc. +Electromechanical / Pinball +Electromechanical / Redemption +Electromechanical / Reels +Electromechanical / Utilities +Electronic Board Game +Electronic Typewriter +Engine Control Unit +Gambling Board +Game Console +Game Console Expansion +Graphic Tablet +Graphics Display Controller +Handheld Child Computers +Handheld Game +Handheld Game Console +Home Computer +In Circuit Emulator +JukeBox +Kit Computer +Laptop / Notebook / Portable +Laser Printer +Matrix Printer +Microcomputer +Misc. +Misc. * Mature * +Mobile Phone +Modem +Multi-cart Board +Network Processor +Not Classified +Pinball +Pinball * Mature * +Pinball / Pachinko +Pinball / Pachinko * Mature * +Player +Pocket Device / Pad / PDA +Portable Media Player +Print Club +Printer Handbook +Programming Machine +Punched Card Computer +Quiz / Chinese +Quiz / French +Quiz / German +Quiz / Italian +Quiz / Japanese +Quiz / Japanese * Mature * +Quiz / Japanese - Music +Quiz / Korean +Quiz / Spanish +Rhythm / Dance +Rhythm / Instruments +Rhythm / Misc. +Robot Control +Satellite Receiver +Single Board Computer +Speech Synthesizer +Synthesizer +System / BIOS +System / Device +Talking Calculator +Telephone / ComputerPhone +Test ROM +Thermal Printer +Toy cars +Training Board +Utilities / Test +Utilities / Update +VTR Control +Virtual Environment +Wavetables Generator +Word-processing Machine +Workstation / Server +""" \ No newline at end of file diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..f554177 --- /dev/null +++ b/gui.py @@ -0,0 +1,413 @@ +import pygame +# pylint: disable=E0611 +from pygame.locals import KEYDOWN +# pylint: enable=E0611 + +from configfile import cfg +from utils import wrap_multi_line, image_from_data + +# pylint: disable=E1121,R0902,R0903,R0912,R0913 + +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) + + +class baseobject(object): + def __init__(self, + surface, + x, + y, + width, + height, + data, + font, + border=False, + fg=WHITE, + bg=BLACK, + name=None, + title=None): + self.surface = surface + self.x = x + self.y = y + self.width = width + self.height = height + self.font = font + self.border = border + self.fontheight = self.font.get_height() + self.fg = fg + self.bg = bg + self.data = data + self.name = name + self.title = title + + if self.width < 1: + for item in self.data: + size = self.font.size(item)[0] + if size > self.width: + self.width = size + if self.width > self.surface.get_width(): + self.surface.get_width() + if self.border: + self.width += 8 + + if self.height < 1: + self.height = self.fontheight * (len(self.data)) + if self.border: + self.height += 8 + if self.height > self.surface.get_height(): + if self.border: + self.height = self.surface.get_height() - 8 - self.y + else: + self.height = self.surface.get_height - self.y + + if self.x == -1: + self.x = int((self.surface.get_width() / 2) - (self.width / 2)) + + if self.y == -1: + self.y = int((self.surface.get_height() / 2) - (self.height / 2)) + + if self.border: + if not self.title: + self.textx = self.x + 4 + self.texty = self.y + 4 + self.textwidth = self.width - 8 + self.textheight = self.height - 8 + else: + self.textx = self.x + 4 + self.texty = self.y + 4 + self.fontheight + self.textwidth = self.width - 8 + self.textheight = self.height - 8 - self.fontheight + else: + if not self.title: + self.textx = self.x + self.texty = self.y + self.textwidth = self.width + self.textheight = self.height + else: + self.textx = self.x + self.texty = self.y + self.fontheight + self.textwidth = self.width + self.textheight = self.height - self.fontheight + + +class menu(baseobject): + def __init__(self, + surface, + x, + y, + width, + height, + data, + font, + border=False, + fg=WHITE, + bg=BLACK, + name=None, + startitem=0, + itemattop=0): + baseobject.__init__(self, surface, x, y, width, height, data, font, + border, fg, bg, name) + self.currentitem = startitem + self.itemattop = itemattop + self.itemsperpage = int(self.textheight / self.fontheight) + self.isrom = True + if str(type(self.data[0])) == "": + self.isrom = False + + def processevent(self, event): # pylint: disable=R0912 + if event.type == KEYDOWN: + if event.key in cfg["KeyUp"] and self.currentitem > 0: + if self.currentitem == self.itemattop: + self.itemattop -= 1 + self.currentitem -= 1 + if event.key in cfg["KeyDown"] and self.currentitem < len( + self.data) - 1: + self.currentitem += 1 + if self.itemattop + self.itemsperpage <= self.currentitem: + self.itemattop += 1 + if event.key in cfg["KeyPgUp"] and self.currentitem > 0: + self.currentitem -= self.itemsperpage - 1 + while self.itemattop > self.currentitem: + self.itemattop -= 1 + if self.itemattop < 0: + self.itemattop = 0 + self.currentitem = 0 + if event.key in cfg["KeyPgDn"] and self.currentitem < len( + self.data) - 1: + self.currentitem += self.itemsperpage - 1 + while self.currentitem - self.itemattop >= self.itemsperpage: + self.itemattop += 1 + if self.itemattop + self.itemsperpage > len(self.data): + self.itemattop = len(self.data) - self.itemsperpage + self.currentitem = len(self.data) - 1 + if event.key in cfg["KeyHome"] and self.currentitem > 0: + self.currentitem = 0 + self.itemattop = 0 + if event.key in cfg["KeyEnd"] and self.currentitem < len( + self.data) - 1: + self.currentitem = len(self.data) - 1 + self.itemattop = self.currentitem - self.itemsperpage + 1 + + def render(self): + pygame.Surface.fill( + self.surface, + self.bg, + rect=pygame.Rect(self.x, self.y, self.width, self.height)) + + if self.border: + pygame.draw.rect(self.surface, self.fg, + pygame.Rect(self.x + 1, self.y + 1, + self.width - 2, self.height - 2), 1) + + for i in range(0, self.itemsperpage): + if i > len(self.data) - 1: + t = pygame.Surface((self.textwidth, self.fontheight)) + t.fill(self.bg) + self.surface.blit(t, (self.textx, + self.texty + i * self.fontheight)) + continue + + if self.isrom: + item = self.data[self.itemattop + i].description + else: + item = self.data[self.itemattop + i] + + if self.itemattop + i == self.currentitem: + s = self.font.render(item, True, self.bg, self.fg) + t = pygame.Surface((self.textwidth, self.fontheight)) + t.fill(self.fg) + t.blit(s, (0, 0)) + self.surface.blit(t, (self.textx, + self.texty + i * self.fontheight)) + else: + s = self.font.render(item, True, self.fg, self.bg) + t = pygame.Surface((self.textwidth, self.fontheight)) + t.fill(self.bg) + t.blit(s, (0, 0)) + self.surface.blit(t, (self.textx, + self.texty + i * self.fontheight)) + + +class notepad(baseobject): + def __init__(self, + surface, + x, + y, + width, + height, + data, + font, + border=False, + fg=WHITE, + bg=BLACK, + name="None", + title=None): + baseobject.__init__(self, surface, x, y, width, height, data, font, + border, fg, bg, name, title) + self.data = wrap_multi_line(data, self.font, self.textwidth) + self.lineattop = 0 + self.linesperpage = int(self.textheight / self.fontheight) + self.lasttopline = int(len(self.data) - self.linesperpage) + + def processevent(self, event): + if event.type == KEYDOWN: + if event.key in cfg["KeyDown"] and self.lineattop < self.lasttopline: + self.lineattop += 1 + elif event.key in cfg["KeyUp"] and self.lineattop > 0: + self.lineattop -= 1 + elif event.key in cfg["KeyPgDn"] and self.lineattop < self.lasttopline: + self.lineattop += self.linesperpage + if self.lineattop > self.lasttopline: + self.lineattop = self.lasttopline + elif event.key in cfg["KeyPgUp"] and self.lineattop > 0: + self.lineattop -= self.linesperpage + if self.lineattop < 0: + self.lineattop = 0 + elif event.key in cfg["KeyHome"] and self.lineattop > 0: + self.lineattop = 0 + elif event.key in cfg["KeyEnd"] and self.lineattop < self.lasttopline: + self.lineattop = self.lasttopline + + def render(self): + pygame.Surface.fill( + self.surface, + self.bg, + rect=pygame.Rect(self.x, self.y, self.width, self.height)) + if self.border: + pygame.draw.rect(self.surface, self.fg, + pygame.Rect(self.x + 1, self.y + 1, + self.width - 2, self.height - 2), 1) + if self.title: + x = (self.textwidth / 2) - ( + self.font.size('%s' % self.title)[0] / 2) + title_surface = pygame.Surface((self.textwidth, + self.fontheight)) + title_surface.fill(self.fg) + title_surface.blit( + self.font.render('%s' % self.title, True, BLACK, self.fg), + (x, 0)) + self.surface.blit(title_surface, + (self.textx, self.texty - self.fontheight)) + for c in range(0, self.linesperpage): + try: + itemsurface = self.font.render(self.data[c + self.lineattop], + True, self.fg, self.bg) + self.surface.blit(itemsurface, + (self.textx, + self.texty + c * self.fontheight)) + except: + pass + + +class image_notepad(baseobject): + def __init__(self, + surface, + x, + y, + width, + height, + data, + font, + border=False, + fg=WHITE, + bg=BLACK, + name="None", + title=None): + baseobject.__init__(self, surface, x, y, width, height, data, font, + border, fg, bg, name, title) + self.currentitem = 0 + self.data = data + + def processevent(self, event): + if event.type == KEYDOWN: + if event.key in cfg["KeyPgDn"]: + if self.currentitem < len(self.data) - 1: + self.currentitem += 1 + elif event.key in cfg["KeyPgUp"]: + if self.currentitem > 0: + self.currentitem -= 1 + + def render(self): + pygame.Surface.fill( + self.surface, + self.bg, + rect=pygame.Rect(self.x, self.y, self.width, self.height)) + if self.border: + pygame.draw.rect(self.surface, self.fg, + pygame.Rect(self.x + 1, self.y + 1, + self.width - 2, self.height - 2), 1) + x = (self.textwidth / 2) - (self.font.size('%s' % self.title)[0] / 2) + title_surface = pygame.Surface((self.textwidth, self.fontheight)) + title_surface.fill(self.fg) + title_surface.blit( + self.font.render('%s' % self.data[self.currentitem][0], True, + BLACK, self.fg), (x, 0)) + self.surface.blit(title_surface, (self.textx, + self.texty - self.fontheight)) + + image_surface = image_from_data(self.data[self.currentitem][1], + (self.textwidth, self.textheight)) + + w, h = image_surface.get_width(), image_surface.get_height() + + x = (self.textwidth / 2) - (w / 2) + self.textx + y = (self.textheight / 2) - (h / 2) + self.texty + + self.surface.blit(image_surface, (x, y)) + + +class gui(): + def __init__(self, surface): + self.surface = surface + self.objects = [] + self.font = pygame.font.Font( + pygame.font.match_font(cfg["Font"], bold=True), cfg["FontSize"]) + # self.fontheight = self.font.get_height() + self.currentobject = 0 + + def waitevent(self): + event = pygame.event.wait() + + if self.objects: + self.objects[-1].processevent(event) + + return event + + def add_menu(self, + x, + y, + width, + height, + data, + border=False, + fg=WHITE, + bg=BLACK, + name="None", + startitem=0, + itemattop=0): + self.currentobject += 1 + self.objects.append( + menu(self.surface, x, y, width, height, data, self.font, border, + fg, bg, name, startitem, itemattop)) + + def add_notepad(self, + x, + y, + width, + height, + data, + border=False, + fg=WHITE, + bg=BLACK, + name="None", + title=None): + self.currentobject += 1 + self.objects.append( + notepad(self.surface, x, y, width, height, data, self.font, border, + fg, bg, name, title)) + + def add_image_notepad(self, + x, + y, + width, + height, + data, + border=False, + fg=WHITE, + bg=BLACK, + name="None", + title=None): + self.currentobject += 1 + self.objects.append( + image_notepad(self.surface, x, y, width, height, data, self.font, + border, fg, bg, name, title)) + + def deletelastobject(self): + del self.objects[-1] + self.currentobject -= 1 + + def getmenuname(self): + return self.objects[-1].name + + def deleteallmenus(self): + while self.currentobject > 1: + self.deletelastobject() + + def getcurrentitem(self, objnum=None): + if objnum is None: + return self.objects[-1].currentitem + return self.objects[objnum].currentitem + + def getitemattop(self): + return self.objects[-1].itemattop + + # TODO: Fix me for itemattop on normal menus + def setcurrentitem(self, selected_item): + self.objects[-1].itemattop = selected_item + self.objects[-1].currentitem = selected_item + + def render(self): + for t in self.objects: + t.render() + + pygame.display.update() diff --git a/images/arcade.icns b/images/arcade.icns new file mode 100644 index 0000000..ff19fd1 Binary files /dev/null and b/images/arcade.icns differ diff --git a/images/arcade.ico b/images/arcade.ico new file mode 100644 index 0000000..f7edfeb Binary files /dev/null and b/images/arcade.ico differ diff --git a/mame.py b/mame.py new file mode 100644 index 0000000..0491bd1 --- /dev/null +++ b/mame.py @@ -0,0 +1,406 @@ +import os +import pickle +import re +import subprocess +import zipfile +from glob import glob + +from lxml import etree +from titlecase import titlecase + + +class MameROM(object): + ''' One Mame ROM ''' + + def __init__(self): + self.name = '' + self.cloneof = None + self.description = '' + self.year = '' + self.manufacturer = '' + self.status = '' + self.category = '' + + def __str__(self): + return self.description + + def __repr__(self): + return '%s("%s", "%s", "%s", "%s", "%s", "%s", "%s")' % ( + self.__class__.__name__, self.name, self.cloneof, self.description, + self.year, self.manufacturer, self.status, self.category) + + +class MameROMs(object): + ''' A Collection of MAME Roms ''' + + def __init__(self, data_dir, hide_mature, cfg): + self.all_roms = [] + self.roms = [] + self.all_categories = [] + self.categories = [] + self.catdict = {} + self.len = 0 + self.data_dir = data_dir + self.hide_mature = hide_mature + self.cfg = cfg + self.emulator_dir = os.path.split(cfg['EXE'])[0] + + self.parse() + self.filter() + + self.snapdir = os.path.join(self.emulator_dir, 'snap') + if os.path.isfile(os.path.join(self.snapdir, 'snap.zip')): + self.snapfile = zipfile.ZipFile( + os.path.join(self.snapdir, "snap.zip")) + else: + self.snapfile = None + self.snapdir = os.path.join(self.snapdir, 'snap') + + self.marqueesdir = os.path.join(self.emulator_dir, 'marquees') + if os.path.isfile(os.path.join(self.marqueesdir, 'marquees.zip')): + self.marqueesfile = zipfile.ZipFile( + os.path.join(self.marqueesdir, "marquees.zip")) + else: + self.marqueesfile = None + self.marqueesdir = os.path.join(self.marqueesdir, 'marquees') + + self.titlesdir = os.path.join(self.emulator_dir, 'titles') + if os.path.isfile(os.path.join(self.titlesdir, 'titles.zip')): + self.titlesfile = zipfile.ZipFile( + os.path.join(self.titlesdir, "titles.zip")) + else: + self.titlesfile = None + self.titlesdir = os.path.join(self.titlesdir, 'titles') + + infofn = os.path.join(self.emulator_dir, 'mameinfo.dat') + if not os.path.isfile(infofn): + self.info = None + else: + with open(infofn, 'rt', encoding='latin1') as f: + self.info = f.read() + + historyfn = os.path.join(self.emulator_dir, 'history.dat') + if not os.path.isfile(historyfn): + self.history = None + else: + with open(historyfn, 'rt', encoding='utf-8') as f: + self.history = f.read() + + self.artwork_dirs = {} + for item in cfg['ArtworkDirs']: + directory = os.path.join(self.emulator_dir, item) + if os.path.isdir(directory): + if os.path.isfile(os.path.join(directory, '%s.zip' % item)): + self.artwork_dirs[titlecase(item)] = { + "dof": os.path.join(directory, '%s.zip' % item), + 'type': 'file' + } + else: + self.artwork_dirs[titlecase(item)] = { + "dof": directory, + 'type': 'dir' + } + + def __len__(self): + return self.len + + def __getitem__(self, item): + return self.roms[item] + + def filter(self): + self.roms = [] + + for rom in self.all_roms: + if rom.status in self.cfg['StatusFilter']: + if self.cfg["Category"] == "All Games" or self.cfg["Category"] == rom.category: + if self.cfg["ShowClones"] or rom.cloneof is None: + if not self.hide_mature or '* Mature *' not in rom.category: + self.roms.append(rom) + + if self.cfg["Sort"] == "Name Asc": + self.roms.sort(key=lambda x: x.description.lower(), reverse=False) + elif self.cfg["Sort"] == "Name Dec": + self.roms.sort(key=lambda x: x.description.lower(), reverse=True) + elif self.cfg["Sort"] == "Year Asc": + self.roms.sort(key=lambda x: x.year.lower(), reverse=False) + elif self.cfg["Sort"] == "Year Dec": + self.roms.sort(key=lambda x: x.year.lower(), reverse=True) + + self.len = len(self.roms) + + if self.hide_mature: + self.categories = [] + for category in self.all_categories: + if '* Mature *' not in category: + self.categories.append(category) + else: + self.categories = self.all_categories + + def parse(self): + ''' Parse xml ''' + + mame_version = self.get_mame_version() + xmlfile = os.path.join(self.emulator_dir, 'mame.xml') + datfile = os.path.join(self.data_dir, 'mame.dat') + + if self.cfg['Version'] != mame_version: + self.cfg['Version'] = mame_version + + if os.path.isfile(xmlfile): + os.unlink(xmlfile) + + if os.path.isfile(datfile): + with open(datfile, 'rb') as i: + temp_mame_version = pickle.load(i) + + if temp_mame_version == mame_version: + self.all_categories = pickle.load(i) + self.all_roms = pickle.load(i) + return True + else: + os.unlink(datfile, 'mame.dat') + + tempcat = {} + catfile = open(os.path.join(self.emulator_dir, 'catver.ini')) + + found = False + for line in catfile: + line = line.strip() + if line == '[VerAdded]': + break + if line == '[Category]': + found = True + if found and line != '' and line[0] != ';' and line[0] != '[': + # zwackery=Platform / Run Jump + game, category = line.split('=') + tempcat[game] = category + if category not in self.all_categories: + self.all_categories.append(category) + self.catdict[category] = 0 + + self.all_categories.sort() + self.all_categories.insert(0, 'All Games') + self.all_categories.append('Unknown') + self.catdict['All Games'] = 1 + self.catdict['Unknown'] = 0 + + if not os.path.isfile(xmlfile): + try: + with open(xmlfile, 'w') as out: + retcode = subprocess.call( + [self.cfg['EXE'], '-listxml'], stdout=out) + if retcode != 0: + try: + os.unlink(xmlfile) + except: + pass + return False + except OSError: + try: + os.unlink(xmlfile) + except: + pass + return False + + # pylint: disable=no-member + + with open(os.path.join(self.data_dir, 'mame_exclude.txt'), 'rt') as f: + exclude = f.readlines() + exclude = [x.strip() for x in exclude] + + tree = etree.parse(xmlfile) + + rom = None + for child in tree.getiterator(): + if child.tag == 'machine': + if rom: + if rom.category not in exclude: + self.all_roms.append(rom) + self.catdict[rom.category] += 1 + rom = None + if 'runnable' in child.attrib and child.attrib['runnable'] == 'yes': + rom = MameROM() + rom.name = child.attrib['name'] + else: + rom = None + if rom and 'cloneof' in child.attrib: + rom.cloneof = child.attrib['cloneof'] + if rom and rom.name in tempcat: + rom.category = tempcat[rom.name] + elif rom and rom.cloneof in tempcat: + rom.category = tempcat[rom.cloneof] + elif rom: + rom.category = 'Unknown' + elif rom and child.tag == 'description': + rom.description = child.text + elif rom and child.tag == 'year': + rom.year = child.text + elif rom and child.tag == 'manufacturer': + rom.manufacturer = child.text + elif rom and child.tag == 'driver': + rom.status = child.attrib['status'] + + if rom: + if rom.category not in exclude: + self.all_roms.append(rom) + + for cat in self.catdict: + if self.catdict[cat] == 0: + del self.all_categories[self.all_categories.index(cat)] + + with open(datfile, 'wb') as output: + pickle.dump(mame_version, output) + pickle.dump(self.all_categories, output) + pickle.dump(self.all_roms, output) + + return True + + def get_mame_version(self): + mamerun = subprocess.run( + [self.cfg['EXE'], '-?'], stdout=subprocess.PIPE) + output = mamerun.stdout.decode('utf-8') + output = output[output.find('v'):] + output = output[:output.find(' ')] + + return output + + def get_info(self, rom): + if not self.info: + return + + search = re.search("\\$info=%s" % rom.name, self.info) + + if not search: + search = re.search("\\$info=%s" % rom.cloneof, self.info) + if not search: + return None + + start = search.start() + + info = self.info[start:self.info.find('$end', start)].splitlines() + + for i in range(len(info) - 1, -1, -1): + if info[i].startswith('$'): + del info[i] + + while info[0].strip() == '': + del info[0] + + while info[len(info) - 1].strip() == '': + del info[len(info) - 1] + + return '\n'.join(info) + + def get_history(self, rom): + if not self.history: + return + + search = re.search("\\$info=%s" % rom.name, self.history) + + if not search: + search = re.search("\\$info=%s" % rom.cloneof, self.history) + if not search: + return None + + start = search.start() + + info = self.history[start:self.history.find('$end', + start)].splitlines() + + for i in range(len(info) - 1, -1, -1): + if info[i].startswith('$'): + del info[i] + + while info[0].strip() == '': + del info[0] + + while info[len(info) - 1].strip() == '': + del info[len(info) - 1] + + return '\n'.join(info) + + def get_snap(self, rom, type_of_image): # pylint : disable=R0912 + data = None + image_file = None + image_dir = None + + if type_of_image == 'snap': + image_dir = self.snapdir + image_file = self.snapfile + if type_of_image == 'title': + image_dir = self.titlesdir + image_file = self.titlesfile + if type_of_image == 'marquee': + image_dir = self.marqueesdir + image_file = self.marqueesfile + + if image_file: + try: + data = image_file.read(rom.name + ".png") + except: + try: + data = image_file.read(rom.cloneof + '.png') + except: + data = None + elif image_dir: + fn = os.path.join(image_dir, rom.name + '.png') + if os.path.isfile(fn): + with open(fn, 'rb') as f: + data = f.read() + elif rom.cloneof: + fn = os.path.join(image_dir, rom.cloneof + '.png') + if os.path.isfile(fn): + with open(fn, 'rb') as f: + data = f.read() + else: + data = None + else: + data = None + else: + data = None + + return data + + def get_artwork(self, rom): + result = [] + + for item in self.artwork_dirs: + title = item + dof = self.artwork_dirs[title]['dof'] + + if self.artwork_dirs[title]['type'] == 'dir': + fn = None + if glob('%s/**/%s.*' % (dof, rom.name), recursive=True) != []: + fn = glob( + '%s/**/%s.*' % (dof, rom.name), recursive=True)[0] + elif glob( + '%s/**/%s.*' % + (dof, rom.cloneof), recursive=True) != []: + fn = glob( + '%s/**/%s.*' % (dof, rom.cloneof), recursive=True)[0] + if fn: + data = None + if os.path.splitext(fn)[1].lower() in [ + '.png', '.jpg', '.jpeg' + ]: + with open(fn, 'rb') as f: + data = f.read() + + if data: + result.append([title, data]) + else: + data = None + try: + with zipfile.ZipFile(dof) as f: + data = f.read('%s.png' % rom.name) + except: + try: + with zipfile.ZipFile(dof) as f: + data = f.read('%s.png' % rom.cloneof) + except: + data = None + + if data: + result.append([title, data]) + + return result diff --git a/mess.py b/mess.py new file mode 100644 index 0000000..dd73f2f --- /dev/null +++ b/mess.py @@ -0,0 +1,281 @@ +import glob +import os +import pickle +import re +import subprocess +from xml.sax.saxutils import unescape +import zipfile + +from lxml import etree + + +class MessROM(object): + ''' One Mame Software List ROM ''' + + def __init__(self): + self.name = '' + self.cloneof = None + self.description = '' + self.year = '' + self.manufacturer = '' + self.category = '' + + def __str__(self): + return self.description + # return '["%s", "%s", "%s", "%s", "%s" "%s", "%s"]' % ( + # self.name, self.cloneof, self.description, self.year, + # self.manufacturer, self.status, self.category) + + def __repr__(self): + return '%s("%s", "%s", "%s", "%s", "%s", "%s")' % ( + self.__class__.__name__, self.name, self.cloneof, self.description, + self.year, self.manufacturer, self.category) + + +class MessROMs(object): + ''' A Collection of MAME Software List Roms ''' + + def __init__(self, data_dir, cfg): + self.all_roms = [] + self.roms = [] + self.categories = [] + self.all_categories = [] + self.catdict = {} + self.len = 0 + self.data_dir = data_dir + self.cfg = cfg + self.emulator_dir = os.path.split(cfg['EXE'])[0] + + self.parse() + self.filter() + + self.snapdir = os.path.join(self.emulator_dir, 'snap') + if os.path.isfile(os.path.join(self.snapdir, 'snap.zip')): + self.snapfile = zipfile.ZipFile( + os.path.join(self.snapdir, "snap.zip")) + else: + self.snapfile = None + self.snapdir = os.path.join(self.snapdir, 'snap') + + infofn = os.path.join(self.emulator_dir, 'messinfo.dat') + if not os.path.isfile(infofn): + self.info = None + else: + with open(infofn, 'rt', encoding='latin1') as f: + self.info = f.read() + + historyfn = os.path.join(self.emulator_dir, 'history.dat') + if not os.path.isfile(historyfn): + self.history = None + else: + with open(historyfn, 'rt', encoding='utf-8') as f: + self.history = f.read() + + def __len__(self): + return self.len + + def __getitem__(self, item): + return self.roms[item] + + def filter(self): + self.roms = [] + + for rom in self.all_roms: + if self.cfg["Category"] == "All Games" or self.cfg["Category"] == rom.category: + if self.cfg["ShowClones"] or rom.cloneof is None: + self.roms.append(rom) + + if self.cfg["Sort"] == "Name Asc": + self.roms.sort(key=lambda x: x.description.lower(), reverse=False) + elif self.cfg["Sort"] == "Name Dec": + self.roms.sort(key=lambda x: x.description.lower(), reverse=True) + elif self.cfg["Sort"] == "Year Asc": + self.roms.sort(key=lambda x: x.year.lower(), reverse=False) + elif self.cfg["Sort"] == "Year Dec": + self.roms.sort(key=lambda x: x.year.lower(), reverse=True) + + self.len = len(self.roms) + + self.categories = self.all_categories + + def parse(self): + """ Parse xml """ + + mess_version = self.get_mess_version() + datfile = os.path.join(self.data_dir, 'mess.dat') + + if self.cfg['Version'] != mess_version: + self.cfg['Version'] = mess_version + + if os.path.isfile(datfile): + os.unlink(datfile) + + if os.path.isfile(datfile): + with open(datfile, 'rb') as i: + temp_mess_version = pickle.load(i) + + if temp_mess_version == mess_version: + self.all_categories = pickle.load(i) + self.all_roms = pickle.load(i) + return True + else: + os.unlink(datfile) + + self.all_categories = [] + self.catdict = {} + + files = glob.glob(os.path.join(self.emulator_dir, "hash", '*.xml')) + for file in files: + with open(file, 'rt', encoding='utf-8') as f: + line = None + while True: + line = f.readline().strip() + if line.startswith('=1.9.3 +configobj>=5.0.6 +appdirs>=1.4.3 +lxml>=3.8.0 +titlecase>=0.11.0 +cx_Freeze>=5.0.2 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..592f497 --- /dev/null +++ b/setup.py @@ -0,0 +1,23 @@ +import os.path +import sys +from cx_Freeze import setup, Executable + +build_exe_options = { + "packages": ["os", "pygame", "lxml"], + "excludes": ["tkinter", "numpy"], + "include_files": ["arcade.png"] +} + +base = None +if sys.platform == "win32": + base = "Win32GUI" + +setup( + name="mfe", + version="0.1", + description="MAME FrontEnd", + options={"build_exe": build_exe_options}, + executables=[ + Executable( + "mfe.py", base=base, icon=os.path.join('images', 'arcade.ico')) + ]) diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..e599e37 --- /dev/null +++ b/utils.py @@ -0,0 +1,135 @@ +from itertools import chain +import io +import os +import subprocess + +import pygame +import pygame.locals + + +def get_pygame_keydict(): + result = {} + + pygame_keys = [ + item for item in dir(pygame.locals) if item.startswith('K_') + ] + + for key in pygame_keys: + result[key] = getattr(pygame.locals, key) + + return result + + +def get_keys(keys_dict, a): + result = [] + for key in a: + result.append(keys_dict[key]) + return result + + +def aspect_scale(img, bx, by): + """ Scales 'img' to fit into box bx/by. + This method will retain the original image's aspect ratio """ + ix, iy = img.get_size() + if ix > iy: + # fit to width + scale_factor = bx / float(ix) + sy = scale_factor * iy + if sy > by: + scale_factor = by / float(iy) + sx = scale_factor * ix + sy = by + else: + sx = bx + else: + # fit to height + scale_factor = by / float(iy) + sx = scale_factor * ix + if sx > bx: + scale_factor = bx / float(ix) + sx = bx + sy = scale_factor * iy + else: + sy = by + + return pygame.transform.scale(img, (int(sx), int(sy))) + + +def image_from_data(data, image_size): + if data: + with io.BytesIO(data) as f: + surface = pygame.image.load(f) + surface = aspect_scale(surface, image_size[0], image_size[1]) + else: + surface = pygame.Surface(image_size) # pylint: disable=E1121 + + return surface + + +def addscanlines(surface): + width = surface.get_width() + for y in range(surface.get_height()): + if y % 2: + pygame.draw.line(surface, (0, 0, 0), (0, y), (width, y)) + + return surface + + +def truncline(text, font, maxwidth): + real = len(text) + stext = text + l = font.size(text)[0] + cut = 0 + a = 0 + done = 1 + while l > maxwidth: + a = a + 1 + n = text.rsplit(None, a)[0] + if stext == n: + cut += 1 + stext = n[:-cut] + else: + stext = n + l = font.size(stext)[0] + real = len(stext) + done = 0 + return real, done, stext + + +def wrapline(text, font, maxwidth): + done = 0 + wrapped = [] + + while not done: + nl, done, stext = truncline(text, font, maxwidth) + wrapped.append(stext.strip()) + text = text[nl:] + return wrapped + + +def wrap_multi_line(text, font, maxwidth): + """ returns text taking new lines into account. + """ + if type(text) is str: + lines = chain(*(wrapline(line, font, maxwidth) + for line in text.splitlines())) + else: + lines = chain(*(wrapline(line, font, maxwidth) for line in text)) + + return list(lines) + + +def run_emulator(emu_type, exe, rom): + old_path = os.getcwd() + os.chdir(os.path.split(exe)[0]) + + if emu_type == 'MAME': + subprocess.run([exe, rom]) + elif emu_type == 'MESS': + subprocess.run([exe, rom]) + elif emu_type == 'CSV': + subprocess.run([exe.replace('', rom)]) + + os.chdir(old_path) + + return True