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);