Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions doc/bt/ZC_DIRECTION_APC.bt
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
#include "inc/common.bt"

ServerHeaderFixed header;

int handle;
int packetString;
int i1;
int i2;
int i3;
int i4;
float f2;
float serverAppTimeOffset;
4 changes: 3 additions & 1 deletion src/Shared/Network/NormalOp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public static class Zone
public const int AccountProperties = 0x4D;
public const int UnkDynamicCastStart = 0x4F;
public const int UnkDynamicCastEnd = 0x50;
public const int NPC_PlayTrack = 0x53;
public const int SetNPCTrackPosition = 0x54;
public const int PadUpdate = 0x59;
public const int PadSetMonsterAltitude = 0x5C;
public const int ParticleEffect = 0x61;
Expand All @@ -71,7 +73,7 @@ public static class Zone
public const int Unknown_EF = 0xF2;
public const int ChannelTraffic = 0x12D;
public const int SetGreetingMessage = 0x136;
public const int Unk13E = 0x13E;
public const int Revive = 0x13E;
public const int SetSessionKey = 0x14F;
public const int ItemDrop = 0x152;
public const int NGSCallback = 0x170;
Expand Down
66 changes: 49 additions & 17 deletions src/ZoneServer/Network/Send.Normal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1179,23 +1179,6 @@ public static void LeapJump(ICombatEntity entity, Position targetPos, float f1,
entity.Map.Broadcast(packet, entity);
}

/// <summary>
/// Purpose unknown. Added for testing purposes, but turned
/// out to not be necessary.
/// </summary>
/// <param name="entity"></param>
/// <param name="b1"></param>
public static void Unk13E(ICombatEntity entity, bool b1)
{
var packet = new Packet(Op.ZC_NORMAL);
packet.PutInt(NormalOp.Zone.Unk13E);

packet.PutInt(entity.Handle);
packet.PutByte(b1);

entity.Map.Broadcast(packet, entity);
}

/// <summary>
/// Starts a time action, displaying a progress bar and
/// potentially putting the character in an animation.
Expand Down Expand Up @@ -1357,6 +1340,55 @@ public static void OpenBook(Character character, string bookName)

character.Connection.Send(packet);
}

/// <summary>
/// Sent with Resurrect Packets?
/// </summary>
/// <param name="conn"></param>
/// <param name="actor"></param>
public static void Revive(IZoneConnection conn, IActor actor)
{
var packet = new Packet(Op.ZC_NORMAL);
packet.PutInt(NormalOp.Zone.Revive);

packet.PutInt(actor.Handle);
packet.PutByte(0);

conn.Send(packet);
}

/// <summary>
/// NPC Position For a script?
/// </summary>
/// <param name="conn"></param>
/// <param name="actor"></param>
public static void SetNPCTrackPosition(IZoneConnection conn, IActor actor)
{
var packet = new Packet(Op.ZC_NORMAL);
packet.PutInt(NormalOp.Zone.SetNPCTrackPosition);

packet.PutInt(actor.Handle);
packet.PutPosition(actor.Position);

conn.Send(packet);
}

/// <summary>
/// NPC Play a Track (Client side direction)
/// </summary>
public static void NPC_PlayTrack(IActor actor, string trackName, int i1, int i2, float f1)
{
var packet = new Packet(Op.ZC_NORMAL);
packet.PutInt(NormalOp.Zone.NPC_PlayTrack);

packet.PutInt(actor.Handle);
packet.PutLpString(trackName);
packet.PutInt(i1);
packet.PutInt(i2);
packet.PutFloat(f1);

actor.Map.Broadcast(packet);
}
}
}
}
22 changes: 22 additions & 0 deletions src/ZoneServer/Network/Send.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4550,5 +4550,27 @@ public static void ZC_ACTION_PKS(IActor toActor, IActor fromActor, byte type, in

toActor.Map.Broadcast(packet);
}

