zefie

webtv mame launcher

Dec 29th, 2025
2,248
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 34.50 KB | None | 0 0
  1. import sys, os, re, subprocess, ctypes, shutil, random
  2. import winreg, win32gui, win32process, win32con, win32pipe, win32file, pywintypes
  3. from PyQt5.QtWidgets import *
  4. from PyQt5.QtCore import *
  5. from PyQt5.QtGui import *
  6. import socket, time
  7. import threading
  8.  
  9.  
  10. # Mapping of machine codes to human-readable names
  11. MACHINE_NAMES = {
  12.     "wtv1bf0": "WebTV Classic (2MB)",
  13.     "wtv1bfe": "WebTV Classic (Prototype)",
  14.     "wtv1dev": "WebTV Classic Dev Box (4MB)",
  15.     "wtv1dv2": "WebTV Classic Dev Box (2MB)",
  16.     "wtv1pal": "WebTV Classic (PAL)",
  17.  
  18.     "wtv2drb": "WebTV Plus (Derby)",
  19.     "wtv2lc2": "WebTV Plus (LC2)",
  20.     "wtv2dev": "WebTV Plus (Virtual 16MB Dev)",
  21.     "wtv2esr": "WebTV Echostar",
  22.     "wtv2jpc": "WebTV Classic (Japan)",
  23.     "wtv2jpp": "WebTV Plus (Japan)",
  24.     "wtv2ncl": "WebTV New Classic (BPS, 8MB)",
  25.     "wtv2npl": "WebTV New Plus (16MB)",
  26.     "wtv2utv": "Ultimate TV",
  27.     "wtv2uvd": "Ultimate TV Dev Box (Fake)",
  28.     "wtv2wld": "WebTV Italian Prototype"
  29. }
  30.  
  31. class MameWorker(QThread):
  32.     window_found = pyqtSignal(int)  # Signal to send back the HWND
  33.  
  34.     def __init__(self, command, parent=None):
  35.         super().__init__(parent)
  36.         self.command = command
  37.         self.mame_hwnd = None
  38.         self.process = None
  39.         self._should_stop = False
  40.  
  41.     def run(self):
  42.         # Launch MAME process
  43.         self.process = subprocess.Popen(self.command)
  44.         pid = self.process.pid
  45.  
  46.         # Try to find the MAME window
  47.         for _ in range(30):  # Try for ~3 seconds
  48.             hwnd = self._find_window_by_pid(pid)
  49.             if hwnd:
  50.                 self.mame_hwnd = hwnd
  51.                 self.window_found.emit(hwnd)
  52.                 break
  53.             time.sleep(0.1)
  54.  
  55.         self.process.wait()
  56.  
  57.     def stop(self):
  58.         self._should_stop = True
  59.         if self.process and self.process.poll() is None:
  60.             self.process.terminate()
  61.             try:
  62.                 self.process.wait(timeout=5)
  63.             except subprocess.TimeoutExpired:
  64.                 self.process.kill()
  65.  
  66.     def _find_window_by_pid(self, target_pid):
  67.         def callback(hwnd, pid_list):
  68.             if win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd):
  69.                 _, pid = win32process.GetWindowThreadProcessId(hwnd)
  70.                 if pid == target_pid:
  71.                     pid_list.append(hwnd)
  72.         hwnds = []
  73.         win32gui.EnumWindows(callback, hwnds)
  74.         return hwnds[0] if hwnds else None
  75.  
  76.     def send_key(self, key):
  77.         try:
  78.             pipe = win32pipe.CreateNamedPipe(
  79.                 r'\\.\pipe\mame_input',
  80.                 win32pipe.PIPE_ACCESS_OUTBOUND,
  81.                 win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_WAIT,
  82.                 1, 65536, 65536,
  83.                 0,
  84.                 None
  85.             )
  86.             win32pipe.ConnectNamedPipe(pipe, None)
  87.             win32file.WriteFile(pipe, (key + "\n").encode())
  88.             win32pipe.DisconnectNamedPipe(pipe)
  89.         except pywintypes.error as e:
  90.             print("Pipe closed or failed:", e)
  91.             win32file.CloseHandle(pipe)
  92.  
  93. class MainWindow(QMainWindow):
  94.     append_text_signal = pyqtSignal(str)
  95.  
  96.     def closeEvent(self, event):
  97.         self.stop_socket_server()
  98.         if self.worker:
  99.             self.stop_mame()
  100.         super().closeEvent(event)  # Ensure default cleanup continues
  101.  
  102.     def __init__(self):
  103.         super().__init__()
  104.         self.diskpath = os.path.join(os.getcwd(), "disks")
  105.         self.setWindowTitle("WebTV MAME Launcher")
  106.         self.setGeometry(100, 100, 400, 100)
  107.         self.server_socket = None
  108.         self.conn = None
  109.         self.server_thread = None
  110.  
  111.  
  112.         self.apply_dark_mode()
  113.  
  114.         # Create the Menu Bar
  115.         self.create_menu_bar()
  116.  
  117.         main_widget = QWidget()
  118.         self.settings = QSettings("zefie", "WebTVLauncher")
  119.         self.setCentralWidget(main_widget)
  120.         layout = QVBoxLayout()
  121.         possible_executables = [
  122.             "webtv.exe",
  123.             "webtv",
  124.             "webtv2.exe",
  125.             "webtv2",
  126.             "webtv1.exe",
  127.             "webtv1",
  128.         ]
  129.         self.executable = None
  130.         for prog in possible_executables:
  131.             path = shutil.which(prog)
  132.             if path:
  133.                 self.executable = path
  134.                 break
  135.         if not self.executable:
  136.             print("Could not find mame executable (webtv, webtv1, webtv2)(.exe)")
  137.             exit(1)
  138.            
  139.         # Dropdown
  140.         self.dropdown = QComboBox()
  141.         pattern = r'\((wtv[^)]+)\)'
  142.         data = subprocess.run([self.executable, '-listbios'], capture_output=True, text=True)
  143.         matches = re.findall(pattern, data.stdout)
  144.         # For each match, check MACHINE_NAMES for a friendly description
  145.         items = []
  146.         for m in matches:
  147.             desc = MACHINE_NAMES.get(m, "")
  148.             if desc:
  149.                 items.append(f"{m} - {desc}")
  150.             else:
  151.                 items.append(m)
  152.         self.dropdown.addItems(items)
  153.         top_layout = QHBoxLayout()
  154.         top_layout.setSizeConstraint(QLayout.SetFixedSize)
  155.         sel_opt = QLabel("Select Machine:")
  156.         top_layout.addWidget(sel_opt)
  157.         ssid_static = QLabel("SSID:")
  158.         ssid_static.setAlignment(Qt.AlignRight)
  159.         top_layout.addWidget(ssid_static)
  160.         self.ssid_label = QLabel("No SSID")
  161.         top_layout.addWidget(self.ssid_label)
  162.         layout.addLayout(top_layout)
  163.         layout.addWidget(self.dropdown)
  164.  
  165.         # Checkboxes
  166.         checkboxes_layout = QHBoxLayout()
  167.         self.verbose = QCheckBox("Verbose")
  168.         self.mamedebug = QCheckBox("MAME Debugger")
  169.         self.serialdbg = QCheckBox("Serial Debug")
  170.         self.diskboot = QCheckBox("Disk Boot")
  171.         self.diskboot.setChecked(True)
  172.  
  173.         self.modem = QCheckBox("Modem")
  174.         self.verbose.setChecked(True)
  175.         self.modem.setChecked(True)
  176.         self.mamedebug.setChecked(False)
  177.         self.serialdbg.setChecked(True)
  178.         self.bitblab = QLabel("BITB:")
  179.         self.bitb = QLineEdit("touchppp.lan.zef:1122");
  180.         checkboxes_layout.addWidget(self.verbose)
  181.         checkboxes_layout.addWidget(self.mamedebug)
  182.         checkboxes_layout.addWidget(self.serialdbg)
  183.         checkboxes_layout.addWidget(self.modem)
  184.         checkboxes_layout.addWidget(self.diskboot)
  185.         bitb_layout = QHBoxLayout()
  186.         bitb_layout.addWidget(self.bitblab)
  187.         bitb_layout.addWidget(self.bitb)
  188.         disk_layout = QHBoxLayout()
  189.         self.disklab = QLabel("Disk:")
  190.         self.disklab.hide()
  191.         self.disk = QComboBox()
  192.         self.scan_disk_list()
  193.        
  194.         bitb_layout.addWidget(self.disklab)
  195.         bitb_layout.addWidget(self.disk)
  196.        
  197.         layout.addLayout(checkboxes_layout)
  198.         layout.addLayout(bitb_layout)
  199.         layout.addLayout(disk_layout)
  200.         # Button
  201.         self.button = QPushButton("Launch MAME")
  202.         self.dropdown.currentTextChanged.connect(self.on_dropdown_changed)
  203.         self.button.clicked.connect(self.on_button_click)
  204.         self.modem.clicked.connect(self.on_modem_click)
  205.         layout.addWidget(self.button)
  206.         self.setMinimumWidth(1280)
  207.         self.setMinimumHeight(792)
  208.         # Multiline Text Area
  209.         self.text_area = QTextEdit()
  210.         self.text_area.setReadOnly(True)
  211.         self.text_area.setContextMenuPolicy(Qt.CustomContextMenu)
  212.         self.text_area.customContextMenuRequested.connect(self.show_text_area_context_menu)
  213.         hbox = QHBoxLayout()
  214.         self.mame_window = QWidget();
  215.         self.mame_window.setFixedSize(800, 600)
  216.         self.mame_window.setFocusPolicy(Qt.ClickFocus)
  217.         self.mame_window.mousePressEvent = lambda event: self.mame_window.setFocus()
  218.         self.mame_window.setStyleSheet("background-color: black; border: 1px solid #555;")
  219.         hbox.addWidget(self.mame_window)
  220.         hbox.addWidget(self.text_area)
  221.         layout.addLayout(hbox)
  222.         input_layout = QHBoxLayout()
  223.         self.single_line_input = QLineEdit()
  224.         self.send_button = QPushButton("Send")
  225.         self.send_button.clicked.connect(lambda: self.send_serial_data(self.single_line_input.text()))
  226.         input_layout.addWidget(self.single_line_input)
  227.         input_layout.addWidget(self.send_button)
  228.         self.single_line_input.returnPressed.connect(self.send_button.click)
  229.         layout.addLayout(input_layout)
  230.         main_widget.setLayout(layout)
  231.         self.text_area.setVisible(self.serialdbg.isChecked())
  232.         self.text_area.setStyleSheet("font-family: monospace; font-size: 8pt;")
  233.         #self.text_area.setLineWrapMode(QTextEdit.NoWrap)
  234.         self.text_area.setPlaceholderText("Serial output will appear here...")
  235.         self.serialdbg.toggled.connect(self.toggleSerialDebug)
  236.         self.append_text_signal.connect(self.handle_serial_data)
  237.         self.text_area.autoFormattingEnabled = False
  238.         self.load_settings()
  239.         self.on_dropdown_changed()
  240.  
  241.     def show_text_area_context_menu(self, position):
  242.         """Show context menu for the text area"""
  243.         context_menu = QMenu(self)
  244.        
  245.         clear_action = QAction("Clear Output", self)
  246.         clear_action.triggered.connect(self.text_area.clear)
  247.         context_menu.addAction(clear_action)
  248.        
  249.         copy_action = QAction("Copy All", self)
  250.         copy_action.triggered.connect(lambda: self.text_area.selectAll() or self.text_area.copy())
  251.         context_menu.addAction(copy_action)
  252.        
  253.         save_action = QAction("Save to File...", self)
  254.         save_action.triggered.connect(self.save_serial_output)
  255.         context_menu.addAction(save_action)
  256.        
  257.         context_menu.exec_(self.text_area.mapToGlobal(position))
  258.  
  259.     def save_serial_output(self):
  260.         """Save serial output to a file"""
  261.         options = QFileDialog.Options()
  262.         file_name, _ = QFileDialog.getSaveFileName(self, "Save Serial Output", "serial_output.txt", "Text Files (*.txt);;All Files (*)", options=options)
  263.        
  264.         if file_name:
  265.             try:
  266.                 with open(file_name, 'w', encoding='utf-8') as f:
  267.                     f.write(self.text_area.toPlainText())
  268.                 QMessageBox.information(self, "Success", f"Serial output saved to:\n{file_name}")
  269.             except Exception as e:
  270.                 QMessageBox.critical(self, "Error", f"Failed to save file:\n{str(e)}")
  271.  
  272.     def getMachine(self):
  273.         return self.dropdown.currentText().split(' ')[0]
  274.  
  275.     def toggleSerialDebug(self, checked):
  276.         self.text_area.setEnabled(checked)
  277.         self.single_line_input.setEnabled(checked)
  278.         self.send_button.setEnabled(checked)
  279.  
  280.     def send_serial_data(self, data):
  281.         self.single_line_input.clear()
  282.         if not data:
  283.             return
  284.         if hasattr(self, 'conn') and self.conn:
  285.             try:
  286.                 self.conn.sendall((data + '\n').encode('utf-8'))
  287.             except Exception as e:
  288.                 self.append_text_signal.emit(f"Send error: {e}")
  289.         else:
  290.             self.append_text_signal.emit("No serial connection established.")
  291.        
  292.     def handle_serial_data(self, data):
  293.         """Append data to the text area."""
  294.         if isinstance(data, bytes):
  295.             data = data.decode(errors='replace')
  296.         # Handle backspace (byte 0x08): remove previous character
  297.         cursor = self.text_area.textCursor()
  298.        
  299.     def handle_serial_data(self, data):
  300.         """Append data to the text area."""
  301.         if isinstance(data, bytes):
  302.             data = data.decode(errors='replace')
  303.            
  304.         # Limit text area size to prevent memory issues with large amounts of data
  305.         current_text = self.text_area.toPlainText()
  306.         if len(current_text) > 100000:  # If more than 100k characters
  307.             # Keep only the last 50k characters
  308.             lines = current_text.split('\n')
  309.             # Keep approximately last half of the lines
  310.             keep_lines = lines[len(lines)//2:]
  311.             self.text_area.setPlainText('\n'.join(keep_lines))
  312.            
  313.         # Handle backspace (byte 0x08): remove previous character
  314.         cursor = self.text_area.textCursor()
  315.        
  316.         if data and (data[-1] == '\x08' or  data[-1] == chr(8)):
  317.             # Remove the last character in the text area
  318.             cursor.movePosition(cursor.End)
  319.             cursor.deletePreviousChar()
  320.             data = data[:-1]
  321.            
  322.         # Handle carriage return (byte 0x0D): move to the start of the line
  323.         if data and (data[-1] == '\r' or data[-1] == chr(13)):
  324.             # Remove the carriage return character
  325.             data = data[:-1]
  326.            
  327.         # Insert the data at the end of the text area
  328.         if data:
  329.             cursor = self.text_area.textCursor()
  330.             cursor.movePosition(cursor.End)
  331.             cursor.insertText(data)
  332.             self.text_area.setTextCursor(cursor)
  333.            
  334.             # Auto-scroll to bottom to show latest data
  335.             scrollbar = self.text_area.verticalScrollBar()
  336.             scrollbar.setValue(scrollbar.maximum())
  337.            
  338.         # Check for "Disk Boot?" prompt after updating display
  339.         last_text = self.text_area.toPlainText()
  340.         if "Disk Boot?" in last_text and hasattr(self, 'conn') and self.conn and self.diskboot.isChecked():
  341.             try:
  342.                 diskboot = "y\n" if self.diskboot.isChecked() else "n\n"
  343.                 self.conn.sendall(diskboot.encode())
  344.                 self.text_area.clear()
  345.             except Exception as e:
  346.                 self.append_text_signal.emit(f"Auto-response error: {e}")
  347.  
  348.     def get_ssid_crc(self, ssid):
  349.         """
  350.        Compute a 2-digit hexadecimal CRC for the SSID.
  351.        Only the first 14 characters of the ssid are used.
  352.        """
  353.         ssid = ssid[:14]
  354.         crc = 0
  355.         # Process every two characters of the SSID as a byte.
  356.         for i in range(0, len(ssid), 2):
  357.             inbyte = int(ssid[i:i+2], 16)
  358.             for _ in range(8, 0, -1):
  359.                 mix = (crc ^ inbyte) & 0x01
  360.                 crc >>= 1
  361.                 if mix != 0:
  362.                     crc ^= 0x8C
  363.                 inbyte >>= 1
  364.         out = format(crc, 'x')
  365.         if len(out) == 1:
  366.             out = "0" + out
  367.         return out
  368.  
  369.     def generate_ssid(self, manufacturer_id_checked):
  370.         """
  371.        Generate an SSID based on a template.
  372.        The template "71xxxxxy00b002" has:
  373.          - 'x' replaced by random hex digits (0-F)
  374.          - 'y' replaced by "1" if manufacturer_id_checked is True; otherwise "0"
  375.        The final SSID is the template plus a computed 2-digit CRC.
  376.        """
  377.         ssid_template = "71xxxxxy00b002"
  378.         ssid = ssid_template
  379.        
  380.         # Replace each occurrence of "x" with a random hex digit
  381.         while "x" in ssid:
  382.             random_hex = format(random.randint(0, 15), 'x')
  383.             ssid = ssid.replace("x", random_hex, 1)
  384.        
  385.         # Replace "y" based on the manufacturer_id_checked flag.
  386.         if manufacturer_id_checked:
  387.             ssid = ssid.replace("y", "1")
  388.         else:
  389.             ssid = ssid.replace("y", "0")
  390.        
  391.         # Append the CRC computed from the first 14 characters.
  392.         ssid_full = ssid + self.get_ssid_crc(ssid)
  393.         return ssid_full
  394.        
  395.  
  396.     def apply_dark_mode(self):
  397.         """Detects Windows dark mode and applies a dark stylesheet."""
  398.         if self.is_windows_dark_mode():
  399.             self.setStyleSheet(self.dark_stylesheet())
  400.  
  401.     def is_windows_dark_mode(self):
  402.         """Checks if Windows is in dark mode by reading the registry."""
  403.         try:
  404.             key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")
  405.             value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
  406.             winreg.CloseKey(key)
  407.             return value == 0  # 0 means dark mode is enabled, 1 means light mode
  408.         except Exception:
  409.             return False  # Default to light mode if registry read fails
  410.  
  411.     def dark_stylesheet(self):
  412.         """Returns a dark mode stylesheet for PyQt5."""
  413.         return """
  414.            QWidget { background-color: #2b2b2b; color: #ffffff; }
  415.            QComboBox, QLineEdit, QTextEdit, QPlainTextEdit { background-color: #3c3f41; color: #ffffff; border: 1px solid #555; }
  416.            QPushButton { background-color: #555; color: white; border-radius: 4px; padding: 5px; }
  417.            QPushButton:hover { background-color: #777; }
  418.            QPushButton:pressed { background-color: #333; }
  419.            QMenuBar { background-color: #2b2b2b; color: white; }
  420.            QMenuBar::item { background: transparent; padding: 5px; }
  421.            QMenuBar::item:selected { background: #444; }
  422.            QMenu { background-color: #2b2b2b; color: white; }
  423.            QMenu::item:selected { background: #444; }
  424.            QLabel { color: white; }
  425.            QCheckBox { color: white; }
  426.        """
  427.        
  428.     def getMachineSSID(self, machine):
  429.         try:
  430.             filename = "roms/" + machine + "/ds2401.bin"
  431.             with open(filename, "rb") as file:
  432.                 data = file.read()
  433.                 hex_str = data.hex()
  434.                 return hex_str
  435.         except FileNotFoundError:
  436.             return "No SSID"
  437.  
  438.     def setMachineSSID(self, machine, ssid):
  439.         filepath = "roms/" + machine
  440.         filename = filepath + "/ds2401.bin"
  441.         os.makedirs(filepath, exist_ok=True)
  442.         with open(filename, "wb") as file:
  443.             data = bytes.fromhex(ssid)
  444.             file.write(data)  
  445.  
  446.     def createSSID(self):
  447.         machine = self.getMachine()
  448.         self.setMachineSSID(machine, self.generate_ssid(True))
  449.         self.readSSID()
  450.        
  451.     def readSSID(self):
  452.         machine = self.getMachine()
  453.         try:
  454.             self.ssid_label.setText(self.getMachineSSID(machine))
  455.         except:
  456.             self.ssid_label.setText("No SSID")          
  457.        
  458.  
  459.     def scan_disk_list(self):
  460.         img_files = [file for file in os.listdir(self.diskpath) if file.lower().endswith('.img')]            
  461.         self.disk.clear()
  462.         self.disk.addItems(img_files)
  463.  
  464.     def create_menu_bar(self):
  465.         """Creates the top menu bar"""
  466.         menubar = self.menuBar()
  467.  
  468.         # File Menu
  469.         file_menu = menubar.addMenu("File")
  470.         exit_action = QAction("Exit", self)
  471.         exit_action.triggered.connect(self.close)
  472.         file_menu.addAction(exit_action)
  473.  
  474.         # Settings Menu
  475.         tools_menu = menubar.addMenu("Tools")
  476.         create_image = QAction("Create 1GB Disk Image", self)
  477.         create_image.triggered.connect(self.create_disk_image_1g)
  478.         create_image2 = QAction("Create 8GB Disk Image", self)
  479.         create_image2.triggered.connect(self.create_disk_image_8g)
  480.         create_ssid = QAction("Generate New SSID", self)
  481.         create_ssid.triggered.connect(self.createSSID)
  482.         tools_menu.addAction(create_image)
  483.         tools_menu.addAction(create_image2)
  484.         tools_menu.addAction(create_ssid)
  485.        
  486.  
  487.         # Settings Menu
  488.         settings_menu = menubar.addMenu("Settings")
  489.         save_action = QAction("Save Settings", self)
  490.         save_action.triggered.connect(self.save_settings)
  491.         settings_menu.addAction(save_action)
  492.  
  493.         # PO Codes Menu
  494.         po_menu = menubar.addMenu("PO Codes")
  495.         po_action = QAction("411 - Technical Info", self)
  496.         po_action.triggered.connect(self.send_po_411)
  497.         po_menu.addAction(po_action)
  498.         po_action = QAction("314159 - Ignore ROM Signature", self)
  499.         po_action.triggered.connect(self.send_po_314159)
  500.         po_menu.addAction(po_action)
  501.         po_action = QAction("32768 - Blast NVRAM", self)
  502.         po_action.triggered.connect(self.send_po_32768)
  503.         po_menu.addAction(po_action)        
  504.         po_action = QAction("93288 - Connect Setup", self)
  505.         po_action.triggered.connect(self.send_po_93288)
  506.         po_menu.addAction(po_action)
  507.         po_action = QAction("8675309 - Minibrowser", self)
  508.         po_action.triggered.connect(self.send_po_8675309)
  509.         po_menu.addAction(po_action)
  510.  
  511.         # Help Menu
  512.         help_menu = menubar.addMenu("Help")
  513.         about_action = QAction("About", self)
  514.         about_action.triggered.connect(self.show_about_dialog)
  515.         help_menu.addAction(about_action)
  516.  
  517.  
  518.     def isMAMERunning(self):
  519.         """Check if MAME is currently running"""
  520.         if hasattr(self, 'worker') and self.worker and self.worker.isRunning():
  521.             return True
  522.         return False
  523.  
  524.  
  525.     def send_po_preamble(self):
  526.         if self.isMAMERunning():
  527.             self.worker.send_key('{F11}')
  528.             self.worker.send_key('{F11}')
  529.  
  530.     def send_po_411(self):
  531.         if self.isMAMERunning():
  532.             self.send_po_preamble()
  533.             self.worker.send_key("411")
  534.         else:
  535.             QMessageBox.warning(self, "Warning", "MAME is not running. Please launch MAME first.")
  536.  
  537.     def send_po_314159(self):
  538.         if self.isMAMERunning():
  539.             self.send_po_preamble()
  540.             self.worker.send_key("314159")
  541.         else:
  542.             QMessageBox.warning(self, "Warning", "MAME is not running. Please launch MAME first.")
  543.  
  544.     def send_po_8675309(self):
  545.         if self.isMAMERunning():
  546.             self.send_po_preamble()
  547.             self.worker.send_key("8675309")
  548.         else:
  549.             QMessageBox.warning(self, "Warning", "MAME is not running. Please launch MAME first.")
  550.  
  551.     def send_po_93288(self):
  552.         if self.isMAMERunning():
  553.             self.send_po_preamble()
  554.             self.worker.send_key("93288")
  555.         else:
  556.             QMessageBox.warning(self, "Warning", "MAME is not running. Please launch MAME first.")
  557.  
  558.     def send_po_32768(self):
  559.         if self.isMAMERunning():
  560.             self.send_po_preamble()
  561.             self.worker.send_key("32768")
  562.         else:
  563.             QMessageBox.warning(self, "Warning", "MAME is not running. Please launch MAME first.")
  564.  
  565.     def create_disk_image_1g(self):
  566.         self.create_disk_image(1)
  567.        
  568.     def create_disk_image_8g(self):
  569.         self.create_disk_image(8)
  570.  
  571.     def create_disk_image(self, sizegb):
  572.         """Opens a save dialog and creates a 1GB empty disk image"""
  573.         options = QFileDialog.Options()
  574.         file_name, _ = QFileDialog.getSaveFileName(self, "Create Disk Image", "", "Disk Image (*.img);;All Files (*)", options=options)
  575.  
  576.         if file_name:
  577.             try:
  578.                 with open(file_name, "wb") as f:
  579.                     f.seek((sizegb * 1024 * 1024 * 1024) - 1)  # Seek to 1GB - 1 byte
  580.                     f.write(b'\0')  # Write a single null byte to allocate space
  581.  
  582.                 QMessageBox.information(self, "Success", f"Disk image created:\n{file_name}")
  583.  
  584.                 # Refresh disk list
  585.                 self.scan_disk_list()
  586.  
  587.             except Exception as e:
  588.                 QMessageBox.critical(self, "Error", f"Failed to create disk image:\n{str(e)}")
  589.  
  590.  
  591.  
  592.     def show_about_dialog(self):
  593.         """Displays an About dialog"""
  594.         QMessageBox.information(self, "About", "WebTV MAME Launcher\nVersion 1.1\nCreated by zefie with PyQt5")
  595.  
  596.     def save_settings(self):
  597.         """Save settings before closing"""
  598.         self.settings.setValue("dropdownIndex", self.dropdown.currentIndex())
  599.         self.settings.setValue("verbose", self.verbose.isChecked())
  600.         self.settings.setValue("modem", self.modem.isChecked())
  601.         self.settings.setValue("mamedebug", self.mamedebug.isChecked())
  602.         self.settings.setValue("serialdbg", self.serialdbg.isChecked())
  603.         self.settings.setValue("bitb", self.bitb.text())
  604.         self.settings.setValue("disk", self.disk.currentText())
  605.  
  606.     def load_settings(self):
  607.         """Load settings on startup"""
  608.         self.dropdown.setCurrentIndex(self.settings.value("dropdownIndex", 0, type=int))
  609.         self.verbose.setChecked(self.settings.value("verbose", True, type=bool))
  610.         self.modem.setChecked(self.settings.value("modem", True, type=bool))
  611.         self.mamedebug.setChecked(self.settings.value("mamedebug", False, type=bool))
  612.         self.serialdbg.setChecked(self.settings.value("serialdbg", True, type=bool))
  613.         self.bitb.setText(self.settings.value("bitb", "touchppp.lan.zef:1122"))
  614.         self.diskboot.setChecked(self.settings.value("diskboot", True, type=bool))
  615.         self.disk.setCurrentText(self.settings.value("disk", ""))      
  616.  
  617.     def on_dropdown_changed(self):
  618.         self.readSSID()
  619.         if self.getMachine()[3] == "1":
  620.             self.disklab.hide()
  621.             self.disk.hide()
  622.         elif (self.getMachine()[3] == "2" and (self.getMachine()[4] == "n" or self.getMachine()[4] == "w")):
  623.             self.disklab.hide()
  624.             self.disk.hide()
  625.         elif self.getMachine()[3] == "2" and (self.getMachine()[4] != "n" and self.getMachine()[4] != "w"):
  626.             self.disklab.show()
  627.             self.disk.show()
  628.            
  629.     def on_modem_click(self):
  630.         if self.modem.isChecked():
  631.             self.bitblab.show()
  632.             self.bitb.show()
  633.         else:
  634.             self.bitblab.hide()
  635.             self.bitb.hide()
  636.  
  637.     def stop_mame(self):
  638.         self.stop_socket_server()
  639.         if self.worker and self.worker.isRunning():
  640.             self.worker.stop()
  641.             self.worker.wait()
  642.         self.button.setText("Launch MAME")
  643.         self.button.clicked.disconnect()
  644.         self.button.clicked.connect(self.on_button_click)
  645.  
  646.     def embed_window(self, hwnd):
  647.         """
  648.        Reparent the MAME window to self.mame_window directly using Win32 APIs.
  649.        """
  650.         parent_hwnd = int(self.mame_window.winId())
  651.  
  652.         # Reparent MAME window to our QWidget
  653.         win32gui.SetParent(hwnd, parent_hwnd)
  654.  
  655.         # Optional: remove window border/style
  656.         style = win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE)
  657.         style = style & ~win32con.WS_CAPTION & ~win32con.WS_THICKFRAME
  658.         win32gui.SetWindowLong(hwnd, win32con.GWL_STYLE, style)
  659.  
  660.         # Resize MAME window to fill self.mame_window
  661.         rect = self.mame_window.rect()
  662.         win32gui.SetWindowPos(
  663.             hwnd, None,
  664.             0, 0, rect.width(), rect.height(),
  665.             win32con.SWP_NOZORDER | win32con.SWP_SHOWWINDOW
  666.         )
  667.  
  668.         # Ensure MAME gets input
  669.         ctypes.windll.user32.SetForegroundWindow(hwnd)
  670.  
  671.     def on_button_click(self):
  672.         self.text_area.clear()
  673.         selected = self.getMachine()
  674.         command = [self.executable, selected]
  675.         command += ["-nomouse", "-nomax", "-window"]        
  676.         if self.verbose.isChecked():
  677.             command += ["-verbose"]
  678.         if self.mamedebug.isChecked():
  679.             command += ["-debug"]
  680.         if self.modem.isChecked():
  681.             if selected[3] == "1":
  682.                 command += ["-spot:modem"]
  683.             elif selected[3] == "2":
  684.                 command += ["-solo:modem"]
  685.             command += ["null_modem"]
  686.             command += ["-bitb1", "socket."+self.bitb.text()]
  687.         if selected[3] == "2" and selected[4] != "n" and selected[4] != "w":
  688.             command += ["-hard1", os.path.join(self.diskpath, self.disk.currentText())]
  689.         if self.serialdbg.isChecked():
  690.             command += ["-bitb2", "socket.127.0.0.1:3344"]
  691.             self.start_socket_server()
  692.         command += ["-skip_gameinfo"]
  693.         command += ["-video", "sdl"]
  694.         command += ["-keyboardprovider", "win32"]
  695.         command += ["-autoboot_script", "mame_input_pipe.lua"]
  696.         self.save_settings()
  697.         print(f"{command}")
  698.         # Run the command in another thread to avoid blocking the UI
  699.         self.worker = MameWorker(command)
  700.         self.worker.window_found.connect(self.embed_window)
  701.         self.worker.start()
  702.         # Soft loop: process events while the worker thread is running
  703.         self.button.setText("Stop MAME")
  704.         self.button.clicked.disconnect()
  705.         self.button.clicked.connect(self.stop_mame)
  706.         while self.worker.isRunning():
  707.             QApplication.processEvents(QEventLoop.AllEvents, 100)
  708.         self.button.setText("Launch MAME")
  709.         self.button.clicked.disconnect()
  710.         self.button.clicked.connect(self.on_button_click)
  711.         self.mame_window.repaint()
  712.                                    
  713.     def start_socket_server(self):
  714.         # Stop any existing server first
  715.         self.stop_socket_server()
  716.        
  717.         def handle_client():
  718.             try:
  719.                 self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  720.                 self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  721.                 # Increase socket buffer sizes for high-volume data
  722.                 self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)
  723.                 self.server_socket.settimeout(1.0)  # Add timeout to prevent hanging
  724.                 self.server_socket.bind(('127.0.0.1', 3344))
  725.                 self.server_socket.listen(1)
  726.                
  727.                 while True:
  728.                     try:
  729.                         self.conn, addr = self.server_socket.accept()
  730.                         # Increase connection buffer and set TCP_NODELAY for faster transmission
  731.                         self.conn.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)
  732.                         self.conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
  733.                         self.conn.settimeout(0.05)  # Shorter timeout for more responsive flushing
  734.                        
  735.                         # Buffer for accumulating partial data
  736.                         data_buffer = b''
  737.                        
  738.                         while True:
  739.                             try:
  740.                                 # Use larger buffer for high-volume data
  741.                                 chunk = self.conn.recv(8192)
  742.                                 if not chunk:
  743.                                     break
  744.                                    
  745.                                 data_buffer += chunk
  746.                                
  747.                                 # Process and emit data more aggressively for better responsiveness
  748.                                 while data_buffer:
  749.                                     # Look for complete lines first
  750.                                     if b'\n' in data_buffer:
  751.                                         line, data_buffer = data_buffer.split(b'\n', 1)
  752.                                         # Include the newline in the output
  753.                                         complete_line = line + b'\n'
  754.                                         self.append_text_signal.emit(complete_line.decode(errors='replace'))
  755.                                     else:
  756.                                         # If no newline, emit data in smaller chunks for responsiveness
  757.                                         # But wait a bit to see if more data comes quickly
  758.                                         if len(data_buffer) > 256:  # Reduced threshold for faster response
  759.                                             # Emit partial data to keep display responsive
  760.                                             self.append_text_signal.emit(data_buffer.decode(errors='replace'))
  761.                                             data_buffer = b''
  762.                                         else:
  763.                                             # Small amount of data, break and wait for more or timeout
  764.                                             break
  765.                                    
  766.                             except socket.timeout:
  767.                                 # On timeout, flush any remaining buffered data immediately
  768.                                 if data_buffer:
  769.                                     self.append_text_signal.emit(data_buffer.decode(errors='replace'))
  770.                                     data_buffer = b''
  771.                                 # Check if we should stop
  772.                                 if not hasattr(self, 'server_socket') or self.server_socket is None:
  773.                                     break
  774.                                 continue
  775.                             except (ConnectionResetError, ConnectionAbortedError, OSError):
  776.                                 # Connection was closed by client
  777.                                 break
  778.                        
  779.                         # Flush any remaining data when connection closes
  780.                         if data_buffer:
  781.                             self.append_text_signal.emit(data_buffer.decode(errors='replace'))
  782.                                
  783.                     except socket.timeout:
  784.                         # Check if we should stop
  785.                         if not hasattr(self, 'server_socket') or self.server_socket is None:
  786.                             break
  787.                         continue
  788.                     except OSError as e:
  789.                         if e.errno == 10048:  # Address already in use
  790.                             self.append_text_signal.emit("Error: Port 3344 already in use. Try again.")
  791.                             break
  792.                         elif "forcibly closed" in str(e) or "connection" in str(e).lower():
  793.                             # Expected disconnection
  794.                             break
  795.                         else:
  796.                             print(f"Socket error: {e}")
  797.                             break
  798.                            
  799.             except Exception as e:
  800.                 self.append_text_signal.emit(f"Socket server error: {e}")
  801.             finally:
  802.                 self.stop_socket_server()
  803.                
  804.         self.server_thread = threading.Thread(target=handle_client, daemon=True)
  805.         self.server_thread.start()
  806.  
  807.     def stop_socket_server(self):
  808.         """Properly close socket server and cleanup resources"""
  809.         if hasattr(self, 'conn') and self.conn:
  810.             try:
  811.                 self.conn.close()
  812.             except:
  813.                 pass
  814.             self.conn = None
  815.            
  816.         if hasattr(self, 'server_socket') and self.server_socket:
  817.             try:
  818.                 self.server_socket.close()
  819.             except:
  820.                 pass
  821.             self.server_socket = None
  822.            
  823.         if hasattr(self, 'server_thread') and self.server_thread and self.server_thread.is_alive():
  824.             self.server_thread.join(timeout=1.0)  # Wait up to 1 second for thread to finish
  825.  
  826.        
  827. if __name__ == "__main__":
  828.     os.environ['PATH'] += ":" + os.path.dirname(os.path.abspath(__file__))
  829.     app = QApplication(sys.argv)
  830.     window = MainWindow()
  831.     window.show()
  832.     app.exec()
  833.     #sys.exit(app.exec_())  
  834.  
Tags: webtv
Advertisement