diff --git a/eslint.config.js b/eslint.config.js index 9122084..5b3e554 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -63,7 +63,8 @@ export default [ "@typescript-eslint/no-inferrable-types": "off", "@typescript-eslint/semi": ["error", "never"], "@typescript-eslint/triple-slash-reference": "off", - "max-len": ["error", { code: 80, tabWidth: 4 }], + "max-len": ["error", { code: 100, tabWidth: 4 }], + "constructor-super": 0, "prettier/prettier": 2 }, files: ["**/*.ts", "**/*.js"] diff --git a/package.json b/package.json index 610137a..35d1f72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openrct2-name-generator", - "version": "0.1", + "version": "0.1.2", "main": "app.js", "license": "MIT", "author": "rolfhermancoen", @@ -52,5 +52,8 @@ "./tests/_setup.cjs" ], "verbose": true + }, + "dependencies": { + "openrct2-flexui": "^0.1.0-prerelease.0" } } diff --git a/src/dictionary/filter.ts b/src/dictionary/filter.ts deleted file mode 100644 index c255338..0000000 --- a/src/dictionary/filter.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { RideType } from "../enum/rideType" -import { VehicleType } from "../enum/vehicleType" -import { NameSet } from "../generator" - -export const filterNameSet = ( - array: readonly NameSet[], - rideType?: RideType, - vehicleType?: VehicleType -) => { - return array.filter((val) => { - if ( - rideType !== undefined && - val.ride && - !val.ride.some((t) => t.toString() === rideType.toString()) - ) { - return false - } - if ( - vehicleType !== undefined && - val.vehicle && - !val.vehicle.some((t) => t.toString() === vehicleType.toString()) - ) { - return false - } - return true - }) -} diff --git a/src/enum/windowClass.ts b/src/enum/windowClass.ts new file mode 100644 index 0000000..946b8fe --- /dev/null +++ b/src/enum/windowClass.ts @@ -0,0 +1,82 @@ +export enum WindowClass { + MainWindow = 0, + TopToolbar = 1, + BottomToolbar = 2, + Tooltip = 5, + Dropdown = 6, + About = 8, + Error = 11, + Ride = 12, + RideConstruction = 13, + SavePrompt = 14, + RideList = 15, + ConstructRide = 16, + DemolishRidePrompt = 17, + Scenery = 18, + Options = 19, + Footpath = 20, + Land = 21, + Water = 22, + Peep = 23, + GuestList = 24, + StaffList = 25, + FirePrompt = 26, + ParkInformation = 27, + Finances = 28, + TitleMenu = 29, + TitleExit = 30, + RecentNews = 31, + ScenarioSelect = 32, + TrackDesignList = 33, + TrackDesignPlace = 34, + NewCampaign = 35, + KeyboardShortcutList = 36, + ChangeKeyboardShortcut = 37, + Map = 38, + TitleLogo = 39, + Banner = 40, + MapTooltip = 41, + EditorObjectSelection = 42, + EditorInventionList = 43, + EditorInventionListDrag = 44, + EditorScenarioOptions = 45, + EditorObjectiveOptions = 46, + ManageTrackDesign = 47, + TrackDeletePrompt = 48, + InstallTrack = 49, + ClearScenery = 50, + SceneryScatter = 51, + NotificationOptions = 109, + Cheats = 110, + Research = 111, + Viewport = 112, + Textinput = 113, + Mapgen = 114, + Loadsave = 115, + LoadsaveOverwritePrompt = 116, + TitleOptions = 117, + LandRights = 118, + Themes = 119, + TileInspector = 120, + Changelog = 121, + Multiplayer = 124, + Player = 125, + NetworkStatus = 126, + ServerList = 127, + ServerStart = 128, + CustomCurrencyConfig = 129, + DebugPaint = 130, + ViewClipping = 131, + ObjectLoadError = 132, + PatrolArea = 133, + Transparency = 134, + AssetPacks = 135, + ResetShortcutKeysPrompt = 136, + Staff = 220, + EditorTrackBottomToolbar = 221, + EditorScenarioBottomToolbar = 222, + Chat = 223, + Console = 224, + Custom = 225, + Null = 255 +} diff --git a/src/filter.ts b/src/filter.ts new file mode 100644 index 0000000..8935bed --- /dev/null +++ b/src/filter.ts @@ -0,0 +1,26 @@ +import { RideType } from "./enum/rideType" +import { VehicleType } from "./enum/vehicleType" +import { NameSet } from "./generator" + +export const filterNameSet = ( + sets: NameSet[], + rideType?: RideType, + vehicleType?: VehicleType +) => { + const matchesRideType = (set: NameSet) => { + return ( + rideType === undefined || + !set.ride || + set.ride?.some((t) => t.toString() === rideType.toString()) + ) + } + + const matchesVehicleType = (set: NameSet) => { + return ( + vehicleType === undefined || + !set.vehicle || + set.vehicle?.some((t) => t.toString() === vehicleType.toString()) + ) + } + return sets.filter((set) => matchesRideType(set) && matchesVehicleType(set)) +} diff --git a/src/generator.ts b/src/generator.ts index 3ea9894..c1f0584 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -1,32 +1,79 @@ import adjectives from "./dictionary/adjective" -import { filterNameSet } from "./dictionary/filter" +import { filterNameSet } from "./filter" import nouns from "./dictionary/nouns" -import suffix from "./dictionary/suffix" +import suffixes from "./dictionary/suffix" import { RideType } from "./enum/rideType" import { VehicleType } from "./enum/vehicleType" +// Define the NameSet type export type NameSet = { value: string - ride?: readonly RideType[] - vehicle?: readonly VehicleType[] + ride?: RideType[] + vehicle?: VehicleType[] } -const getRand = (array: readonly NameSet[], emptyChange = 0) => { - if (Math.random() < emptyChange) { +// Type for the memoization cache key +type CacheKey = + `${"adjective" | "noun" | "suffix"}-${RideType | "none"}-${VehicleType | "none"}` + +// Configuration object for empty chance values +const EMPTY_CHANCE_CONFIG = { + adjective: 0.1, + suffix: 0.8 +} + +// Utility function to get a random element from an array +const getRandomElement = ( + array: NameSet[], + emptyChance = 0 +): NameSet | null => { + if (Math.random() < emptyChance) { + return null + } + + if (array.length === 0) { return null } - return array[Math.floor(Math.random() * array.length)] + + const randomIndex = Math.floor(Math.random() * array.length) + return array[randomIndex] ?? null } +// Memoize the filterNameSet results to improve performance +const memoizeFilterNameSet = (() => { + const cache: Record = {} as Record + + return ( + source: NameSet[], + type: "adjective" | "noun" | "suffix", + rideType?: RideType, + vehicleType?: VehicleType + ): NameSet[] => { + const key: CacheKey = `${type}-${rideType ?? "none"}-${vehicleType ?? "none"}` + + if (!cache[key]) { + cache[key] = filterNameSet(source, rideType, vehicleType) + } + return cache[key] || [] + } +})() + +// Generate a name based on optional rideType and vehicleType export const generateName = ( rideType?: RideType, vehicleType?: VehicleType -) => { - return [ - getRand(filterNameSet(adjectives, rideType, vehicleType), 0.1)?.value, - getRand(filterNameSet(nouns, rideType, vehicleType))?.value, - getRand(filterNameSet(suffix, rideType, vehicleType), 0.8)?.value - ] - .filter((val) => val !== undefined) - .join(" ") +): string => { + const adjective = getRandomElement( + memoizeFilterNameSet(adjectives, "adjective", rideType, vehicleType), + EMPTY_CHANCE_CONFIG.adjective + )?.value + const noun = getRandomElement( + memoizeFilterNameSet(nouns, "noun", rideType, vehicleType) + )?.value + const suffix = getRandomElement( + memoizeFilterNameSet(suffixes, "suffix", rideType, vehicleType), + EMPTY_CHANCE_CONFIG.suffix + )?.value + + return [adjective, noun, suffix].filter(Boolean).join(" ") } diff --git a/src/main.ts b/src/main.ts index ededfd3..e8cd3a9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,25 +1,99 @@ import { RideType } from "./enum/rideType" -import { VehicleType } from "./enum/vehicleType" import { generateName } from "./generator" +import { WindowClass } from "./enum/windowClass" +import { mainWindow } from "./ui/mainWindow" + +const hasPremadeTrackDesignWindowOpen = () => { + for (let index = 0; index < ui.windows; index++) { + const w = ui.getWindow(index) + + if ( + w.classification == WindowClass.TrackDesignList || + w.classification == WindowClass.TrackDesignPlace + ) { + return true + } + } + return false +} + +const getAllExistingRideNames = () => { + const rideNames = [] + for (let i = 0; i <= map.numRides; i++) { + const ride = map.getRide(i) + if (ride) { + rideNames.push(ride.name) + } + } + return rideNames +} + +const setRideName = (rideType: RideType, ride: number) => { + if (hasPremadeTrackDesignWindowOpen()) { + return + } + + const existingNames = getAllExistingRideNames() + + let foundName = null + while (!foundName) { + const name = generateName(rideType) + + if (existingNames.every((n) => n !== name)) { + foundName = name + } + } + + context.executeAction( + "ridesetname", + { ride, name: foundName }, + ({ error, errorMessage, errorTitle }) => { + if (error) { + console.log(`${errorTitle}: ${errorMessage}`) + } else { + console.log(`Successfully named rideId ${ride} to ${foundName}`) + } + } + ) +} + +const openPluginWindow = () => { + // Check if game is up-to-date... + const version = context.apiVersion + if (version < 75) { + // 75 => https://github.com/OpenRCT2/OpenRCT2/pull/19305 + showUpdateError( + "The version of OpenRCT2 you are currently playing is too old for this plugin." + ) + return + } + + // Show the current instance if one is active. + mainWindow.open() +} + +/** + * Report to the player that they need to update the game, + * both ingame and in console. + */ +function showUpdateError(message: string): void { + const title = "Please update the game! " + + ui.showError(title, message) + console.log("[NameGenerator] " + title + message) +} export function main() { - // Register a menu item under the map icon: - // if (typeof ui !== "undefined") { - // ui.registerMenuItem("NameGenerator", () => onClickMenuItem()) - // } + if (typeof ui !== "undefined") { + ui.registerMenuItem("NameGenerator", () => openPluginWindow()) + } context.subscribe("action.execute", (event) => { switch (event.action) { case "ridecreate": { - if ("rideObject" in event.args && "rideType" in event.args) { - const object = objectManager.getObject( - "ride", - event.args.rideObject as number - ) - console.log( - generateName( - event.args.rideType as RideType, - object.identifier as VehicleType - ) + if ("rideType" in event.args && "ride" in event.result) { + setRideName( + event.args.rideType as RideType, + event.result.ride as number ) } break diff --git a/src/nameSet.ts b/src/nameSet.ts index b71200a..78adf85 100644 --- a/src/nameSet.ts +++ b/src/nameSet.ts @@ -2,21 +2,18 @@ import { RideType } from "./enum/rideType" import adjectives from "./dictionary/adjective" import nouns from "./dictionary/nouns" import suffixes from "./dictionary/suffix" +import { NameSet } from "./generator" +import { filterNameSet } from "./filter" -export const getNouns = () => nouns - -export const getRideTypeNouns = (rideType: RideType) => { - return nouns.filter( - (val) => !val.ride || val.ride.some((r) => r === rideType) - ) -} +export const getNouns = (): NameSet[] => nouns +export const getRideTypeNouns = (rideType: RideType): NameSet[] => + filterNameSet(getNouns(), rideType) export const getAdjectives = () => adjectives +export const getRideTypeAdjectives = (rideType: RideType): NameSet[] => + filterNameSet(getAdjectives(), rideType) -export const getRideTypeAdjectives = (rideType: RideType) => { - return adjectives.filter( - (val) => !val.ride || val.ride.some((r) => r === rideType) - ) -} +export const getSuffixes = (): NameSet[] => suffixes -export const getSuffixes = () => suffixes +export const getRideTypeSuffixes = (rideType: RideType): NameSet[] => + filterNameSet(getSuffixes(), rideType) diff --git a/src/ui/mainWindow.ts b/src/ui/mainWindow.ts new file mode 100644 index 0000000..971488a --- /dev/null +++ b/src/ui/mainWindow.ts @@ -0,0 +1,20 @@ +import { label, window } from "openrct2-flexui" + +export const mainWindow = window({ + title: "NameGenerator", + width: 320, + height: 60, + spacing: 5, + content: [ + label({ + text: "github.com/rolfhermancoen/OpenRCT2-NameGenerator", + tooltip: "Go to this URL to check for the latest updates", + alignment: "centred", + disabled: true + }), + label({ + text: "version 0.1.1", + alignment: "centred" + }) + ] +}) diff --git a/tests/generator/filter.test.ts b/tests/generator/filter.test.ts index 73028b2..8fb5131 100644 --- a/tests/generator/filter.test.ts +++ b/tests/generator/filter.test.ts @@ -1,5 +1,5 @@ import test from "ava" -import { filterNameSet } from "../../src/dictionary/filter" +import { filterNameSet } from "../../src/filter" import { FERRIS_WHEEL, MERRY_GO_ROUND } from "../../src/enum/rideType" export const TEST_NAME_SET = [ diff --git a/tests/generator/generator.test.ts b/tests/generator/generator.test.ts index 1fbe627..4c6bedb 100644 --- a/tests/generator/generator.test.ts +++ b/tests/generator/generator.test.ts @@ -38,16 +38,18 @@ test("generates a random name", (t) => { }) test("generates a random name based on rideType", (t) => { - const keys = Object.keys(RideType).filter((key) => !isNumeric(key)) + const keys = Object.keys(RideType).filter((key) => isNumeric(key)) for (let i = 0; i < keys.length; i++) { - if (keys[i].search("UNKNOWN") === 0) { + if ( + SKIPPABLE_RIDE_TYPES.some( + (t) => t.toString() === keys[i].toString() + ) + ) { continue } - const name = generateName( - RideType[keys[i] as unknown as number] as unknown as RideType - ) + const name = generateName(keys[i] as unknown as RideType) t.is(typeof name, "string", keys[i]) t.not(name, "", keys[i]) diff --git a/yarn.lock b/yarn.lock index ef53d34..ca3d2da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2675,6 +2675,11 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +openrct2-flexui@^0.1.0-prerelease.0: + version "0.1.0-prerelease.0" + resolved "https://registry.yarnpkg.com/openrct2-flexui/-/openrct2-flexui-0.1.0-prerelease.0.tgz#2b3d95dd279c9bd551386d47eb312308776b755a" + integrity sha512-eJrCLJSua/J+Q5InJSGwGJKC+2PUc4a/RarJlFQqKtadhX6dk3qfgwL7SIzr0MMVdj6WS3Hboya2wHXHdKNvVA== + openrct2-mocks@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/openrct2-mocks/-/openrct2-mocks-0.1.6.tgz#8f8f9291b63283035114930c4ea7a1b8c7c4308a"