/// <summary>
/// Sends ZC_DIRECTION_APC packet to initialize track animation.
/// </summary>
/// <param name="conn">The connection to send to.</param>
/// <param name="actor">The actor to animate.</param>
/// <param name="packetStringId">The packet string ID for the track.</param>
/// <param name="i1">Track start index.</param>
/// <param name="i2">Track end index.</param>
/// <param name="f1">Time parameter.</param>
public static void ZC_DIRECTION_APC(IZoneConnection conn, IActor actor, int packetStringId, int i1, int i2, float f1)
{
var packet = new Packet(Op.ZC_DIRECTION_APC);

packet.PutInt(actor.Handle);
packet.PutInt(packetStringId);
packet.PutInt(i1);
packet.PutInt(i2);
packet.PutFloat(f1);

conn.Send(packet);
}
}
}
105 changes: 105 additions & 0 deletions src/ZoneServer/Scripting/Shortcuts.Npcs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,111 @@ public static Npc AddNpc(int monsterId, string name, string uniqueName, string m
return monster;
}

/// <summary>
/// Adds new NPC to the world with explicit Y coordinate.
/// </summary>
/// <param name="monsterId"></param>
/// <param name="name"></param>
/// <param name="uniqueName"></param>
/// <param name="map"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="z"></param>
/// <param name="direction"></param>
/// <param name="dialog"></param>
/// <exception cref="ArgumentException"></exception>
public static Npc AddNpc(int monsterId, string name, string uniqueName, string map, double x, double y, double z, double direction, DialogFunc dialog = null)
{
if (!ZoneServer.Instance.World.TryGetMap(map, out var mapObj))
throw new ArgumentException($"Map '{map}' not found.");

if (ZoneServer.Instance.World.TryGetMonster(a => a.UniqueName == uniqueName, out _))
throw new ArgumentException($"An NPC with the unique name '{uniqueName}' already exists.");

var pos = new Position((float)x, (float)y, (float)z);

// Wrap name in localization code if applicable
if (Dialog.IsLocalizationKey(name))
{
name = Dialog.WrapLocalizationKey(name);
}
// Insert line breaks in tagged NPC names that don't have one
else if (name.StartsWith('[') && !name.Contains("{nl}"))
{
var endIndex = name.LastIndexOf("] ");
if (endIndex != -1)
{
// Remove space and insert new line instead.
name = name.Remove(endIndex + 1, 1);
name = name.Insert(endIndex + 1, "{nl}");
}
}

var location = new Location(mapObj.Id, pos);
var dir = new Direction(direction);

var monster = new Npc(monsterId, name, location, dir);
monster.UniqueName = uniqueName;

if (dialog != null)
monster.SetClickTrigger("DYNAMIC_DIALOG", dialog);

mapObj.AddMonster(monster);

return monster;
}

/// <summary>
/// Adds new NPC to the world with explicit Y coordinate.
/// </summary>
/// <param name="monsterId"></param>
/// <param name="name"></param>
/// <param name="map"></param>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="z"></param>
/// <param name="direction"></param>
/// <param name="dialog"></param>
/// <returns></returns>
public static Npc AddNpc(int monsterId, string name, string map, double x, double y, double z, double direction, DialogFunc dialog = null)
{
var uniqueId = Interlocked.Increment(ref UniqueNpcNameId);
var uniqueName = $"__NPC{uniqueId}__";

return AddNpc(monsterId, name, uniqueName, map, x, y, z, direction, dialog);
}

/// <summary>
/// Adds a Track NPC to the world.
/// </summary>
/// <param name="monsterId">Monster ID for the NPC</param>
/// <param name="name">Display name (usually empty for track NPCs)</param>
/// <param name="map">Map class name</param>
/// <param name="x">X coordinate</param>
/// <param name="y">Y coordinate</param>
/// <param name="z">Z coordinate</param>
/// <param name="direction">Facing direction</param>
/// <param name="trackString">Track animation string</param>
/// <param name="i1">Track start index (default: 2)</param>
/// <param name="i2">Track end index (default: 5)</param>
/// <returns>The created NPC, or null if map is invalid</returns>
public static Npc AddTrackNPC(int monsterId, string name, string map, double x, double y, double z, double direction, string trackString, int i1 = 2, int i2 = 5)
{
if (string.IsNullOrEmpty(map) || map == "None")
{
Yggdrasil.Logging.Log.Debug($"Skipped adding Track NPC {monsterId} - {name} at {x},{y},{z} because of invalid map: {map}");
return null;
}

var npc = AddNpc(monsterId, name, map, x, y, z, direction);
npc.Visibility = World.Actors.ActorVisibility.Always;
npc.AddEffect(new World.Actors.Effects.ReviveEffect());
npc.AddEffect(new World.Actors.Effects.SetTrackPosition());
npc.AddEffect(new World.Actors.Effects.DirectionAPC(trackString, i1, i2));

return npc;
}

