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()); }