RunUO Community

This is a sample guest message. Register a free account today to become a member! Once signed in, you'll be able to participate on this site by adding your own topics and posts, as well as connect with other members through your own private inbox!

Consume via Targeting

Murzin

Knight
khaz, using a dictionary is probably far more complex than required.

all i would do, is have the double click reveal them, make sure they are in range, and make sure its a playermobile.

after those checks, have it call a sub method passing the player

inside that method, send the user 2 targets, filling wmap and tmap as targeted, if both are satisfied, delete both maps and spawn the new one.

you dont really need to worry about persistence in saving the state.

but you would want to make that sub method called out of doubleclick private, not public.

private void MapCheck( PlayerMobile from )
{
<code here>
}

by using private, multiple people could use the altar at the same time. as the method is private, a separate instance is created for each iteration.

the only caution you would want is to make sure you empty all variables to ensure the C# garbage collector removes them as quick as possible to prevent memory leaks/bloat.
 

Khaz

Knight
I disagree :)

You can't send a client two targets at once. The second one overrides the first.

I continue to prefer deleting the maps as they're targeted. It is really just a matter of opinion, and in my opinion, it provides a better user experience because the map cannot be moved, used, or otherwise lost if it is deleted as soon as it is used for the altar. Persisting this state is important because a lot could happen between selecting the first and the second map, and (again for user experience) it would be really sad if the altar deleted one map and then forgot that it did so on the next world load.

The CorgulContext example also provides the OP (and any other readers) with more opportunities to make the altar more sophisticated. It could store a timestamp of when the reward was given, with an expiration or second reward delay built in; different levels of rewards could be given based on previous sacrifices; etc. Options... :)

Also, your understanding of the way member accessibility (public, private, protected, etc) works is fuzzy. Marking a method private does not affect threading, class constructing, or instancing of any kind. A private member simply cannot be accessed by an external class; marking a member as private means that it is private to the declaring class: no other class, whatever the relation to the declaring class, can access the member. Again, it's only an accessibility flag, nothing to do with instances.
 

Pure Insanity

Sorceror
Why not just set the item's map to Internal, until you are certain both should be deleted? Then they can't be messed with, altered, ect. That way if the 2nd target is canceled, you could set the map back to what it previously was. Then you'd only be deleting the items when you were absolutely certain you should be deleting them.
 

Khaz

Knight
Why not just set the item's map to Internal, until you are certain both should be deleted? Then they can't be messed with, altered, ect. That way if the 2nd target is canceled, you could set the map back to what it previously was. Then you'd only be deleting the items when you were absolutely certain you should be deleting them.
I'm not a fan of this method because if the server is interrupted by a crash or other blocking event, it won't have the chance to handle target cancel and the map will be stuck in Internal. Then cleanup will need to be done to find and recover it.
I guess in that event world save probably won't happen either so the map wouldnt be saved in Internal, but I dont know what the probability numbers would be for that :p
 

Vorspire

Knight
khaz, using a dictionary is probably far more complex than required.

A dictionary is fine, a static dictionary is better.

all i would do, is have the double click reveal them, make sure they are in range, and make sure its a playermobile.

after those checks, have it call a sub method passing the player

inside that method, send the user 2 targets, filling wmap and tmap as targeted, if both are satisfied, delete both maps and spawn the new one.
The dictionary pattern works fine and would do exactly this but offers more independance.

you dont really need to worry about persistence in saving the state.
This is true, but only if you haven't modified anything during targeting, like deleting one of the maps for example, then persisting the state would be necessary.

but you would want to make that sub method called out of doubleclick private, not public.

private void MapCheck( PlayerMobile from )
{
<code here>
}

by using private, multiple people could use the altar at the same time. as the method is private, a separate instance is created for each iteration.

This is completely wrong, a private method does not behave like that at all.
The private modifier means that the method cannot be called from any class other than the containing class.
What you're describing is a static context and even then, there are never two instances of any given method.
The difference between non-static methods and static-methods is that non-static methods are implemented on each instance of the object whereas static methods are invoked by using the containing Type name as an identifier to access them.

the only caution you would want is to make sure you empty all variables to ensure the C# garbage collector removes them as quick as possible to prevent memory leaks/bloat.
The C# GC will only collect objects that have gone out of scope and have no intrinsic references to them left in the managed memory.
The GC doesn't collect these unused objects instantly, it's indeterminate. The object finalizer can be called anytime between losing scope and the program exiting.
Any object that goes out of scope and loses reference still might not have it's finalizer called for many GC cycles, because the GC queues these objects into categories called generations.


