Hello all. Way back in the day, I used to be a pretty hardcore RunUO coder (I'm a software engineer by trade), but it's been many years since I've delved into this stuff. ServUO seems to be the most stable thing going and I'm working on several new mods to help non-portaling players not have to use the moongate network--but, of course, I wanna make it as complicated as most free-shard coders seem to. That being said, I'm trying to find the multi-tile mapping for the new galleons, but I'm a bit lost on how these boats are generated. I can see where all the interactable items are added to the world (cannons, addon spots, wheel, tillerman, etc), but I can't seem to figure out how the actual boat item is created. I see the itemId is 64 for the Brittania Ship, but where is the logic that puts together the base item for it? I'm obviously missing something very simple here. I'm trying to be lazyt here and not have to add the pieces item by item and figure out where they fit together.
 
A multi is a single item, the client itself puts all the pieces together when rendering.
 
I was afraid someone was going to say that. Fortunately, I've devised a fairly simple way to create the addon and modify it without restarting the server u00sing a JSON file that I re-read when the addon is initialized. I'm thinking of giving it a timer or some other trigger to update itself if we want to modify addons in bulk (I'm thinking for buildings and the like that we may want for our quests).
Post automatically merged:

If anyone is interested, I accomplished this by the following code:

In the Server Utility.cs file, I added the following class (for reasons I'll go into later on as I contribute more).

Utility.cs:
public static class JsonUtility
{
    public static string Serialize(object obj)
    {
        return JsonConvert.SerializeObject(obj);
    }

    public static object Deserialize<T>(string json)
    {
        return JsonConvert.DeserializeObject<T>(json);
    }
}

You will need to install NewtonsoftJson in your Server nuget manager and include the using directive in the Utility file.

Next, I had to support removing addon components without destroying the parent addon (which is done in the OnAfterDelete override), so--and this might be hacky, I'm not sure--I modified the Item.Delete method to look like this:

Item.cs:
public virtual void Delete()
{
    Delete(false);
}

public void Delete(bool bypassOnAfterDelete)
{
    if (Deleted)
    {
        return;
    }
    else if (!World.OnDelete(this))
    {
        return;
    }

    OnDelete();

    var items = LookupItems();

    if (items != null)
    {
        for (int i = items.Count - 1; i >= 0; --i)
        {
            if (i < items.Count)
            {
                items[i].OnParentDeleted(this);
            }
        }
    }

    SendRemovePacket();

    SetFlag(ImplFlag.Deleted, true);

    if (Parent is Mobile)
    {
        ((Mobile)Parent).RemoveItem(this);
    }
    else if (Parent is Item)
    {
        ((Item)Parent).RemoveItem(this);
    }

    ClearBounce();

    if (m_Map != null)
    {
        if (m_Parent == null)
        {
            m_Map.OnLeave(this);
        }
        m_Map = null;
    }

    World.RemoveItem(this);

    if (!bypassOnAfterDelete)
    {
        OnAfterDelete();
    }

    FreeCache();
}

Then, we create the addon itself:

JsonAddon.cs:
using System.Collections.Generic;
using System.IO;
using Server.Items;

namespace Server.Customs
{
    public class JsonAddon : BaseAddon
    {
        [Constructable]
        public JsonAddon()
        {
            Name = "default";
            LoadFromJson();
        }

        public JsonAddon(Serial serial)
            : base(serial)
        {
        }

        public override bool HandlesOnSpeech => true;

        public override void OnSpeech(SpeechEventArgs e)
        {
            if (e.Handled)
            {
                return;
            }

            Mobile m = e.Mobile;

            if (!m.InRange(GetWorldLocation(), 12))
            {
                return;
            }

            if (e.Speech == "reload")
            {
                Reload();
            }

            if (e.Speech.Contains("setfile"))
            {
                Name = e.Speech.Replace("setfile ", string.Empty);
                Reload();
            }
        }

        private void Reload()
        {
            List<AddonComponent> toRemove = new List<AddonComponent>(Components);
            LoadFromJson();

            foreach (var remove in toRemove)
            {
                Components.Remove(remove);
                remove.Delete(true);
            }
        }

        private void LoadFromJson()
        {
            if (!Directory.Exists("JsonAddons"))
            {
                Directory.CreateDirectory("JsonAddons");
            }

            var file = $"JsonAddons/{Name}.json";
            if (!File.Exists(file))
            {
                var newjson = "[{\"I\":41,\"X\":0,\"Y\":0,\"Z\":0}]";
                File.WriteAllText(file, newjson);
            }

            var json = File.ReadAllText(file);
            var addonComponents = (List<JsonAddonComponent>)JsonUtility.Deserialize<List<JsonAddonComponent>>(json);
            foreach (var a in addonComponents)
            {
                this.AddComponent(new AddonComponent(a.I), a.X, a.Y, a.Z);
            }
        }

        public override void Serialize(GenericWriter writer)
        {
            // probably gonna add some more nifty to this later, like the ability to add NPCs to the addon automatically and the like.
            base.Serialize(writer);

            writer.Write((int)0); // version
        }

        public override void Deserialize(GenericReader reader)
        {
            base.Deserialize(reader);

            int version = reader.ReadInt();
        }
    }
}

Now, in-game, when you add the addon, it will default to "%ServUORoot%\JsonAddons\default.json", which it will create both directory and file if they don't exist. Then, you can add a file there (be sure it's NAME.json) and load it by saying "setfile NAME" (no .json at the end) and it will load the file and reload its components to match. If you modify the JSON file the addon is already set to, you can say "reload" in-game and it will simply reload the addon from the same file again.
 
Last edited:
Neat solution! The OnDelete mod shouldn't be necessary though, you can set component.Addon = null then delete the component without it deleting the add-on.

Also, it would be nice to see the JsonAddonComponent class too :)
 
Back