Jump to content

Multi Level MessageBox Minor Issue


DKong27

Recommended Posts

I'm working on a mod with some MessageBoxes working as a configuration menu. I've had some issues along the way but it seems to be working fine now except for a minor issue.

 

Each level of the menu has a Return option which ideally would return the menu to the previous stage, but instead it closes the menu entirely. Not really sure why it would act this way.

scn DKTrainingGuideConfigScript

short button
short menulevel

begin OnEquip

	MessageBox "Trainer Seeking configuration. What would you like to do?", "Return", "Track Trainers", "Stop Tracking Quest"
	SetStage DKTrainingGuideQuest 11
	set menulevel to 1
	
end

begin MenuMode

	if menulevel == 0
		
		return
		
	else
	
		set button to GetButtonPressed
		
	endif
	
	
	
	if menulevel == 1

		if button == 0
		
			set menulevel to 0
			return
		
		elseif button == 1

			MessageBox "What specialization is your desired skill under?", "Return"  "Combat", "Magic", "Stealth"
			set menulevel to 2
			
		elseif button == 2

			SetStage DKTrainingGuideQuest 200
			set menulevel to 0
		
		endif
				
	elseif menulevel == 2

		if button == 0
		
			set menulevel to 1
			;return
		
		elseif button == 1

			MessageBox "Which combat skill do you want to find trainers for?", "Return", "Armorer", "Athletics", "Block", "Blunt", "Blade", "Hand to Hand", "Heavy Armor"
			set menulevel to 3
			
		elseif button == 2

			MessageBox "Which magic skill do you want to find trainers for?", "Return", "Alchemy", "Alteration", "Conjuration", "Destruction", "Illusion", "Mysticism", "Restoration"
			set menulevel to 4
			
		elseif button == 3

			MessageBox "Which stealth skill do you want to find trainers for?", "Return", "Acrobatics", "Light Armor", "Marksman", "Mercantile", "Security", "Sneak", "Speechcraft"
			set menulevel to 5
		
		endif

	elseif menulevel == 3

		if button == 0

			set menulevel to 2
			;return

		elseif button == 1

			SetStage DKTrainingGuideQuest 104
			set menulevel to 0

		elseif button == 2

			SetStage DKTrainingGuideQuest 105
			set menulevel to 0

		elseif button == 3

			SetStage DKTrainingGuideQuest 106
			set menulevel to 0

		elseif button == 4

			SetStage DKTrainingGuideQuest 107
			set menulevel to 0

		elseif button == 5

			SetStage DKTrainingGuideQuest 108
			set menulevel to 0

		elseif button == 6

			SetStage DKTrainingGuideQuest 111
			set menulevel to 0

		elseif button == 7

			SetStage DKTrainingGuideQuest 112
			set menulevel to 0

		endif

	elseif menulevel == 4

		if button == 0

			set menulevel to 2
			;return

		elseif button == 1

			SetStage DKTrainingGuideQuest 102
			set menulevel to 0

		elseif button == 2

			SetStage DKTrainingGuideQuest 103
			set menulevel to 0

		elseif button == 3

			SetStage DKTrainingGuideQuest 109
			set menulevel to 0

		elseif button == 4

			SetStage DKTrainingGuideQuest 110
			set menulevel to 0

		elseif button == 5

			SetStage DKTrainingGuideQuest 113
			set menulevel to 0

		elseif button == 6

			SetStage DKTrainingGuideQuest 117
			set menulevel to 0

		elseif button == 7

			SetStage DKTrainingGuideQuest 118
			set menulevel to 0

		endif

	elseif menulevel == 5

		if button == 0

			set menulevel to 2
			;return

		elseif button == 1

			SetStage DKTrainingGuideQuest 101
			set menulevel to 0

		elseif button == 2

			SetStage DKTrainingGuideQuest 114
			set menulevel to 0

		elseif button == 3

			SetStage DKTrainingGuideQuest 115
			set menulevel to 0

		elseif button == 4

			SetStage DKTrainingGuideQuest 116
			set menulevel to 0

		elseif button == 5

			SetStage DKTrainingGuideQuest 119
			set menulevel to 0

		elseif button == 6

			SetStage DKTrainingGuideQuest 120
			set menulevel to 0

		elseif button == 7

			SetStage DKTrainingGuideQuest 121
			set menulevel to 0

		endif

	endif


end

Link to comment
Share on other sites

If that is attached to an item that is equipped, and the 'level 0' menu is shown upon equipping the item (OnEquip block), you could, in addition to setting menu level to 0, equip that item on player, so that OnEquip block is run and the first menu is displayed. Like

set menulevel to 0
EquipMe
Return
Also, you set menulevel to, for example, 2, but you do not display a menu. After the

set menulevel to 2
Return
the script will next go to where you have

If MenuLevel == 2
and there is nothing to display a menu in there, also nothing to register a button press. Maybe something like that could work for displaying the different menus:

ScriptName AMenuExampleScript

short Button
short MenuLevel

Begin OnEquip Player

    If ( MenuLevel == 0 )
        set MenuLevel to 1
        MessageBox "Hello! Pick a menu, please.", "Exit menus" "Show menu 1" "Show menu 2"
    ElseIf ( MenuLevel == 2 )
        MessageBox "This is menu 1...", "Back"
    ElseIf ( MenuLevel == 3 )
        MessageBox "This is menu 2...", "Back"
    EndIf

End

Begin GameMode

    If ( MenuLevel <= 0 )
        Return
    EndIf

    set Button to GetButtonPressed

    If ( Button < 0 )
       Return
    EndIf

    If ( MenuLevel == 1 )
        If ( Button == 0 )
            set MenuLevel to 0
        ElseIf ( Button == 1 )
            set MenuLevel to 2
            EquipMe
        ElseIf ( Button == 2 )
            set MenuLevel to 3
            EquipMe
        EndIf
        Return
    EndIf

    If ( MenuLevel == 2 )
        If ( Button == 0 )
            set MenuLevel to 0
            EquipMe
        EndIf
        Return
    EndIf

    If ( MenuLevel == 3 )
        If ( Button == 0 )
            set MenuLevel to 0
            EquipMe
        EndIf
        Return
    EndIf

End
I have not tested that, though. But the idea should work. MessageBox is MenuMode, and the pressed button needs to be checked, if I recall correctly, in the next frame after pressing it. Or after the menu has closed. But it needs to be checked by GetButtonPressed immediately. This is why I put it at the top of GameMode block. That way, when the button is known, it is possible to use it for the rest of the GameMode block. I also put the menu displays in OnEquip, as it makes it easier to display them. Just set the MenuLevel to what you want and then equip the item. If MenuLevel is 0, the 'main menu' is shown. Hopefully that helps. Just remember to display a menu using MessageBox, or there will not be one. And that GetButtonPressed needs to be checked pretty fast.

 

Edit: Oops. Just ignore the basics ranting. :P Seems you know what you are doing. The main problem with your script was not using MessageBox to display a menu after changing the MenuLevel variable.

Edited by PhilippePetain
Link to comment
Share on other sites

PhilipperPetain is right. The problem is the MenuMode block.

The OnEquip block open the first message and enter in MenuMode. Then you click on an option, which close the message and exit MenuMode.

Now, you expect to check the Button pressed to open another menu, but you're not in MenuMode, so the script won't run.

 

GetButtonPressed (usually) doesn't immediately return the button pressed, so you may need to check for 2-3 frames before it return anything. Also, if you call GetButtonPressed a second time it won't return the button again (it return once, then the value is threw away), so it's fine the way you do: store the result in the variable, so you can check it as many times you want.

 

 

Finally, there are 2 problems in the PhilippePetain's solition:

 

1) Better keep the messages in the GameMode block and make the OnEquip initialize the first one. Sometimes the menu get broken (on its own, for no reason), so having a way to re-initialize the menu is vital, while putting all MessageBox in the OnEquip block has no advantage over GameMode.

 

2) With the "If (button < 0)" condition, you'll break the menu if there's a stage where you don't show a menu, because it still expect expect a button pressed, but without message there's none, and it will end in an infinite loop. It would need to be initialized again ==> (1). You need to create a "wait for button" mode, which you enable only when showing a message.

Begin OnEquip
  MessageBox "Trainer Seeking configuration. What would you like to do?", "Return", "Track Trainers", "Stop Tracking Quest"
  SetStage DKTrainingGuideQuest 11
  Set menulevel to 1
  Set waitForButton to 1
End

Begin GameMode
  If ( MenuLevel <= 0 )
    Return
  EndIf

  If (waitForButton)
    Set button To GetButtonPressed
    If (button < 0)
      Return
    EndIf
    Set waitForButton To 0
  EndIf

  If (menulevel == 1)
    If (button == 0)
      Set menulevel to 0
    ElseIf (button == 1)
      Set menulevel to 2
      Set waitForButton To 1     ;must show a message, so set this to 1
      MessageBox "..."
    ElseIf (button == 2)
      SetStage someQuest 10   ;no message, no waitForButton
      Set menulevel to 0
  ElseIf (menulevel == 2)
    ...
Edited by forli
Link to comment
Share on other sites

Thank you, forli. I actually did not notice DKong27 had put it all in MenuMode. :P I suppose I just assumed it was GameMode he had all the things in. Good to know about the possible breaking issue, though. I like to have all things organised, so that is why I stuffed all the MessageBoxes in OnEquip, as having them scattered around GameMode block is confusing for me.

 

The one I thought about should not break because of ( Button < 0 ), though, as the stuff after it is only relevant when a button has been pressed - thanks to putting all MessageBoxes in OnEquip. So that GameMode is, in that odd example of mine, only used for checking GetButtonPressed and then carrying out different tasks. So it should not break because of that condition. Or otherwise, as long as there is a MessageBox that 'matches' the current MenuLevel variable. I think?

Link to comment
Share on other sites

I'll do an example: Look at your code and think about this menu:

FRAME A, GameMode/OnEquip block: it show a MessageBox: "Do you want to restore your health?", "Yes", "No".

FRAME B: (some frames after Frame A) You click on "Yes" (button 0).

FRAME C, GameMode block: (some frames after FRAME B) GetButtonPressed catch the "yes" button and return 0. It makes all checks and it reach the condition "if (button == 0)" and execute the content: restore your health and set menulevel to 1 to return to the main menu (no MessageBox).

FRAME D, GameMode block: GetButtonPressed return -1 (now and forever), because no MessageBox has been displayed at FRAME C, so it will loop there forever (or until you manually show another MessageBox or put a skip variable like waitForButton). The menu is broken.

 

With "waitForButton", you can say at RUN 2) "No, now there's no messagebox, skip the button check", and the main menu will open as expected.

 

Anyway, I would not use an OnEquip block for organize the messages, because I would not use a token at all. I would use a quest instead.

The token's GameMode block keep running every frame, even when the menu is closed, with extra/useless work for the CPU (and the CPU is pure gold in Oblivion). Having a quest for this is way better, because you can totally stop the quest when you want to close the menu.

The token retain the OnEquip block, which only start the quest and initialize/reset the menu, then the GameMode block and the messages are both managed by the GameMode block in the quest script.

Link to comment
Share on other sites

I would use a quest too. Avoiding GameMode block on objects is usually the first priority on my list, making all scripts that are run regularly as tiny as possible the second. Not to forget general optimisations by Return and such. :)

 

And to continue the defence of my idea, one that I will probably never use, but one that I think should work...

 

The example actually should display a menu at "frame C", as it should, from "frame C" onward:

Frame C -> Do the things associated with the button, set MenuLevel to 0 so that 'main menu' is displayed AND call EquipMe, so that the item is equipped

Frame D -> OnEquip block is run, as the item is equipped, and a menu is displayed

 

So the EquipMe should, after setting the MenuLevel variable, equip the item, which should trigger the OnEquip block and therefore display a menu. That is then MenuMode, and after a button has been pressed (GameMode entered) the mod will check the button and then go through all the stuff again (find correct piece of code to execute, then execute it and possibly start the menu display again by setting MenuLevel and equipping the item). When menus are all exited ('main menu' is closed), MenuLevel is set to 0 so that, upon equipping the item, 'main menu' is displayed - and when MenuLevel is 0, GameMode processing is cut right at the beginning (with Return command), reducing all GameMode CPU usage to extreme minimum.

 

Does that make sense? Or have I misunderstood something? Because I still think it makes sense. It relies on EquipMe command triggering the OnEquip block, though. It does trigger it, right... ?

Edited by PhilippePetain
Link to comment
Share on other sites

Ok wrong example, because as you said, it works if you call EquipMe in the "no message" frame.

This means you can skip a single frame... but what about 2 or more?

 

There are operations which needs more frames to complete, like (examples):

- moving an unmovable objects like a container (Disable in frame 1, Reset3DState in frame 2, Enable in frame 3)

- deleting a dynamic reference (Disable in frame 1, DeleteReference in frame 2).

- moving the player itself and doing something on destination (MoveTo in frame 1, do something in frame 2. You can do both them in the same frame because the script engine stops for the rest of the frame after you call MoveTo on the player).

- Adding/removing an item from an actor and killing him or viceversa (If you do both operations in the same frame, you risk a CTD).

 

In all these cases (and there are more), you don't call EquipMe for 2 or more consecutive frames, and it will break at frame 2.

Link to comment
Share on other sites

Oh. Thank you. I did not know there were such things to consider. Now I need to rethink some of my scripts, as I think I might have done some of that somewhere. At least with a current, luckily unfinished project if mine. Thank you for informing me of those. Having to do things in separate frames never crossed my mind and might just be the reason why some things I have tried have failed no matter how I have done it. So thank you.

 

So - if I understood correctly - if it takes a few frames to get from EquipMe (which will display a menu) to GetButtonPressed in GameMode returning the button (at which frame EquipMe is possibly called again), it should not break. But if it happens too fast, it can cause issues with EquipMe.

 

Also, out of curiosity, but related to a project to mine: if I use an OBSE OnDeath event handler to remove items from an NPC to a container (handler calls a custom function to move items), the event handler is not called in the frame the NPC dies and moving items in the custom function with RemoveMeIR does not cause issues? Can OnDeath event handler happen to be called in the frame an actor dies and therefore cause issues with RemoveMeIR (moving items)? OnDeath seems to fire a bit after the NPC has died, as items are removed a little after death, so using OnDeath with RemoveMeIR should be safe? But, for example, adding RemoveItem or such in OnDeath block on an actor script will risk CTD?

 

And is there a list of things that require more frames somewhere? On the Construction Set wiki, perhaps? I have not yet found one and it would be good to know which things require special attention so that I do not put them in the same frame.

Link to comment
Share on other sites

Yes, you call MessageBox, then few frames later (not immediately) the message show. Every second the messagebox, many frames pass (your FPS each second). When you click the button, the messagebox close and the GetButtonPressed command only detect the button pressed few frames later (it may be 1-5 frames, depending on what's going on in the Oblivion engine).

 

If it happens too fast? I don't know if calling EquipMe quickly can cause any issue (unless the OnEquip block does something dangerous!). When GetButtonPressed return the button, the message is already closed and it's safe to show another one.

 

All event handler are called immediately in the frame the even happens (but not in the middle of a running script!). If you hit an actor with your sword, the OnHit event fire immediately, followed by OnHealthDamage. If the damage is enough to kill him (and you didn't restored it in the OnHealthDamage handler, OR you manually killed it in one of those 2 handlers), the OnDeath event fire immediately after OnHealthDamage.

All them in the same frame you hit the actor. No delay.

Some handlers, like OnActorEquip, are called even before the event (OnActorEquip is called when you select an item to equip it, but just before the item is actually equipped, so even before its OnEquip block).

 

As for removing item from dead enemies, the CTD happens (sometimes) when removing a scripted item (like scripted tokens) in the same frame the actor dies, and under particular conditions which I'm still trying to track down. Also, if a token must remove itself with RemoveMe, do that 5-10 frames after the actor is dead. I had a bad experience with VR when some tokens where firing CTD like a machinegun for this cause.

Also, UVIII has a common crash caused by RemoveAllItems and Kill called on the same actor in the same frame.

Not-scripted items should be safe most of the times (but I can't say exactly when!).

 

A list? Unluckily, no, you'll have to figure out! When you use a set of commands, ensure they can all work in the same frame.

DeleteReference is an example: it require a disabled reference, but sometimes it doesn't work if the reference has been disabled in the same frame , so you need to disable, "return" this frame, then call DeleteReference (read the CS wiki discussions about this command, and about any other command)

Link to comment
Share on other sites

All right. Thank you. The DeleteReference was good to know. Also good to know how the event handlers are called. I think I misread something in your post about EquipMe and got confused. I also forgot there are actually frames shown when a menu is displayed... now how did that happen. My brain is clearly on holiday, too. :)

 

Time to check how removing scripted items works in my project. The function first uses GetItems with ForEach to unequip all items and only then uses ForEach with a temp ref to RemoveMeIR all items (excluding 'token' and unplayable clothing or armour) to a container. RemoveAllItems sounds suspicious, so I use RemoveMeIR - it also makes it possible to exclude certain items, as well as calculate a bounty if, for example, an NPC 'sort of' pickpockets another NPC at player's command. So maybe RemoveMeIR with the "ForEach TempRef <- ContainerRef" could help avoid issues with RemoveAllItems? I have had some crashes with it, though, but as it can be made check each item individually, it should be possible, for example with ConScribe, to log item removal and, if it crashes on RemoveMeIR, check which item caused it from the log. That is not possible with RemoveAllItems. If RemoveMeIR still causes crashes, that is. I have discovered that using the ForEach with temp ref in a custom function is pretty handy and allows for more customisablity than RemoveAllItems. That custom function can then be called instead of RemoveAllItems.


ScriptName MyRemoveAllItemsFunction

ref rTemp
ref rContainer
ref rMe

Begin Function { rContainer }

    let rMe := GetSelf

    PrintD "Test: Remove all items from " + $rMe + "..."

    ForEach rTemp <- rMe
        If ( rContainer )
            DebugPrint "Test: '%n'.RemoveMeIR (formID %i) -> %n (formID %i)", rTemp rTemp rContainer rContainer
            rTemp.RemoveMeIR rContainer
        Else
            DebugPrint "Test: '%n'.RemoveMeIR (formID %i)", rTemp rTemp
            rTemp.RemoveMeIR
        EndIf
    Loop

    PrintD "Test: Removed all items from " + $rMe

End
Something like that? So that the last logged debug line will tell which item was about to be removed when crash happened. I am not sure what formID that will return for the objects, though, but name should be 'available'. Just an idea, though, in case it would happen to be useful in tracking down the source of crashes.

 

Edit: Something like that would always require a reference as an argument, though, which could be made by a ref variable (in a quest or in the calling script maybe) that could either be empty or contain a reference, I think, as long as it is something that can be accepted as a ref argument.

Edited by PhilippePetain
Link to comment
Share on other sites

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...