import * as fse from "fs-extra" ; import { dirname , join , resolve , basename , extname } from "path" ; import { getUserDir , readLocalSetting } from "./utils/preferences_grab" ; import { inheritYYFile } from "./utils/yy_inherit" ; import { spawn } from "child_process" ; import { RubberEventEmitter } from "./rubber-events" ; import { IRuntimeIndex , IBuildMeta , IBuildSteamOptions , IBuildPreferences , IBuildTargetOptions , IWindowsOptions } from "./build-typings" ; import { EventEmitter } from "events" ; import { uuid } from "./utils/uuid" ; const tempFolder = process . env . TEMP ; const appdataFolder = process . env . APPDATA ; export interface IRubberOptions { /** Path to the .yyp file, this can be relative or absolute */ projectPath : string ; /** Use YoYoCompiler instead of VM, default false */ yyc ? : boolean ; /** Set Debugger Port, default disable debugger */ debug ? : number ; /** Enable Verbose on IGOR.exe, default false */ verbose ? : boolean ; /** Toggle whether to use the EA version */ ea ? : boolean ; /** Set GameMaker configuration, default "default" */ config ? : string ; /** Type of build to run. */ build : "test" | "zip" | "installer" ; /** Output of the build, set "" if build="test" */ outputPath : string ; /** Alternate Runtime Location */ runtimeLocation ? : string ; /** The Runtime to Use*/ theRuntime ? : string ; /** Alternate GameMakerStudio2 Install Directory */ gamemakerLocation ? : string ; /** Alternate GameMakerStudio2 ProgramData Directory */ gamemakerDataLocation ? : string ; /** Target Device Config File Directory */ deviceConfigFileLocation ? : string ; /** Target Device Name*/ targetDeviceName ? : string ; platform : "windows" | "mac" | "linux" | "ios" | "android" | "ps4" | "xboxone" | "switch" | "html5" | "uwp" ; } param projectFile param options export function compile ( options , clearRemoteCache = false ) { const emitter = new EventEmitter ( ) as RubberEventEmitter ; const projectFile = resolve ( options . projectPath ) ; const platform = options . platform ; const theRuntime = options . theRuntime ? options . theRuntime : "" ; let component = "" ; let componentBuild = "" ; let defaultPackageKey = "Package" ; let requireRemoteClient = false ; let packageOnly = false ; let targetMask = "64" ; switch ( platform ) { case "android" : component = "Android" ; componentBuild = "android.build_module" ; targetMask = "8" ; break ; case "switch" : component = "Switch" ; componentBuild = "switch.build_module" ; targetMask = "144115188075855872" ; break ; case "windows" : defaultPackageKey = "PackageZip" ; component = "Windows" ; componentBuild = "Windows.build_module" ; break ; case "mac" : component = "Mac" ; componentBuild = "Mac.build_module" ; requireRemoteClient = true ; targetMask = "2" ; break ; case "ios" : component = "iOS" ; componentBuild = "ios.build_module" ; requireRemoteClient = true ; packageOnly = true ; targetMask = "4" ; break ; case "linux" : component = "Linux" ; componentBuild = "Linux.build_module" ; requireRemoteClient = true ; targetMask = "128" ; break ; default : component = "Unsupported" ; break ; } let targetDeviceName = options . targetDeviceName ? options . targetDeviceName : "" ; const asyncRun = async ( ) => { if ( component === "Unsupported" ) throw new Error ( "Cannot compile to unsupported platform '" + platform + "'" ) ; if ( packageOnly && options . build !== "zip" ) throw new Error ( "Can only build package for '" + platform + "'" ) ; if ( component !== "Windows" && options . build === "installer" ) throw new Error ( "Only Windows can build installer'" ) ; if ( tempFolder === undefined || appdataFolder === undefined ) { throw new Error ( "%temp% and/or %appdata% is missing in the environment variables." ) ; } if ( process . env . username === undefined ) { throw new Error ( "%username% is missing in the environment variables." ) ; } if ( typeof options . gamemakerDataLocation === "undefined" ) { if ( options . ea ) { options . gamemakerDataLocation = "C:\\ProgramData\\GameMakerStudio2-EA" ; } else { options . gamemakerDataLocation = "C:\\ProgramData\\GameMakerStudio2" ; } } if ( typeof options . gamemakerLocation === "undefined" || options . gamemakerLocation === "" ) { if ( options . ea ) { options . gamemakerLocation = "C:\\Program Files\\GameMaker Studio 2-EA" ; } else { options . gamemakerLocation = "C:\\Program Files\\GameMaker Studio 2" ; } } else { if ( ! ( await fse . pathExists ( options . gamemakerLocation ) ) ) { throw new Error ( "The alternative GMS installation directory does not exist!" ) ; } } let deviceConfig ; let targetOptionValues ; let iosHostMacValues ; if ( ( typeof options . deviceConfigFileLocation === "undefined" || options . deviceConfigFileLocation === "" ) ) { if ( requireRemoteClient ) { throw new Error ( "This platform requires a target device config file" ) ; } } else if ( ! ( await fse . pathExists ( options . deviceConfigFileLocation ) ) ) { throw new Error ( "The Target device config file does not exist!" ) ; } else { try { deviceConfig = ( await fse . readJson ( options . deviceConfigFileLocation ) ) ; } catch ( e ) { throw new Error ( "Invalid Target device config file." ) ; } let devices ; switch ( platform ) { case "android" : devices = deviceConfig [ platform ] . Auto ; break ; default : devices = deviceConfig [ platform ] ; break ; } if ( targetDeviceName === "" ) { targetDeviceName = Object . keys ( devices ) [ 0 ] ; } else { if ( ! devices [ targetDeviceName ] ) { throw new Error ( "Cannot find target device name in the target device config file" ) ; } } targetOptionValues = devices [ targetDeviceName ] ; if ( platform == "ios" ) { iosHostMacValues = deviceConfig . mac [ targetOptionValues . hostmac ] ; if ( ! iosHostMacValues ) { throw new Error ( "Building for iOS requires the host Mac info in the device config file" ) ; } } } emitter . emit ( "compileStatus" , "Starting Rubber\n" ) ; const projectDir = dirname ( projectFile ) ; const projectName = basename ( projectFile ) . substring ( 0 , basename ( projectFile ) . length - extname ( projectFile ) . length ) ; if ( ! ( await fse . pathExists ( join ( projectDir , "options" , "main" , "inherited" , "options_main.inherited.yy" ) ) ) ) { throw new Error ( "Missing options_main.inherited.yy. This can be because of a partial project, or the usage of a differen parent project structure." ) ; } const guid_match = ( await fse . readFile ( join ( projectDir , "options" , "main" , "inherited" , "options_main.inherited.yy" ) ) ) . toString ( ) . match ( '"option_gameguid": "(.*?)"' ) ; if ( ! guid_match ) { throw new Error ( "options_main.inherited.yy is missing project GUID, cannot identify project." ) ; } const guid = guid_match [ 1 ] . replace ( "{" , "" ) . replace ( "}" , "" ) ; const buildTempPath = join ( tempFolder , "gamemaker-rubber" , guid ) ; let runtimeLocation = "" ; if ( options . runtimeLocation ) { runtimeLocation = options . runtimeLocation ; } else { const runtimeIndexPath = join ( options . gamemakerDataLocation , "runtime.json" ) ; if ( await fse . pathExists ( runtimeIndexPath ) ) { let runtimes ; try { runtimes = JSON . parse ( ( await fse . readFile ( runtimeIndexPath ) ) . toString ( ) ) ; } catch ( e ) { throw new Error ( "Invalid GameMaker Studio 2 Runtime Index. Reinstall GameMaker." ) ; } if ( theRuntime === "" ) { if ( typeof runtimes . active !== "string" ) { throw new Error ( "GameMaker has no active runtime, start up GameMaker and compile a project first." ) ; } runtimeLocation = runtimes [ runtimes . active ] ; } else { if ( runtimes [ theRuntime ] ) { runtimeLocation = runtimes [ theRuntime ] ; } else { throw new Error ( "Cannot find the chosen runtime. Make sure the input is correct and the runtime is downloaded." ) ; } } runtimeLocation = runtimeLocation . substring ( 0 , runtimeLocation . indexOf ( "&" ) ) ; } else { throw new Error ( "Cannot Locate GameMaker Studio 2 Runtimes. Either GameMaker is installed somewhere else, or is not been setup." ) ; } } const userDir = await getUserDir ( ) ; const licensePlist = ( await fse . readFile ( join ( userDir , "licence.plist" ) ) ) . toString ( ) ; const allowedComponents = ( licensePlist . match ( /<key>components<\/key>.*?\n.*?<string>(.*?)<\/string>/ ) as any ) [ 1 ] . split ( ";" ) ; if ( ! allowedComponents . includes ( componentBuild ) ) { throw new Error ( "You dont have the permission to build a GameMaker game for " + platform + ". This happens if your logged in as someone else, or didnt buy the module." ) ; } await fse . mkdirs ( join ( buildTempPath , "GMCache" ) ) ; await fse . mkdirs ( join ( buildTempPath , "GMTemp" ) ) ; await fse . mkdirs ( join ( buildTempPath , "Output" ) ) ; emitter . emit ( "compileStatus" , "Creating Build Data\n" ) ; const buildMeta = { applicationPath : join ( options . gamemakerLocation , options . ea ? "GameMakerStudio-EA.exe" : "GameMakerStudio.exe" ) , assetCompiler : "" , compile_output_file_name : join ( buildTempPath , "Output" , projectName + ".win" ) , config : ( options . config ) ? options . config : "default" , debug : ( options . debug !== undefined ) ? "True" : "False" , debuggerPort : ( options . debug ) ? options . debug . toString ( ) : "6509" , helpPort : "51290" , macros : join ( buildTempPath , "macros.json" ) , outputFolder : join ( buildTempPath , "Output" ) , preferences : join ( buildTempPath , "preferences.json" ) , projectDir : projectDir , projectName : projectName , projectPath : projectFile , runtimeLocation : runtimeLocation , steamOptions : join ( buildTempPath , "steam_options.yy" ) , targetFile : options . outputPath , targetMask , targetOptions : join ( buildTempPath , "targetoptions.json" ) , tempFolder : join ( buildTempPath , "GMTemp" ) , useShaders : "True" , userDir : userDir , verbose : ( options . verbose === true ) ? "True" : "False" , } ; await fse . writeFile ( join ( buildTempPath , "build.bff" ) , JSON . stringify ( buildMeta ) ) ; const macros = { "project_name" : projectName , "project_dir" : projectDir , "UserProfileName" : process . env . username , "custom.tempdir" : buildTempPath , "custom.gm_cache" : "${custom.tempdir}\\GMCache" , "custom.gm_temp" : "${custom.tempdir}\\GMTemp" , "custom.output" : "${custom.tempdir}\\Output" , "project_full_filename" : "${project_dir}\\${project_name}.yyp" , "options_dir" : "${project_dir}\\options" , "project_cache_directory_name" : "GMCache" , "asset_compiler_cache_directory" : "${custom.tempdir}" , "project_dir_inherited_BaseProject" : "${runtimeLocation}\\BaseProject" , "project_full_inherited_BaseProject" : "${runtimeLocation}\\BaseProject\\BaseProject.yyp" , "base_project" : "${runtimeLocation}\\BaseProject\\BaseProject.yyp" , "base_options_dir" : "${runtimeLocation}\\BaseProject\\options" , "local_directory" : "${ApplicationData}\\${program_dir_name}" , "local_cache_directory" : "${local_directory}\\Cache" , "temp_directory" : "${custom.gm_temp}" , "system_directory" : "${CommonApplicationData}\\${program_dir_name}" , "system_cache_directory" : "${system_directory}\\Cache" , "runtimeBaseLocation" : "${system_cache_directory}\\runtimes" , "runtimeLocation" : runtimeLocation , "igor_path" : "${runtimeLocation}\\bin\\Igor.exe" , "asset_compiler_path" : "${runtimeLocation}\\bin\\GMAssetCompiler.exe" , "lib_compatibility_path" : "${runtimeLocation}\\lib\\compatibility.zip" , "runner_path" : "${runtimeLocation}\\windows\\Runner.exe" , "webserver_path" : "${runtimeLocation}\\bin\\GMWebServer.exe" , "html5_runner_path" : "${runtimeLocation}\\html5\\scripts.html5.zip" , "adb_exe_path" : "platform-tools\\adb.exe" , "java_exe_path" : "bin\\java.exe" , "licenses_path" : "${exe_path}\\Licenses" , "keytool_exe_path" : "bin\\keytool.exe" , "openssl_exe_path" : "bin\\openssl.exe" , "GMS_name" : options . ea ? "GameMakerStudio2-EA" : "GameMakerStudio2" , "program_dir_name" : "${GMS_name}" , "program_name" : "${GMS_name}" , "program_name_pretty" : "${GMS_name}" , "default_font" : "Open Sans" , "default_style" : "Regular" , "default_font_size" : "9" , "ApplicationData" : "${UserProfile}\\AppData\\Roaming" , "CommonApplicationData" : "C:\\ProgramData" , "ProgramFiles" : "C:\\Program Files" , "ProgramFilesX86" : "C:\\Program Files (x86)" , "CommonProgramFiles" : "C:\\Program Files\\Common Files" , "CommonProgramFilesX86" : "C:\\Program Files (x86)\\Common Files" , "UserProfile" : "C:\\Users\\${UserProfileName}" , "TempPath" : "${UserProfile}\\AppData\\Local" , "exe_path" : options . ea ? "${ProgramFiles}\\GameMaker Studio 2-EA" : "${ProgramFiles}\\GameMaker Studio 2" , } ; await fse . writeFile ( join ( buildTempPath , "macros.json" ) , JSON . stringify ( macros ) ) ; const steamOptions = { steamsdk_path : await readLocalSetting ( "machine.Platform Settings.Steam.steamsdk_path" ) , } ; await fse . writeFile ( join ( buildTempPath , "steam_options.yy" ) , JSON . stringify ( steamOptions ) ) ; const targetoptions = { /**
             * 1. If remote client is not required, only need to know if building for YYC or VM
             * 2. If building for iOS, need to also read the config info from the host Mac
            */ runtime : options . yyc ? "YYC" : "VM" , displayname : requireRemoteClient && targetOptionValues . displayname ? targetOptionValues . displayname : "" , productType : requireRemoteClient && targetOptionValues . productType ? targetOptionValues . productType : "" , version : requireRemoteClient && targetOptionValues . version ? targetOptionValues . version : "" , device : requireRemoteClient && targetOptionValues . device ? targetOptionValues . device : "" , type : requireRemoteClient && targetOptionValues . type ? targetOptionValues . type : "" , status : requireRemoteClient && targetOptionValues . status ? targetOptionValues . status : "" , hostmac : requireRemoteClient && targetOptionValues . hostmac ? targetOptionValues . hostmac : "" , deviceIP : requireRemoteClient && targetOptionValues . deviceIP ? targetOptionValues . deviceIP : "" , target_ip : requireRemoteClient && targetOptionValues . target_ip ? targetOptionValues . target_ip : "" , hostname : platform === "ios" ? iosHostMacValues . hostname : ( requireRemoteClient && targetOptionValues . hostname ? targetOptionValues . hostname : "" ) , username : platform === "ios" ? iosHostMacValues . username : ( requireRemoteClient && targetOptionValues . username ? targetOptionValues . username : "" ) , encrypted_password : platform === "ios" ? iosHostMacValues . encrypted_password : ( requireRemoteClient && targetOptionValues . encrypted_password ? targetOptionValues . encrypted_password : "" ) , install_dir : platform === "ios" ? iosHostMacValues . install_dir : ( requireRemoteClient && targetOptionValues . install_dir ? targetOptionValues . install_dir : "" ) } ; await fse . writeFile ( join ( buildTempPath , "targetoptions.json" ) , JSON . stringify ( targetoptions ) ) ; emitter . emit ( "compileStatus" , "Running IGOR\n" ) ; const exportType = options . build == "test" ? "Run" : ( options . build === "zip" ? defaultPackageKey : "PackageNsis" ) ; const igorArgs = [ "-options=" + join ( buildTempPath , "build.bff" ) , "--" , component , clearRemoteCache ? "Clean" : exportType ] ; const igor = spawn ( join ( runtimeLocation , "bin" , "Igor.exe" ) , igorArgs ) ; const igorErrors = [ ] ; const lanchingGame = options . build == "test" ; let igorState = "igor" ; igor . stdout . on ( 'data' , ( data ) => { if ( lanchingGame && data . toString ( ) . includes ( "Runner.exe  -game" ) ) { emitter . emit ( "compileFinished" , igorErrors ) ; emitter . emit ( "gameStarted" ) ; igorState = "game" ; } if ( igorState == "igor" ) { if ( data . toString ( ) . toLowerCase ( ) . startsWith ( "error" ) ) { igorErrors . push ( data . toString ( ) ) ; } emitter . emit ( "compileStatus" , data . toString ( ) ) ; } else { emitter . emit ( "gameStatus" , data . toString ( ) ) ; } emitter . emit ( "rawStdout" , data . toString ( ) ) ; } ) ; igor . stderr . on ( 'data' , ( data ) => { emitter . emit ( "rawStdout" , data . toString ( ) ) ; if ( igorState == "igor" ) { emitter . emit ( "compileStatus" , data . toString ( ) ) ; } else { emitter . emit ( "gameStatus" , data . toString ( ) ) ; } } ) ; igor . on ( 'close' , async ( code ) => { if ( ! lanchingGame ) { emitter . emit ( "compileFinished" , igorErrors ) ; } else { if ( code !== 0 ) emitter . emit ( "compileFinished" , igorErrors ) ; emitter . emit ( "gameFinished" ) ; } if ( code !== 0 || igorErrors . length > 0 ) { emitter . emit ( "error" , new Error ( "IGOR Failed. Check compile log." ) ) ; } emitter . emit ( "allFinished" , igorErrors ) ; } ) ; } ; setTimeout ( ( ) => { asyncRun ( ) . catch ( error => { emitter . emit ( "error" , error ) ; } ) ; } , 0 ) ; return emitter ; } /** Cleans rubber's cache for the project. */ export async function clearCache ( projectPath ) { if ( tempFolder === undefined ) { throw new Error ( "%temp% is missing in the environment variables." ) ; } const projectDir = dirname ( projectPath ) ; if ( ! ( await fse . pathExists ( join ( projectDir , "options" , "main" , "inherited" , "options_main.inherited.yy" ) ) ) ) { throw new Error ( "Missing options_main.inherited.yy. This can be because of a partial project, or the usage of a differen parent project structure." ) ; } const guid_match = ( await fse . readFile ( join ( projectDir , "options" , "main" , "inherited" , "options_main.inherited.yy" ) ) ) . toString ( ) . match ( '"option_gameguid": "(.*?)"' ) ; if ( ! guid_match ) { throw new Error ( "options_main.inherited.yy is missing project GUID, cannot identify project." ) ; } const guid = guid_match [ 1 ] . replace ( "{" , "" ) . replace ( "}" , "" ) ; await fse . remove ( join ( tempFolder , "gamemaker-rubber" , guid ) ) ; } /** Use Igor's Clean function to clear remote client's cache */ export function clearCacheRemote ( options ) { return compile ( options , true ) ; }