I *strongly* suggest you read a book or two about C# before spreading misinformation with such confidence.
 

Erevan

Sorceror
Completely forgot about this thread over the last week. Just as an update, Khaz's rewrite (post #33) works perfectly. I added Vorspire's suggested change for CorgulContext in post #36, and it compiled just fine. Thanks for all of the help everyone, and the ton of information provided.
 

Erevan

Sorceror
Forgive me if this seems like a silly question, but what exactly am I missing that is causing users to be unable to re-use the altar more than once? Preferably, they should be able to use it once every few hours, but I'd settle for just clearing whatever restriction is being added. I'm assuming it has to do with tableCount, but I'm not 100% certain.
 

Erevan

Sorceror
Absolutely. It hasn't been altered much from the fixes applied above.

Code:
using System;
using Server;
using Server.Items;
using System.Collections;
using System.Collections.Generic;
using Server.Gumps;
using Server.Mobiles;
using Server.ContextMenus;
using Server.Prompts;
using Server.Targeting;
using Server.Network;
 
public class CorgulAltar : Item
{
    private Dictionary<Mobile, CorgulContext> m_MapTable;
 
    public Dictionary<Mobile, CorgulContext> MapTable
    {
        get{ return m_MapTable; }
        private set
        {
            m_MapTable = value;
        }
    }
 
    [Constructable]
    public CorgulAltar() : base(0x35EF)
    {
        Name = "Sacrificial Altar";
        Hue = 2075;
        Movable = false;
 
        MapTable = new Dictionary<Mobile, CorgulContext>();
    }
 
    public CorgulAltar(Serial s) : base(s) {}
 
    public override void OnDoubleClick(Mobile from)
    {
        if(!MapTable.ContainsKey(from) || !MapTable[from].SacrificedTMap)
        {
            MapTable[from] = new CorgulContext(from);
         
            from.SendMessage("Select a treasure map..");
            from.BeginTarget(2, false, TargetFlags.None, new TargetCallback( OnTargetTMap ));
        }
        else if(!MapTable[from].RewardReceived())
        {
            from.SendMessage("You've already sacrificed a treasure map. You must now sacrifice a world map..");
            from.BeginTarget(2, false, TargetFlags.None, new TargetCallback( OnTargetWMap ));
        }
        else
        {
            from.SendMessage("You have already received your reward from this altar.");
        }
    }
 
    private void OnTargetTMap(Mobile from, object targeted)
    {
        if(targeted is TreasureMap)
        {
            MapTable[from].SacrificedTMap = true;
         
            ((TreasureMap)targeted).Delete();
         
            from.SendMessage("Your offering has been accepted. The price of blood will be taken when you sacrifice your world map.");
            from.BeginTarget(2, false, TargetFlags.None, new TargetCallback( OnTargetWMap ));
        }
        else
        {
            from.SendMessage("That is not a treasure map!");
            from.BeginTarget(2, false, TargetFlags.None, new TargetCallback( OnTargetTMap ));
        }
    }
 
    private void OnTargetWMap(Mobile from, object targeted)
    {
        if(targeted is WorldMap)
        {
            MapTable[from].SacrificedWMap = true;
         
            ((WorldMap)targeted).Delete();
         
            from.SendMessage("Your final offering has been accepted and the blood cost has been collected.");
            from.Hits = 1;
         
            from.AddToBackpack(new CorgulMap());
        }
        else
        {
            from.SendMessage("That is not a world map!");
            from.BeginTarget(2, false, TargetFlags.None, new TargetCallback( OnTargetWMap ));
        }
    }
 
    public override void Serialize(GenericWriter writer)
    {
        base.Serialize(writer);
     
        writer.Write((int)0);
     
        writer.Write((int)MapTable.Count);
     
        foreach(KeyValuePair<Mobile, CorgulContext> kvp in MapTable)
        {
            writer.WriteMobile<Mobile>(kvp.Key);
            kvp.Value.Serialize(writer);
        }
    }
 
    public override void Deserialize(GenericReader reader)
    {
        base.Deserialize(reader);
     
        int version = reader.ReadInt();
     
        switch(version)
        {
            case 0:
                int tableCount = reader.ReadInt();
             
                MapTable = new Dictionary<Mobile, CorgulContext>(tableCount);
             
                for(int i = 0; i < tableCount; i++)
                {
                    MapTable[reader.ReadMobile<Mobile>()] = new CorgulContext(reader);
                }
             
                break;
        }
    }
 
    //private class CorgulContext
    public sealed class CorgulContext
    {
        private Mobile m_Mobile;
        private bool m_TMap;
        private bool m_WMap;
     
        public Mobile Mobile
        {
            get{ return m_Mobile; }
            set{ m_Mobile = value; }
        }
     
        public bool SacrificedTMap
        {
            get{ return m_TMap; }
            set{ m_TMap = value; }
        }
     
        public bool SacrificedWMap
        {
            get{ return m_WMap; }
            set{ m_WMap = value; }
        }
     
        public CorgulContext(Mobile from)
        {
            m_Mobile = from;
        }
     
        public bool RewardReceived()
        {
            return (SacrificedTMap && SacrificedWMap);
        }
     
        public void Serialize(GenericWriter writer)
        {
            writer.Write((int)0);
         
            writer.WriteMobile<Mobile>(Mobile);
            writer.Write(SacrificedTMap);
            writer.Write(SacrificedWMap);
        }
     
        public CorgulContext(GenericReader reader)
        {
            int version = reader.ReadInt();
         
            switch(version)
            {
                case 0:
                    Mobile = reader.ReadMobile<Mobile>();
                    SacrificedTMap = reader.ReadBool();
                    SacrificedWMap = reader.ReadBool();
                 
                    break;
            }
        }
    }
}
 

daat99

Moderator
Staff member
You can do what you want in 2 steps:
1. You need to add a new "DateTime" property which stores the time of the last sacrifice (and either serialize it or reset it on server restart).
2. Change the "SacrificedTMap" property getter to return true or false based on how long it was since the last sacrifice.
 

Erevan

Sorceror
Wasn't sure that'd work.. thanks Daat. I'll give it a shot and post back if I run into any troubles. :)
 

Erevan

Sorceror
Just as an update (wanted to show my work).. I believe I've found a working solution. Likely not the most sophisticated way of doing things, but it works.

Added a LastUse property (and UseDelay) to reference later...

Code:
private static readonly TimeSpan UseDelay = TimeSpan.FromSeconds(10.0);
    private DateTime m_LastUse;
    
    [CommandProperty( AccessLevel.GameMaster )]
    public DateTime LastUse
    {
        get
        {
            return this.m_LastUse;
        }
        set
        {
            this.m_LastUse = value;
        }
    }

In the OnDoubleClick method I added a simple check..
Code:
if(DateTime.Now > (this.m_LastUse + UseDelay))
        {
            MapTable[from].SacrificedWMap = false;
            MapTable[from].SacrificedTMap = false;
        }

Then in the OnTargetWMap method, I added an update to the DateTime value (this is the final "step" in the sacrifice process, so this was the best place to drop this line):
Code:
this.m_LastUse = DateTime.Now;

Now, the UseDelay will actually be quite a bit higher. I set it to 10 seconds for testing purposes. I'm not going to bother serializing because the UseDelay won't be days, it'll only be a few hours. I figure if I do a reboot, it isn't a big deal if they're able to use it immediately after coming back online.
 

daat99

Moderator
Staff member
You need to serialize/deserialize the "LastUsed" value if you don't want it to reset every server restart.
 

Erevan

Sorceror
Evidently my non-sophisticated edits are problematic..

Code:
System.Collections.Generic.KeyNotFoundException: The given key was not present in the dictionary.
   at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
   at CorgulAltar.OnDoubleClick(Mobile from) in c:\Users\Administrator\Desktop\MyServer\Scripts\Custom Systems\- Publish 16 -\CorgulAltar.cs:line 58
   at Server.Mobile.Use(Item item) in c:\Users\Administrator\Desktop\MyServer\Server\Mobile.cs:line 4882
   at Server.Engines.XmlSpawner2.XmlAttach.UseReq(NetState state, PacketReader pvSrc) in c:\Users\Administrator\Desktop\MyServer\Scripts\Custom Systems\XmlSpawner 3.26c\XmlEngines\XmlAttach\XmlAttach.cs:line 1773
   at Server.Network.MessagePump.HandleReceive(NetState ns) in c:\Users\Administrator\Desktop\MyServer\Server\Network\MessagePump.cs:line 272
   at Server.Network.MessagePump.Slice() in c:\Users\Administrator\Desktop\MyServer\Server\Network\MessagePump.cs:line 126
   at Server.Core.Main(String[] args) in c:\Users\Administrator\Desktop\MyServer\Server\Main.cs:line 683

Any ideas? Not experienced with Dictionary keys at all (even though it seems self-explanatory).
 

daat99

Moderator
Staff member
I can't help much without seeing the code but the problem is self-explanatory indeed:
"The given key was not present in the dictionary"

You're trying to access the dictionary with a key that doesn't exist.
 

Erevan

Sorceror
I've posted it a couple times already, didn't want to get slapped for cluttering the thread with code every time I post. :p In any case, here it is again:

Code:
using System;
using Server;
using Server.Items;
using System.Collections;
using System.Collections.Generic;
using Server.Gumps;
using Server.Mobiles;
using Server.ContextMenus;
using Server.Prompts;
using Server.Targeting;
using Server.Network;
 
public class CorgulAltar : Item
{
    private static readonly TimeSpan UseDelay = TimeSpan.FromHours(24.0);
    private DateTime m_LastUse;
   
    [CommandProperty( AccessLevel.GameMaster )]
    public DateTime LastUse
    {
        get
        {
            return this.m_LastUse;
        }
        set
        {
            this.m_LastUse = value;
        }
    }
 
    private Dictionary<Mobile, CorgulContext> m_MapTable;
 
    public Dictionary<Mobile, CorgulContext> MapTable
    {
        get{ return m_MapTable; }
        private set
        {
            m_MapTable = value;
        }
    }
 
    [Constructable]
    public CorgulAltar() : base(0x35EF)
    {
        Name = "Sacrificial Altar";
        Hue = 2075;
        Movable = false;
 
        MapTable = new Dictionary<Mobile, CorgulContext>();
    }
 
    public CorgulAltar(Serial s) : base(s) {}
 
    public override void OnDoubleClick(Mobile from)
    {
        if(DateTime.Now > (this.m_LastUse + UseDelay))
        {
 
            MapTable[from].SacrificedWMap = false;
            MapTable[from].SacrificedTMap = false;
        }
       
        if(!MapTable.ContainsKey(from) || !MapTable[from].SacrificedTMap)
        {
            MapTable[from] = new CorgulContext(from);
       
            from.SendMessage("Select a treasure map..");
            from.BeginTarget(2, false, TargetFlags.None, new TargetCallback( OnTargetTMap ));
        }
        else if(!MapTable[from].RewardReceived())
        {
            from.SendMessage("You've already sacrificed a treasure map. You must now sacrifice a world map..");
            from.BeginTarget(2, false, TargetFlags.None, new TargetCallback( OnTargetWMap ));
        }
        else
        {
            from.SendMessage("You have already received your reward from this altar.");
            from.SendMessage("Please wait at least one hour before attempting another sacrifice.");
        }
    }
 
    private void OnTargetTMap(Mobile from, object targeted)
    {
        if(targeted is TreasureMap)
        {
            MapTable[from].SacrificedTMap = true;
       
            ((TreasureMap)targeted).Delete();
       
            from.SendMessage("Your offering has been accepted. The price of blood will be taken when you sacrifice your world map.");
            from.BeginTarget(2, false, TargetFlags.None, new TargetCallback( OnTargetWMap ));
        }
        else
        {
            from.SendMessage("That is not a treasure map!");
            from.BeginTarget(2, false, TargetFlags.None, new TargetCallback( OnTargetTMap ));
        }
    }
 
    private void OnTargetWMap(Mobile from, object targeted)
    {
        if(targeted is WorldMap)
        {
            MapTable[from].SacrificedWMap = true;
       
            ((WorldMap)targeted).Delete();
       
            from.SendMessage("Your final offering has been accepted and the blood cost has been collected.");
            from.Hits = 1;
       
            from.AddToBackpack(new CorgulMap());
           
            this.m_LastUse = DateTime.Now;
        }
        else
        {
            from.SendMessage("That is not a world map!");
            from.BeginTarget(2, false, TargetFlags.None, new TargetCallback( OnTargetWMap ));
        }
    }
 
    public override void Serialize(GenericWriter writer)
    {
        base.Serialize(writer);
   
        writer.Write((int)0);
   
        writer.Write((int)MapTable.Count);
   
        foreach(KeyValuePair<Mobile, CorgulContext> kvp in MapTable)
        {
            writer.WriteMobile<Mobile>(kvp.Key);
            kvp.Value.Serialize(writer);
        }
    }
 
