Removed the code to check for game window. The previous code breaks if you remove FPS from title of game window in your RPCS3 settings.
Cleaned up the fade screen. Hopefully it works much smoother with the added progress bars.
Module is now capable of "updating" the archived PPU and DATA files. If it notices a change in the new files, it will overwrite the old archive with a new one. Also capable of archiving DATA files in multiple folders. Some games produce multiple DATA folders.
Edit: Quick fix to text file method.
Cleaned up the fade screen. Hopefully it works much smoother with the added progress bars.
Module is now capable of "updating" the archived PPU and DATA files. If it notices a change in the new files, it will overwrite the old archive with a new one. Also capable of archiving DATA files in multiple folders. Some games produce multiple DATA folders.
Code:
MEmu := "RPCS3"
MEmuV := "v0.0.7-9165-8ca53f9c Alpha"
MURL := ["https://rpcs3.net"]
MAuthor := ["slashin8r", "wallmachine", "jviegas", "brolly", "aurel102001"]
MVersion := "1.1.1"
MDate := "2019-11-24"
MCRC := ""
iCRC := ""
MID := ""
MSystem := ["Sony Playstation 3"]
;----------------------------------------------------------------------------
; Notes:
; [IMPORTANT]
; There are three methods for using this module.
;
; METHOD 1: Use Archives
; 1) Archive your game folders, rename them to the title of the game, place them all within the same folder, then add that folder as a romPath in RocketLauncher.
; 2) Add the archive's extension ("7z", "rar", "zip", etc.) for the RPCS3 emulator within RocketLauncher.
; 3) Generate a new database based on the new rom path.
; 4) Audit all games for the system and enjoy.
;
; METHOD 2: Use Shortcuts
; 1) Create shortcuts of your EBOOT.BIN files, rename them to the title of the game, place them all within the same folder, then add that folder as a romPath in RocketLauncher.
; 2) Add the "lnk" (LNK) extension for the RPCS3 emulator within RocketLauncher.
; 3) Generate a new database based on the new rom path.
; 4) Audit all games for the system and enjoy.
;
; METHOD 3: Use Text Files
; 1) Place game folders all within the same folder. It is best to not rename these folders. They will have names such as "BLES#####", "BCUS#####", "NPUB#####", etc.
; 2) Create a text file for every game, named as the title of the game. Open the text file and add the name of the game folder on the first line. ("BLES#####", "BCUS#####", "NPUB#####", etc.)
; 3) Place these text files within the folder storing your games. If you wish to have the text files in a separate folder, then you must include the full path to folder storing your games in the "textGameFolder" variable found below.
; 4) Add the folder containing your text files as a romPath in RocketLauncher.
; 5) Add the "txt" extension for the RPCS3 emulator within RocketLauncher.
; 6) Generate a new database based on the new rom path.
; 7) Audit all games for the system and enjoy.
; [IMPORTANT]
;
;
;
; To save additional space, this module now automatically compresses each game's PPU folder and Game Data folder (if exists) as long as 7z is enabled.
; PPU file saved in: %romPath%\cache\%romName%_%A_ComputerName%.7z
; Game Data file saved in: %romPath%\data\%romName%_data.7z
;
; Example:
; Rom File = "D:\PS3\PS3 Test Game.rar"
; A_ComputerName = "PC001"
;
; PPU File = "D:\PS3\cache\PS3 Test Game_PC001.7z")
; Game Data File = "D:\PS3\data\PS3 Test Game_data.7z")
;
;
;
; Known Issues:
; None
;
;
;
; Coming Soon:
; Please make requests
;----------------------------------------------------------------------------
StartModule()
BezelGUI()
FadeInStart()
primaryExe := new Emulator(emuPath . "\" . executable)
primaryWindowClassName := "Qt5QWindowIcon"
emuPrimaryWindow := new Window(new WindowTitle(,primaryWindowClassName, executable))
; Finding emulator config file
Rpcs3config := new File(emuPath . "\config.yml")
Rpcs3config.CheckFile("Could not find RPCS3 config.yml! Run your emulator, go to Settings and save them.")
; Finding emulator games file
Rpcs3games := new File(emuPath . "\games.yml")
Rpcs3games.CheckFile("Could not find RPCS3 games.yml! Update this file with your list of game serials and game installation directories.")
textGameFolder := moduleIni.Read("Settings", "GamesDirectory","",,1)
archivePPU := moduleIni.Read("Settings", "ArchivePPU","false",,1)
archiveDATA := moduleIni.Read("Settings", "ArchiveDATA","false",,1)
removePPU := moduleIni.Read("Settings", "RemovePPU","false",,1)
removeDATA := moduleIni.Read("Settings", "RemoveDATA","false",,1)
BezelStart()
hideEmuObj := Object(emuPrimaryWindow,1)
HideAppStart(hideEmuObj,hideEmu)
; Set variables
gameSevenZEnabled := sevenZEnabled
gameType := ""
gameSerial := ""
gameDirectoryJunction := "false"
gamePPU := "false"
gamePPUarchive := romPath . "\cache\" . romName . "_" . A_ComputerName . ".7z"
gamePPUromPath := romPath . "\cache"
gamePPUromName := romName . "_" . A_ComputerName
gamePPUromExtension := ".7z"
gamePPUemuPath := emuPath . "\cache"
gameDATA := "false"
gameDATAarchive := romPath . "\data\" . romName . "_data.7z"
gameDATAromPath := romPath . "\data"
gameDATAromName := romName . "_data"
gameDATAromExtension := "7z"
gameDATAemuPath := emuPath . "\dev_hdd0\game"
textRomFolder := ""
If (romExtension = ".txt")
{
FileReadLine, textRomFolder, %romPath%\%romName%%romExtension%, 1
If (textGameFolder = "")
{
If (textRomFolder = "")
{
gameSerial := romName
If (Fileexist(romPath . "\" . romName . "\PS3_GAME\USRDIR\EBOOT.BIN"))
{
gameType := "disc"
romPath := romPath . "\" . romName . "\PS3_GAME\USRDIR"
}
Else If (Fileexist(romPath . "\" . romName . "\USRDIR\EBOOT.BIN"))
{
gameType := "game"
romPath := romPath . "\" . romName . "\USRDIR"
}
Else
{
ScriptError("Unable to find EBOOT.BIN within the folder: """ . romPath . "\" . romName . "\""")
}
}
Else
{
gameSerial := textRomFolder
If (Fileexist(romPath . "\" . textRomFolder . "\PS3_GAME\USRDIR\EBOOT.BIN"))
{
gameType := "disc"
romPath := romPath . "\" . textRomFolder . "\PS3_GAME\USRDIR"
}
Else If (Fileexist(romPath . "\" . textRomFolder . "\USRDIR\EBOOT.BIN"))
{
gameType := "game"
romPath := romPath . "\" . textRomFolder . "\USRDIR"
}
Else
{
ScriptError("Unable to find EBOOT.BIN within the folder: """ . romPath . "\" . textRomFolder . "\""")
}
}
}
Else
{
If (textRomFolder = "")
{
gameSerial := romName
If (Fileexist(textGameFolder . "\" . romName . "\PS3_GAME\USRDIR\EBOOT.BIN"))
{
gameType := "disc"
romPath := textGameFolder . "\" . romName . "\PS3_GAME\USRDIR"
}
Else If (Fileexist(textGameFolder . "\" . romName . "\USRDIR\EBOOT.BIN"))
{
gameType := "game"
romPath := textGameFolder . "\" . romName . "\USRDIR"
}
Else
{
ScriptError("Unable to find EBOOT.BIN within the folder: """ . textGameFolder . "\" . romName . "\""")
}
}
Else
{
gameSerial := textRomFolder
If (Fileexist(textGameFolder . "\" . textRomFolder . "\PS3_GAME\USRDIR\EBOOT.BIN"))
{
gameType := "disc"
romPath := textGameFolder . "\" . textRomFolder . "\PS3_GAME\USRDIR"
}
Else If (Fileexist(textGameFolder . "\" . textRomFolder . "\USRDIR\EBOOT.BIN"))
{
gameType := "game"
romPath := textGameFolder . "\" . textRomFolder . "\USRDIR"
}
Else
{
ScriptError("Unable to find EBOOT.BIN within the folder: """ . textGameFolder . "\" . textRomFolder . "\""")
}
}
}
}
Else If (romExtension = ".lnk" && Fileexist(romPath . "\" . romName . romExtension))
{
FileGetShortCut, %romPath%\%romName%%romExtension%, shortcutTarget
StringReplace, romPath, shortcutTarget, \EBOOT.BIN
}
Else If (InStr(sevenZFormats, romExtension)) {
gameSerial := RLObject.getZipRootFolder(romPath . "\" . romName . romExtension)
}
; Store old values for later
originalRomPath := romPath
originalRomName := romName
originalRomExtension := romExtension
originalSevenZRomPath := sevenZRomPath
originalGameName := gameInfo["Name"].Value
; Find game location
gameFolder := romPath
If (sevenZEnabled = "true" && InStr(sevenZFormats, romExtension))
{
gameFolder := sevenZExtractPath . "\" . romName
If (sevenZAttachSystemName = "true")
{
gameFolder := sevenZExtractPath . "\" . systemName . "\" . romName
}
}
Else If (romExtension = ".lnk" || romExtension = ".txt")
{
StringReplace, gameFolder, gameFolder, \USRDIR
StringReplace, gameFolder, gameFolder, \PS3_GAME
}
;MsgBox, gamePPUarchive: %gamePPUarchive%`ngamePPUemuPath: %gamePPUemuPath%`ngameDATAarchive: %gameDATAarchive%`ngameDATAemuPath: %gameDATAemuPath%
; Extract PPU files if they exist
If (archivePPU = "true" && Fileexist(gamePPUarchive))
{
gameInfo["Name"].Value := "EXTRACTING COMPILED PPU FILES"
romExPercentage := 0
layer3Percentage := 0
rootFolder := RLObject.getZipRootFolder(gamePPUarchive)
romExSize := RLObject.getZipExtractedSize(gamePPUarchive)
sevenZRomPath := gamePPUemuPath . "\" . rootFolder
use7zAnimation := "true"
Gdip_GraphicsClear(Fade_G5)
Gosub, UpdateFadeFor7z
; 7z(gamePPUromPath, gamePPUromName, gamePPUromExtension, gamePPUemuPath)
RunWait, %ComSpec% /c ""%sevenZPath%" x "%gamePPUarchive%" -aos -o"%gamePPUemuPath%", sevenZPID, Hide
gamePPU := "true"
}
; Extract Game Data files if they exist
If (archiveDATA = "true" && Fileexist(gameDATAarchive))
{
gameInfo["Name"].Value := "EXTRACTING ADDITIONAL GAME DATA"
romExPercentage := 0
layer3Percentage := 0
rootFolder := RLObject.getZipRootFolder(gameDATAarchive)
romExSize := RLObject.getZipExtractedSize(gameDATAarchive)
sevenZRomPath := gameDATAemuPath . "\" . rootFolder
use7zAnimation := "true"
Gdip_GraphicsClear(Fade_G5)
Gosub, UpdateFadeFor7z
; 7z(gameDATAromPath, gameDATAromName, gameDATAromExtension, gameDATAemuPath)
RunWait, %ComSpec% /c ""%sevenZPath%" x "%gameDATAarchive%" -aos -o"%gameDATAemuPath%", sevenZPID, Hide
gameDATA := "true"
}
If (gamePPU := "true" || gameDATA := "true")
{
sevenZRomPath := originalSevenZRomPath
gameInfo["Name"].Value := originalGameName
Gdip_GraphicsClear(Fade_G5)
}
romPath := originalRomPath
romName := originalRomName
romExtension := originalRomExtension
romExPercentage := 0
layer3Percentage := 0
romExSize := 1000000000
use7zAnimation := "true"
Gosub, UpdateFadeFor7z
7z(romPath, romName, romExtension, sevenZExtractPath)
romFolder := romPath
IfInString, romPath, \USRDIR
{
FoundPos := InStr(romPath, "\USRDIR") + 6
StringLeft, romPath, romPath, %FoundPos%
}
; Find the game location and determine the type of game (disc or hdd)
StringRight, stringTest, romFolder, 6
If (stringTest = "USRDIR")
{
gameType := "game"
StringReplace, romFolder, romFolder, \USRDIR
StringRight, stringTest, romFolder, 8
If (stringTest = "PS3_GAME")
{
gameType := "disc"
StringReplace, romFolder, romFolder, \PS3_GAME
}
If (romExtension = ".lnk" || (romExtension = ".txt" && textRomFolder != "") || (gameSerial != "" && gameType != ""))
{
If (romExtension = ".lnk" || romExtension = ".bin")
{
StringRight, gameSerial, romFolder, 9
}
; Create the directory junction
If (!Fileexist(emuPath . "\dev_hdd0\" . gameType . "\" . gameSerial))
{
If (gameSerial != "" && gameType != "")
{
RunWait, %ComSpec% /c "mklink /J "%emuPath%\dev_hdd0\%gameType%\%gameSerial%" "%gameFolder%", , Hide
gameDirectoryJunction := "true"
}
Else
{
ScriptError("Game serial not found. If this is your first time loading this game, please check that its game serial and path were added to the games.yml file and try running again.")
}
}
}
}
Else If (sevenZEnabled = "true" && (romPath = originalRomPath || romPath = "") && InStr(sevenZFormats, romExtension))
{
ScriptError("Rom was not found in the USRDIR directory. Please make sure ""bin"" is an RPCS3 extension and also verify the folder structure of your rom.")
}
;MsgBox, romPath: %romPath%`nromName: %romName%`nromExtension: %romExtension%`ngameType: %gameType%`ngameSerial: %gameSerial%`ngameDirectoryJunction: %gameDirectoryJunction%`ngamePPU: %gamePPU%`ngameDATA: %gameDATA%`nsevenZEnabled: %sevenZEnabled%`ngameSevenZEnabled: %gameSevenZEnabled%
; Run the game
If (gameType = "game")
{
primaryExe.Run(" """ . emuPath . "\dev_hdd0\" . gameType . "\" . gameSerial . "\USRDIR\EBOOT.BIN""")
}
Else If (Fileexist(emuPath . "\dev_hdd0\" . gameType . "\" . gameSerial . "\PS3_GAME\USRDIR\EBOOT.BIN"))
{
primaryExe.Run(" """ . emuPath . "\dev_hdd0\" . gameType . "\" . gameSerial . "\PS3_GAME\USRDIR\EBOOT.BIN""")
}
Else If (Fileexist(romPath . "\PS3_GAME\USRDIR\EBOOT.BIN"))
{
primaryExe.Run(" """ . romPath . "\PS3_GAME\USRDIR\EBOOT.BIN""")
}
Else If (Fileexist(romPath . "\EBOOT.BIN"))
{
primaryExe.Run(" """ . romPath . "\EBOOT.BIN""")
}
Else If (Fileexist(romPath . "\" . romName . ".BIN"))
{
primaryExe.Run(" """ . romPath . "\" . romName . ".BIN""")
}
Else If (Fileexist(romPath . "\" . romName . romExtension))
{
primaryExe.Run(" """ . romPath . "\" . romName . romExtension . """")
}
Else
{
ScriptError("Unable to find EBOOT.BIN file. Please make sure ""bin"" (and ""lnk"" [LNK] if you are loading from shortcuts) is an RPCS3 extension and also verify the folder structure of your rom.")
}
; Waiting for main emulator window
emuPrimaryWindow.Wait()
emuPrimaryWindow.Active()
emuPrimaryWindow.WaitActive()
If (gameSerial = "" && ((sevenZEnabled = "true" && InStr(sevenZFormats, romExtension)) || romExtension = ".txt"))
{
; Find the game serial
If (Fileexist(emuPath . "\games.yml") && (gameSerial = "" || textRomFolder = ""))
{
Loop, read, %emuPath%\games.yml
{
StringReplace, gameLine, A_LoopReadLine, /, \, All
IfInString, gameLine, %gameFolder%
{
StringLeft, gameSerial, gameLine, 9
break
}
}
}
; Create the directory junction
If (!Fileexist(emuPath . "\dev_hdd0\" . gameType . "\" . gameSerial))
{
If (gameSerial != "" && gameType != "")
{
RunWait, %ComSpec% /c "mklink /J "%emuPath%\dev_hdd0\%gameType%\%gameSerial%" "%gameFolder%", , Hide
gameDirectoryJunction := "true"
}
Else
{
ScriptError("Game serial not found in your RPCS3 games.yml file. If this is your first time loading this game, please check that its game serial and path were added to the games.yml file and try running again.")
}
}
}
; Waiting 3 seconds to see if compiling window appears
If (gameSerial != "" && gamePPU != "true")
{
WinWait, Compiling, , 3
If (!ErrorLevel)
{
gameInfo["Name"].Value := "COMPILING PPU FILES"
use7zAnimation := "true"
Gdip_GraphicsClear(Fade_G5)
Gosub, UpdateFadeFor7z
WinWaitClose, Compiling
gameInfo["Name"].Value := originalGameName
use7zAnimation := "true"
Gdip_GraphicsClear(Fade_G5)
Gosub, UpdateFadeFor7z
}
}
BezelDraw()
HideAppEnd(hideEmuObj,hideEmu)
FadeInExit()
primaryExe.Process("WaitClose")
; Remove the PPU files if they were extracted or creates PPU archive file if needed
If ((archivePPU = "true" || removePPU = "true") && gameSerial != "")
{
IfExist, %gamePPUemuPath%\%gameSerial%
{
If (archivePPU = "true")
{
IfNotExist, %gamePPUarchive%
{
FadeInStart()
gameInfo["Name"].Value := "ARCHIVING COMPILED PPU FILES"
romExPercentage := 0
layer3Percentage := 0
use7zAnimation := "true"
Gdip_GraphicsClear(Fade_G5)
Gosub, UpdateFadeFor7z
RunWait, %ComSpec% /c ""%sevenZPath%" a "%gamePPUarchive%" "%gamePPUemuPath%\%gameSerial%", , Hide
FadeInExit()
}
Else
{
MostRecentTime := 0
Loop, %gamePPUemuPath%\%gameSerial%\*, 2
{
If (A_LoopFileTimeModified > MostRecentTime)
{
MostRecentTime := A_LoopFileTimeModified
}
}
FileGetTime, gamePPUtime, %gamePPUarchive%, M
If (MostRecentTime > gamePPUtime)
{
FadeInStart()
gameInfo["Name"].Value := "ARCHIVING COMPILED PPU FILES"
romExPercentage := 0
layer3Percentage := 0
use7zAnimation := "true"
Gdip_GraphicsClear(Fade_G5)
Gosub, UpdateFadeFor7z
RunWait, %ComSpec% /c ""%sevenZPath%" a "%gamePPUarchive%" "%gamePPUemuPath%\%gameSerial%", , Hide
FadeInExit()
}
}
}
If (removePPU = "true")
{
FileRemoveDir, %gamePPUemuPath%\%gameSerial%, 1
}
}
Else
{
MostRecentTime := 0
MostRecentFile := ""
MostRecentName := ""
Loop, %gamePPUemuPath%\*, 2
{
If (A_LoopFileTimeModified > MostRecentTime)
{
MostRecentTime := A_LoopFileTimeModified
MostRecentFile := A_LoopFileLongPath
MostRecentName := A_LoopFileName
}
}
If (StrLen(MostRecentName) = 9)
{
If (archivePPU = "true")
{
IfNotExist, %gamePPUarchive%
{
FadeInStart()
gameInfo["Name"].Value := "ARCHIVING COMPILED PPU FILES"
romExPercentage := 0
layer3Percentage := 0
use7zAnimation := "true"
Gdip_GraphicsClear(Fade_G5)
Gosub, UpdateFadeFor7z
RunWait, %ComSpec% /c ""%sevenZPath%" a "%gamePPUarchive%" "%MostRecentFile%", , Hide
FadeInExit()
}
Else
{
MostRecentTime := 0
Loop, %gamePPUemuPath%\%MostRecentName%\*, 2
{
If (A_LoopFileTimeModified > MostRecentTime)
{
MostRecentTime := A_LoopFileTimeModified
}
}
FileGetTime, gamePPUtime, %gamePPUarchive%, M
If (MostRecentTime > gamePPUtime)
{
FadeInStart()
gameInfo["Name"].Value := "ARCHIVING COMPILED PPU FILES"
romExPercentage := 0
layer3Percentage := 0
use7zAnimation := "true"
Gdip_GraphicsClear(Fade_G5)
Gosub, UpdateFadeFor7z
RunWait, %ComSpec% /c ""%sevenZPath%" a "%gamePPUarchive%" "%MostRecentFile%", , Hide
FadeInExit()
}
}
}
If (removePPU = "true")
{
FileRemoveDir, %MostRecentFile%, 1
}
}
}
}
; Remove the Game Data files if they were extracted or creates Game Data archive file if needed
If ((archiveDATA = "true" || removeDATA = "true") && gameSerial != "" && gameType = "disc")
{
archiveFileArray := []
archiveFileCount := 0
Loop, %gameDATAemuPath%\*, 2
{
IfInString, A_LoopFileName, %gameSerial%
{
archiveFileCount := archiveFileCount + 1
archiveFileArray[archiveFileCount] := A_LoopFileLongPath
}
}
MostRecentTime := 0
MostRecentFile := ""
MostRecentName := ""
Loop, %gameDATAemuPath%\*, 2
{
If (A_LoopFileTimeCreated > MostRecentTime)
{
MostRecentTime := A_LoopFileTimeCreated
MostRecentFile := A_LoopFileLongPath
MostRecentName := A_LoopFileName
}
}
fileOne := MostRecentFile . "\ICON0.PNG"
fileTwo := emuPath . "\dev_hdd0\" . gameType . "\" . gameSerial . "\PS3_GAME\ICON0.PNG"
FileGetSize, dataSizeOne, %fileOne%
FileGetSize, dataSizeTwo, %fileTwo%
FileGetVersion, dataVersionOne, %fileOne%
FileGetVersion, dataVersionTwo, %fileTwo%
StringLeft, testRecentFour, MostRecentName, 4
StringLeft, testSerialFour, gameSerial, 4
If (dataSizeOne = dataSizeTwo && dataVersionOne = dataVersionTwo && MostRecentName != ".locks" && MostRecentName != "TEST12345" && testRecentFour = testSerialFour)
{
archiveFileCount := archiveFileCount + 1
archiveFileArray[archiveFileCount] := MostRecentFile
}
If (archiveFileCount > 0)
{
archiveFileString := ""
Loop % archiveFileCount
{
If (archiveFileString != "")
{
archiveFileString := archiveFileString . " "
}
archiveFileString := archiveFileString . """" . archiveFileArray[A_Index] . """"
}
If (archiveDATA = "true")
{
IfNotExist, %gameDATAarchive%
{
FadeInStart()
gameInfo["Name"].Value := "ARCHIVING ADDITIONAL GAME DATA"
romExPercentage := 0
layer3Percentage := 0
use7zAnimation := "true"
Gdip_GraphicsClear(Fade_G5)
Gosub, UpdateFadeFor7z
RunWait, %ComSpec% /c ""%sevenZPath%" a -mx=0 "%gameDATAarchive%" %archiveFileString%, , Hide
FadeInExit()
}
Else
{
Loop % archiveFileCount
{
archiveElement := archiveFileArray[A_Index]
MostRecentTime := 0
Loop, %archiveElement%\*, 2
{
If (A_LoopFileTimeModified > MostRecentTime)
{
MostRecentTime := A_LoopFileTimeModified
}
}
FileGetTime, gameDATAtime, %gameDATAarchive%, M
FileGetTime, gameDATAarchiveTime, %archiveElement%, M
If (MostRecentTime > gameDATAtime || gameDATAarchiveTime > gameDATAtime)
{
FadeInStart()
gameInfo["Name"].Value := "ARCHIVING ADDITIONAL GAME DATA"
romExPercentage := 0
layer3Percentage := 0
use7zAnimation := "true"
Gdip_GraphicsClear(Fade_G5)
Gosub, UpdateFadeFor7z
RunWait, %ComSpec% /c ""%sevenZPath%" a -mx=0 "%gameDATAarchive%" %archiveFileString%, , Hide
FadeInExit()
Break
}
}
}
}
If (removeDATA = "true")
{
Loop % archiveFileCount
{
archiveElement := archiveFileArray[A_Index]
FileRemoveDir, %archiveElement%, 1
}
}
}
}
; Remove the directory junction
If (gameDirectoryJunction = "true")
{
RunWait, %ComSpec% /c "rmdir "%emuPath%\dev_hdd0\%gameType%\%gameSerial%", , Hide
}
7zCleanUp()
BezelExit()
FadeOutExit()
ExitModule()
CloseProcess:
FadeOutStart()
emuPrimaryWindow.Close()
Process, Close, %executable%
Return
BezelLabel:
disableHideBorder := "true"
disableHideTitleBar := "true"
disableHideToggleMenu := "true"
Return
Edit: Quick fix to text file method.
Last edited: