using System;
using System.Collections.Generic;
using System.Linq;
namespace codelanguage;
/// Creates a separate page for every registered type, field, proc and verb.
internal static class TypeReferenceTopics
{
public static IEnumerable Create()
{
TypeRegistry.EnsureInitialized();
var types = TypeRegistry.GetAllTypes().Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray();
var procedures = new Dictionary>(StringComparer.OrdinalIgnoreCase);
var fields = new Dictionary>(StringComparer.OrdinalIgnoreCase);
foreach (var path in types)
{
var definition = TypeRegistry.Get(path);
if (definition == null) continue;
var id = TypeId(path);
var typeName = path.TrimStart('/');
var vars = definition.DefaultVars.Keys.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray();
var procs = definition.Procs.Keys.Concat(definition.Verbs.Keys).Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToArray();
foreach (var procName in procs)
{
if (!procedures.TryGetValue(procName, out var owners)) procedures[procName] = owners = new();
owners.Add((path, definition.Verbs.ContainsKey(procName)));
}
foreach (var varName in vars)
{
if (!fields.TryGetValue(varName, out var owners)) fields[varName] = owners = new();
owners.Add((path, definition.DefaultVars[varName]));
}
var related = new List();
if (definition.ParentPath != null) related.Add(TypeId(definition.ParentPath));
related.AddRange(vars.Select(v => VarId(path, v)));
related.AddRange(procs.Select(ProcId));
AddSpecialRelated(path, related);
yield return new ReferenceTopic
{
Id = id,
Title = typeName,
Summary = DescribeType(path, definition.ParentPath, vars.Length, procs.Length),
Categories = { "types" },
SeeAlso = string.Join(",", related.Distinct(StringComparer.OrdinalIgnoreCase)),
Sections =
{
new Section("Type path and inheritance", new ParagraphContentLine($"Use {path} in declarations, path checks, and construction. " +
(definition.ParentPath == null ? "This is a root type in the registered hierarchy." : $"It inherits the variables and procedures of {definition.ParentPath}."))),
new Section("When to use this type", new ParagraphContentLine(DescribeTypeUsage(path))),
new Section("Declared members", new ParagraphContentLine($"This type declares {vars.Length} variables and {procs.Length} procedures or verbs. Derived types inherit its members; the linked member pages show defaults and known behavior.")),
new Section("Beginner example", new CodeContentLine($"var/typepath = {path}\nif(istype(src, typepath))\n world << \"This value is a {typeName}.\"", "Store a type path, then use istype to check an existing value.")),
new Section("Expert example", new CodeContentLine($"proc/is_{Identifier(typeName)}(value)\n return istype(value, {path})", "Put a repeated type check behind a named helper.")),
}
};
}
foreach (var pair in fields.OrderBy(p => p.Key, StringComparer.OrdinalIgnoreCase))
{
var name = pair.Key;
var owners = pair.Value;
var ownerPaths = owners.Select(o => o.Path).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
var links = ownerPaths.Select(TypeId).Append("var").Append("syntax").ToList();
if (name.Contains("loc", StringComparison.OrdinalIgnoreCase)) links.AddRange(new[] { "pixloc", "locate" });
if (name.Contains("icon", StringComparison.OrdinalIgnoreCase) || name.Contains("overlay", StringComparison.OrdinalIgnoreCase)) links.AddRange(new[] { "icon", "image" });
yield return new ReferenceTopic
{
Id = VarId("", name),
Title = name + " var",
Summary = $"{DescribeVar(name)} Owner types: {string.Join(", ", ownerPaths)}.",
Categories = { "variables", "typevars" },
SeeAlso = string.Join(",", links.Distinct(StringComparer.OrdinalIgnoreCase)),
Sections =
{
new Section("Owners", new ParagraphContentLine(string.Join(", ", ownerPaths))),
new Section("Default values", new ParagraphContentLine(string.Join("; ", owners.Select(o => o.Path + ": " + SafeValue(o.Value))))),
new Section("What it is", new ParagraphContentLine(DescribeVar(name) + " Derived types inherit parent variables and may provide their own value.")),
new Section("Beginner example", new CodeContentLine($"var/current_value = src.{name}\nsrc.{name} = current_value")),
new Section("Expert example", new CodeContentLine($"proc/read_{Slug(name)}()\n var/current = src.{name}\n if(isnull(current)) return initial(src.{name})\n return current")),
}
};
}
foreach (var pair in procedures.OrderBy(p => p.Key, StringComparer.OrdinalIgnoreCase))
{
var name = pair.Key;
var owners = pair.Value;
var ownerPaths = owners.Select(o => o.Path).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
var primaryOwner = ownerPaths[0];
var isVerb = owners.Any(o => o.IsVerb);
var hook = DescribeHookParameters(name, ownerPaths);
var links = ownerPaths.Select(TypeId).Append("parent-call").Append("proc").ToList();
if (name.Equals("Login", StringComparison.OrdinalIgnoreCase)) links.Add(ProcId("Logout"));
if (name.Equals("Logout", StringComparison.OrdinalIgnoreCase)) links.Add(ProcId("Login"));
if (ownerPaths.Contains("/client", StringComparer.OrdinalIgnoreCase))
{
links.Add(TypeId("/mob"));
links.Add(VarId("/client", "key"));
links.Add(VarId("/client", "mob"));
}
yield return new ReferenceTopic
{
Id = ProcId(name),
Title = name + "()",
Summary = $"{DescribeProc(name)} Owner: {string.Join(", ", ownerPaths.Select(p => p.TrimStart('/')))}.",
Categories = { isVerb ? "verbs" : "procs", "typeprocs" },
SeeAlso = string.Join(",", links.Distinct(StringComparer.OrdinalIgnoreCase)),
Sections =
{
new Section("Owner and declaration", new ParagraphContentLine(string.Join(", ", ownerPaths.Select(p => p.TrimStart('/')))), new CodeContentLine(hook.Signature)),
new Section("What it does", new ParagraphContentLine(DescribeProc(name) + " Override it on a listed owner type to customize the hook. The inherited implementation can be invoked with ..().")),
new Section("Parameters and use", new ParagraphContentLine(hook.Description)),
new Section("Beginner example", new CodeContentLine($"{hook.Signature}\n world << \"{name} called on [src].\"\n {ParentCall(name)}", "Use the callback's documented arguments, then preserve inherited handling.")),
new Section("Expert example", new CodeContentLine($"{hook.Signature}\n // Add validation or custom behavior using the parameters above.\n {ParentCall(name)}", "Keep the hook contract intact when extending a built-in type.")),
}
};
}
}
public static string TypeId(string path) => "type-" + Slug(path);
public static string VarId(string path, string name) => "var-" + Slug(name);
public static string ProcId(string name) => "typeproc-" + Slug(name);
private static string DescribeType(string path, string? parentPath, int vars, int procs)
{
var role = path.ToLowerInvariant() switch
{
"/datum" => "the base for non-map game objects and other data values",
"/atom" => "the base for map entities with location, appearance, and interaction state",
"/atom/movable" => "the base for atoms that can change location",
"/mob" => "a movable, player-like game entity",
"/obj" => "a movable object in the world, such as an item or obstacle",
"/turf" => "a tile or cell in the world map",
"/area" => "a region that groups map turfs and supplies area-level state",
"/world" => "the single global world controller and world configuration object",
"/client" => "a connected player's client session and presentation settings",
"/list" => "an ordered collection that can also store key/value entries",
_ => $"a registered Craft++ type derived from {parentPath ?? "the root runtime types"}"
};
return $"{path} is {role}. It declares {vars} variables and {procs} procedures or verbs" +
(parentPath == null ? "." : $" and inherits from {parentPath}.");
}
private static string DescribeTypeUsage(string path) => path.ToLowerInvariant() switch
{
"/datum" => "Use datum subtypes for state and behavior that do not need a map location or appearance.",
"/atom" => "Use atom subtypes for values that exist on the map or participate in atom interactions.",
"/atom/movable" => "Use movable atom subtypes when the value must change its location during play.",
"/mob" => "Use mob subtypes for living or controllable entities. Client connection state is optional and may be null.",
"/obj" => "Use object subtypes for items, fixtures, and other movable map objects that are not mobs.",
"/turf" => "Use turf subtypes to define map cells and their defaults or interactions.",
"/area" => "Use area subtypes to group turfs under shared names, settings, or behavior.",
"/world" => "Use world for global configuration and world-level events; it is a singleton rather than a type to construct repeatedly.",
"/client" => "Use client state for per-connection UI and preferences; obtain it from a connected mob's client reference.",
"/list" => "Use lists for ordered values or associative key/value storage. Prefer list literals or list() construction over creating a /list directly.",
_ => $"Use this subtype when its specialized defaults or procedures fit the value you need. Check its parent and member pages before overriding inherited behavior."
};
private static (string Signature, string Description) DescribeHookParameters(string name, string[] owners)
{
var clientOwned = owners.Contains("/client", StringComparer.OrdinalIgnoreCase);
var signature = name.ToLowerInvariant() switch
{
"bump" => "Bump(atom/Obstacle)",
"move" => "Move(NewLoc, Dir=0, step_x=0, step_y=0)",
"enter" or "exit" => $"{name}(atom/movable/O, atom/{(name.Equals("enter", StringComparison.OrdinalIgnoreCase) ? "OldLoc" : "newloc")})",
"entered" or "exited" => $"{name}(atom/movable/Obj, atom/{(name.Equals("entered", StringComparison.OrdinalIgnoreCase) ? "OldLoc" : "newloc")})",
"cross" or "uncross" => $"{name}(atom/movable/O)",
"crossed" or "uncrossed" => $"{name}(atom/movable/O)",
"click" or "dblclick" => clientOwned ? $"{name}(object, location, control, params)" : $"{name}(location, control, params)",
"mousedown" or "mouseup" or "mousedrag" or "mousedrop" or "mouseenter" or "mouseentered" or "mouseexited" or "mousemove" or "mousewheel"
=> clientOwned ? $"{name}(object, location, control, params)" : $"{name}(location, control, params)",
"topic" => clientOwned ? "Topic(href, list/href_list, hsrc)" : "Topic(href, list/href_list)",
"new" or "del" or "stat" or "read" or "write" => $"{name}()",
_ => $"{name}()"
};
var detail = name.ToLowerInvariant() switch
{
"bump" => "Obstacle is the atom blocking movement. This callback reports a failed move; it does not itself grant passage.",
"move" => "NewLoc is the destination; Dir is a direction; step_x and step_y are pixel-step offsets. Dir and offsets default to zero. The return value indicates whether movement succeeded.",
"enter" or "exit" => "O is the moving atom and the second argument is its previous or requested location. Return 1 to allow the move or 0 to reject it.",
"entered" or "exited" => "Obj is the atom that moved; the location argument identifies its previous or new location. These are after-movement notifications.",
"cross" or "uncross" => "O is the moving atom. Return 1 to allow crossing or 0 to block it.",
"crossed" or "uncrossed" => "O is the atom that crossed or stopped overlapping this atom. These callbacks report the completed interaction.",
"click" or "dblclick" => clientOwned
? "object is the clicked atom (when applicable); location describes the click location; control names the client control; params carries event parameters."
: "location describes the click location; control names the client control; params carries event parameters.",
"mousedown" or "mouseup" or "mousedrag" or "mousedrop" or "mouseenter" or "mouseentered" or "mouseexited" or "mousemove" or "mousewheel"
=> "The event arguments identify the affected object, location, control, and event parameter text. The exact object argument depends on whether the hook is on /client or /atom.",
"topic" => clientOwned
? "href is the clicked link data, href_list is its parsed associative list, and hsrc is the referenced object when present."
: "href is the clicked link data and href_list is its parsed associative list.",
"new" => "New() is called during construction. A New hook on an atom can also receive its initial location; check the target engine's type-specific signature.",
"del" => "Del() runs during deletion. Release references or other owned state here; avoid returning or using the value after deletion.",
"stat" => "Stat() is called to contribute to a status panel; it normally takes no explicit arguments.",
"read" or "write" => "These savefile hooks participate in persistence. The registered stub does not describe the savefile argument, so check the target engine signature before overriding.",
_ => "The type registry includes this procedure name but does not declare formal parameters. Confirm the target engine's signature before overriding it."
};
return (signature, detail + " The Craft++ registry stores these hooks as stubs, so BYOND signatures describe the compatibility target and are not a guarantee that every callback is implemented.");
}
private static string ParentCall(string name) => name.ToLowerInvariant() switch
{
"enter" or "exit" or "cross" or "uncross" or "move" => "return ..()",
"del" => "..()",
_ => "..()"
};
private static string DescribeProc(string name) => name.ToLowerInvariant() switch
{
"new" => "New() runs when a value is constructed and can initialize its state.",
"del" => "Del() runs during deletion and can release or detach related state.",
"login" => "Login() runs when a client or mob enters its logged-in state.",
"logout" => "Logout() runs when a client or mob leaves its logged-in state.",
"click" => "Click() handles a click on the owning atom or client view.",
"dblclick" => "DblClick() handles a double-click interaction.",
"bump" => "Bump() handles a movable atom colliding with a blocking atom.",
"move" => "Move() attempts to move a movable atom to a destination and reports whether it succeeded.",
"enter" => "Enter() can accept or reject an atom entering this location.",
"entered" => "Entered() runs after an atom has entered this location.",
"exit" => "Exit() can accept or reject an atom leaving this location.",
"exited" => "Exited() runs after an atom has left this location.",
"cross" => "Cross() can accept or reject an atom crossing this atom.",
"crossed" => "Crossed() runs after another atom crosses this atom.",
"uncross" => "Uncross() can accept or reject an atom uncrossing this atom.",
"uncrossed" => "Uncrossed() runs after another atom uncrosses this atom.",
"topic" => "Topic() handles a client or datum topic request, commonly generated by an href action.",
"stat" => "Stat() contributes status data to the owning mob or object display.",
"read" => "Read() participates in reading an object's state from a savefile.",
"write" => "Write() participates in writing an object's state to a savefile.",
"mousedown" => "MouseDown() handles a pointer button press over the owning atom or client control.",
"mouseup" => "MouseUp() handles a pointer button release.",
"mousedrag" => "MouseDrag() handles dragging from one atom or control to another.",
"mousedrop" => "MouseDrop() handles dropping a dragged value onto the owning atom or control.",
"mouseenter" => "MouseEntered() runs when the pointer enters the owning atom's hit area.",
"mouseentered" => "MouseEntered() runs when the pointer enters the owning atom's hit area.",
"mouseexited" => "MouseExited() runs when the pointer leaves the owning atom's hit area.",
"mousemove" => "MouseMove() handles pointer movement over the owning atom or client control.",
"mousewheel" => "MouseWheel() handles a scroll-wheel action over the owning atom or client control.",
"command" => "Command() handles a client command or submitted command text.",
"north" or "south" or "east" or "west" or "northeast" or "northwest" or "southeast" or "southwest" => $"{name}() is a directional movement hook invoked for the corresponding direction.",
_ => $"{name}() is a built-in type procedure hook registered on one or more types.",
};
private static string DescribeVar(string name) => name.ToLowerInvariant() switch
{
"name" => "The display name used when this value is presented to players or tools.",
"desc" => "Descriptive text associated with this atom or value.",
"type" => "The runtime type path of this value.",
"parent_type" => "The parent type path used for inherited vars and procs.",
"loc" => "The containing location of a movable atom, commonly a turf or another atom.",
"contents" => "The list of atoms and values contained by this atom.",
"client" => "The connected client associated with this mob, or null when disconnected.",
"mob" => "The mob currently controlled by this client.",
"key" => "The user's BYOND key associated with this mob or client.",
"ckey" => "A normalized lowercase form of the user's key.",
"icon" => "The icon resource used to draw this atom or image.",
"icon_state" => "The named state selected from the assigned icon resource.",
"dir" => "The direction used to select directional icon frames and movement orientation.",
"density" => "Whether this atom blocks movement and participates in collision checks.",
"opacity" => "Whether this atom blocks sight through its location.",
"invisibility" => "The visibility threshold that controls which clients can see this atom.",
"see_invisible" => "The invisibility threshold this mob or client can see.",
"pixel_x" => "The visual horizontal pixel offset applied when drawing this atom.",
"pixel_y" => "The visual vertical pixel offset applied when drawing this atom.",
"pixel_z" => "The visual depth offset used by map formats that support it.",
"step_x" => "The horizontal pixel movement offset within a tile.",
"step_y" => "The vertical pixel movement offset within a tile.",
"x" or "y" or "z" => $"The {name} map coordinate of this atom's location.",
"overlays" => "Visual values drawn over this atom's base appearance.",
"underlays" => "Visual values drawn beneath this atom's base appearance.",
"alpha" => "The opacity channel used to draw this atom, from transparent to opaque.",
"color" => "The color modulation applied to this atom's appearance.",
"layer" => "The drawing order of this atom relative to other objects on its plane.",
"plane" => "The rendering plane that controls compositing and draw grouping.",
"verbs" => "The player-facing verbs currently associated with this value.",
"view" => "The map view distance configured for this world or client.",
"time" => "The current world time value, measured in deciseconds since startup.",
"fps" => "The target frame rate configured for this client or world.",
_ => $"The built-in {name} variable declared on the listed type or types.",
};
private static void AddSpecialRelated(string path, List related)
{
if (path.Equals("/client", StringComparison.OrdinalIgnoreCase))
{
related.Add("type-mob");
related.Add(ProcId("Login"));
related.Add(ProcId("Logout"));
}
if (path.Equals("/world", StringComparison.OrdinalIgnoreCase)) related.Add("world");
if (path.Equals("/list", StringComparison.OrdinalIgnoreCase)) related.Add("list");
if (path.Equals("/atom", StringComparison.OrdinalIgnoreCase)) related.Add(TypeId("/atom/movable"));
}
private static string SafeValue(CraftValue value)
{
try { return value.Type == "/list" ? "An empty/default list value." : value.ToString(); }
catch { return "Runtime-provided default."; }
}
private static string Slug(string value)
{
var chars = value.Trim('/').ToLowerInvariant().Select(c => char.IsLetterOrDigit(c) ? c : '-').ToArray();
return string.Join("-", new string(chars).Split('-', StringSplitOptions.RemoveEmptyEntries));
}
private static string Identifier(string value)
=> new string(value.Trim('/').Select(c => char.IsLetterOrDigit(c) ? c : '_').ToArray());
}