Index: TFP-WebServer/WebServer/src/WebAPI/APIs/Log.cs
===================================================================
--- TFP-WebServer/WebServer/src/WebAPI/APIs/Log.cs	(revision 457)
+++ 	(revision )
@@ -1,87 +1,0 @@
-using System.Collections.Generic;
-using JetBrains.Annotations;
-using Utf8Json;
-
-namespace Webserver.WebAPI.APIs {
-	[UsedImplicitly]
-	public class Log : AbsRestApi {
-		private const int maxCount = 1000;
-
-		private static readonly byte[] jsonKeyEntries = JsonWriter.GetEncodedPropertyNameWithBeginObject ("entries");
-		private static readonly byte[] jsonKeyFirstLine = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("firstLine");
-		private static readonly byte[] jsonKeyLastLine = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("lastLine");
-
-		private static readonly byte[] jsonMsgKey = JsonWriter.GetEncodedPropertyNameWithBeginObject ("msg");
-		private static readonly byte[] jsonTypeKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("type");
-		private static readonly byte[] jsonTraceKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("trace");
-		private static readonly byte[] jsonIsotimeKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("isotime");
-		private static readonly byte[] jsonUptimeKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("uptime");
-		
-		protected override void HandleRestGet (RequestContext _context) {
-			if (_context.Request.QueryString ["count"] == null || !int.TryParse (_context.Request.QueryString ["count"], out int count)) {
-				count = 50;
-			}
-
-			if (count == 0) {
-				count = 1;
-			}
-
-			if (count > maxCount) {
-				count = maxCount;
-			}
-
-			if (count < -maxCount) {
-				count = -maxCount;
-			}
-
-			if (_context.Request.QueryString ["firstLine"] == null || !int.TryParse (_context.Request.QueryString ["firstLine"], out int firstLine)) {
-				firstLine = count > 0 ? LogBuffer.Instance.OldestLine : LogBuffer.Instance.LatestLine;
-			}
-			
-			PrepareEnvelopedResult (out JsonWriter writer);
-			
-			writer.WriteRaw (jsonKeyEntries);
-			
-			List<LogBuffer.LogEntry> logEntries = LogBuffer.Instance.GetRange (ref firstLine, count, out int lastLine);
-
-			writer.WriteBeginArray ();
-
-			for (int i = 0; i < logEntries.Count; i++) {
-				LogBuffer.LogEntry logEntry = logEntries [i];
-				
-				if (i > 0) {
-					writer.WriteValueSeparator ();
-				}
-
-				writer.WriteRaw (jsonMsgKey);
-				writer.WriteString (logEntry.Message);
-
-				writer.WriteRaw (jsonTypeKey);
-				writer.WriteString (logEntry.Type.ToStringCached ());
-
-				writer.WriteRaw (jsonTraceKey);
-				writer.WriteString (logEntry.Trace);
-
-				writer.WriteRaw (jsonIsotimeKey);
-				writer.WriteString (logEntry.IsoTime);
-
-				writer.WriteRaw (jsonUptimeKey);
-				writer.WriteString (logEntry.Uptime.ToString ());
-
-				writer.WriteEndObject ();
-			}
-
-			writer.WriteEndArray ();
-
-			writer.WriteRaw (jsonKeyFirstLine);
-			writer.WriteInt32 (firstLine);
-			
-			writer.WriteRaw (jsonKeyLastLine);
-			writer.WriteInt32 (lastLine);
-			
-			writer.WriteEndObject ();
-
-			SendEnvelopedResult (_context, ref writer);
-		}
-	}
-}
Index: TFP-WebServer/WebServer/src/WebAPI/APIs/LogApi.cs
===================================================================
--- TFP-WebServer/WebServer/src/WebAPI/APIs/LogApi.cs	(revision 459)
+++ TFP-WebServer/WebServer/src/WebAPI/APIs/LogApi.cs	(revision 459)
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using Utf8Json;
+
+namespace Webserver.WebAPI.APIs {
+	[UsedImplicitly]
+	public class LogApi : AbsRestApi {
+		private const int maxCount = 1000;
+
+		private static readonly byte[] jsonKeyEntries = JsonWriter.GetEncodedPropertyNameWithBeginObject ("entries");
+		private static readonly byte[] jsonKeyFirstLine = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("firstLine");
+		private static readonly byte[] jsonKeyLastLine = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("lastLine");
+
+		private static readonly byte[] jsonMsgKey = JsonWriter.GetEncodedPropertyNameWithBeginObject ("msg");
+		private static readonly byte[] jsonTypeKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("type");
+		private static readonly byte[] jsonTraceKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("trace");
+		private static readonly byte[] jsonIsotimeKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("isotime");
+		private static readonly byte[] jsonUptimeKey = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("uptime");
+
+		public LogApi () : base ("Log") {
+		}
+
+		protected override void HandleRestGet (RequestContext _context) {
+			if (_context.Request.QueryString ["count"] == null || !int.TryParse (_context.Request.QueryString ["count"], out int count)) {
+				count = 50;
+			}
+
+			if (count == 0) {
+				count = 1;
+			}
+
+			if (count > maxCount) {
+				count = maxCount;
+			}
+
+			if (count < -maxCount) {
+				count = -maxCount;
+			}
+
+			if (_context.Request.QueryString ["firstLine"] == null || !int.TryParse (_context.Request.QueryString ["firstLine"], out int firstLine)) {
+				firstLine = count > 0 ? LogBuffer.Instance.OldestLine : LogBuffer.Instance.LatestLine;
+			}
+			
+			PrepareEnvelopedResult (out JsonWriter writer);
+			
+			writer.WriteRaw (jsonKeyEntries);
+			
+			List<LogBuffer.LogEntry> logEntries = LogBuffer.Instance.GetRange (ref firstLine, count, out int lastLine);
+
+			writer.WriteBeginArray ();
+
+			for (int i = 0; i < logEntries.Count; i++) {
+				LogBuffer.LogEntry logEntry = logEntries [i];
+				
+				if (i > 0) {
+					writer.WriteValueSeparator ();
+				}
+
+				writer.WriteRaw (jsonMsgKey);
+				writer.WriteString (logEntry.Message);
+
+				writer.WriteRaw (jsonTypeKey);
+				writer.WriteString (logEntry.Type.ToStringCached ());
+
+				writer.WriteRaw (jsonTraceKey);
+				writer.WriteString (logEntry.Trace);
+
+				writer.WriteRaw (jsonIsotimeKey);
+				writer.WriteString (logEntry.IsoTime);
+
+				writer.WriteRaw (jsonUptimeKey);
+				writer.WriteString (logEntry.Uptime.ToString ());
+
+				writer.WriteEndObject ();
+			}
+
+			writer.WriteEndArray ();
+
+			writer.WriteRaw (jsonKeyFirstLine);
+			writer.WriteInt32 (firstLine);
+			
+			writer.WriteRaw (jsonKeyLastLine);
+			writer.WriteInt32 (lastLine);
+			
+			writer.WriteEndObject ();
+
+			SendEnvelopedResult (_context, ref writer);
+		}
+	}
+}
Index: TFP-WebServer/WebServer/src/WebAPI/APIs/OpenAPI.cs
===================================================================
--- TFP-WebServer/WebServer/src/WebAPI/APIs/OpenAPI.cs	(revision 459)
+++ TFP-WebServer/WebServer/src/WebAPI/APIs/OpenAPI.cs	(revision 459)
@@ -0,0 +1,24 @@
+using System.Net;
+using JetBrains.Annotations;
+using Webserver.Permissions;
+
+namespace Webserver.WebAPI.APIs {
+	[UsedImplicitly]
+	public class OpenAPI : AbsRestApi {
+		public OpenAPI (Web _parentWeb) : base (_parentWeb) {
+		}
+
+		protected override void HandleRestGet (RequestContext _context) {
+			string path = _context.RequestPath;
+			
+			if (!ParentWeb.OpenApiHelpers.TryGetOpenApiSpec (path, out string specText)) {
+				WebUtils.WriteText (_context.Response, $"Spec for {path} not found", HttpStatusCode.NotFound);
+				return;
+			}
+
+			WebUtils.WriteText (_context.Response, specText, _mimeType: "text/x-yaml");
+		}
+
+		public override int DefaultPermissionLevel () => AdminWebModules.PermissionLevelGuest;
+	}
+}
Index: TFP-WebServer/WebServer/src/WebAPI/APIs/OpenAPI.openapi.yaml
===================================================================
--- TFP-WebServer/WebServer/src/WebAPI/APIs/OpenAPI.openapi.yaml	(revision 459)
+++ TFP-WebServer/WebServer/src/WebAPI/APIs/OpenAPI.openapi.yaml	(revision 459)
@@ -0,0 +1,38 @@
+openapi: 3.1.0
+info:
+  title: OpenAPI
+  version: 1
+
+
+paths:
+  /api/OpenAPI/openapi.yaml:
+    get:
+      operationId: OpenAPI.openapi
+      responses:
+        200:
+          content:
+            application/json: {}
+          description: OpenAPI master document
+      tags:
+        - Meta
+  /api/OpenAPI/{name}.openapi.yaml:
+    get:
+      operationId: OpenAPI.getAPI
+      parameters:
+        - in: path
+          name: name
+          required: true
+          schema:
+            pattern: '[^\/#\?]+?'
+            type: string
+      responses:
+        200:
+          content:
+            application/json: {}
+          description: OpenAPI spec for API
+        404:
+          content:
+            application/json: {}
+          description: No OpenAPI spec found for the API name
+      tags:
+        - Meta
Index: TFP-WebServer/WebServer/src/WebAPI/APIs/Permissions/RegisterUser.cs
===================================================================
--- TFP-WebServer/WebServer/src/WebAPI/APIs/Permissions/RegisterUser.cs	(revision 457)
+++ TFP-WebServer/WebServer/src/WebAPI/APIs/Permissions/RegisterUser.cs	(revision 459)
@@ -95,10 +95,10 @@
 			// Log info
 			string crossplatformidString = regData.CrossPlatformUserId == null ? "" : $", crossplatform ID {regData.CrossPlatformUserId.CombinedString}";