/// <summary>
/// Creates a custom shop.
/// </summary>
Expand Down
47 changes: 47 additions & 0 deletions src/ZoneServer/World/Actors/Actor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Collections.Concurrent;
using Melia.Shared.World;
using Melia.Zone.Network;
using Melia.Zone.World.Actors.Components;
using Melia.Zone.World.Actors.Effects;
using Melia.Zone.World.Maps;

namespace Melia.Zone.World.Actors
Expand Down Expand Up @@ -35,6 +37,11 @@ public interface IActor
/// </summary>
Direction Direction { get; set; }

/// <summary>
/// Returns the actor's visibility.
/// </summary>
public ActorVisibility Visibility { get; set; }

/// <summary>
/// Returns a list of effects that are attached to the actor.
/// </summary>
Expand All @@ -51,11 +58,24 @@ public abstract class Actor : IActor
/// </summary>
public ConcurrentBag<AttachableEffect> AttachableEffects { get; } = new ConcurrentBag<AttachableEffect>();

/// <summary>
/// Returns the effects component for this actor.
/// </summary>
public EffectsComponent Effects { get; }

/// <summary>
/// Returns the actor's unique handle.
/// </summary>
public int Handle { get; } = ZoneServer.Instance.World.CreateHandle();

/// <summary>
/// Creates a new actor instance.
/// </summary>
public Actor()
{
this.Effects = new EffectsComponent(this);
}

/// <summary>
/// Returns the actor's display name.
/// </summary>
Expand All @@ -81,6 +101,11 @@ public Map Map
/// </summary>
public Direction Direction { get; set; }

/// <summary>
/// Gets or sets the actor's visibility.
/// </summary>
public ActorVisibility Visibility { get; set; } = ActorVisibility.Everyone;


/// <summary>
/// Attaches an effect to the actor that is displayed alongside it.
Expand All @@ -95,6 +120,15 @@ public void AttachEffect(string packetString, float scale = 1)
if (this.Map != Map.Limbo)
Send.ZC_NORMAL.AttachEffect(this, effect.PacketString, effect.Scale);
}

/// <summary>
/// Adds an effect to the actor.
/// </summary>
/// <param name="effect">The effect to add.</param>
public void AddEffect(Effect effect)
{
this.Effects.AddEffect(effect);
}
}

/// <summary>
Expand Down Expand Up @@ -123,4 +157,17 @@ public AttachableEffect(string packetString, float scale)
this.Scale = scale;
}
}

/// <summary>
/// Defines an actor's visibility.
/// </summary>
public enum ActorVisibility
{
NoOne,
Individual,
Party,
Track,
Everyone,
Always,
}
}
19 changes: 19 additions & 0 deletions src/ZoneServer/World/Actors/Characters/Character.cs
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,9 @@ public void LookAround()
Send.ZC_NORMAL.AttachEffect(this.Connection, monster, effect.PacketString, effect.Scale);
}

if (monster is Actor actor)
actor.Effects.ShowEffects(this.Connection);

if (monster is ICombatEntity entity)
{
Send.ZC_FACTION(this.Connection, monster, entity.Faction);
Expand Down Expand Up @@ -999,6 +1002,8 @@ public void LookAround()
Send.ZC_NORMAL.AttachEffect(this.Connection, character, effect.PacketString, effect.Scale);
}

character.Effects.ShowEffects(this.Connection);

if (character.Components.Get<BuffComponent>()?.Count != 0)
Send.ZC_BUFF_LIST(this.Connection, character);
}
Expand Down Expand Up @@ -1041,6 +1046,20 @@ public void CloseEyes()
}
}

/// <summary>
/// Returns if a character can see an actor.
/// </summary>
public bool CanSee(IActor actor)
{
if (actor.Visibility == ActorVisibility.Always)
return true;
if (actor == null)
return false;
if (!this.Position.InRange2D(actor.Position, Maps.Map.VisibleRange))
return false;
return true;
}

/// <summary>
/// Sets direction and updates clients.
/// </summary>
Expand Down
Loading