diff --git a/Scripts/Python/ki/__init__.py b/Scripts/Python/ki/__init__.py index 76cd3d1ef8..8472910749 100644 --- a/Scripts/Python/ki/__init__.py +++ b/Scripts/Python/ki/__init__.py @@ -41,6 +41,8 @@ *==LICENSE==* """ +from __future__ import annotations + MaxVersionNumber = 58 MinorVersionNumber = 52 @@ -84,7 +86,7 @@ xKIChat.KIBlackbar = KIBlackbar KIMini = ptAttribGUIDialog(2, "The KIMini dialog") xKIChat.KIMini = KIMini -KIYesNo = ptAttribGUIDialog(3, "The KIYesNo dialog") +KIYesNo = ptAttribGUIDialog(3, "The KIYesNo dialog") # Thou shalt not use. BigKI = ptAttribGUIDialog(5, "The BigKI (Mr. BigStuff)") xKIChat.BigKI = BigKI NewItemAlert = ptAttribGUIDialog(7, "The new item alert dialog") @@ -207,10 +209,6 @@ def __init__(self): self.alertTimerActive = False self.alertTimeToUse = kAlertTimeDefault - # Yes/No dialog globals. - self.YNWhatReason = kGUI.YNQuit - self.YNOutsideSender = None - # Player book globals. self.bookOfferer = None self.offerLinkFromWho = None @@ -289,7 +287,6 @@ def __del__(self): PtUnloadDialog("KIMarkerFolder") PtUnloadDialog("KIMarkerTimeMenu") PtUnloadDialog("KIMarkerTypeMenu") - PtUnloadDialog("KIYesNo") PtUnloadDialog("KINewItemAlert") PtUnloadDialog("OptionsMenuGUI") PtUnloadDialog("IntroBahroBgGUI") @@ -337,7 +334,6 @@ def OnInit(self): PtLoadDialog("KIMarkerFolder", self.key) PtLoadDialog("KIMarkerTimeMenu", self.key) PtLoadDialog("KIMarkerTypeMenu", self.key) - PtLoadDialog("KIYesNo", self.key) PtLoadDialog("KINewItemAlert", self.key) PtLoadDialog("OptionsMenuGUI") PtLoadDialog("IntroBahroBgGUI") @@ -373,11 +369,6 @@ def OnFirstUpdate(self): xBookGUIs.LoadAllBookGUIs() - logoutText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoLogoutTextID)) - logoutText.hide() - logoutButton = ptGUIControlButton(KIYesNo.dialog.getControlFromTag(kGUI.YesNoLogoutButtonID)) - logoutButton.hide() - ## Called by Plasma when the player updates his account. # This includes switching avatars and changing passwords. Because the KI # gets started at initial load time, the KI needs to be re-initialized once @@ -645,8 +636,6 @@ def OnGUINotify(self, ID, control, event): self.ProcessNotifyVolumeExpanded(control, event) elif ID == KIAgeOwnerExpanded.id: self.ProcessNotifyAgeOwnerExpanded(control, event) - elif ID == KIYesNo.id: - self.ProcessNotifyYesNo(control, event) elif ID == NewItemAlert.id: self.ProcessNotifyNewItemAlert(control, event) elif ID == KICreateMarkerGameGUI.id: @@ -700,26 +689,13 @@ def OnKIMsg(self, command, value): KIPlayerExpanded.dialog.hide() BigKI.dialog.hide() KIOnAnim.animation.skipToTime(1.5) - # If an outsider has a Yes/No up, tell them No. - if self.YNWhatReason == kGUI.YNOutside: - if self.YNOutsideSender is not None: - note = ptNotify(self.key) - note.clearReceivers() - note.addReceiver(self.YNOutsideSender) - note.netPropagate(0) - note.netForce(0) - note.setActivate(0) - note.addVarNumber("YesNo", 0) - note.send() - self.YNOutsideSender = None + # Hide the Yeesha Book. if self.yeeshaBook: self.yeeshaBook.hide() PtToggleAvatarClickability(True) plybkCB = ptGUIControlCheckBox(KIBlackbar.dialog.getControlFromTag(kGUI.PlayerBookCBID)) plybkCB.setChecked(0) - self.YNWhatReason = kGUI.YNQuit - KIYesNo.dialog.hide() elif command == kEnableKIandBB: self.KIDisabled = False self.chatMgr.KIDisabled = False @@ -752,13 +728,6 @@ def OnKIMsg(self, command, value): KIMicroBlackbar.dialog.showNoReset() else: KIBlackbar.dialog.showNoReset() - elif command == kYesNoDialog: - self.YNWhatReason = kGUI.YNOutside - self.YNOutsideSender = value[1] - yesText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoTextID)) - yesText.setStringW(value[0]) - self.LocalizeDialog(1) - KIYesNo.dialog.show() elif command == kAddPlayerDevice: if "

" in value: self.pelletImager = value.rstrip("

") @@ -891,35 +860,19 @@ def OnKIMsg(self, command, value): if not self.waitingForAnimation and not self.KIDisabled: PtShowDialog("OptionsMenuGUI") elif command == kKIOKDialog or command == kKIOKDialogNoQuit: - reasonField = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoTextID)) - try: - localized = kLoc.OKDialogDict[value] - except KeyError: - localized = "UNTRANSLATED: " + str(value) - reasonField.setStringW(localized) - noButton = ptGUIControlButton(KIYesNo.dialog.getControlFromTag(kGUI.NoButtonID)) - noButton.hide() - noBtnText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.NoButtonTextID)) - noBtnText.hide() - yesBtnText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesButtonTextID)) - yesBtnText.setStringW(PtGetLocalizedString("KI.YesNoDialog.OKButton")) - self.YNWhatReason = kGUI.YNQuit - if command == kKIOKDialogNoQuit: - self.YNWhatReason = kGUI.YNNoReason - KIYesNo.dialog.show() + # FIXME: This handling should be moved into the engine. + localized = kLoc.OKDialogDict.get(value, f"UNTRANSLATED: {value}") + dialogType = PtConfirmationType.OK if command == kKIOKDialogNoQuit else PtConfirmationType.ForceQuit + PtYesNoDialog(None, localized, dialogType=dialogType) + elif command == kYesNoDialog: + # This should never happen but is here for completeness's sake. + PtYesNoDialog(value[1], value[0]) elif command == kDisableYeeshaBook: self.isYeeshaBookEnabled = False elif command == kEnableYeeshaBook: self.isYeeshaBookEnabled = True elif command == kQuitDialog: - yesText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoTextID)) - yesText.setStringW(PtGetLocalizedString("KI.Messages.LeaveGame")) - self.LocalizeDialog() - logoutText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoLogoutTextID)) - logoutText.show() - logoutButton = ptGUIControlButton(KIYesNo.dialog.getControlFromTag(kGUI.YesNoLogoutButtonID)) - logoutButton.show() - KIYesNo.dialog.show() + PtLocalizedYesNoDialog(None, "KI.Messages.LeaveGame", dialogType=PtConfirmationType.ConfirmQuit) elif command == kDisableEntireYeeshaBook: self.isEntireYeeshaBookEnabled = False elif command == kEnableEntireYeeshaBook: @@ -1382,6 +1335,18 @@ def SetupKI(self): BigKI.dialog.hide() self.chatMgr.ToggleChatMode(0) + # Clear out all chat on microKI. + chatArea = ptGUIControlMultiLineEdit(KIMicro.dialog.getControlFromTag(kGUI.ChatDisplayArea)) + chatArea.setString("") + chatArea.moveCursor(PtGUIMultiLineDirection.kBufferStart) + KIMicro.dialog.refreshAllControls() + + # Clear out all chat on miniKI. + chatArea = ptGUIControlMultiLineEdit(KIMini.dialog.getControlFromTag(kGUI.ChatDisplayArea)) + chatArea.setString("") + chatArea.moveCursor(PtGUIMultiLineDirection.kBufferStart) + KIMini.dialog.refreshAllControls() + # Remove unneeded kFontShadowed flags (as long as we can't do that directly in the PRPs) for dialogAttr in (BigKI, KIListModeDialog, KIJournalExpanded, KIPictureExpanded, KIPlayerExpanded, KIAgeOwnerExpanded, KISettings, KIMarkerFolderExpanded, KICreateMarkerGameGUI): for i in range(dialogAttr.dialog.getNumControls()): @@ -1542,21 +1507,6 @@ def DoKILight(self, state, ff, remaining=0): else: PtDebugPrint("xKI.DoKILight(): Couldn't find any responders.", level=kErrorLevel) - #~~~~~~~~~~~~~~# - # Localization # - #~~~~~~~~~~~~~~# - - ## Gets the appropriate localized values for a Yes/No dialog. - def LocalizeDialog(self, dialog_type=0): - - confirm = "KI.YesNoDialog.QuitButton" - if dialog_type == 1: - confirm = "KI.YesNoDialog.YESButton" - yesButton = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesButtonTextID)) - noButton = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.NoButtonTextID)) - yesButton.setStringW(PtGetLocalizedString(confirm)) - noButton.setStringW(PtGetLocalizedString("KI.YesNoDialog.NoButton")) - #~~~~~~~~~# # Pellets # #~~~~~~~~~# @@ -1960,7 +1910,7 @@ def CreateMarkerGame(self): # Make sure the player has enough room. if not self.CanMakeMarkerGame(): PtDebugPrint("xKI.CreateMarkerGame(): Aborting Marker Game creation request, player has reached the limit of Marker Games.", level=kDebugDumpLevel) - self.ShowKIFullErrorMsg(PtGetLocalizedString("KI.Messages.FullMarkerGames")) + self.ShowKIFullErrorMsg("FullMarkerGames") return # The player can now launch the Marker Game creation GUI. @@ -2037,7 +1987,7 @@ def CreateAMarker(self): self.markerGameManager.AddMarker(PtGetAgeName(), avaCoord, markerName) PtDebugPrint("xKI.CreateAMarker(): Creating marker at: ({}, {}, {}).".format(avaCoord.getX(), avaCoord.getY(), avaCoord.getZ())) else: - self.ShowKIFullErrorMsg(PtGetLocalizedString("KI.Messages.FullMarkers")) + self.ShowKIFullErrorMsg("FullMarkers") ## Perform the necessary operations to switch to a Marker Game. def SetWorkingToCurrentMarkerGame(self): @@ -2736,19 +2686,8 @@ def CanMakeMarker(self): #~~~~~~~~# ## Displays a OK dialog-based error message to the player. - def ShowKIFullErrorMsg(self, msg): - - self.YNWhatReason = kGUI.YNKIFull - reasonField = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoTextID)) - reasonField.setStringW(msg) - yesButton = ptGUIControlButton(KIYesNo.dialog.getControlFromTag(kGUI.YesButtonID)) - yesButton.hide() - yesBtnText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesButtonTextID)) - yesBtnText.hide() - noBtnText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.NoButtonTextID)) - noBtnText.setStringW(PtGetLocalizedString("KI.YesNoDialog.OKButton")) - KIYesNo.dialog.show() - + def ShowKIFullErrorMsg(self, msg: str): + PtLocalizedYesNoDialog(None, f"KI.Messages.{msg}", dialogType=PtConfirmationType.OK) ## Display an error message in the SendTo field. def SetSendToErrorMessage(self, message): @@ -3373,7 +3312,7 @@ def TakePicture(self): PtAtTimeCallback(self.key, 0.25, kTimers.TakeSnapShot) else: # Put up an error message. - self.ShowKIFullErrorMsg(PtGetLocalizedString("KI.Messages.FullImages")) + self.ShowKIFullErrorMsg("FullImages") ## Create a new Journal entry through the miniKI. def MiniKICreateJournalNote(self): @@ -3416,7 +3355,7 @@ def MiniKICreateJournalNote(self): dragbar.anchor() else: # Put up an error message. - self.ShowKIFullErrorMsg(PtGetLocalizedString("KI.Messages.FullNotes")) + self.ShowKIFullErrorMsg("FullNotes") #~~~~~~~# # BigKI # @@ -3812,6 +3751,23 @@ def BigKISetChanging(self): gps3.setString("0") PtAtTimeCallback(self.key, 5, kTimers.BKITODCheck) + @property + def BKCurrentContentTitle(self) -> str: + content = self.BKCurrentContent + if isinstance(content, ptVaultNodeRef): + content = content.getChild() + if isinstance(content, ptVaultNode): + if imageNode := content.upcastToImageNode(): + return xCensor.xCensor(imageNode.getTitleW(), self.censorLevel) + if markerNode := content.upcastToMarkerGameNode(): + return xCensor.xCensor(markerNode.getGameName(), self.censorLevel) + if playerInfoNode := content.upcastToPlayerInfoNode(): + return xCensor.xCensor(playerInfoNode.playerGetName(), self.censorLevel) + if textNode := content.upcastToTextNoteNode(): + return xCensor.xCensor(textNode.getTitleW(), self.censorLevel) + # Any other types of content? Implement it yourself. + return "" + #~~~~~~~~~~~~~~~~~~# # BigKI Refreshing # #~~~~~~~~~~~~~~~~~~# @@ -5393,14 +5349,7 @@ def ProcessNotifyBlackbar(self, control, event): if PtIsDialogLoaded("KIMini"): KIMini.dialog.hide() elif bbID == kGUI.ExitButtonID: - yesText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoTextID)) - yesText.setStringW(PtGetLocalizedString("KI.Messages.LeaveGame")) - self.LocalizeDialog(0) - logoutText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoLogoutTextID)) - logoutText.show() - logoutButton = ptGUIControlButton(KIYesNo.dialog.getControlFromTag(kGUI.YesNoLogoutButtonID)) - logoutButton.show() - KIYesNo.dialog.show() + PtLocalizedYesNoDialog(None, "KI.Messages.LeaveGame", dialogType=PtConfirmationType.ConfirmQuit) elif bbID == kGUI.PlayerBookCBID: if control.isChecked(): curBrainMode = PtGetLocalAvatar().avatar.getCurrentMode() @@ -5447,14 +5396,7 @@ def ProcessNotifyMicroBlackbar(self, control, event): elif event == kAction or event == kValueChanged: bbID = control.getTagID() if bbID == kGUI.ExitButtonID: - yesText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoTextID)) - yesText.setStringW(PtGetLocalizedString("KI.Messages.LeaveGame")) - self.LocalizeDialog(0) - logoutText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoLogoutTextID)) - logoutText.show() - logoutButton = ptGUIControlButton(KIYesNo.dialog.getControlFromTag(kGUI.YesNoLogoutButtonID)) - logoutButton.show() - KIYesNo.dialog.show() + PtLocalizedYesNoDialog(None, "KI.Messages.LeaveGame", dialogType=PtConfirmationType.ConfirmQuit) elif bbID == kGUI.PlayerBookCBID: if control.isChecked(): curBrainMode = PtGetLocalAvatar().avatar.getCurrentMode() @@ -6011,17 +5953,7 @@ def ProcessNotifyPictureExpanded(self, control, event): if self.IsContentMutable(self.BKCurrentContent): self.BigKIEnterEditMode(kGUI.BKEditFieldPICTitle) elif peID == kGUI.BKIPICDeleteButton: - self.YNWhatReason = kGUI.YNDelete - elem = self.BKCurrentContent.getChild() - elem = elem.upcastToImageNode() - if elem is not None: - picTitle = elem.imageGetTitle() - else: - picTitle = "" - yesText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoTextID)) - yesText.setStringW(PtGetLocalizedString("KI.Messages.DeletePicture", [xCensor.xCensor(picTitle, self.censorLevel)])) - self.LocalizeDialog(1) - KIYesNo.dialog.show() + PtLocalizedYesNoDialog(self.HandleBigKIDeleteConfirmation, "KI.Messages.DeletePicture", self.BKCurrentContentTitle) elif peID == kGUI.BKIPICTitleEdit: self.BigKISaveEdit(1) elif event == kFocusChange: @@ -6048,17 +5980,7 @@ def ProcessNotifyJournalExpanded(self, control, event): if self.IsContentMutable(self.BKCurrentContent): self.BigKIEnterEditMode(kGUI.BKEditFieldJRNNote) elif jeID == kGUI.BKIJRNDeleteButton: - self.YNWhatReason = kGUI.YNDelete - elem = self.BKCurrentContent.getChild() - elem = elem.upcastToTextNoteNode() - if elem is not None: - jrnTitle = elem.noteGetTitle() - else: - jrnTitle = "" - yesText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoTextID)) - yesText.setStringW(PtGetLocalizedString("KI.Messages.DeleteJournal", [xCensor.xCensor(jrnTitle, self.censorLevel)])) - self.LocalizeDialog(1) - KIYesNo.dialog.show() + PtLocalizedYesNoDialog(self.HandleBigKIDeleteConfirmation, "KI.Messages.DeletePicture", self.BKCurrentContentTitle) # Is it one of the editing boxes? elif jeID == kGUI.BKIJRNTitleEdit or jeID == kGUI.BKIJRNNoteEdit: if self.IsContentMutable(self.BKCurrentContent): @@ -6084,21 +6006,7 @@ def ProcessNotifyPlayerExpanded(self, control, event): plID = control.getTagID() # Is it one of the buttons? if plID == kGUI.BKIPLYDeleteButton: - self.YNWhatReason = kGUI.YNDelete - elem = self.BKCurrentContent.getChild() - elem = elem.upcastToPlayerInfoNode() - if elem is not None: - plyrName = elem.playerGetName() - else: - plyrName = "" - try: - pfldName = self.BKFolderListOrder[self.BKFolderSelected] - except LookupError: - pfldName = "" - yesText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoTextID)) - yesText.setStringW(PtGetLocalizedString("KI.Messages.DeletePlayer", [xCensor.xCensor(plyrName, self.censorLevel), pfldName])) - self.LocalizeDialog(1) - KIYesNo.dialog.show() + PtLocalizedYesNoDialog(self.HandleBigKIDeleteConfirmation, "KI.Messages.DeletePlayer", self.BKCurrentContentTitle) elif plID == kGUI.BKIPLYPlayerIDEditBox: self.BigKICheckSavePlayer() elif event == kFocusChange: @@ -6274,150 +6182,6 @@ def ProcessNotifyAgeOwnerExpanded(self, control, event): PtDebugPrint("xKI.ProcessNotifyAgeOwnerExpanded(): Neighborhood is None while trying to update description.", level=kDebugDumpLevel) self.BKAgeOwnerEditDescription = False - ## Process notifications originating from a YesNo dialog. - # Yes/No dialogs are omnipresent throughout Uru. Those processed here are: - # - Quitting dialog (quit/logout/cancel). - # - Deleting dialog (yes/no); various such dialogs. - # - Link offer dialog (yes/no). - # - Outside sender dialog (?). - # - KI Full dialog (OK); just a notification. - def ProcessNotifyYesNo(self, control, event): - - if event == kAction or event == kValueChanged: - ynID = control.getTagID() - if self.YNWhatReason == kGUI.YNQuit: - if ynID == kGUI.YesButtonID: - PtConsole("App.Quit") - elif ynID == kGUI.NoButtonID: - KIYesNo.dialog.hide() - logoutText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoLogoutTextID)) - logoutText.hide() - logoutButton = ptGUIControlButton(KIYesNo.dialog.getControlFromTag(kGUI.YesNoLogoutButtonID)) - logoutButton.hide() - elif ynID == kGUI.YesNoLogoutButtonID: - KIYesNo.dialog.hide() - logoutText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoLogoutTextID)) - logoutText.hide() - logoutButton = ptGUIControlButton(KIYesNo.dialog.getControlFromTag(kGUI.YesNoLogoutButtonID)) - logoutButton.hide() - - # Clear out all chat on microKI. - chatArea = ptGUIControlMultiLineEdit(KIMicro.dialog.getControlFromTag(kGUI.ChatDisplayArea)) - chatArea.setString("") - chatArea.moveCursor(PtGUIMultiLineDirection.kBufferStart) - KIMicro.dialog.refreshAllControls() - - # Clear out all chat on miniKI. - chatArea = ptGUIControlMultiLineEdit(KIMini.dialog.getControlFromTag(kGUI.ChatDisplayArea)) - chatArea.setString("") - chatArea.moveCursor(PtGUIMultiLineDirection.kBufferStart) - KIMini.dialog.refreshAllControls() - - linkmgr = ptNetLinkingMgr() - ageLink = ptAgeLinkStruct() - - ageInfo = ptAgeInfoStruct() - ageInfo.setAgeFilename("StartUp") - - spawnPoint = ptSpawnPointInfo() - spawnPoint.setName("LinkInPointDefault") - - ageLink.setAgeInfo(ageInfo) - ageLink.setSpawnPoint(spawnPoint) - ageLink.setLinkingRules(PtLinkingRules.kBasicLink) - linkmgr.linkToAge(ageLink) - - elif self.YNWhatReason == kGUI.YNDelete: - if ynID == kGUI.YesButtonID: - # Remove the current element - if self.BKCurrentContent is not None: - delFolder = self.BKCurrentContent.getParent() - delElem = self.BKCurrentContent.getChild() - if delFolder is not None and delElem is not None: - # Are we removing a visitor from an Age we own? - tFolder = delFolder.upcastToFolderNode() - if tFolder is not None and tFolder.folderGetType() == PtVaultStandardNodes.kCanVisitFolder: - PtDebugPrint("xKI.ProcessNotifyYesNo(): Revoking visitor.", level=kDebugDumpLevel) - delElem = delElem.upcastToPlayerInfoNode() - # Need to refind the folder that has the ageInfo in it. - ageFolderName = self.BKFolderListOrder[self.BKFolderSelected] - ageFolder = self.BKFolderLineDict[ageFolderName] - # Revoke invite. - ptVault().unInvitePlayerToAge(ageFolder.getAgeInstanceGuid(), delElem.playerGetID()) - # Are we removing a player from a player list? - elif delFolder.getType() == PtVaultNodeTypes.kPlayerInfoListNode and delElem.getType() == PtVaultNodeTypes.kPlayerInfoNode: - PtDebugPrint("xKI.ProcessNotifyYesNo(): Removing player from folder.", level=kDebugDumpLevel) - delFolder = delFolder.upcastToPlayerInfoListNode() - delElem = delElem.upcastToPlayerInfoNode() - delFolder.playerlistRemovePlayer(delElem.playerGetID()) - self.BKPlayerSelected = None - sendToField = ptGUIControlTextBox(BigKI.dialog.getControlFromTag(kGUI.BKIPlayerLine)) - sendToField.setString(" ") - # Are we removing a journal entry? - else: - # See if this is a Marker Game folder that is being deleted. - if delElem.getType() == PtVaultNodeTypes.kMarkerGameNode: - if self.markerGameManager.IsActive(delElem): - self.markerGameManager.StopGame() - - self.BKCurrentContent = None - delFolder.removeNode(delElem) - PtDebugPrint("xKI.ProcessNotifyYesNo(): Deleting element from folder.", level=kDebugDumpLevel) - else: - PtDebugPrint("xKI.ProcessNotifyYesNo(): Tried to delete bad Vault node or delete from bad folder.", level=kErrorLevel) - self.ChangeBigKIMode(kGUI.BKListMode) - self.RefreshPlayerList() - self.YNWhatReason = kGUI.YNQuit - KIYesNo.dialog.hide() - elif self.YNWhatReason == kGUI.YNOfferLink: - self.YNWhatReason = kGUI.YNQuit - KIYesNo.dialog.hide() - if ynID == kGUI.YesButtonID: - if self.offerLinkFromWho is not None: - PtDebugPrint("xKI.ProcessNotifyYesNo(): Linking to offered age {}.".format(self.offerLinkFromWho.getDisplayName()), level=kDebugDumpLevel) - link = ptAgeLinkStruct() - link.setLinkingRules(PtLinkingRules.kBasicLink) - link.setAgeInfo(self.offerLinkFromWho) - ptNetLinkingMgr().linkToAge(link) - self.offerLinkFromWho = None - self.offerLinkFromWho = None - elif self.YNWhatReason == kGUI.YNOutside: - self.YNWhatReason = kGUI.YNQuit - KIYesNo.dialog.hide() - if self.YNOutsideSender is not None: - note = ptNotify(self.key) - note.clearReceivers() - note.addReceiver(self.YNOutsideSender) - note.netPropagate(0) - note.netForce(0) - # Is it a good return? - if ynID == kGUI.YesButtonID: - note.setActivate(1) - note.addVarNumber("YesNo", 1) - # Or a bad return? - elif ynID == kGUI.NoButtonID: - note.setActivate(0) - note.addVarNumber("YesNo", 0) - note.send() - self.YNOutsideSender = None - elif self.YNWhatReason == kGUI.YNKIFull: - KIYesNo.dialog.hide() - yesButton = ptGUIControlButton(KIYesNo.dialog.getControlFromTag(kGUI.YesButtonID)) - yesButton.show() - yesBtnText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesButtonTextID)) - yesBtnText.show() - noBtnText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.NoButtonTextID)) - noBtnText.setStringW(PtGetLocalizedString("KI.YesNoDialog.NOButton")) - self.YNWhatReason = kGUI.YNQuit - else: - self.YNWhatReason = kGUI.YNQuit - KIYesNo.dialog.hide() - self.YNOutsideSender = None - elif event == kExitMode: - self.YNWhatReason = kGUI.YNQuit - KIYesNo.dialog.hide() - self.YNOutsideSender = None - ## Process notifications originating from a new item alert dialog. # Such alerts make either the KI's icon or the Yeesha Book icon # flash for a while. @@ -6558,17 +6322,8 @@ def ProcessNotifyMarkerFolderExpanded(self, control, event): elif mFldrID == kGUI.MarkerFolderTimePullDownBtn or mFldrID == kGUI.MarkerFolderTimeArrow: KIMarkerFolderPopupMenu.menu.show() elif mFldrID == kGUI.MarkerFolderDeleteBtn: - self.YNWhatReason = kGUI.YNDelete - elem = self.BKCurrentContent.getChild() - elem = elem.upcastToMarkerGameNode() - if elem is not None: - mfTitle = elem.getGameName() - else: - mfTitle = "" - yesText = ptGUIControlTextBox(KIYesNo.dialog.getControlFromTag(kGUI.YesNoTextID)) - yesText.setStringW(PtGetLocalizedString("KI.Messages.DeletePicture", [xCensor.xCensor(mfTitle, self.censorLevel)])) - self.LocalizeDialog(1) - KIYesNo.dialog.show() + PtLocalizedYesNoDialog(self.HandleBigKIDeleteConfirmation, + "KI.Messages.DeletePicture", self.BKCurrentContentTitle) elif event == kFocusChange: titleEdit = ptGUIControlEditBox(KIMarkerFolderExpanded.dialog.getControlFromTag(kGUI.MarkerFolderTitleEB)) # Is the editbox enabled and something other than the button is getting the focus? @@ -6693,3 +6448,53 @@ def HandleVaultTypeEvents(self, event, tupData): PtDebugPrint("xKI.HandleVaultTypeEvents(): A Vault operation failed (operation, resultCode): ", tupData, level=kDebugDumpLevel) else: PtDebugPrint("xKI.HandleVaultTypeEvents(): Unknown Vault event: {}.".format(event), level=kWarningLevel) + + + #~~~~~~~~~~~~~~~~~~~~~# + # Confirmation Events # + #~~~~~~~~~~~~~~~~~~~~~# + + def HandleBigKIDeleteConfirmation(self, value: int) -> None: + if value == PtConfirmationResult.No: + return + + # Remove the current element + if self.BKCurrentContent is not None: + delFolder = self.BKCurrentContent.getParent() + delElem = self.BKCurrentContent.getChild() + if delFolder is not None and delElem is not None: + # Are we removing a visitor from an Age we own? + tFolder = delFolder.upcastToFolderNode() + if tFolder is not None and tFolder.folderGetType() == PtVaultStandardNodes.kCanVisitFolder: + PtDebugPrint("xKI.HandleBigKIDeleteConfirmation(): Revoking visitor.", level=kDebugDumpLevel) + delElem = delElem.upcastToPlayerInfoNode() + # Need to refind the folder that has the ageInfo in it. + ageFolderName = self.BKFolderListOrder[self.BKFolderSelected] + ageFolder = self.BKFolderLineDict[ageFolderName] + # Revoke invite. + ptVault().unInvitePlayerToAge(ageFolder.getAgeInstanceGuid(), delElem.playerGetID()) + # Are we removing a player from a player list? + elif delFolder.getType() == PtVaultNodeTypes.kPlayerInfoListNode and delElem.getType() == PtVaultNodeTypes.kPlayerInfoNode: + PtDebugPrint("xKI.HandleBigKIDeleteConfirmation(): Removing player from folder.", level=kDebugDumpLevel) + delFolder = delFolder.upcastToPlayerInfoListNode() + delElem = delElem.upcastToPlayerInfoNode() + delFolder.playerlistRemovePlayer(delElem.playerGetID()) + self.BKPlayerSelected = None + sendToField = ptGUIControlTextBox(BigKI.dialog.getControlFromTag(kGUI.BKIPlayerLine)) + sendToField.setString(" ") + # Are we removing a journal entry? + else: + # See if this is a Marker Game folder that is being deleted. + if delElem.getType() == PtVaultNodeTypes.kMarkerGameNode: + if self.markerGameManager.IsActive(delElem): + self.markerGameManager.StopGame() + + self.BKCurrentContent = None + delFolder.removeNode(delElem) + PtDebugPrint("xKI.HandleBigKIDeleteConfirmation(): Deleting element from folder.", level=kDebugDumpLevel) + else: + PtDebugPrint("xKI.HandleBigKIDeleteConfirmation(): Tried to delete bad Vault node or delete from bad folder.", level=kErrorLevel) + self.ChangeBigKIMode(kGUI.BKListMode) + self.RefreshPlayerList() + else: + PtDebugPrint("xKI.HandleBigKIDeleteConfirmation(): Tried to delete nothing?") diff --git a/Scripts/Python/ki/xKIConstants.py b/Scripts/Python/ki/xKIConstants.py index f05fe5426c..a0b398e442 100644 --- a/Scripts/Python/ki/xKIConstants.py +++ b/Scripts/Python/ki/xKIConstants.py @@ -503,23 +503,7 @@ class kGUI: BKEditFieldJRNTitle = 0 BKEditFieldJRNNote = 1 BKEditFieldPICTitle = 2 - - # Yes/No dialog. - YesNoTextID=12 - YesButtonID = 10 - NoButtonID = 11 - YesButtonTextID = 60 - NoButtonTextID = 61 - YesNoLogoutButtonID = 62 - YesNoLogoutTextID = 63 - YNQuit = 0 - YNDelete = 1 - YNOfferLink = 2 - YNOutside = 3 - YNKIFull = 4 - YNWanaPlay = 5 - YNNoReason = 6 - + # Question note dialog. QNTitle = 100 QNMessage = 101 diff --git a/Scripts/Python/plasma/Plasma.py b/Scripts/Python/plasma/Plasma.py index 7c02d1feb3..532499fd95 100644 --- a/Scripts/Python/plasma/Plasma.py +++ b/Scripts/Python/plasma/Plasma.py @@ -40,6 +40,10 @@ Mead, WA 99021 *==LICENSE==* """ + +from __future__ import annotations +from typing import Callable, Tuple, Union + def PtAcceptInviteInGame(friendName,inviteKey): """Sends a VaultTask to the server to perform the invite""" pass @@ -631,6 +635,12 @@ def PtLocalAvatarRunKeyDown(): """Returns true if the run key is being held down for the local avatar""" pass +def PtLocalizedYesNoDialog(cb: Union[None, Callable, ptKey], path: str, *args, /, *, dialogType: int = PtConfirmationType.YesNo) -> None: + """This will display a confirmation dialog to the user with the localized text `path` + with any optional localization `args` applied. This dialog _has_ to be answered by the user, + and their answer will be returned in a Notify message or callback given by `cb`.""" + ... + def PtMaxListenDistSq(): """Returns the maximum distance (squared) of the listen range""" pass @@ -872,11 +882,11 @@ def PtWhatGUIControlType(guiKey): """Returns the control type of the key passed in""" pass -def PtYesNoDialog(selfkey,dialogMessage): - """This will display a Yes/No dialog to the user with the text dialogMessage -This dialog _has_ to be answered by the user. -And their answer will be returned in a Notify message.""" - pass +def PtYesNoDialog(cb: Union[None, ptKey, Callable], message: str, /, dialogType: int = PtConfirmationType.YesNo) -> None: + """This will display a confirmation dialog to the user with the text `message`. This dialog + _has_ to be answered by the user, and their answer will be returned in a Notify message + or callback given by `cb`.""" + ... class ptAgeInfoStruct: """Class to hold AgeInfo struct data""" diff --git a/Scripts/Python/plasma/PlasmaConstants.py b/Scripts/Python/plasma/PlasmaConstants.py index f1e9d99089..f2d3dfdc7b 100644 --- a/Scripts/Python/plasma/PlasmaConstants.py +++ b/Scripts/Python/plasma/PlasmaConstants.py @@ -114,6 +114,20 @@ class PtButtonNotifyTypes: kNotifyOnDown = 1 kNotifyOnUpAndDown = 2 +class PtConfirmationResult: + OK = 1 + Cancel = 0 + Yes = 1 + No = 0 + Quit = 1 + Logout = 62 + +class PtConfirmationType: + OK = 0 + ConfirmQuit = 1 + ForceQuit = 2 + YesNo = 3 + class PtCCRPetitionType: """(none)""" kGeneralHelp = 0 diff --git a/Scripts/Python/xDialogStartUp.py b/Scripts/Python/xDialogStartUp.py index dbbe20cc98..75a8e72852 100644 --- a/Scripts/Python/xDialogStartUp.py +++ b/Scripts/Python/xDialogStartUp.py @@ -216,12 +216,6 @@ def BeginAgeUnLoad(self,avatar): if GUIDiag6a.dialog.isEnabled(): PtHideDialog("GUIDialog06a") - ########################### - def OnNotify(self,state,id,events): - if id==(-1): ## callback from delete yes/no dialog (hopefully) ## - if state: - PtConsole("App.Quit") - ########################### def OnGUINotify(self,id,control,event): global gSelectedSlot @@ -245,7 +239,7 @@ def OnGUINotify(self,id,control,event): PtShowDialog("GUIDialog05") elif tagID == k4aQuitID: ## Quit ## - PtYesNoDialog(self.key,"Are you sure you want to quit?") + PtLocalizedYesNoDialog(None, "KI.Messages.LeaveGame", dialogType=PtConfirmationType.ConfirmQuit) elif tagID == k4aPlayer01: ## Click Event ## if gPlayerList[0]: @@ -280,7 +274,7 @@ def OnGUINotify(self,id,control,event): ## Or Else?? ## elif tagID == k4bQuitID: ## Quit ## - PtYesNoDialog(self.key,"Are you sure you want to quit?") + PtLocalizedYesNoDialog(None, "KI.Messages.LeaveGame", dialogType=PtConfirmationType.ConfirmQuit) elif tagID == k4bDeleteID: ## Delete Explorer ## if gSelectedSlot: @@ -322,7 +316,7 @@ def OnGUINotify(self,id,control,event): elif id == GUIDiag6.id: if event == kAction or event == kValueChanged: if tagID == k6QuitID: ## Quit ## - PtYesNoDialog(self.key,"Are you sure you want to quit?") + PtLocalizedYesNoDialog(None, "KI.Messages.LeaveGame", dialogType=PtConfirmationType.ConfirmQuit) elif tagID == k6BackID: ## Back To Player Select ## PtHideDialog("GUIDialog06") diff --git a/Sources/Plasma/Apps/plClient/plClient.cpp b/Sources/Plasma/Apps/plClient/plClient.cpp index 140ec6f493..56433bb953 100644 --- a/Sources/Plasma/Apps/plClient/plClient.cpp +++ b/Sources/Plasma/Apps/plClient/plClient.cpp @@ -135,6 +135,7 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include "pfAnimation/plAnimDebugList.h" #include "pfAudio/plListener.h" #include "pfCamera/plVirtualCamNeu.h" +#include "pfCharacter/pfConfirmationMgr.h" #include "pfCharacter/pfMarkerMgr.h" #include "pfConsole/pfConsole.h" #include "pfConsole/pfConsoleDirSrc.h" @@ -246,6 +247,9 @@ bool plClient::Shutdown() // Let the resmanager know we're going to be shutting down. hsgResMgr::ResMgr()->BeginShutdown(); + // This guy may send callbacks that release resources + pfConfirmationMgr::Shutdown(); + // Must kill off all movies before shutting down audio. IKillMovies(); @@ -1386,6 +1390,9 @@ bool plClient::StartInit() fGameGUIMgr->RegisterAs( kGameGUIMgr_KEY ); fGameGUIMgr->Init(); + // Yes/No dialog handler + pfConfirmationMgr::Init(); + plgAudioSys::Activate(true); // diff --git a/Sources/Plasma/FeatureLib/pfCharacter/CMakeLists.txt b/Sources/Plasma/FeatureLib/pfCharacter/CMakeLists.txt index 5589b9e713..853caed42d 100644 --- a/Sources/Plasma/FeatureLib/pfCharacter/CMakeLists.txt +++ b/Sources/Plasma/FeatureLib/pfCharacter/CMakeLists.txt @@ -1,10 +1,12 @@ set(pfCharacter_SOURCES + pfConfirmationMgr.cpp pfMarkerInfo.cpp pfMarkerMgr.cpp ) set(pfCharacter_HEADERS pfCharacterCreatable.h + pfConfirmationMgr.h pfMarkerInfo.h pfMarkerMgr.h ) @@ -15,15 +17,18 @@ target_link_libraries( PUBLIC CoreLib pnKeyedObject + plMessage PRIVATE pnMessage + pnNetCommon pnNucleusInc pnSceneObject - plMessage plModifier plNetClient plResMgr plStatusLog + pfGameGUIMgr + pfLocalizationMgr pfMessage INTERFACE pnFactory diff --git a/Sources/Plasma/FeatureLib/pfCharacter/pfCharacterCreatable.h b/Sources/Plasma/FeatureLib/pfCharacter/pfCharacterCreatable.h index fb1c937464..395c572b61 100644 --- a/Sources/Plasma/FeatureLib/pfCharacter/pfCharacterCreatable.h +++ b/Sources/Plasma/FeatureLib/pfCharacter/pfCharacterCreatable.h @@ -45,6 +45,9 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include "pnFactory/plCreator.h" +#include "pfConfirmationMgr.h" +REGISTER_NONCREATABLE(pfConfirmationMgr); + #include "pfMarkerMgr.h" REGISTER_NONCREATABLE(pfMarkerMgr); diff --git a/Sources/Plasma/FeatureLib/pfCharacter/pfConfirmationMgr.cpp b/Sources/Plasma/FeatureLib/pfCharacter/pfConfirmationMgr.cpp new file mode 100644 index 0000000000..23ce694ca7 --- /dev/null +++ b/Sources/Plasma/FeatureLib/pfCharacter/pfConfirmationMgr.cpp @@ -0,0 +1,453 @@ +/*==LICENSE==* + +CyanWorlds.com Engine - MMOG client, server and tools +Copyright (C) 2011 Cyan Worlds, Inc. + +This program 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. + +This program 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 this program. If not, see . + +Additional permissions under GNU GPL version 3 section 7 + +If you modify this Program, or any covered work, by linking or +combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK, +NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent +JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK +(or a modified version of those libraries), +containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA, +PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG +JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the +licensors of this Program grant you additional +permission to convey the resulting work. Corresponding Source for a +non-source form of such a combination shall include the source code for +the parts of OpenSSL and IJG JPEG Library used as well as that of the covered +work. + +You can contact Cyan Worlds, Inc. by email legal@cyan.com + or by snail mail at: + Cyan Worlds, Inc. + 14617 N Newport Hwy + Mead, WA 99021 + +*==LICENSE==*/ + +#include "pfConfirmationMgr.h" + +#include + +#include "plgDispatch.h" +#include "plTimerCallbackManager.h" + +#include "pnMessage/plNotifyMsg.h" +#include "pnNetCommon/plNetApp.h" + +#include "plMessage/plConfirmationMsg.h" +#include "plMessage/plConsoleMsg.h" +#include "plMessage/plLinkToAgeMsg.h" +#include "plMessage/plTimerCallbackMsg.h" + +#include "pfGameGUIMgr/pfGameGUIMgr.h" +#include "pfGameGUIMgr/pfGUIDialogHandlers.h" +#include "pfGameGUIMgr/pfGUIDialogMod.h" +#include "pfGameGUIMgr/pfGUIControlMod.h" +#include "pfGameGUIMgr/pfGUITextBoxMod.h" +#include "pfLocalizationMgr/pfLocalizationMgr.h" +#include "pfMessage/pfGameGUIMsg.h" +#include "pfMessage/pfGUINotifyMsg.h" +#include "pfMessage/pfKIMsg.h" + +using namespace ST::literals; + +//////////////////////////////////////////////////////////////////////////////// + +// From GUI_District_KIYesNo.prp, so don't change them. +constexpr uint32_t kMessageTextTag = 12U; +constexpr uint32_t kLogoutTextTag = 63U; // Leftmost +constexpr uint32_t kYesTextTag = 60U; // Center +constexpr uint32_t kNoTextTag = 61U; // Rightmost +constexpr uint32_t kLogoutButtonTag = 62U; +constexpr uint32_t kYesButtonTag = 10U; +constexpr uint32_t kNoButtonTag = 11U; + +//////////////////////////////////////////////////////////////////////////////// + +class pfConfirmationDialogProc : public pfGUIDialogProc +{ + friend class pfConfirmationMgr; + pfConfirmationMgr* fMgr; + + inline void ISetText(const ST::string& text, uint32_t tagID) + { + pfGUITextBoxMod* mod = pfGUITextBoxMod::ConvertNoRef(fDialog->GetControlFromTag(tagID)); + hsAssert(mod != nullptr, "You sure about this, boss?"); + mod->SetText(text.to_wchar().data()); + } + + inline void ISetLocalizedText(const ST::string& path, uint32_t tagID) + { + ISetText(pfLocalizationMgr::Instance().GetString(path), tagID); + } + + void ILayoutYesNo(const ST::string& text) + { + fDialog->GetControlFromTag(kLogoutTextTag)->SetVisible(false); + fDialog->GetControlFromTag(kLogoutButtonTag)->SetVisible(false); + fDialog->GetControlFromTag(kYesTextTag)->SetVisible(true); + fDialog->GetControlFromTag(kYesButtonTag)->SetVisible(true); + fDialog->GetControlFromTag(kNoTextTag)->SetVisible(true); + fDialog->GetControlFromTag(kNoButtonTag)->SetVisible(true); + ISetLocalizedText("KI.YesNoDialog.YESButton"_st, kYesTextTag); + ISetLocalizedText("KI.YesNoDialog.NoButton"_st, kNoTextTag); + ISetText(text, kMessageTextTag); + } + + void ILayoutSingle(const ST::string& message, const ST::string& button) + { + fDialog->GetControlFromTag(kLogoutTextTag)->SetVisible(false); + fDialog->GetControlFromTag(kLogoutButtonTag)->SetVisible(false); + fDialog->GetControlFromTag(kYesTextTag)->SetVisible(false); + fDialog->GetControlFromTag(kYesButtonTag)->SetVisible(false); + fDialog->GetControlFromTag(kNoTextTag)->SetVisible(true); + fDialog->GetControlFromTag(kNoButtonTag)->SetVisible(true); + ISetLocalizedText(button, kNoTextTag); + ISetText(message, kMessageTextTag); + } + + void ILayoutQuit(const ST::string& text) + { + bool canLogout = plNetClientApp::GetInstance()->GetPlayerID() != 0; + fDialog->GetControlFromTag(kLogoutTextTag)->SetVisible(canLogout); + fDialog->GetControlFromTag(kLogoutButtonTag)->SetVisible(canLogout); + fDialog->GetControlFromTag(kYesTextTag)->SetVisible(true); + fDialog->GetControlFromTag(kYesButtonTag)->SetVisible(true); + fDialog->GetControlFromTag(kNoTextTag)->SetVisible(true); + fDialog->GetControlFromTag(kNoButtonTag)->SetVisible(true); + if (canLogout) + ISetText("Logout"_st, kLogoutTextTag); // FIXME: This is missing from the LOC files. + ISetLocalizedText("KI.YesNoDialog.QuitButton"_st, kYesTextTag); + ISetLocalizedText("KI.YesNoDialog.NoButton"_st, kNoTextTag); + ISetText(text, kMessageTextTag); + } + +public: + pfConfirmationDialogProc(pfConfirmationMgr* mgr) + : fMgr(mgr) + { } + + ~pfConfirmationDialogProc() = default; + + void OnInit() override + { + if (!fMgr->fPending.empty()) + fDialog->Show(); + } + + void OnShow() override + { + // Prevent dialog trolling... + if (fMgr->fPending.empty()) { + fDialog->Hide(); + return; + } + + fMgr->fState = pfConfirmationMgr::State::WaitingForInput; + + const auto& msg = fMgr->fPending.front(); + ST::string text; + if (auto locMsg = plLocalizedConfirmationMsg::ConvertNoRef(msg.Get()); locMsg != nullptr) + text = pfLocalizationMgr::Instance().GetString(locMsg->GetText(), locMsg->GetArgs()); + else + text = msg->GetText(); + + switch (msg->GetType()) { + case plConfirmationMsg::Type::ConfirmQuit: + ILayoutQuit(text); + break; + case plConfirmationMsg::Type::ForceQuit: + ILayoutSingle(text, "KI.YesNoDialog.QuitButton"_st); + break; + case plConfirmationMsg::Type::OK: + ILayoutSingle(text, "KI.YesNoDialog.OKButton"_st); + break; + case plConfirmationMsg::Type::YesNo: + ILayoutYesNo(text); + break; + DEFAULT_FATAL(msg->GetType()); + } + } + + void OnHide() override + { + switch (fMgr->fState) { + case pfConfirmationMgr::State::WaitingForInput: + // Prevent dialog trolling... + fDialog->Show(); + break; + case pfConfirmationMgr::State::Ready: + // If another confirmation is already available, we don't want to just show it now. + // Instead, wait a short period of time, then re-process. + plgTimerCallbackMgr::NewTimer(.5f, new plTimerCallbackMsg(fMgr->GetKey(), (int32_t)fMgr->fState)); + fMgr->fState = pfConfirmationMgr::State::Delaying; + break; + } + } + + void DoSomething(pfGUIControlMod* ctrl) override + { + plConfirmationMsg::Result result; + switch (ctrl->GetTagID()) { + case kLogoutButtonTag: + result = plConfirmationMsg::Result::Logout; + break; + case kYesButtonTag: + result = plConfirmationMsg::Result::Yes; + break; + case kNoButtonTag: + switch (fMgr->fPending.front()->GetType()) { + case plConfirmationMsg::Type::ForceQuit: + case plConfirmationMsg::Type::OK: + result = plConfirmationMsg::Result::OK; + break; + default: + result = plConfirmationMsg::Result::No; + break; + } + break; + DEFAULT_FATAL(ctrl->GetTagID()); + } + + fMgr->ISendResult(result, pfConfirmationMgr::State::Ready); + fDialog->Hide(); + } + + void OnDestroy() override + { + // Crap... Someone has thrown the dialog away from underneath us. + // If anybody is still around, cancel anything waiting on input. + fMgr->ISendResult(plConfirmationMsg::Result::Cancel, pfConfirmationMgr::State::Alive); + } + + void OnControlEvent(ControlEvt event) override + { + // There is only one control event, atm... Someone pressed escape + // thereby closing the dialog. Therefore, just send a cancel. + if (event == kExitMode) { + fMgr->ISendResult(plConfirmationMsg::Result::Cancel, pfConfirmationMgr::State::Ready); + fDialog->Hide(); + } + } +}; + +//////////////////////////////////////////////////////////////////////////////// + +pfConfirmationMgr::pfConfirmationMgr() + : fState(State::Alive), + fProc(new pfConfirmationDialogProc(this)) +{ + // Prevent the GUI system from killing us. Screw that comment in the GUI header. + fProc->IncRef(); +} + +pfConfirmationMgr::~pfConfirmationMgr() +{ + // Any pending items that are callbacks fire now as being cancelled. + while (!fPending.empty()) { + const auto& msg = fPending.front(); + std::visit([](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v>) + arg(plConfirmationMsg::Result::Cancel); + }, msg->GetCallback()); + fPending.pop(); + } + + if (fProc->fDialog) { + fProc->fDialog->SetHandler(nullptr); + if (pfGameGUIMgr::GetInstance()) + pfGameGUIMgr::GetInstance()->UnloadDialog(fProc->fDialog); + } + + if (fProc->DecRef()) + delete fProc; +} + +//////////////////////////////////////////////////////////////////////////////// + +void pfConfirmationMgr::ISendResult(plConfirmationMsg::Result result, State newState) +{ + if (fPending.empty()) + return; + + fState = pfConfirmationMgr::State::ProcessingInput; + + const auto& msg = fPending.front(); + + // If a quit was requested, don't rely on any downstream processing. Just post + // a quit message to happen on the next dispatcher pump. + bool wantQuit = (msg->GetType() == plConfirmationMsg::Type::ConfirmQuit && + result == plConfirmationMsg::Result::Quit); + bool forceQuit = msg->GetType() == plConfirmationMsg::Type::ForceQuit; + if (wantQuit || forceQuit) { + plConsoleMsg* quitMsg = new plConsoleMsg(plConsoleMsg::kExecuteLine, "App.Quit"_st); + plgDispatch::Dispatch()->MsgQueue(quitMsg); + } + + // Again, don't rely on bugprone Python code to handle critical functionality. + bool wantLogout = (msg->GetType() == plConfirmationMsg::Type::ConfirmQuit && + result == plConfirmationMsg::Result::Logout); + if (wantLogout) { + plLinkToAgeMsg* logoutMsg = new plLinkToAgeMsg(); + logoutMsg->AddReceiver(plNetClientApp::GetInstance()->GetKey()); + logoutMsg->PlayLinkSfx(false, false); + logoutMsg->GetAgeLink()->GetAgeInfo()->SetAgeFilename("StartUp"_st); + logoutMsg->GetAgeLink()->SpawnPoint().SetTitle("Default"_st); + logoutMsg->GetAgeLink()->SpawnPoint().SetName("LinkInPointDefault"_st); + plgDispatch::Dispatch()->MsgQueue(logoutMsg); + } + + std::visit([result](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v>) { + // New: High level, potentially stateful functor + arg(result); + } else if constexpr (std::is_same_v) { + // Old: Send a notify message to whoever called (probably) `PtYesNoDialog()` + plNotifyMsg* notifyMsg = new plNotifyMsg; + notifyMsg->AddReceiver(arg); + notifyMsg->SetBCastFlag(plMessage::kNetPropagate, false); + notifyMsg->SetState((float)result); + notifyMsg->AddVariableEvent("YesNo"_st, (int32_t)result); + notifyMsg->Send(); + } else { + static_assert(std::is_same_v, "non-exhaustive visitor"); + } + }, msg->GetCallback()); + + fPending.pop(); + fState = newState; +} + +//////////////////////////////////////////////////////////////////////////////// + +void pfConfirmationMgr::ILoadDialog() +{ + // We need to be particularly careful that some old xKI.py doesn't run wild + // over us. + pfGameGUIMgr* gui = pfGameGUIMgr::GetInstance(); + pfGUIDialogMod* dialog = gui->GetDialogFromString("KIYesNo"); + if (dialog) { + fState = State::Ready; + dialog->SetHandler(fProc); + + // The default pfGUIDialogMod proc ate the OnInit() call. So, here's another one. + fProc->OnInit(); + } else { + fState = State::WaitingForDialogLoad; + gui->LoadDialog("KIYesNo"_st, GetKey(), "GUI"); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +bool pfConfirmationMgr::MsgReceive(plMessage* msg) +{ + plConfirmationMsg* confirmMsg = plConfirmationMsg::ConvertNoRef(msg); + if (confirmMsg) { + fPending.emplace(confirmMsg); + if (fState == State::Ready) + fProc->fDialog->Show(); + else if (fState == State::Alive) + ILoadDialog(); + return true; + } + + pfKIMsg* kiMsg = pfKIMsg::ConvertNoRef(msg); + if (kiMsg) { + // This seems a little objectionable, IMO, but it keeps the behavior consistent. + // When the disable KI message comes in, we force-cancel any confirmations, but + // the old behavior did not "remember" this and allows any new dialogs to pop up, + // even though the KI is supposedly disabled. + if (kiMsg->GetCommand() == pfKIMsg::kDisableKIandBB) { + if (fState == State::WaitingForInput) { + while (!fPending.empty()) + ISendResult(plConfirmationMsg::Result::Cancel, State::Ready); + fProc->fDialog->Hide(); + } + } + return true; + } + + pfGUINotifyMsg* guiNotifyMsg = pfGUINotifyMsg::ConvertNoRef(msg); + if (guiNotifyMsg) { + // Handle the dialog LOAD so we can insert our own dialog proc + if (guiNotifyMsg->GetEvent() == pfGUINotifyMsg::kDialogLoaded) { + hsAssert(fState == State::WaitingForDialogLoad, "Unexpected dialog load"); + fState = State::Ready; + pfGUIDialogMod* dialog = pfGUIDialogMod::ConvertNoRef(guiNotifyMsg->GetControlKey()->VerifyLoaded()); + dialog->SetHandler(fProc); + + // The default pfGUIDialogMod proc ate the OnInit() call. So, here's another one. + fProc->OnInit(); + } else { + hsAssert(false, "Unexpected GUINotifyMsg"); + } + + // No other GUI notify messages should come though because we have + // changed out the notify proc to one that does not send messages. + return true; + } + + plTimerCallbackMsg* timerMsg = plTimerCallbackMsg::ConvertNoRef(msg); + if (timerMsg) { + // Someone might delete the dialog out from under us eg by PtUnLoadDialog("KIYesNo")... + if (fState == State::Delaying) + fState = (State)timerMsg->fID; + + if (!fPending.empty()) { + if (fState == State::Alive) + ILoadDialog(); + else if (fState == State::Ready) + fProc->fDialog->Show(); + else + hsAssert(false, "Unexpected state on timer callback"); + } + return true; + } + + return hsKeyedObject::MsgReceive(msg); +} + +//////////////////////////////////////////////////////////////////////////////// + +void pfConfirmationMgr::Init() +{ + if (s_instance == nullptr) { + s_instance = new pfConfirmationMgr; + s_instance->RegisterAs(kConfirmationMgr_KEY); + + plgDispatch::Dispatch()->RegisterForType(plConfirmationMsg::Index(), s_instance->GetKey()); + plgDispatch::Dispatch()->RegisterForExactType(pfKIMsg::Index(), s_instance->GetKey()); + } +} + +void pfConfirmationMgr::Shutdown() +{ + if (s_instance) { + plgDispatch::Dispatch()->UnRegisterForType(plConfirmationMsg::Index(), s_instance->GetKey()); + plgDispatch::Dispatch()->UnRegisterForExactType(pfKIMsg::Index(), s_instance->GetKey()); + + s_instance->UnRegisterAs(kConfirmationMgr_KEY); // UnRefs us + s_instance = nullptr; + } +} + +pfConfirmationMgr* pfConfirmationMgr::s_instance{}; diff --git a/Sources/Plasma/FeatureLib/pfCharacter/pfConfirmationMgr.h b/Sources/Plasma/FeatureLib/pfCharacter/pfConfirmationMgr.h new file mode 100644 index 0000000000..1b6dc6e33c --- /dev/null +++ b/Sources/Plasma/FeatureLib/pfCharacter/pfConfirmationMgr.h @@ -0,0 +1,97 @@ +/*==LICENSE==* + +CyanWorlds.com Engine - MMOG client, server and tools +Copyright (C) 2011 Cyan Worlds, Inc. + +This program 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. + +This program 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 this program. If not, see . + +Additional permissions under GNU GPL version 3 section 7 + +If you modify this Program, or any covered work, by linking or +combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK, +NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent +JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK +(or a modified version of those libraries), +containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA, +PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG +JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the +licensors of this Program grant you additional +permission to convey the resulting work. Corresponding Source for a +non-source form of such a combination shall include the source code for +the parts of OpenSSL and IJG JPEG Library used as well as that of the covered +work. + +You can contact Cyan Worlds, Inc. by email legal@cyan.com + or by snail mail at: + Cyan Worlds, Inc. + 14617 N Newport Hwy + Mead, WA 99021 + +*==LICENSE==*/ + +#ifndef pfConfirmationMgr_inc +#define pfConfirmationMgr_inc + +#include + +#include "pnKeyedObject/hsKeyedObject.h" + +#include "plMessage/plConfirmationMsg.h" + +class pfConfirmationDialogProc; +class pfGUINotifyMsg; + +class pfConfirmationMgr : public hsKeyedObject +{ +protected: + friend class pfConfirmationDialogProc; + + enum class State : int32_t + { + Alive, + WaitingForDialogLoad, + Ready, + WaitingForInput, + ProcessingInput, + Delaying, + }; + + std::queue> fPending; + State fState; + pfConfirmationDialogProc* fProc; + + // "Can't delete an incomplete type" my ass... + static pfConfirmationMgr* s_instance; + +protected: + void ISendResult(plConfirmationMsg::Result result, State newState); + void ILoadDialog(); + +public: + pfConfirmationMgr(); + pfConfirmationMgr(const pfConfirmationMgr&) = delete; + pfConfirmationMgr(pfConfirmationMgr&&) = delete; + ~pfConfirmationMgr(); + + static void Init(); + static void Shutdown(); + +public: + CLASSNAME_REGISTER(pfConfirmationMgr); + GETINTERFACE_ANY(pfConfirmationMgr, hsKeyedObject); + + bool MsgReceive(plMessage* msg) override; +}; + +#endif diff --git a/Sources/Plasma/FeatureLib/pfConsole/pfGameConsoleCommands.cpp b/Sources/Plasma/FeatureLib/pfConsole/pfGameConsoleCommands.cpp index 1f60301479..d34e1a7bf0 100644 --- a/Sources/Plasma/FeatureLib/pfConsole/pfGameConsoleCommands.cpp +++ b/Sources/Plasma/FeatureLib/pfConsole/pfGameConsoleCommands.cpp @@ -81,6 +81,7 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include "plAvatar/plAvatarMgr.h" #include "plGImage/plMipmap.h" #include "plMessage/plAvatarMsg.h" +#include "plMessage/plConfirmationMsg.h" #include "plPipeline/plCaptureRender.h" #include "pfConsoleCore/pfConsoleCmd.h" @@ -217,6 +218,38 @@ PF_CONSOLE_CMD( Game_GUI, CreateDialog, "string name", "" ) pfGUICtrlGenerator::Instance().GenerateDialog( params[ 0 ] ); } +PF_CONSOLE_CMD(Game_GUI, Confirm, "int type", "Shows a sample confirmation dialog") +{ + plConfirmationMsg* msg; + auto type = (plConfirmationMsg::Type)(int32_t)params[0]; + + switch (type) { + case plConfirmationMsg::Type::ConfirmQuit: + msg = new plLocalizedConfirmationMsg("KI.Messages.LeaveGame"); + break; + case plConfirmationMsg::Type::ForceQuit: + msg = new plConfirmationMsg("Time to die, my friend."); + break; + case plConfirmationMsg::Type::YesNo: + msg = new plConfirmationMsg("Do you understand me?"); + msg->SetCallback( + [PrintString](plConfirmationMsg::Result result) { + if (result == plConfirmationMsg::Result::No) { + PrintString("Well that's too bad."); + } else { + PrintString("Woo-hoo!"); + } + } + ); + break; + default: + msg = new plConfirmationMsg("Whatever, man."); + break; + } + + msg->SetType(type); + msg->Send(); +} #endif diff --git a/Sources/Plasma/FeatureLib/pfGameGUIMgr/pfGUIDialogMod.cpp b/Sources/Plasma/FeatureLib/pfGameGUIMgr/pfGUIDialogMod.cpp index d4a9e995f2..5d1ae58ded 100644 --- a/Sources/Plasma/FeatureLib/pfGameGUIMgr/pfGUIDialogMod.cpp +++ b/Sources/Plasma/FeatureLib/pfGameGUIMgr/pfGUIDialogMod.cpp @@ -607,7 +607,7 @@ void pfGUIDialogMod::Hide() //// GetControlFromTag /////////////////////////////////////////////////////// -pfGUIControlMod *pfGUIDialogMod::GetControlFromTag( uint32_t tagID ) +pfGUIControlMod *pfGUIDialogMod::GetControlFromTag( uint32_t tagID ) const { for (pfGUIControlMod* ctrl : fControls) { diff --git a/Sources/Plasma/FeatureLib/pfGameGUIMgr/pfGUIDialogMod.h b/Sources/Plasma/FeatureLib/pfGameGUIMgr/pfGUIDialogMod.h index 15ccb682c9..fce46f35a9 100644 --- a/Sources/Plasma/FeatureLib/pfGameGUIMgr/pfGUIDialogMod.h +++ b/Sources/Plasma/FeatureLib/pfGameGUIMgr/pfGUIDialogMod.h @@ -182,8 +182,8 @@ class pfGUIDialogMod : public plSingleModifier pfGUIControlMod *GetFocus() { return fFocusCtrl; } pfGUIDialogMod *GetNext() { return fNext; } - uint32_t GetTagID() { return fTagID; } - pfGUIControlMod *GetControlFromTag( uint32_t tagID ); + uint32_t GetTagID() const { return fTagID; } + pfGUIControlMod *GetControlFromTag( uint32_t tagID ) const; void SetHandler( pfGUIDialogProc *hdlr ); pfGUIDialogProc *GetHandler() const { return fHandler; } diff --git a/Sources/Plasma/FeatureLib/pfPython/CMakeLists.txt b/Sources/Plasma/FeatureLib/pfPython/CMakeLists.txt index 7b82c03dd9..4a20590812 100644 --- a/Sources/Plasma/FeatureLib/pfPython/CMakeLists.txt +++ b/Sources/Plasma/FeatureLib/pfPython/CMakeLists.txt @@ -95,6 +95,7 @@ set(pfPython_HEADERS cyParticleSys.h cyPhysics.h cyPythonInterface.h + plPythonCallable.h pfPythonCreatable.h plPythonFileMod.h plPythonHelpers.h @@ -262,6 +263,7 @@ target_link_libraries( PUBLIC CoreLib pnNucleusInc + plMessage PRIVATE pnEncryption pnInputCore diff --git a/Sources/Plasma/FeatureLib/pfPython/cyMisc.cpp b/Sources/Plasma/FeatureLib/pfPython/cyMisc.cpp index 20e990ab79..6a8fd5112f 100644 --- a/Sources/Plasma/FeatureLib/pfPython/cyMisc.cpp +++ b/Sources/Plasma/FeatureLib/pfPython/cyMisc.cpp @@ -1246,28 +1246,6 @@ void cyMisc::SendKIRegisterImagerMsg(const char* imagerName, pyKey& sender) plgDispatch::MsgSend( msg ); } -///////////////////////////////////////////////////////////////////////////// -// -// Function : YesNoDialog -// PARAMETERS : sender - who set this and will get the notify -// : message - message to put up in YesNo dialog -// -// PURPOSE : Put up Yes/No dialog -// -// RETURNS : nothing -// - -void cyMisc::YesNoDialog(pyKey& sender, const ST::string& thestring) -{ - // create the mesage to send - pfKIMsg *msg = new pfKIMsg( pfKIMsg::kYesNoDialog ); - - msg->SetSender(sender.getKey()); - msg->SetString(thestring); - // send it off - plgDispatch::MsgSend( msg ); -} - ///////////////////////////////////////////////////////////////////////////// // // Function : RateIt diff --git a/Sources/Plasma/FeatureLib/pfPython/cyMisc.h b/Sources/Plasma/FeatureLib/pfPython/cyMisc.h index acc539996c..0941bf540f 100644 --- a/Sources/Plasma/FeatureLib/pfPython/cyMisc.h +++ b/Sources/Plasma/FeatureLib/pfPython/cyMisc.h @@ -504,18 +504,6 @@ class cyMisc static void SendKIGZMarkerMsg(int32_t markerNumber, pyKey& sender); static void SendKIRegisterImagerMsg(const char* imagerName, pyKey& sender); - ///////////////////////////////////////////////////////////////////////////// - // - // Function : YesNoDialog - // PARAMETERS : sender - sender's key (to get the reply) - // : value - extra value - // - // PURPOSE : Send message to the KI, to tell it things to do - // - // RETURNS : nothing - // - static void YesNoDialog(pyKey& sender, const ST::string& thestring); - ///////////////////////////////////////////////////////////////////////////// // // Function : RateIt diff --git a/Sources/Plasma/FeatureLib/pfPython/cyMiscGlue2.cpp b/Sources/Plasma/FeatureLib/pfPython/cyMiscGlue2.cpp index 2bd5222a77..60e5ab32ee 100644 --- a/Sources/Plasma/FeatureLib/pfPython/cyMiscGlue2.cpp +++ b/Sources/Plasma/FeatureLib/pfPython/cyMiscGlue2.cpp @@ -40,34 +40,154 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com *==LICENSE==*/ +#include "cyMisc.h" + #include -#include "pyKey.h" -#include "cyMisc.h" -#include "pyGlueHelpers.h" +#include + +#include "pyEnum.h" #include "pyColor.h" +#include "pyGlueHelpers.h" +#include "pyKey.h" #include "pyPlayer.h" -#include "pyEnum.h" +#include "plPythonCallable.h" -// for enums +#include "plMessage/plConfirmationMsg.h" #include "plNetCommon/plNetCommon.h" #include "plResMgr/plLocalization.h" #include "plMessage/plLOSRequestMsg.h" +namespace plPythonCallable +{ + template<> + inline void IBuildTupleArg(PyObject* tuple, size_t idx, plConfirmationMsg::Result value) + { + PyTuple_SET_ITEM(tuple, idx, PyLong_FromSsize_t((Py_ssize_t)value)); + } +}; -PYTHON_GLOBAL_METHOD_DEFINITION(PtYesNoDialog, args, "Params: selfkey,dialogMessage\nThis will display a Yes/No dialog to the user with the text dialogMessage\n" - "This dialog _has_ to be answered by the user.\n" - "And their answer will be returned in a Notify message.") +PYTHON_GLOBAL_METHOD_DEFINITION_WKEY(PtYesNoDialog, args, kwargs, + "Params: cb, message, /, dialogType\n" + "This will display a confirmation dialog to the user with the text `message` " + "This dialog _has_ to be answered by the user, " + "and their answer will be returned in a Notify message or callback given by `cb`.") { - PyObject* keyObj = nullptr; + const char* keywords[]{ "", "", "dialogType", nullptr }; + constexpr std::string_view kErrorMsg = "PtYesNoDialog expects a ptKey or callable, " + "a string or localization path, and an optional int."; + PyObject* cbObj; ST::string text; - if (!PyArg_ParseTuple(args, "OO&", &keyObj, PyUnicode_STStringConverter, &text) || !pyKey::Check(keyObj)) { - PyErr_SetString(PyExc_TypeError, "PtYesNoDialog expects a ptKey and a string"); + plConfirmationMsg::Type dialogType = plConfirmationMsg::Type::YesNo; + + if (!PyArg_ParseTupleAndKeywords(args, kwargs, + "OO&|I", const_cast(keywords), + &cbObj, + PyUnicode_STStringConverter, &text, + &dialogType)) { + PyErr_SetString(PyExc_TypeError, kErrorMsg.data()); PYTHON_RETURN_ERROR; } - pyKey* key = pyKey::ConvertFrom(keyObj); - cyMisc::YesNoDialog(*key, text); - PYTHON_RETURN_NONE + + plConfirmationMsg::Callback cb; + if (pyKey::Check(cbObj)) { + cb = pyKey::ConvertFrom(cbObj)->getKey(); + } else if (PyCallable_Check(cbObj)) { + plPythonCallable::BuildCallback<1>("PtYesNoDialog", cbObj, cb); + } else if (cbObj != Py_None) { + PyErr_SetString(PyExc_TypeError, kErrorMsg.data()); + PYTHON_RETURN_ERROR; + } + + // We already have the message class definition included, so just send from here. + auto msg = new plConfirmationMsg(std::move(text), dialogType, std::move(cb)); + msg->Send(); + + PYTHON_RETURN_NONE; +} + +PYTHON_GLOBAL_METHOD_DEFINITION_WKEY(PtLocalizedYesNoDialog, args, kwargs, + "Params: cb, path, *args, /, *, dialogType\n" + "This will display a confirmation dialog to the user with the localized text `path` " + "with any optional localization `args` applied. This dialog _has_ to be answered by the user, " + "and their answer will be returned in a Notify message or callback given by `cb`.") +{ + constexpr std::string_view kErrorMsg = "PtLocalizedYesNoDialog expects a ptKey or callable, " + "a string, optional localization arguments, and an " + "optional int."; + + // We cannot use PyArg_ParseTuple or PyArg_ParseTupleAndKeywords due to our usage + // of *args. While we could accept a single sequence for our localization arguments + // and get that functionality back, the interface would not be very Pythonic. + if (!PyTuple_Check(args) || PyTuple_Size(args) < 2) { + PyErr_SetString(PyExc_TypeError, kErrorMsg.data()); + PYTHON_RETURN_ERROR; + } + + PyObject* cbObj = PyTuple_GET_ITEM(args, 0); + plConfirmationMsg::Callback cb; + if (pyKey::Check(cbObj)) { + cb = pyKey::ConvertFrom(cbObj)->getKey(); + } else if (PyCallable_Check(cbObj)) { + plPythonCallable::BuildCallback<1>("PtLocalizedYesNoDialog", cbObj, cb); + } else if (cbObj != Py_None) { + PyErr_SetString(PyExc_TypeError, kErrorMsg.data()); + PYTHON_RETURN_ERROR; + } + + PyObject* pathObj = PyTuple_GET_ITEM(args, 1); + if (!PyUnicode_Check(pathObj)) { + PyErr_SetString(PyExc_TypeError, kErrorMsg.data()); + PYTHON_RETURN_ERROR; + } + ST::string path = PyUnicode_AsSTString(pathObj); + + constexpr Py_ssize_t kLocArgOffset = 2; + const Py_ssize_t totalArgs = PyTuple_Size(args); + std::vector locArgs(totalArgs - kLocArgOffset); + for (Py_ssize_t i = kLocArgOffset; i < totalArgs; ++i) { + PyObject* arg = PyTuple_GET_ITEM(args, i); + if (PyUnicode_Check(arg)) { + locArgs[i - kLocArgOffset] = PyUnicode_AsSTString(arg); + } else { + pyObjectRef argStr = PyObject_Str(arg); + if (!argStr) + // Don't blow away the internal error state + PYTHON_RETURN_ERROR; + locArgs[i - kLocArgOffset] = PyUnicode_AsSTString(argStr.Get()); + } + } + + plConfirmationMsg::Type dialogType = plConfirmationMsg::Type::YesNo; + if (kwargs) { + if (!PyArg_ValidateKeywordArguments(kwargs)) { + PyErr_SetString(PyExc_TypeError, kErrorMsg.data()); + PYTHON_RETURN_ERROR; + } + + PyObject* dialogTypeObj = PyDict_GetItemString(kwargs, "dialogType"); + if (dialogTypeObj != nullptr) { + if (PyLong_Check(dialogTypeObj)) { + dialogType = (plConfirmationMsg::Type)PyLong_AsLong(dialogTypeObj); + } else if (PyNumber_Check(dialogTypeObj)) { + // The weird internal enum type isn't an int but implements the number protocol. + pyObjectRef dialogTypeLong = PyNumber_Long(dialogTypeObj); + if (!dialogTypeLong) { + PyErr_SetString(PyExc_TypeError, kErrorMsg.data()); + PYTHON_RETURN_ERROR; + } + dialogType = (plConfirmationMsg::Type)PyLong_AsLong(dialogTypeLong.Get()); + } else { + PyErr_SetString(PyExc_TypeError, kErrorMsg.data()); + PYTHON_RETURN_ERROR; + } + } + } + + auto msg = new plLocalizedConfirmationMsg(std::move(path), std::move(locArgs), dialogType, std::move(cb)); + msg->Send(); + + PYTHON_RETURN_NONE; } PYTHON_GLOBAL_METHOD_DEFINITION(PtRateIt, args, "Params: chronicleName,dialogPrompt,onceFlag\nShows a dialog with dialogPrompt and stores user input rating into chronicleName") @@ -442,6 +562,7 @@ void cyMisc::AddPlasmaMethods2(PyObject* m) { PYTHON_START_GLOBAL_METHOD_TABLE(cyMisc2) PYTHON_GLOBAL_METHOD(PtYesNoDialog) + PYTHON_GLOBAL_METHOD(PtLocalizedYesNoDialog) PYTHON_GLOBAL_METHOD(PtRateIt) PYTHON_GLOBAL_METHOD(PtExcludeRegionSet) @@ -479,6 +600,22 @@ void cyMisc::AddPlasmaMethods2(PyObject* m) void cyMisc::AddPlasmaConstantsClasses(PyObject *m) { + PYTHON_ENUM_START(PtConfirmationResult) + PYTHON_ENUM_ELEMENT(PtConfirmationResult, OK, plConfirmationMsg::Result::OK) + PYTHON_ENUM_ELEMENT(PtConfirmationResult, Cancel, plConfirmationMsg::Result::Cancel) + PYTHON_ENUM_ELEMENT(PtConfirmationResult, Yes, plConfirmationMsg::Result::Yes) + PYTHON_ENUM_ELEMENT(PtConfirmationResult, No, plConfirmationMsg::Result::No) + PYTHON_ENUM_ELEMENT(PtConfirmationResult, Quit, plConfirmationMsg::Result::Quit) + PYTHON_ENUM_ELEMENT(PtConfirmationResult, Logout, plConfirmationMsg::Result::Logout) + PYTHON_ENUM_END(m, PtConfirmationResult) + + PYTHON_ENUM_START(PtConfirmationType) + PYTHON_ENUM_ELEMENT(PtConfirmationType, OK, plConfirmationMsg::Type::OK) + PYTHON_ENUM_ELEMENT(PtConfirmationType, ConfirmQuit, plConfirmationMsg::Type::ConfirmQuit) + PYTHON_ENUM_ELEMENT(PtConfirmationType, ForceQuit, plConfirmationMsg::Type::ForceQuit) + PYTHON_ENUM_ELEMENT(PtConfirmationType, YesNo, plConfirmationMsg::Type::YesNo) + PYTHON_ENUM_END(m, PtConfirmationType) + PYTHON_ENUM_START(PtCCRPetitionType) PYTHON_ENUM_ELEMENT(PtCCRPetitionType, kGeneralHelp,plNetCommon::PetitionTypes::kGeneralHelp) PYTHON_ENUM_ELEMENT(PtCCRPetitionType, kBug, plNetCommon::PetitionTypes::kBug) diff --git a/Sources/Plasma/FeatureLib/pfPython/plPythonCallable.h b/Sources/Plasma/FeatureLib/pfPython/plPythonCallable.h new file mode 100644 index 0000000000..a16ac47f9e --- /dev/null +++ b/Sources/Plasma/FeatureLib/pfPython/plPythonCallable.h @@ -0,0 +1,201 @@ +/*==LICENSE==* + +CyanWorlds.com Engine - MMOG client, server and tools +Copyright (C) 2011 Cyan Worlds, Inc. + +This program 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. + +This program 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 this program. If not, see . + +Additional permissions under GNU GPL version 3 section 7 + +If you modify this Program, or any covered work, by linking or +combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK, +NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent +JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK +(or a modified version of those libraries), +containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA, +PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG +JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the +licensors of this Program grant you additional +permission to convey the resulting work. Corresponding Source for a +non-source form of such a combination shall include the source code for +the parts of OpenSSL and IJG JPEG Library used as well as that of the covered +work. + +You can contact Cyan Worlds, Inc. by email legal@cyan.com + or by snail mail at: + Cyan Worlds, Inc. + 14617 N Newport Hwy + Mead, WA 99021 + +*==LICENSE==*/ + +#ifndef _pyPythonCallable_h_ +#define _pyPythonCallable_h_ + +#include +#include +#include + +#include +#include + +#include "plProfile.h" + +#include "cyPythonInterface.h" +#include "pyGlueHelpers.h" +#include "pyObjectRef.h" + +plProfile_Extern(PythonUpdate); + +namespace plPythonCallable +{ + template + inline void IBuildTupleArg(PyObject* tuple, size_t idx, ArgT value) = delete; + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, bool value) + { + PyTuple_SET_ITEM(tuple, idx, PyBool_FromLong(value ? 1 : 0)); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, char value) + { + PyTuple_SET_ITEM(tuple, idx, PyUnicode_FromFormat("%c", (int)value)); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, const char* value) + { + PyTuple_SET_ITEM(tuple, idx, PyUnicode_FromString(value)); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, double value) + { + PyTuple_SET_ITEM(tuple, idx, PyFloat_FromDouble(value)); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, float value) + { + PyTuple_SET_ITEM(tuple, idx, PyFloat_FromDouble(value)); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, int8_t value) + { + PyTuple_SET_ITEM(tuple, idx, PyLong_FromLong(value)); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, int16_t value) + { + PyTuple_SET_ITEM(tuple, idx, PyLong_FromLong(value)); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, int32_t value) + { + PyTuple_SET_ITEM(tuple, idx, PyLong_FromLong(value)); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, PyObject* value) + { + PyTuple_SET_ITEM(tuple, idx, value); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, pyObjectRef& value) + { + PyTuple_SET_ITEM(tuple, idx, value.Release()); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, const ST::string& value) + { + PyTuple_SET_ITEM(tuple, idx, PyUnicode_FromSTString(value)); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, uint8_t value) + { + PyTuple_SET_ITEM(tuple, idx, PyLong_FromSize_t(value)); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, uint16_t value) + { + PyTuple_SET_ITEM(tuple, idx, PyLong_FromSize_t(value)); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, uint32_t value) + { + PyTuple_SET_ITEM(tuple, idx, PyLong_FromSize_t(value)); + } + + inline void IBuildTupleArg(PyObject* tuple, size_t idx, wchar_t value) + { + PyTuple_SET_ITEM(tuple, idx, PyUnicode_FromFormat("%c", (int)value)); + } + + template + inline void BuildTupleArgs(PyObject* tuple, Arg&& arg) + { + IBuildTupleArg(tuple, (Size - 1), std::forward(arg)); + } + + template + inline void BuildTupleArgs(PyObject* tuple, Arg0&& arg0, Args&&... args) + { + IBuildTupleArg(tuple, (Size - (sizeof...(args) + 1)), std::forward(arg0)); + BuildTupleArgs(tuple, std::forward(args)...); + } + + template + [[nodiscard]] + inline std::function BuildCallback(ST::string parentCall, PyObject* callable) + { + hsAssert(PyCallable_Check(callable) != 0, "BuildCallback() expects a Python callable."); + + pyObjectRef cb(callable, pyObjectNewRef); + return [cb = std::move(cb), parentCall = std::move(parentCall)](_CBArgsT&&... args) -> void { + pyObjectRef tuple = PyTuple_New(sizeof...(args)); + BuildTupleArgs(tuple.Get(), std::forward<_CBArgsT>(args)...); + + plProfile_BeginTiming(PythonUpdate); + pyObjectRef result = PyObject_CallObject(cb.Get(), tuple.Get()); + plProfile_EndTiming(PythonUpdate); + + if (!result) { + // Stash the error state so we can get some info about the + // callback before printing the exception itself. + PyObject* ptype, * pvalue, * ptraceback; + PyErr_Fetch(&ptype, &pvalue, &ptraceback); + pyObjectRef repr = PyObject_Repr(cb.Get()); + PythonInterface::WriteToLog(ST::format("Error executing '{}' callback for '{}'", + PyUnicode_AsSTString(repr.Get()), + parentCall)); + PyErr_Restore(ptype, pvalue, ptraceback); + PyErr_Print(); + } + }; + } + + template + inline void BuildCallback(ST::string parentCall, PyObject* callable, + std::function& cb) + { + cb = BuildCallback<_CBArgsT...>(std::move(parentCall), callable); + } + + template + inline void BuildCallback(ST::string parentCall, PyObject* callable, + std::variant<_VariantArgsT...>& cb) + { + std::variant_alternative_t<_AlternativeN, std::decay_t> cbFunc; + BuildCallback(std::move(parentCall), callable, cbFunc); + cb = std::move(cbFunc); + } +}; + +#endif diff --git a/Sources/Plasma/FeatureLib/pfPython/plPythonFileMod.cpp b/Sources/Plasma/FeatureLib/pfPython/plPythonFileMod.cpp index b4f795fb80..574ba84dc9 100644 --- a/Sources/Plasma/FeatureLib/pfPython/plPythonFileMod.cpp +++ b/Sources/Plasma/FeatureLib/pfPython/plPythonFileMod.cpp @@ -54,6 +54,7 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include "pyGeometry3.h" #include "pyKey.h" #include "pyObjectRef.h" +#include "plPythonCallable.h" #include "hsResMgr.h" #include "hsStream.h" @@ -349,98 +350,14 @@ T* plPythonFileMod::IScriptWantsMsg(func_num methodId, plMessage* msg) const // PURPOSE : Builds Python argument tuple for method calling. // -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, bool value) +namespace plPythonCallable { - PyTuple_SET_ITEM(tuple, idx, PyBool_FromLong(value ? 1 : 0)); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, char value) -{ - PyTuple_SET_ITEM(tuple, idx, PyUnicode_FromFormat("%c", (int)value)); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, ControlEventCode value) -{ - PyTuple_SET_ITEM(tuple, idx, PyLong_FromLong((long)value)); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, const char* value) -{ - PyTuple_SET_ITEM(tuple, idx, PyUnicode_FromString(value)); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, double value) -{ - PyTuple_SET_ITEM(tuple, idx, PyFloat_FromDouble(value)); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, float value) -{ - PyTuple_SET_ITEM(tuple, idx, PyFloat_FromDouble(value)); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, int8_t value) -{ - PyTuple_SET_ITEM(tuple, idx, PyLong_FromLong(value)); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, int16_t value) -{ - PyTuple_SET_ITEM(tuple, idx, PyLong_FromLong(value)); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, int32_t value) -{ - PyTuple_SET_ITEM(tuple, idx, PyLong_FromLong(value)); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, PyObject* value) -{ - PyTuple_SET_ITEM(tuple, idx, value); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, pyObjectRef& value) -{ - PyTuple_SET_ITEM(tuple, idx, value.Release()); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, const ST::string& value) -{ - PyTuple_SET_ITEM(tuple, idx, PyUnicode_FromSTString(value)); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, uint8_t value) -{ - PyTuple_SET_ITEM(tuple, idx, PyLong_FromSize_t(value)); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, uint16_t value) -{ - PyTuple_SET_ITEM(tuple, idx, PyLong_FromSize_t(value)); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, uint32_t value) -{ - PyTuple_SET_ITEM(tuple, idx, PyLong_FromSize_t(value)); -} - -static inline void IBuildTupleArg(PyObject* tuple, size_t idx, wchar_t value) -{ - PyTuple_SET_ITEM(tuple, idx, PyUnicode_FromFormat("%c", (int)value)); -} - -template -static inline void IBuildTupleArgs(PyObject* tuple, Arg&& arg) -{ - IBuildTupleArg(tuple, (Size - 1), std::forward(arg)); -} - -template -static inline void IBuildTupleArgs(PyObject* tuple, Arg0&& arg0, Args&&... args) -{ - IBuildTupleArg(tuple, (Size - (sizeof...(args) + 1)), std::forward(arg0)); - IBuildTupleArgs(tuple, std::forward(args)...); -} + template<> + inline void IBuildTupleArg(PyObject* tuple, size_t idx, ControlEventCode value) + { + PyTuple_SET_ITEM(tuple, idx, PyLong_FromLong((long)value)); + } +}; template void plPythonFileMod::ICallScriptMethod(func_num methodId, Args&&... args) @@ -450,7 +367,7 @@ void plPythonFileMod::ICallScriptMethod(func_num methodId, Args&&... args) return; pyObjectRef tuple = PyTuple_New(sizeof...(args)); - IBuildTupleArgs(tuple.Get(), std::forward(args)...); + plPythonCallable::BuildTupleArgs(tuple.Get(), std::forward(args)...); plProfile_BeginTiming(PythonUpdate); pyObjectRef retVal = PyObject_CallObject(callable, tuple.Get()); diff --git a/Sources/Plasma/FeatureLib/pfPython/pyGlueHelpers.h b/Sources/Plasma/FeatureLib/pfPython/pyGlueHelpers.h index 631c956955..eacdd11a07 100644 --- a/Sources/Plasma/FeatureLib/pfPython/pyGlueHelpers.h +++ b/Sources/Plasma/FeatureLib/pfPython/pyGlueHelpers.h @@ -614,7 +614,7 @@ static PyObject *methodName(PyObject *self) /* and now for the actual function * #define PYTHON_ENUM_START(enumName) std::vector> enumName##_enumValues{ // for each element of the enum -#define PYTHON_ENUM_ELEMENT(enumName, elementName, elementValue) std::make_tuple(ST_LITERAL(#elementName), elementValue), +#define PYTHON_ENUM_ELEMENT(enumName, elementName, elementValue) std::make_tuple(ST_LITERAL(#elementName), (Py_ssize_t)elementValue), // to finish off and define the enum #define PYTHON_ENUM_END(m, enumName) }; pyEnum::MakeEnum(m, #enumName, enumName##_enumValues); diff --git a/Sources/Plasma/FeatureLib/pfPython/pyObjectRef.h b/Sources/Plasma/FeatureLib/pfPython/pyObjectRef.h index 9761640988..461b1a3475 100644 --- a/Sources/Plasma/FeatureLib/pfPython/pyObjectRef.h +++ b/Sources/Plasma/FeatureLib/pfPython/pyObjectRef.h @@ -46,6 +46,9 @@ You can contact Cyan Worlds, Inc. by email legal@cyan.com #include #include "HeadSpin.h" +struct pyObjectNewRef_Type{}; +constexpr pyObjectNewRef_Type pyObjectNewRef; + /** RAII reference count helper for Python objects. */ class pyObjectRef { @@ -57,6 +60,15 @@ class pyObjectRef /** Steals ownership of this object reference. */ pyObjectRef(PyObject* object) : fPyObject(object) { } + /** Increments the reference count of this object. */ + pyObjectRef(PyObject* object, pyObjectNewRef_Type) + : fPyObject(object) + { + Py_INCREF(object); + } + + pyObjectRef(std::nullptr_t, pyObjectNewRef_Type) = delete; + pyObjectRef(const pyObjectRef& copy) : fPyObject(copy.fPyObject) { diff --git a/Sources/Plasma/NucleusLib/inc/plCreatableIndex.h b/Sources/Plasma/NucleusLib/inc/plCreatableIndex.h index b76d62b5bd..5fbd58d6fe 100644 --- a/Sources/Plasma/NucleusLib/inc/plCreatableIndex.h +++ b/Sources/Plasma/NucleusLib/inc/plCreatableIndex.h @@ -368,6 +368,7 @@ CLASS_INDEX_LIST_START CLASS_INDEX(plRidingAnimatedPhysicalDetector), CLASS_INDEX(plVolumeSensorConditionalObjectNoArbitration), CLASS_INDEX(plPXSubWorld), + CLASS_INDEX(pfConfirmationMgr), //--------------------------------------------------------- // Keyed objects above this line, unkeyed (such as messages) below.. //--------------------------------------------------------- @@ -956,6 +957,8 @@ CLASS_INDEX_LIST_START CLASS_INDEX(pl3DPipeline), CLASS_INDEX(plGLPipeline), CLASS_INDEX(plSDLModifierStateMsg), + CLASS_INDEX(plConfirmationMsg), + CLASS_INDEX(plLocalizedConfirmationMsg), CLASS_INDEX_LIST_END #endif // plCreatableIndex_inc diff --git a/Sources/Plasma/NucleusLib/pnKeyedObject/plFixedKey.cpp b/Sources/Plasma/NucleusLib/pnKeyedObject/plFixedKey.cpp index 4a625195f3..4ca9f8f5e0 100644 --- a/Sources/Plasma/NucleusLib/pnKeyedObject/plFixedKey.cpp +++ b/Sources/Plasma/NucleusLib/pnKeyedObject/plFixedKey.cpp @@ -113,6 +113,7 @@ static constexpr plKeySeed SeedList[] = { { kJournalBookMgr_KEY, CLASS_INDEX_SCOPED( pfJournalBook ), "kJournalBookMgr_KEY", }, { kAgeLoader_KEY, CLASS_INDEX_SCOPED( plAgeLoader), "kAgeLoader_KEY", }, { kBuiltIn3rdPersonCamera_KEY, CLASS_INDEX_SCOPED( plCameraModifier1 ), "kBuiltIn3rdPersonCamera_KEY", }, + { kConfirmationMgr_KEY, CLASS_INDEX_SCOPED( pfConfirmationMgr ), "kConfirmationMgr_KEY", }, { kLast_Fixed_KEY, CLASS_INDEX_SCOPED( plSceneObject ), "kLast_Fixed_KEY", } }; diff --git a/Sources/Plasma/NucleusLib/pnKeyedObject/plFixedKey.h b/Sources/Plasma/NucleusLib/pnKeyedObject/plFixedKey.h index 0e2cbc2ba4..803d9b2948 100644 --- a/Sources/Plasma/NucleusLib/pnKeyedObject/plFixedKey.h +++ b/Sources/Plasma/NucleusLib/pnKeyedObject/plFixedKey.h @@ -85,6 +85,7 @@ enum plFixedKeyId kJournalBookMgr_KEY, kAgeLoader_KEY, kBuiltIn3rdPersonCamera_KEY, + kConfirmationMgr_KEY, kLast_Fixed_KEY }; diff --git a/Sources/Plasma/PubUtilLib/plMessage/CMakeLists.txt b/Sources/Plasma/PubUtilLib/plMessage/CMakeLists.txt index b616a484b4..ed59e094b9 100644 --- a/Sources/Plasma/PubUtilLib/plMessage/CMakeLists.txt +++ b/Sources/Plasma/PubUtilLib/plMessage/CMakeLists.txt @@ -67,6 +67,7 @@ set(plMessage_HEADERS plClimbMsg.h plCollideMsg.h plCondRefMsg.h + plConfirmationMsg.h plConnectedToVaultMsg.h plConsoleMsg.h plDeviceRecreateMsg.h diff --git a/Sources/Plasma/PubUtilLib/plMessage/plConfirmationMsg.h b/Sources/Plasma/PubUtilLib/plMessage/plConfirmationMsg.h new file mode 100644 index 0000000000..adb1a7fb4d --- /dev/null +++ b/Sources/Plasma/PubUtilLib/plMessage/plConfirmationMsg.h @@ -0,0 +1,149 @@ +/*==LICENSE==* + +CyanWorlds.com Engine - MMOG client, server and tools +Copyright (C) 2011 Cyan Worlds, Inc. + +This program 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. + +This program 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 this program. If not, see . + +Additional permissions under GNU GPL version 3 section 7 + +If you modify this Program, or any covered work, by linking or +combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK, +NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent +JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK +(or a modified version of those libraries), +containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA, +PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG +JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the +licensors of this Program grant you additional +permission to convey the resulting work. Corresponding Source for a +non-source form of such a combination shall include the source code for +the parts of OpenSSL and IJG JPEG Library used as well as that of the covered +work. + +You can contact Cyan Worlds, Inc. by email legal@cyan.com + or by snail mail at: + Cyan Worlds, Inc. + 14617 N Newport Hwy + Mead, WA 99021 + +*==LICENSE==*/ + +#ifndef plConfirmationMsg_inc +#define plConfirmationMsg_inc + +// I hope you like big STL headers... +#include +#include +#include + +#include + +#include "pnMessage/plMessage.h" + +/** Show an in-game confirmation GUI dialog. */ +class plConfirmationMsg : public plMessage +{ +public: + enum class Result : int32_t + { + OK = 1, + Cancel = 0, + Yes = 1, + No = 0, + Quit = 1, + Logout = 62, + }; + + using Callback = std::variant, plKey>; + + enum class Type : uint32_t + { + /** Informational item for the user with only the possibility to OK it. */ + OK, + + /** + * The quit dialog. Do not use for an error. + * If the user requests a quit or logout, then any optional callback will be + * dispatched, then the client will quit or logout on the next main thread + * evaluation. + */ + ConfirmQuit, + + /** Notify user about an exception case, then force quit the client. */ + ForceQuit, + + /** Requests the user to answer Yes or No to a question. */ + YesNo, + }; + +protected: + ST::string fMessage; + Type fDialogType; + Callback fCallback; + +public: + plConfirmationMsg() + { + SetBCastFlag(plMessage::kBCastByType); + } + + plConfirmationMsg(ST::string msg, Type type = Type::OK, Callback cb = {}) + : fMessage(std::move(msg)), + fDialogType(type), + fCallback(std::move(cb)) + { + SetBCastFlag(plMessage::kBCastByType); + } + + CLASSNAME_REGISTER(plConfirmationMsg); + GETINTERFACE_ANY(plConfirmationMsg, plMessage); + + void Read(hsStream*, hsResMgr*) override { FATAL("no"); } + void Write(hsStream*, hsResMgr*) override { FATAL("no"); } + + ST::string GetText() const { return fMessage; } + Type GetType() const { return fDialogType; } + Callback GetCallback() const { return fCallback; } + + void SetText(ST::string msg) { fMessage = std::move(msg); } + void SetType(Type type) { fDialogType = type; } + void SetCallback(Callback cb) { fCallback = std::move(cb); } +}; + + +/** Show a localized in-game confirmation GUI dialog. */ +class plLocalizedConfirmationMsg : public plConfirmationMsg +{ +protected: + std::vector fArgs; + +public: + plLocalizedConfirmationMsg() = default; + plLocalizedConfirmationMsg(ST::string path, std::vector args = {}, + Type type = Type::OK, Callback cb = {}) + : plConfirmationMsg(std::move(path), type, std::move(cb)), + fArgs(std::move(args)) + { } + + CLASSNAME_REGISTER(plLocalizedConfirmationMsg); + GETINTERFACE_ANY(plLocalizedConfirmationMsg, plConfirmationMsg); + + const std::vector& GetArgs() const { return fArgs; } + std::vector& GetArgs() { return fArgs; } + + void SetArgs(std::vector args) { fArgs = std::move(args); } +}; + +#endif diff --git a/Sources/Plasma/PubUtilLib/plMessage/plMessageCreatable.h b/Sources/Plasma/PubUtilLib/plMessage/plMessageCreatable.h index 0e2fc3e134..222b4a2527 100644 --- a/Sources/Plasma/PubUtilLib/plMessage/plMessageCreatable.h +++ b/Sources/Plasma/PubUtilLib/plMessage/plMessageCreatable.h @@ -107,6 +107,10 @@ REGISTER_CREATABLE(plCollideMsg); #include "plCondRefMsg.h" REGISTER_CREATABLE(plCondRefMsg); +#include "plConfirmationMsg.h" +REGISTER_NONCREATABLE(plConfirmationMsg); +REGISTER_NONCREATABLE(plLocalizedConfirmationMsg); + #include "plConnectedToVaultMsg.h" REGISTER_CREATABLE(plConnectedToVaultMsg);