-			global::Log.Out ($"[Web] User registered: Username '{username}' for platform ID {regData.PlatformUserId.CombinedString}{crossplatformidString}");
+			Log.Out ($"[Web] User registered: Username '{username}' for platform ID {regData.PlatformUserId.CombinedString}{crossplatformidString}");
 
 			if (AdminWebUsers.Instance.HasUser (regData.PlatformUserId, regData.CrossPlatformUserId, out AdminWebUsers.WebUser existingUser)) {
 				// Remove existing username of player, only allowing one user per player
 
-				global::Log.Out ($"[Web] Re-registration, replacing existing username '{existingUser.Name}'");
+				Log.Out ($"[Web] Re-registration, replacing existing username '{existingUser.Name}'");
 				AdminWebUsers.Instance.RemoveUser (existingUser.Name);
 			}
Index: TFP-WebServer/WebServer/src/WebAPI/APIs/WorldState/Animal.openapi.yaml
===================================================================
--- TFP-WebServer/WebServer/src/WebAPI/APIs/WorldState/Animal.openapi.yaml	(revision 459)
+++ TFP-WebServer/WebServer/src/WebAPI/APIs/WorldState/Animal.openapi.yaml	(revision 459)
@@ -0,0 +1,58 @@
+openapi: 3.1.0
+info:
+  title: Animal
+  version: 1
+
+components:
+  schemas:
+    AnimalElement:
+      type: object
+      properties:
+        id:
+          $ref: '#/components/schemas/TypeEntityId'
+        name:
+          type: string
+          examples:
+            - animalStag
+        position:
+          $ref: '#/components/schemas/TypeVector3i'
+      required:
+        - id
+        - name
+        - position
+
+    AnimalList:
+      type: array
+      items:
+        $ref: '#/components/schemas/AnimalElement'
+
+
+paths:
+  /api/animal:
+    get:
+      tags:
+        - WorldState
+      summary: Animals list
+      description: Fetch a list of the currently spawned animals in the world
+      operationId: animal.get
+      responses:
+        200:
+          description: List of animals
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  data:
+                    $ref: '#/components/schemas/AnimalList'
+                  meta:
+                    $ref: '#/components/schemas/ResultEnvelopeMeta'
+                required:
+                  - data
+                  - meta
+        403:
+          $ref: '#/components/responses/Unauthorized'
+      security:
+        - apiTokenName: []
+          apiTokenSecret: []
+        - sessionCookie: []
Index: TFP-WebServer/WebServer/src/WebAPI/APIs/WorldState/Player.cs
===================================================================
--- TFP-WebServer/WebServer/src/WebAPI/APIs/WorldState/Player.cs	(revision 457)
+++ TFP-WebServer/WebServer/src/WebAPI/APIs/WorldState/Player.cs	(revision 459)
@@ -93,5 +93,5 @@
 			ClientInfo ci = ConnectionManager.Instance.Clients.ForUserId (_nativeUserId);
 			if (ci == null) {
-				global::Log.Warning ($"[Web] Player.GET: ClientInfo null");
+				Log.Warning ($"[Web] Player.GET: ClientInfo null");
 				return;
 			}
