Documentation
¶
Overview ¶
Zaparoo Core Copyright (c) 2026 The Zaparoo Project Contributors. SPDX-License-Identifier: GPL-3.0-or-later
This file is part of Zaparoo Core.
Zaparoo Core is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Zaparoo Core is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Zaparoo Core. If not, see <http://www.gnu.org/licenses/>.
Zaparoo Core Copyright (c) 2026 The Zaparoo Project Contributors. SPDX-License-Identifier: GPL-3.0-or-later
This file is part of Zaparoo Core.
Zaparoo Core is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Zaparoo Core is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Zaparoo Core. If not, see <http://www.gnu.org/licenses/>.
Zaparoo Core Copyright (c) 2026 The Zaparoo Project Contributors. SPDX-License-Identifier: GPL-3.0-or-later
This file is part of Zaparoo Core.
Zaparoo Core is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Zaparoo Core is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Zaparoo Core. If not, see <http://www.gnu.org/licenses/>.
Zaparoo Core Copyright (c) 2026 The Zaparoo Project Contributors. SPDX-License-Identifier: GPL-3.0-or-later
This file is part of Zaparoo Core.
Zaparoo Core is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Zaparoo Core is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Zaparoo Core. If not, see <http://www.gnu.org/licenses/>.
Package pinup integrates the PinUP Popper virtual pinball frontend. Popper keeps its library in PUPDatabase.db, a SQLite file in the PinUPSystem folder, and takes remote commands through its web remote (PuPServer.exe) while PinUpMenu.exe is running. Everything except the Win32 bindings is free of build tags so the behaviour can be tested on any platform.
Zaparoo Core Copyright (c) 2026 The Zaparoo Project Contributors. SPDX-License-Identifier: GPL-3.0-or-later
This file is part of Zaparoo Core.
Zaparoo Core is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Zaparoo Core is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Zaparoo Core. If not, see <http://www.gnu.org/licenses/>.
Zaparoo Core Copyright (c) 2026 The Zaparoo Project Contributors. SPDX-License-Identifier: GPL-3.0-or-later
This file is part of Zaparoo Core.
Zaparoo Core is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
Zaparoo Core is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with Zaparoo Core. If not, see <http://www.gnu.org/licenses/>.
Index ¶
- Constants
- Variables
- func CandidateExecutables(e *Emulator) []string
- func DefaultInstallDirs() []string
- func InstallDirFromServerPath(value string) string
- func IsPinball(e *Emulator) bool
- func MatchesExecutable(exe string, candidates []string) bool
- func NewLauncher(i *Integration) platforms.Launcher
- func NormalizeExecutable(name string) string
- func ParseLocalServer32(value string) string
- func ParseTablePath(path string) (int, error)
- func ScanResults(lib Library) []platforms.ScanResult
- func ScanResultsWithRoot(lib Library, installRoot string) []platforms.ScanResult
- func ServerURL(port int) string
- func TableExecutables(table *Table, e *Emulator) []string
- func TablePath(gameID int, display string) string
- type Deps
- type Emulator
- type EmulatorClass
- type Frontend
- type HTTPDoer
- type Install
- type Integration
- func (i *Integration) Available(cfg *config.Instance) error
- func (i *Integration) EmulatorRunning() bool
- func (i *Integration) Launch(cfg *config.Instance, path string) error
- func (i *Integration) Locate(cfg *config.Instance) (Install, error)
- func (i *Integration) Scan(ctx context.Context, cfg *config.Instance) ([]platforms.ScanResult, error)
- func (i *Integration) Stop()
- func (i *Integration) StopTable() error
- type Library
- type Locator
- type ProcessInfo
- type ProcessLister
- type Remote
- type Table
- type Timeouts
Constants ¶
const ( // DefaultServerPort is where Popper itself starts PuPServer when the web // remote is enabled in its menu script. DefaultServerPort = 80 // CoreServerPort is the port Core uses when it has to start PuPServer. // VPin Studio uses 8091 for the same purpose, so a different port keeps the // two from fighting over one process. CoreServerPort = 8095 // ServerSocketPort is the websocket port PuPServer requires alongside // -wwwport. Core only uses the HTTP side. ServerSocketPort = 8888 )
const EventEmuExit = 15
EventEmuExit is the Popper function event, as accepted by the web remote's /pupkey/<id> route, that closes the running table through the emulator's exit script and returns Popper to the wheel. The number is Popper's own.
const LauncherID = "PinUPPopper"
LauncherID identifies the Popper launcher to users and in ActiveMedia.
Variables ¶
var ( // ErrNoActiveTable is returned by StopTable when Core is not tracking a // Popper launch. ErrNoActiveTable = errors.New("no active PinUP Popper table") // ErrTableRunning is returned when a table did not exit within the wait // after Popper was asked to close it. ErrTableRunning = errors.New("a PinUP Popper table is still running") // ErrWebRemoteMissing is returned when the install lacks PuPServer.exe, // the only documented way to ask Popper to launch a table. ErrWebRemoteMissing = errors.New("PinUP Popper web remote (PuPServer.exe) is not installed") // ErrNoStopMechanism is returned when no web remote is available to ask // Popper to close a table. ErrNoStopMechanism = errors.New("PinUP Popper web remote is not available to close the table") )
var ErrNotInstalled = errors.New("PinUP Popper is not installed")
ErrNotInstalled is returned when no PinUP System folder can be found.
var ErrTooManyTables = errors.New("PinUP Popper database exceeds the table limit")
ErrTooManyTables is returned when the database holds more visible tables than Core is willing to index.
Functions ¶
func CandidateExecutables ¶
CandidateExecutables returns the process images that may be the running table for an emulator, most specific first: the process Popper is configured to watch, then the executables its program is known to run under. Exact names carry the .exe suffix; entries ending in * are prefixes.
func DefaultInstallDirs ¶
func DefaultInstallDirs() []string
DefaultInstallDirs lists where PinUP System is normally installed: the Baller Installer's vPinball tree and a bare PinUPSystem folder, on each of the first few drive letters.
func InstallDirFromServerPath ¶
InstallDirFromServerPath derives the PinUP System folder from the registered PinUP Player executable path. It splits on either separator itself so the Windows value can be parsed on any platform.
func MatchesExecutable ¶
MatchesExecutable reports whether an image path or name is one of the candidate executables, by exact name or by prefix for entries ending in *.
func NewLauncher ¶
func NewLauncher(i *Integration) platforms.Launcher
NewLauncher builds the PinUP Popper launcher around an integration. The lifecycle is external: Popper starts and owns the emulator, and the integration publishes ActiveMedia once it has adopted that process.
func NormalizeExecutable ¶
NormalizeExecutable reduces a process name or path to a bare image name with the .exe suffix Popper's tools and Windows both report.
func ParseLocalServer32 ¶
ParseLocalServer32 extracts the executable path from a COM LocalServer32 registry value, which may be quoted and may carry command-line arguments.
func ParseTablePath ¶
ParseTablePath extracts the Popper GameID from a popper:// virtual path.
func ScanResults ¶
func ScanResults(lib Library) []platforms.ScanResult
ScanResults converts a library into indexable media. Every table becomes a popper:// virtual path keyed by GameID; the display name is what users see.
func ScanResultsWithRoot ¶
func ScanResultsWithRoot(lib Library, installRoot string) []platforms.ScanResult
ScanResultsWithRoot adds local table-file provenance when Popper provides a trustworthy games directory and filename.
func TableExecutables ¶
TableExecutables is CandidateExecutables with the table's own alternate launcher executable, when Popper is configured to run it with one, placed first.
Types ¶
type Deps ¶
type Deps struct {
ActiveMedia func() *models.ActiveMedia
SetActiveMedia func(*models.ActiveMedia)
Frontend Frontend
Processes ProcessLister
Clock clockwork.Clock
HTTP HTTPDoer
Locator Locator
Timeouts Timeouts
}
Deps are the platform hooks and external boundaries the integration uses. Every boundary is an interface or function so tests can script Popper, the emulator processes and the clock. The emulator process is never handed to the platform: it is followed by PID, and stopped only through Popper, so the platform's process-tree kill cannot leave Popper stuck mid-launch.
type Emulator ¶
type Emulator struct {
Name string
Display string
MediaDir string
GamesDir string
GamesExt string
LaunchScript string
ProcessName string
WindowTitle string
ID int
Visible bool
}
Emulator is a Popper emulator definition: how tables of one kind are launched and closed, and where their media lives.
type EmulatorClass ¶
type EmulatorClass int
EmulatorClass groups Popper emulators by the pinball program they run, which decides the executables Core watches when Popper does not name one.
const ( // ClassNone is an emulator that does not run pinball tables. ClassNone EmulatorClass = iota ClassVisualPinball ClassFuturePinball ClassPinballFX ClassPinballM ClassZaccaria ClassProPinball // ClassOtherPinball is a pinball emulator with no known executable list; // only Popper's own ProcessName can identify its process. ClassOtherPinball )
func ClassifyEmulator ¶
func ClassifyEmulator(e *Emulator) EmulatorClass
ClassifyEmulator decides which pinball program an emulator runs, or ClassNone when it is not a pinball emulator at all. The game file extension is the most reliable signal, then the emulator's own names; the launch script is only consulted when neither says anything.
type Frontend ¶
type Frontend interface {
// StartMenu launches PinUpMenu.exe detached, with the install folder as
// its working directory, the way Popper's own startup script does.
StartMenu(ctx context.Context, inst *Install) error
// StartServer launches PuPServer.exe, the web remote, listening on port.
StartServer(ctx context.Context, inst *Install, port int) error
}
Frontend starts Popper's programs. Only the operations with side effects live here; whether the menu or web server is running is read from the process list, so tests script both through one fake. Events go through the web remote: Popper's own Launch\SendPuPEvent.exe does not close a table on Popper 2.0, so it is not used.
type Install ¶
Install is a located PinUP System folder. ServerExe is empty when the install does not ship the web remote.
type Integration ¶
type Integration struct {
// contains filtered or unexported fields
}
Integration launches tables through Popper and tracks the emulator process Popper starts for them. It owns ActiveMedia for Popper launches, as the launcher has an external lifecycle.
func NewIntegration ¶
func NewIntegration(deps *Deps) *Integration
NewIntegration wires the integration; nil boundaries get real ones.
func (*Integration) Available ¶
func (i *Integration) Available(cfg *config.Instance) error
Available reports whether Popper is installed.
func (*Integration) EmulatorRunning ¶
func (i *Integration) EmulatorRunning() bool
EmulatorRunning reports whether the emulator of the tracked launch is still alive. It lets the platform refuse to report a stop it cannot confirm.
func (*Integration) Launch ¶
func (i *Integration) Launch(cfg *config.Instance, path string) error
Launch asks Popper to start a table and begins tracking the emulator it spawns. It returns once the request has been sent; ActiveMedia is published by a watcher when the emulator appears.
func (*Integration) Locate ¶
func (i *Integration) Locate(cfg *config.Instance) (Install, error)
Locate resolves the install from config, registry and default paths.
func (*Integration) Scan ¶
func (i *Integration) Scan(ctx context.Context, cfg *config.Instance) ([]platforms.ScanResult, error)
Scan reads the launchable tables. An absent install contributes nothing rather than failing the scan; a configured directory that does not hold Popper is a real error the user should see.
func (*Integration) Stop ¶
func (i *Integration) Stop()
Stop ends every watcher and forgets the tables they followed.
func (*Integration) StopTable ¶
func (i *Integration) StopTable() error
StopTable asks Popper to close the running table and waits, bounded, for the emulator to go. It never clears ActiveMedia itself: the platform does that once the stop is confirmed, and the watcher's own exit handling is idempotent. A timeout is an error, which the platform reports as a failed stop because Core holds no process handle to escalate with.
type Library ¶
Library is the launchable part of a Popper database: visible tables that belong to visible emulators classified as pinball.
func ReadLibrary ¶
ReadLibrary loads the launchable tables from a Popper database. The file is opened read-only with a short busy timeout because Popper and its setup tool write to it while they run.
type Locator ¶
Locator finds the PinUP System folder. Registry, when set, resolves the folder PinUP Player registered itself from; Candidates are the default install locations tried last.
type ProcessInfo ¶
ProcessInfo is one running process as the integration sees it: enough to match it against an emulator's executables.
type ProcessLister ¶
type ProcessLister interface {
List() ([]ProcessInfo, error)
}
ProcessLister enumerates running processes. It is an interface so tests can script which emulator and Popper processes exist at each moment.
func NewProcessLister ¶
func NewProcessLister() ProcessLister
NewProcessLister returns the real process lister.
type Remote ¶
type Remote struct {
// contains filtered or unexported fields
}
Remote talks to Popper's web remote (PuPServer.exe). Every call is a GET that PuPServer answers with 200 while PinUpMenu.exe is running.
func (*Remote) LaunchGame ¶
LaunchGame asks Popper to start a table by GameID, exactly as selecting it on the wheel would.
type Table ¶
type Table struct {
Name string
Display string
FileName string
Manufacturer string
GameType string
Category string
Theme string
Notes string
Author string
AltExe string
ID int
EmulatorID int
Year int
Players int
Rating int
}
Table is one Popper game row. Name is the table file's basename without extension, which is also the stem every media file for the table is named after. ID is Popper's GameID and survives renames and metadata edits. AltExe is the alternate launcher executable Popper runs this table with instead of the emulator's default, when one is set.
func (*Table) DisplayName ¶
DisplayName returns the name shown for the table, falling back to the file stem when Popper has no display title.
type Timeouts ¶
type Timeouts struct {
// MenuStart is how long PinUpMenu.exe may take to appear after Core
// starts it.
MenuStart time.Duration
// ServerStart is how long the web remote may take to answer after Core
// starts PuPServer.exe.
ServerStart time.Duration
// LaunchAccept is how long the emulator process may take to appear after
// Popper is asked to launch a table.
LaunchAccept time.Duration
// LaunchRetry is how long to wait for the emulator process before asking
// Popper again. Popper drops a launch that arrives while it is still
// returning to the wheel after the previous table, and it starts the
// emulator within a second when it accepts, so a quiet few seconds means
// the request was lost rather than slow.
LaunchRetry time.Duration
// StopWait is how long the emulator may take to exit after Popper is
// asked to close the table. It covers a table that is still loading when
// the request arrives, because Popper drops exits until the emulator is
// up, and killing the emulator instead leaves Popper's launch helpers
// waiting on a player window for up to a minute, ignoring every launch.
StopWait time.Duration
// ExitRetry is how often the exit event is repeated while waiting, since
// a request that lands during the load is dropped rather than queued.
ExitRetry time.Duration
// LaunchSettle is the pause after PinUpMenu.exe appears before it is sent
// commands.
LaunchSettle time.Duration
// Poll is the process list polling interval.
Poll time.Duration
}
Timeouts bound every wait the integration performs. Nothing waits without one, but the values are generous: Popper ignores an exit request while the emulator is still loading a table, and only Popper can close a table without leaving it stuck, so a stop that lands during a load has to outwait the load.
func DefaultTimeouts ¶
func DefaultTimeouts() Timeouts
DefaultTimeouts returns the production waits.