    public override void Deserialize(GenericReader reader)
    {
        base.Deserialize(reader);
   
        int version = reader.ReadInt();
   
        switch(version)
        {
            case 0:
                int tableCount = reader.ReadInt();
           
                MapTable = new Dictionary<Mobile, CorgulContext>(tableCount);
           
                for(int i = 0; i < tableCount; i++)
                {
                    MapTable[reader.ReadMobile<Mobile>()] = new CorgulContext(reader);
                }
           
                break;
        }
    }
 
    //private class CorgulContext
    public sealed class CorgulContext
    {
        private Mobile m_Mobile;
        private bool m_TMap;
        private bool m_WMap;
   
        public Mobile Mobile
        {
            get{ return m_Mobile; }
            set{ m_Mobile = value; }
        }
   
        public bool SacrificedTMap
        {
            get{ return m_TMap; }
            set{ m_TMap = value; }
        }
   
        public bool SacrificedWMap
        {
            get{ return m_WMap; }
            set{ m_WMap = value; }
        }
   
        public CorgulContext(Mobile from)
        {
            m_Mobile = from;
        }
   
        public bool RewardReceived()
        {
            return (SacrificedTMap && SacrificedWMap);
        }
   
        public void Serialize(GenericWriter writer)
        {
            writer.Write((int)0);
       
            writer.WriteMobile<Mobile>(Mobile);
            writer.Write(SacrificedTMap);
            writer.Write(SacrificedWMap);
        }
   
        public CorgulContext(GenericReader reader)
        {
            int version = reader.ReadInt();
       
            switch(version)
            {
                case 0:
                    Mobile = reader.ReadMobile<Mobile>();
                    SacrificedTMap = reader.ReadBool();
                    SacrificedWMap = reader.ReadBool();
               
                    break;
            }
        }
    }
}

I'm assuming my check LastUse in OnDoubleClick is what is causing the issue (it's looking for a key that isn't there as you said). Not sure how I should rework that, as that part is what sets the keys back to false so that they can re-use the altar after the UseDelay expires.

I know what the problem is, and what needs to be done, just not how to do it. Kinda equivalent to knowing where you need to go, what you want to do, but not how to get there.
 

daat99

Moderator
Staff member
Code:
        if(!MapTable.ContainsKey(from) || !MapTable[from].SacrificedTMap)
        {
            MapTable[from] = new CorgulContext(from);
       
            from.SendMessage("Select a treasure map..");
            from.BeginTarget(2, false, TargetFlags.None, new TargetCallback( OnTargetTMap ));
        }
In the if statement you check if the map has the key "from" and if it doesn't than you enter the code.

Then in your code you try to access the map value with the key "from" but you already know that it might not be there.

You need to break this if statement into 2 conditions:
1. Check if the map has the key and if not add it.
2. Check if the map[key] value is sacraficed or not and do whatever you want with it.

PS
It's always better to post the full information you have (errors, description, and code) without counting on old posts.
People that try to help you usually doesn't have the time to try and "connect the dots" between all the threads posts.
Another thing is that you might have changed something and forgot about it, posting the updated code avoids such issues too.
 

Erevan

Sorceror
So, if I'm understanding that correctly, simply adding..

Code:
if(!MapTable.ContainsKey(from))
        {
            MapTable[from] = new CorgulContext(from);
        }

.. to my LastUse check (the part that sets the keys to false so that the altar can be re-used), should do the trick, correct? I'd be adding a check if the keys exist at all, if not, add them (just to prevent the crash). My concern is, wouldn't that circumvent the whole purpose of the UseDelay?

Also, on your PS.. all good points. Just didn't want to get warned for spamming or anything. :)
 

daat99

Moderator
Staff member
1. When you do "MapTable[from]" you are telling the program that it should expect a key "from" inside the dictionary "MapTable".
You need to add this key before you tell the program to expect it there.
Example:
Code:
if ( !MapTable.ContainsKey(from) )
    MapTable.Add(from, new CorgulContext(from));
In this example I told the MapTable that I want to add a new "KeyValuePair" where the key is "from" and the value is the "new CorgulContext(from)".

After I added that KeyValuePair I can assume the MapTable has the key "from" and use it.

It's always good to check if it has it before you are using it though.

PS.
Nobody will warn you for spamming if you post real code and problems.
Providing people the information they need in order to help you is a good thing.
 

jayates

Sorceror
I've downloaded RunUO 2.3 and there are very few items from High Seas... such as Corgul and his alter. Does this code work ok and may I use it?
 
Top