@@ -101,5 +101,5 @@
 
 			if (entity == null) {
-				global::Log.Warning ($"[Web] Player.GET: EntityPlayer null");
+				Log.Warning ($"[Web] Player.GET: EntityPlayer null");
 				return;
 			}
Index: TFP-WebServer/WebServer/src/WebAPI/OpenAPI.master.yaml
===================================================================
--- TFP-WebServer/WebServer/src/WebAPI/OpenAPI.master.yaml	(revision 459)
+++ TFP-WebServer/WebServer/src/WebAPI/OpenAPI.master.yaml	(revision 459)
@@ -0,0 +1,128 @@
+info:
+  title: 7 Days To Die WebAPI
+  version: 1.0.0
+openapi: 3.1.0
+servers:
+  - url: /
+
+components:
+  schemas:
+    ResultEnvelopeMeta:
+      type: object
+      properties:
+        serverTime:
+          type: string
+          format: date-time
+        requestMethod:
+          type: string
+          examples:
+            - GET
+            - POST
+        requestSubpath:
+          type: string
+          examples:
+            - search
+          description: >-
+            URL path (without query arguments) starting from after the API name,
+            excluding the initial slash '/'
+        requestBody:
+          type:
+            - object
+            - 'null'
+          examples:
+            - id: a
+              filter: 123
+          description: If erroneous request had a valid JSON body it is included here
+        errorCode:
+          type: string
+          examples:
+            - GET_WITH_BODY
+            - INVALID_COMMAND
+          description: Custom error identifier
+        exceptionMessage:
+          type: string
+          description: Message (If API execution threw an exception)
+        exceptionTrace:
+          type: string
+          description: Stack trace (If API execution threw an exception)
+      required:
+        - serverTime
+        - requestMethod
+        - requestSubpath
+        - requestBody
+        - errorCode
+
+    TypeEntityId:
+      type: integer
+      minimum: 0
+      examples:
+        - 171
+      description: World-unique EntityID
+
+    TypeVector3i:
+      type: object
+      properties:
+        x:
+          type: integer
+          examples:
+            - -123
+        y:
+          type: integer
+          examples:
+            - 61
+        z:
+          type: integer
+          examples:
+            - 734
+      required:
+        - x
+        - 'y'
+        - z
+      description: 3D vector in full blocks
+
+
+  responses:
+    HttpEmptyEnvelopedResponse:
+      description: Empty result
+      content:
+        application/json:
+          schema:
+            type: object
+            properties:
+              data:
+                type: array
+                items:
+                  type: 'null'
+                maxItems: 0
+                examples:
+                  - []
+              meta:
+                $ref: '#/components/schemas/ResultEnvelopeMeta'
+            required:
+              - data
+              - meta
+
+    Unauthorized:
+      description: Not authorized to call this operation
+
+
+  securitySchemes:
+    apiTokenName:
+      type: apiKey
+      in: header
+      name: X-SDTD-API-TOKENNAME
+      description: Authentication by a "WebToken" with token name and token secret, passed in as header fields. This specifies the token name.
+    apiTokenSecret:
+      type: apiKey
+      in: header
+      name: X-SDTD-API-SECRET
+      description: Authentication by a "WebToken" with token name and token secret, passed in as header fields. This specifies the token secret.
+    sessionCookie:
+      type: apiKey
+      in: cookie
+      name: sid
+      description: Authentication by session cookie. A cookie can be received from the session endpoints.
+
+
+paths:
+  allOf:
Index: TFP-WebServer/WebServer/src/WebAPI/OpenApiHelpers.cs
===================================================================
--- TFP-WebServer/WebServer/src/WebAPI/OpenApiHelpers.cs	(revision 459)
+++ TFP-WebServer/WebServer/src/WebAPI/OpenApiHelpers.cs	(revision 459)
@@ -0,0 +1,78 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Text;
+
+namespace Webserver.WebAPI {
+	public class OpenApiHelpers {
+		private const string masterResourceName = "openapi.master.yaml";
+		private const string masterDocName = "openapi.yaml";
+
+		private const string masterDocRefLineBegin = "    - $ref: ";
+		private const string masterDocRefLineEnd = "#/paths";
+
+		private readonly Dictionary<string, string> specs = new CaseInsensitiveStringDictionary<string> ();
+
+		public OpenApiHelpers () {
+			loadMainSpec ();
+			Web.ServerInitialized += _ => {
+				buildMainSpecRefs ();
+			};
+		}
+
+		private void loadMainSpec () {
+			Assembly apiAssembly = GetType ().Assembly;
+
+			string specText = ResourceHelpers.GetManifestResourceText (apiAssembly, masterResourceName, true);
+			if (specText == null) {
+				Log.Warning ($"[Web] Failed loading main OpenAPI spec from assembly '{Path.GetFileName (apiAssembly.Location)}'");
+				return;
+			}
+
+			specs.Add (masterDocName, specText);
+			Log.Out ($"[Web] Loaded main OpenAPI spec");
+		}
+
+		private void buildMainSpecRefs () {
+			if (!TryGetOpenApiSpec (null, out string mainSpec)) {
+				return;
+			}
+
+			StringBuilder sb = new StringBuilder (mainSpec);
+			
+			foreach ((string apiSpecName, _) in specs) {
+				if (apiSpecName.Equals (masterDocName)) {
+					continue;
+				}
+
+				sb.AppendLine ($"{masterDocRefLineBegin}./{apiSpecName}{masterDocRefLineEnd}");
+			}
+
+			specs[masterDocName] = sb.ToString ();
+			
+			Log.Out ("[Web] OpenAPI preparation done");
+		}
+
+		public void LoadOpenApiSpec (AbsWebAPI _api) {
+			Assembly apiAssembly = _api.GetType ().Assembly;
+			string apiName = _api.Name;
+			string apiSpecName = $"{apiName}.openapi.yaml";
+
+			string specText = ResourceHelpers.GetManifestResourceText (apiAssembly, apiSpecName, true);
+			if (specText == null) {
+				return;
+			}
+
+			Log.Out ($"[Web] Loaded OpenAPI spec for '{apiName}'");
+			specs.Add (apiSpecName, specText);
+		}
+
+		public bool TryGetOpenApiSpec (string _name, out string _specText) {
+			if (string.IsNullOrEmpty (_name)) {
+				_name = masterDocName;
+			}
+
+			return specs.TryGetValue (_name, out _specText);
+		}
+	}
+}
