├── UI_Screenshots ├── 1426736056.png ├── 1426736071.png ├── 1426736084.png ├── 1426736092.png ├── 1426736099.png ├── 1426736115.png ├── 1426736131.png ├── 1426736139.png ├── 1426736147.png ├── 1426736156.png ├── 1426736164.png ├── 1426736171.png ├── 1426736179.png ├── 1426736191.png ├── 1426736199.png ├── 1426736207.png ├── 1426736217.png ├── 1426736236.png ├── 1426736245.png ├── 1426736253.png ├── 1426736262.png ├── 1426736271.png ├── 1426736279.png ├── 1426736288.png ├── 1426736297.png ├── 1426736307.png ├── 1426736314.png ├── 1426736323.png ├── 1426736331.png ├── 1426736341.png ├── 1426736348.png ├── 1426736379.png ├── 1426736386.png ├── 1426736393.png ├── 1426736404.png ├── 1426736416.png ├── 1426736423.png ├── 1426736430.png ├── 1426736436.png ├── 1426736463.png ├── 1426736470.png └── 1426736477.png ├── README.md ├── CityWebServer ├── packages.config ├── Models │ ├── PolicyInfo.cs │ ├── ChirperMessage.cs │ ├── CityInfo.cs │ ├── PublicTransportLine.cs │ ├── PopulationGroup.cs │ ├── Economy.cs │ └── DistrictInfo.cs ├── Helpers │ ├── EnumExtensions.cs │ ├── CitizenExtensions.cs │ ├── NameValueCollectionExtensions.cs │ ├── DistrictExtensions.cs │ ├── TemplateHelper.cs │ ├── ConfigurationHelper.cs │ └── ApacheMimeTypes.cs ├── UserModInfo.cs ├── RequestHandlers │ ├── MessageRequestHandler.cs │ ├── BudgetRequestHandler.cs │ ├── BuildingRequestHandler.cs │ ├── VehicleRequestHandler.cs │ ├── TransportRequestHandler.cs │ └── CityInfoRequestHandler.cs ├── Properties │ └── AssemblyInfo.cs ├── Retrievers │ └── ChirpRetriever.cs ├── WebServer.cs ├── wwwroot │ ├── script.js │ ├── index.html │ ├── knockout.mapping-latest.js │ └── bootstrap.min.js ├── WebsiteButton.cs ├── CityWebServer.csproj └── IntegratedWebServer.cs ├── CityWebServer.Extensibility ├── packages.config ├── ILogAppender.cs ├── IResponseFormatter.cs ├── IWebServer.cs ├── LogAppenderEventArgs.cs ├── ResponseFormatters │ ├── PlainTextResponseFormatter.cs │ ├── HtmlResponseFormatter.cs │ └── JsonResponseFormatter.cs ├── Properties │ └── AssemblyInfo.cs ├── IRequestHandler.cs ├── RestfulRequestHandlerBase.cs ├── CityWebServer.Extensibility.csproj └── RequestHandlerBase.cs ├── Assemblies └── _PUT_ASSEMBLIES_HERE.txt ├── SampleWebServerExtension ├── UserModInfo.cs ├── SampleRequestHandler.cs ├── Properties │ └── AssemblyInfo.cs └── SampleWebServerExtension.csproj ├── CityWebServer.sln └── .gitignore /UI_Screenshots/1426736056.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736056.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736071.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736071.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736084.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736084.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736092.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736092.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736099.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736099.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736115.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736115.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736131.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736131.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736139.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736139.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736147.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736147.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736156.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736156.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736164.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736164.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736171.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736171.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736179.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736179.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736191.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736191.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736199.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736199.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736207.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736207.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736217.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736217.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736236.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736236.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736245.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736245.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736253.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736253.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736262.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736262.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736271.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736271.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736279.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736279.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736288.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736288.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736297.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736297.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736307.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736307.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736314.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736314.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736323.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736323.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736331.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736331.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736341.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736341.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736348.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736348.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736379.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736379.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736386.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736386.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736393.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736393.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736404.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736416.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736416.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736423.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736423.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736430.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736430.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736436.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736436.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736463.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736463.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736470.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736470.png -------------------------------------------------------------------------------- /UI_Screenshots/1426736477.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rychard/CityWebServer/HEAD/UI_Screenshots/1426736477.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CityWebServer 2 | Adds a web server to Cities: Skylines 3 | 4 | ![Screenshot](http://i.imgur.com/U3dD0vd.png) 5 | -------------------------------------------------------------------------------- /CityWebServer/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /CityWebServer.Extensibility/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /CityWebServer.Extensibility/ILogAppender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CityWebServer.Extensibility 4 | { 5 | public interface ILogAppender 6 | { 7 | event EventHandler LogMessage; 8 | } 9 | } -------------------------------------------------------------------------------- /CityWebServer/Models/PolicyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CityWebServer.Models 4 | { 5 | public class PolicyInfo 6 | { 7 | public String Name { get; set; } 8 | 9 | public Boolean Enabled { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /CityWebServer.Extensibility/IResponseFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace CityWebServer.Extensibility 4 | { 5 | public abstract class IResponseFormatter 6 | { 7 | public abstract void WriteContent(HttpListenerResponse response); 8 | } 9 | } -------------------------------------------------------------------------------- /CityWebServer/Models/ChirperMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CityWebServer.Models 4 | { 5 | public class ChirperMessage 6 | { 7 | public int SenderID { get; set; } 8 | 9 | public String SenderName { get; set; } 10 | 11 | public String Text { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /CityWebServer.Extensibility/IWebServer.cs: -------------------------------------------------------------------------------- 1 | namespace CityWebServer.Extensibility 2 | { 3 | public interface IWebServer 4 | { 5 | /// 6 | /// Gets an array containing all currently registered request handlers. 7 | /// 8 | IRequestHandler[] RequestHandlers { get; } 9 | } 10 | } -------------------------------------------------------------------------------- /CityWebServer/Helpers/EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace CityWebServer.Helpers 5 | { 6 | public static class EnumHelper 7 | { 8 | public static IEnumerable GetValues() 9 | { 10 | return (T[])Enum.GetValues(typeof(T)); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /CityWebServer.Extensibility/LogAppenderEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CityWebServer.Extensibility 4 | { 5 | public class LogAppenderEventArgs : EventArgs 6 | { 7 | public String LogLine { get; set; } 8 | 9 | public LogAppenderEventArgs(String logLine) 10 | { 11 | LogLine = logLine; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /CityWebServer/Models/CityInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CityWebServer.Models 4 | { 5 | public class CityInfo 6 | { 7 | public String Name { get; set; } 8 | 9 | public DateTime Time { get; set; } 10 | 11 | public DistrictInfo GlobalDistrict { get; set; } 12 | 13 | public DistrictInfo[] Districts { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /CityWebServer/Helpers/CitizenExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ColossalFramework; 3 | 4 | namespace CityWebServer.Helpers 5 | { 6 | public static class CitizenExtensions 7 | { 8 | public static String GetName(this Citizen citizen) 9 | { 10 | return Singleton.instance.GetCitizenName(citizen.m_instance); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /CityWebServer/Models/PublicTransportLine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CityWebServer.Models 4 | { 5 | public class PublicTransportLine 6 | { 7 | public String Name { get; set; } 8 | 9 | public int VehicleCount { get; set; } 10 | 11 | public int StopCount { get; set; } 12 | 13 | public PopulationGroup[] Passengers { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /CityWebServer/UserModInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ICities; 3 | 4 | namespace CityWebServer 5 | { 6 | public class UserModInfo : IUserMod 7 | { 8 | public String Name 9 | { 10 | get { return "Integrated Web Server"; } 11 | } 12 | 13 | public String Description 14 | { 15 | get { return "Host a web-server allowing you to communicate with the game via a web-browser."; } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Assemblies/_PUT_ASSEMBLIES_HERE.txt: -------------------------------------------------------------------------------- 1 | This directory must contain the following files: 2 | 3 | - Assembly-CSharp.dll 4 | - ColossalManaged.dll 5 | - ICities.dll 6 | - UnityEngine.dll 7 | - UnityEngine.UI.dll 8 | 9 | These files can be found in the following location: 10 | 11 | \SteamApps\common\Cities_Skylines\Cities_Data\Managed 12 | 13 | Copy those files into this directory. 14 | 15 | (I personally prefer to create symbolic links, but copying them is the easier solution.) -------------------------------------------------------------------------------- /CityWebServer/Models/PopulationGroup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CityWebServer.Models 4 | { 5 | public class PopulationGroup 6 | { 7 | public String Name { get; set; } 8 | 9 | public int Amount { get; set; } 10 | 11 | public PopulationGroup() 12 | { 13 | } 14 | 15 | public PopulationGroup(String name, int amount) 16 | { 17 | Name = name; 18 | Amount = amount; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /SampleWebServerExtension/UserModInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ICities; 3 | 4 | namespace SampleWebServerExtension 5 | { 6 | public class UserModInfo : IUserMod 7 | { 8 | public String Name 9 | { 10 | get { return "Sample Web Server Extension"; } 11 | } 12 | 13 | public String Description 14 | { 15 | get { return "Adds a sample page to the integrated web server. Doesn't do anything without it!"; } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /SampleWebServerExtension/SampleRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using CityWebServer.Extensibility; 4 | 5 | namespace SampleWebServerExtension 6 | { 7 | public class SampleRequestHandler : RequestHandlerBase 8 | { 9 | public SampleRequestHandler(IWebServer server) 10 | : base(server, new Guid("1a255904-bf72-406e-b5e2-c5a43fdd9bba"), "Sample", "Rychard", 100, "/Sample") 11 | { 12 | } 13 | 14 | public override IResponseFormatter Handle(HttpListenerRequest request) 15 | { 16 | const String content = "This is a sample page!"; 17 | 18 | return HtmlResponse(content); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /CityWebServer.Extensibility/ResponseFormatters/PlainTextResponseFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Text; 4 | 5 | namespace CityWebServer.Extensibility.Responses 6 | { 7 | internal class PlainTextResponseFormatter : IResponseFormatter 8 | { 9 | private readonly String _content; 10 | private readonly HttpStatusCode _statusCode; 11 | 12 | public PlainTextResponseFormatter(String content, HttpStatusCode statusCode) 13 | { 14 | _content = content; 15 | _statusCode = statusCode; 16 | } 17 | 18 | public override void WriteContent(HttpListenerResponse response) 19 | { 20 | byte[] buf = Encoding.UTF8.GetBytes(_content); 21 | 22 | response.StatusCode = (int)_statusCode; 23 | response.ContentType = "text/plain"; 24 | response.ContentLength64 = buf.Length; 25 | response.OutputStream.Write(buf, 0, buf.Length); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /CityWebServer.Extensibility/ResponseFormatters/HtmlResponseFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Text; 4 | 5 | namespace CityWebServer.Extensibility.Responses 6 | { 7 | internal class HtmlResponseFormatter : IResponseFormatter 8 | { 9 | private readonly String _content; 10 | private readonly HttpStatusCode _statusCode; 11 | 12 | public HtmlResponseFormatter(String content, HttpStatusCode statusCode = HttpStatusCode.OK) 13 | { 14 | _content = content; 15 | _statusCode = statusCode; 16 | } 17 | 18 | public override void WriteContent(HttpListenerResponse response) 19 | { 20 | byte[] buf = Encoding.UTF8.GetBytes(_content); 21 | 22 | response.StatusCode = (int)_statusCode; 23 | response.ContentType = "text/html"; 24 | response.ContentLength64 = buf.Length; 25 | response.OutputStream.Write(buf, 0, buf.Length); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /CityWebServer/RequestHandlers/MessageRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using CityWebServer.Extensibility; 4 | using CityWebServer.Retrievers; 5 | using JetBrains.Annotations; 6 | 7 | namespace CityWebServer.RequestHandlers 8 | { 9 | [UsedImplicitly] 10 | public class MessageRequestHandler : RequestHandlerBase 11 | { 12 | private readonly ChirpRetriever _chirpRetriever; 13 | 14 | public MessageRequestHandler(IWebServer server) 15 | : base(server, new Guid("b4efeced-1dbb-435a-8999-9f8adaa5036e"), "Chirper Messages", "Rychard", 100, "/Messages") 16 | { 17 | _chirpRetriever = new ChirpRetriever(); 18 | _chirpRetriever.LogMessage += (sender, args) => { OnLogMessage(args.LogLine); }; 19 | } 20 | 21 | public override IResponseFormatter Handle(HttpListenerRequest request) 22 | { 23 | // TODO: Customize request handling. 24 | var messages = _chirpRetriever.Messages; 25 | 26 | return JsonResponse(messages); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /CityWebServer.Extensibility/ResponseFormatters/JsonResponseFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Text; 3 | 4 | namespace CityWebServer.Extensibility.Responses 5 | { 6 | internal class JsonResponseFormatter : IResponseFormatter 7 | { 8 | private readonly T _content; 9 | private readonly HttpStatusCode _statusCode; 10 | 11 | public JsonResponseFormatter(T content, HttpStatusCode statusCode) 12 | { 13 | _content = content; 14 | _statusCode = statusCode; 15 | } 16 | 17 | public override void WriteContent(HttpListenerResponse response) 18 | { 19 | var writer = new JsonFx.Json.JsonWriter(); 20 | var serializedData = writer.Write(_content); 21 | 22 | byte[] buf = Encoding.UTF8.GetBytes(serializedData); 23 | 24 | response.StatusCode = (int)_statusCode; 25 | response.ContentType = "text/json"; 26 | response.ContentLength64 = buf.Length; 27 | response.OutputStream.Write(buf, 0, buf.Length); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /CityWebServer/Helpers/NameValueCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Specialized; 3 | using System.Linq; 4 | 5 | namespace CityWebServer.Helpers 6 | { 7 | public static class NameValueCollectionExtensions 8 | { 9 | /// 10 | /// Determines whether the specified key exists in the current collection. 11 | /// 12 | /// Returns true if the specified key exists, otherwise false 13 | public static Boolean HasKey(this NameValueCollection nvc, String key) 14 | { 15 | return nvc.AllKeys.Any(obj => obj == key); 16 | } 17 | 18 | /// 19 | /// Gets the value of the specified key as an integer. 20 | /// 21 | /// Returns the value of the integer in the specified key, or null if the value is not a valid integer. 22 | public static int? GetInteger(this NameValueCollection nvc, String key) 23 | { 24 | var value = nvc.Get(key); 25 | int result; 26 | if (Int32.TryParse(value, out result)) 27 | { 28 | return result; 29 | } 30 | return null; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /CityWebServer/RequestHandlers/BudgetRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using CityWebServer.Extensibility; 4 | using CityWebServer.Extensibility.Responses; 5 | using ColossalFramework; 6 | 7 | namespace CityWebServer.RequestHandlers 8 | { 9 | public class BudgetRequestHandler : RequestHandlerBase 10 | { 11 | public BudgetRequestHandler(IWebServer server) 12 | : base(server, new Guid("87205a0d-1b53-47bd-91fa-9cddf0a3bd9e"), "Budget", "Rychard", 100, "/Budget") 13 | { 14 | } 15 | 16 | public override IResponseFormatter Handle(HttpListenerRequest request) 17 | { 18 | // TODO: Expand upon this to expose substantially more information. 19 | var economyManager = Singleton.instance; 20 | long income; 21 | long expenses; 22 | economyManager.GetIncomeAndExpenses(new ItemClass(), out income, out expenses); 23 | 24 | Decimal formattedIncome = Math.Round(((Decimal)income / 100), 2); 25 | Decimal formattedExpenses = Math.Round(((Decimal)expenses / 100), 2); 26 | 27 | var content = String.Format("Income: {0:C}{2}Expenses: {1:C}", formattedIncome, formattedExpenses, Environment.NewLine); 28 | 29 | return new PlainTextResponseFormatter(content, HttpStatusCode.OK); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /SampleWebServerExtension/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("SampleWebServerExtension")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("SampleWebServerExtension")] 12 | [assembly: AssemblyCopyright("Copyright © 2015")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("4f860aa2-80ae-427e-b9ea-b31b40d67d8a")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.*")] -------------------------------------------------------------------------------- /CityWebServer/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("CityWebServer")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("CityWebServer")] 12 | [assembly: AssemblyCopyright("Copyright © 2015")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("51b22de1-b43e-402a-988e-a23c96f8052a")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.*")] 35 | 36 | //[assembly: AssemblyFileVersion("1.0.0.0")] -------------------------------------------------------------------------------- /CityWebServer.Extensibility/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("CityWebServer.Extensibility")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("CityWebServer.Extensibility")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("8161110e-476e-493b-bdcf-fce98ee77f8a")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.*")] 36 | [assembly: InternalsVisibleTo("CityWebServer")] -------------------------------------------------------------------------------- /CityWebServer/RequestHandlers/BuildingRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using CityWebServer.Extensibility; 5 | using ColossalFramework; 6 | 7 | namespace CityWebServer.RequestHandlers 8 | { 9 | public class BuildingRequestHandler : RequestHandlerBase 10 | { 11 | public BuildingRequestHandler(IWebServer server) 12 | : base(server, new Guid("03897cb0-d53f-4189-a613-e7d22705dc2f"), "Building", "Rychard", 100, "/Building") 13 | { 14 | } 15 | 16 | public override IResponseFormatter Handle(HttpListenerRequest request) 17 | { 18 | var buildingManager = Singleton.instance; 19 | 20 | if (request.Url.AbsolutePath.StartsWith("/Building/List")) 21 | { 22 | List buildingIDs = new List(); 23 | 24 | var len = buildingManager.m_buildings.m_buffer.Length; 25 | for (ushort i = 0; i < len; i++) 26 | { 27 | if (buildingManager.m_buildings.m_buffer[i].m_flags == Building.Flags.None) { continue; } 28 | 29 | buildingIDs.Add(i); 30 | } 31 | 32 | return JsonResponse(buildingIDs); 33 | } 34 | 35 | foreach (var building in buildingManager.m_buildings.m_buffer) 36 | { 37 | if (building.m_flags == Building.Flags.None) { continue; } 38 | 39 | // TODO: Something with Buildings. 40 | } 41 | 42 | return JsonResponse(""); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /CityWebServer/Models/Economy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CityWebServer.Models 4 | { 5 | public class Economy 6 | { 7 | public IncomeExpense[] IncomesAndExpenses { get; set; } 8 | 9 | public TaxRate[] TaxRates { get; set; } 10 | } 11 | 12 | public class TaxRate 13 | { 14 | public String GroupName { get; set; } 15 | 16 | public int Rate { get; set; } 17 | 18 | // Tax Rate: Low-Density Residential 19 | // Tax Rate: High-Density Residential 20 | // Tax Rate: Low-Density Commercial 21 | // Tax Rate: High-Density Commercial 22 | // Tax Rate: Industry 23 | // Tax Rate: Offices 24 | } 25 | 26 | public class IncomeExpense 27 | { 28 | public String Group { get; set; } 29 | 30 | public String SubGroup { get; set; } 31 | 32 | public Double Amount { get; set; } 33 | 34 | // Tax Income: Low-Density Residential 35 | // Tax Income: High-Density Residential 36 | // Tax Income: Low-Density Commercial 37 | // Tax Income: High-Density Commercial 38 | // Tax Income: Industry 39 | // Tax Income: Offices 40 | 41 | // Income: Citizens 42 | // Income: Tourists 43 | 44 | // Income: Bus/Train/Metro? 45 | 46 | // Upkeep Expense: Roads 47 | // Upkeep Expense: Electricity 48 | // Upkeep Expense: Water 49 | // Upkeep Expense: Garbage 50 | // Upkeep Expense: Unique Buildings 51 | // Upkeep Expense: Healthcare 52 | // Upkeep Expense: Education 53 | // Upkeep Expense: Police 54 | // Upkeep Expense: Firefighters 55 | // Upkeep Expense: Parks 56 | // Upkeep Expense: Bus/Train/Metro? 57 | // Upkeep Expense: Taxes??? 58 | // Upkeep Expense: Policy 59 | } 60 | } -------------------------------------------------------------------------------- /CityWebServer.Extensibility/IRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace CityWebServer.Extensibility 5 | { 6 | /// 7 | /// Represents a handler for servicing requests received by the web server. 8 | /// 9 | public interface IRequestHandler 10 | { 11 | IWebServer Server { get; } 12 | 13 | /// 14 | /// Gets a unique identifier for this handler. Only one handler can be loaded with a given identifier. 15 | /// 16 | Guid HandlerID { get; } 17 | 18 | /// 19 | /// Gets the priority of this request handler. A request will be handled by the request handler with the lowest priority. 20 | /// 21 | int Priority { get; } 22 | 23 | /// 24 | /// Gets the display name of this request handler. 25 | /// 26 | String Name { get; } 27 | 28 | /// 29 | /// Gets the author of this request handler. 30 | /// 31 | String Author { get; } 32 | 33 | /// 34 | /// Gets the absolute path to the main page for this request handler. Your class is responsible for handling requests at this path. 35 | /// 36 | /// 37 | /// When set to a value other than null, the Web Server will show this url as a link on the home page. 38 | /// 39 | String MainPath { get; } 40 | 41 | /// 42 | /// Returns a value that indicates whether this handler is capable of servicing the given request. 43 | /// 44 | Boolean ShouldHandle(HttpListenerRequest request); 45 | 46 | /// 47 | /// Handles the specified request. The method should not close the stream. 48 | /// 49 | IResponseFormatter Handle(HttpListenerRequest request); 50 | } 51 | } -------------------------------------------------------------------------------- /CityWebServer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CityWebServer", "CityWebServer\CityWebServer.csproj", "{61A80D3C-8A1E-44DA-A044-A16F1ABBA149}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CityWebServer.Extensibility", "CityWebServer.Extensibility\CityWebServer.Extensibility.csproj", "{DB96EFB4-FA45-4ACC-8D51-7ED37065CC79}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWebServerExtension", "SampleWebServerExtension\SampleWebServerExtension.csproj", "{7DF15DF6-C475-4866-9111-F5150C1336E1}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {61A80D3C-8A1E-44DA-A044-A16F1ABBA149}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {61A80D3C-8A1E-44DA-A044-A16F1ABBA149}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {61A80D3C-8A1E-44DA-A044-A16F1ABBA149}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {61A80D3C-8A1E-44DA-A044-A16F1ABBA149}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {DB96EFB4-FA45-4ACC-8D51-7ED37065CC79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {DB96EFB4-FA45-4ACC-8D51-7ED37065CC79}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {DB96EFB4-FA45-4ACC-8D51-7ED37065CC79}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {DB96EFB4-FA45-4ACC-8D51-7ED37065CC79}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {7DF15DF6-C475-4866-9111-F5150C1336E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {7DF15DF6-C475-4866-9111-F5150C1336E1}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {7DF15DF6-C475-4866-9111-F5150C1336E1}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {7DF15DF6-C475-4866-9111-F5150C1336E1}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /CityWebServer/Helpers/DistrictExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CityWebServer.Models; 3 | 4 | namespace CityWebServer.Helpers 5 | { 6 | public static class DistrictExtensions 7 | { 8 | public static Boolean IsValid(this District district) 9 | { 10 | return (district.m_flags != District.Flags.None); 11 | } 12 | 13 | public static Boolean IsAlive(this District district) 14 | { 15 | // Get the flags on the district, to ensure we don't access garbage memory if it doesn't have a flag for District.Flags.Created 16 | Boolean alive = ((district.m_flags & District.Flags.Created) == District.Flags.Created); 17 | return alive; 18 | } 19 | 20 | public static PopulationGroup[] GetPopulation(this District district) 21 | { 22 | PopulationGroup[] ageGroups = 23 | { 24 | new PopulationGroup("Children", district.GetChildrenCount()), 25 | new PopulationGroup("Teen", district.GetTeenCount()), 26 | new PopulationGroup("YoungAdult", district.GetYoungAdultCount()), 27 | new PopulationGroup("Adult", district.GetAdultCount()), 28 | new PopulationGroup("Senior", district.GetSeniorCount()) 29 | }; 30 | return ageGroups; 31 | } 32 | 33 | public static int GetChildrenCount(this District district) 34 | { 35 | return (int)district.m_childData.m_finalCount; 36 | } 37 | 38 | public static int GetTeenCount(this District district) 39 | { 40 | return (int)district.m_teenData.m_finalCount; 41 | } 42 | 43 | public static int GetYoungAdultCount(this District district) 44 | { 45 | return (int)district.m_youngData.m_finalCount; 46 | } 47 | 48 | public static int GetAdultCount(this District district) 49 | { 50 | return (int)district.m_adultData.m_finalCount; 51 | } 52 | 53 | public static int GetSeniorCount(this District district) 54 | { 55 | return (int)district.m_seniorData.m_finalCount; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /CityWebServer/RequestHandlers/VehicleRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using CityWebServer.Extensibility; 6 | using ColossalFramework; 7 | using JetBrains.Annotations; 8 | 9 | namespace CityWebServer.RequestHandlers 10 | { 11 | [UsedImplicitly] 12 | public class VehicleRequestHandler : RequestHandlerBase 13 | { 14 | public VehicleRequestHandler(IWebServer server) 15 | : base(server, new Guid("2be6546a-d416-4939-8e08-1d0b739be835"), "Vehicle", "Rychard", 100, "/Vehicle") 16 | { 17 | } 18 | 19 | public override IResponseFormatter Handle(HttpListenerRequest request) 20 | { 21 | var vehicleManager = Singleton.instance; 22 | 23 | if (request.Url.AbsolutePath.StartsWith("/Vehicle/List")) 24 | { 25 | List vehicleIds = new List(); 26 | 27 | var len = vehicleManager.m_vehicles.m_buffer.Length; 28 | for (ushort i = 0; i < len; i++) 29 | { 30 | if (vehicleManager.m_vehicles.m_buffer[i].m_flags == Vehicle.Flags.None) { continue; } 31 | 32 | vehicleIds.Add(i); 33 | } 34 | 35 | return JsonResponse(vehicleIds); 36 | } 37 | 38 | List s = new List(); 39 | 40 | foreach (var vehicle in vehicleManager.m_vehicles.m_buffer) 41 | { 42 | if (vehicle.m_flags == Vehicle.Flags.None) { continue; } 43 | 44 | if ((vehicle.m_flags & Vehicle.Flags.Spawned) == Vehicle.Flags.Spawned && (vehicle.m_flags & Vehicle.Flags.Created) == Vehicle.Flags.Created) 45 | { 46 | var origin = (vehicle.m_sourceBuilding); 47 | var target = (vehicle.m_targetBuilding); 48 | 49 | if (origin > 0) { s.Add(origin); } 50 | if (target > 0) { s.Add(target); } 51 | } 52 | } 53 | 54 | var grouped = s.GroupBy(obj => obj).Select(group => new { BuildingID = group.Key, Count = group.Count() }).OrderByDescending(obj => obj.Count).Select(obj => new { Building = BuildingManager.instance.GetBuildingName(obj.BuildingID, new InstanceID()), obj.Count }).ToList(); 55 | 56 | return JsonResponse(grouped); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /CityWebServer/Retrievers/ChirpRetriever.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using CityWebServer.Extensibility; 5 | using CityWebServer.Models; 6 | using ColossalFramework; 7 | using ICities; 8 | 9 | namespace CityWebServer.Retrievers 10 | { 11 | public class ChirpRetriever : ILogAppender 12 | { 13 | public event EventHandler LogMessage; 14 | 15 | private void OnLogMessage(String message) 16 | { 17 | var handler = LogMessage; 18 | if (handler != null) 19 | { 20 | handler(this, new LogAppenderEventArgs(message)); 21 | } 22 | } 23 | 24 | private readonly MessageManager _manager; 25 | private List _messages; 26 | 27 | public ChirperMessage[] Messages 28 | { 29 | get { return _messages.ToArray(); } 30 | set { _messages = value.ToList(); } 31 | } 32 | 33 | public ChirpRetriever() 34 | { 35 | _manager = Singleton.instance; 36 | _manager.m_messagesUpdated += ManagerOnMMessagesUpdated; 37 | _manager.m_newMessages += ManagerOnMNewMessages; 38 | _messages = new List(); 39 | } 40 | 41 | private void ManagerOnMNewMessages(IChirperMessage message) 42 | { 43 | try 44 | { 45 | var msg = new ChirperMessage 46 | { 47 | SenderID = (int)message.senderID, 48 | SenderName = message.senderName, 49 | Text = message.text 50 | }; 51 | _messages.Add(msg); 52 | } 53 | catch (Exception ex) 54 | { 55 | OnLogMessage(ex.ToString()); 56 | } 57 | } 58 | 59 | private void ManagerOnMMessagesUpdated() 60 | { 61 | try 62 | { 63 | var messages = _manager.GetRecentMessages(); 64 | _messages = messages.Select(obj => new ChirperMessage 65 | { 66 | SenderID = (int)obj.GetSenderID(), 67 | SenderName = obj.GetSenderName(), 68 | Text = obj.GetText(), 69 | }).ToList(); 70 | } 71 | catch (Exception ex) 72 | { 73 | OnLogMessage(ex.ToString()); 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /CityWebServer.Extensibility/RestfulRequestHandlerBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace CityWebServer.Extensibility 5 | { 6 | public abstract class RestfulRequestHandlerBase : RequestHandlerBase 7 | { 8 | public RestfulRequestHandlerBase(IWebServer server, Guid handlerID, String name, String author, int priority, String mainPath) 9 | : base(server, handlerID, name, author, priority, mainPath) 10 | { 11 | } 12 | 13 | public override Guid HandlerID { get { return _handlerID; } } 14 | 15 | public override int Priority { get { return _priority; } } 16 | 17 | public override string Name { get { return _name; } } 18 | 19 | public override string Author { get { return _author; } } 20 | 21 | public override string MainPath { get { return _mainPath; } } 22 | 23 | public override bool ShouldHandle(HttpListenerRequest request) 24 | { 25 | return (request.Url.AbsolutePath.StartsWith(_mainPath, StringComparison.OrdinalIgnoreCase)); 26 | } 27 | 28 | public override IResponseFormatter Handle(HttpListenerRequest request) 29 | { 30 | switch (request.HttpMethod) 31 | { 32 | case "GET": 33 | return HandleGetRequest(request); 34 | 35 | case "POST": 36 | return HandlePostRequest(request); 37 | 38 | case "PUT": 39 | return HandlePutRequest(request); 40 | 41 | case "DELETE": 42 | return HandleDeleteRequest(request); 43 | 44 | default: 45 | return JsonResponse("400 Bad Request", HttpStatusCode.BadRequest); 46 | } 47 | } 48 | 49 | protected virtual IResponseFormatter HandleGetRequest(HttpListenerRequest request) 50 | { 51 | return JsonResponse("405 Method Not Allowed", HttpStatusCode.MethodNotAllowed); 52 | } 53 | 54 | protected virtual IResponseFormatter HandlePostRequest(HttpListenerRequest request) 55 | { 56 | return JsonResponse("405 Method Not Allowed", HttpStatusCode.MethodNotAllowed); 57 | } 58 | 59 | protected virtual IResponseFormatter HandlePutRequest(HttpListenerRequest request) 60 | { 61 | return JsonResponse("405 Method Not Allowed", HttpStatusCode.MethodNotAllowed); 62 | } 63 | 64 | protected virtual IResponseFormatter HandleDeleteRequest(HttpListenerRequest request) 65 | { 66 | return JsonResponse("405 Method Not Allowed", HttpStatusCode.MethodNotAllowed); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /CityWebServer/RequestHandlers/TransportRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using CityWebServer.Extensibility; 6 | using CityWebServer.Models; 7 | using ColossalFramework; 8 | using JetBrains.Annotations; 9 | 10 | namespace CityWebServer.RequestHandlers 11 | { 12 | [UsedImplicitly] 13 | public class TransportRequestHandler : RequestHandlerBase 14 | { 15 | public TransportRequestHandler(IWebServer server) 16 | : base(server, new Guid("89c8ef27-fc8c-4fe8-9793-1f6432feb179"), "Transport", "Rychard", 100, "/Transport") 17 | { 18 | } 19 | 20 | public override IResponseFormatter Handle(HttpListenerRequest request) 21 | { 22 | var transportManager = Singleton.instance; 23 | 24 | var lines = transportManager.m_lines.m_buffer; 25 | List lineModels = new List(); 26 | 27 | foreach (var line in lines) 28 | { 29 | if (line.m_flags == TransportLine.Flags.None) { continue; } 30 | 31 | var passengers = line.m_passengers; 32 | List passengerGroups = new List 33 | { 34 | new PopulationGroup("Child", (int) passengers.m_childPassengers.m_finalCount), 35 | new PopulationGroup("Teen", (int) passengers.m_teenPassengers.m_finalCount), 36 | new PopulationGroup("Young Adult", (int) passengers.m_youngPassengers.m_finalCount), 37 | new PopulationGroup("Adult", (int) passengers.m_adultPassengers.m_finalCount), 38 | new PopulationGroup("Senior", (int) passengers.m_seniorPassengers.m_finalCount), 39 | new PopulationGroup("Tourist", (int) passengers.m_touristPassengers.m_finalCount), 40 | new PopulationGroup("Resident", (int) passengers.m_residentPassengers.m_finalCount), 41 | new PopulationGroup("Car-Owning", (int) passengers.m_carOwningPassengers.m_finalCount) 42 | }; 43 | 44 | var stops = line.CountStops(0); // The parameter is never used. 45 | var vehicles = line.CountVehicles(0); // The parameter is never used. 46 | 47 | var lineModel = new PublicTransportLine 48 | { 49 | Name = String.Format("{0} {1}", line.Info.name, (int)line.m_lineNumber), 50 | StopCount = stops, 51 | VehicleCount = vehicles, 52 | Passengers = passengerGroups.ToArray(), 53 | }; 54 | lineModels.Add(lineModel); 55 | } 56 | 57 | lineModels = lineModels.OrderBy(obj => obj.Name).ToList(); 58 | 59 | return JsonResponse(lineModels); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /CityWebServer/WebServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | 5 | namespace CityWebServer 6 | { 7 | public class WebServer 8 | { 9 | private readonly HttpListener _listener = new HttpListener(); 10 | private readonly Action _responderMethod; 11 | 12 | public WebServer(String[] prefixes, Action method) 13 | { 14 | if (!HttpListener.IsSupported) { throw new NotSupportedException("This wouldn't happen if you upgraded your operating system more than once a decade."); } 15 | 16 | // URI prefixes are required, for example: 17 | // "http://localhost:8080/index/". 18 | if (prefixes == null || prefixes.Length == 0) { throw new ArgumentException("prefixes"); } 19 | 20 | // A responder method is required 21 | if (method == null) { throw new ArgumentException("method"); } 22 | 23 | foreach (String s in prefixes) 24 | { 25 | _listener.Prefixes.Add(s); 26 | } 27 | 28 | _responderMethod = method; 29 | _listener.Start(); 30 | } 31 | 32 | public WebServer(Action method, params String[] prefixes) 33 | : this(prefixes, method) 34 | { 35 | } 36 | 37 | public void Run() 38 | { 39 | ThreadPool.QueueUserWorkItem(o => 40 | { 41 | try 42 | { 43 | while (_listener.IsListening) 44 | { 45 | ThreadPool.QueueUserWorkItem(RequestHandlerCallback, _listener.GetContext()); 46 | } 47 | } 48 | catch { } // Suppress exceptions. 49 | }); 50 | } 51 | 52 | private void RequestHandlerCallback(Object context) 53 | { 54 | var ctx = context as HttpListenerContext; 55 | try 56 | { 57 | if (ctx != null) 58 | { 59 | var request = ctx.Request; 60 | var response = ctx.Response; 61 | 62 | // Allow accessing pages from pages hosted from another local web-server, such as IIS, for instance. 63 | response.AddHeader("Access-Control-Allow-Origin", "http://localhost"); 64 | 65 | _responderMethod(request, response); 66 | } 67 | } 68 | catch { } // Suppress any exceptions. 69 | finally 70 | { 71 | if (ctx != null) 72 | { 73 | // Ensure that the stream is never left open. 74 | ctx.Response.OutputStream.Close(); 75 | } 76 | } 77 | } 78 | 79 | public void Stop() 80 | { 81 | _listener.Stop(); 82 | _listener.Close(); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /CityWebServer/wwwroot/script.js: -------------------------------------------------------------------------------- 1 | function initializeChart() { 2 | var c = new Highcharts.Chart({ 3 | chart: { 4 | renderTo: 'chart', 5 | defaultSeriesType: 'spline', 6 | events: { } 7 | }, 8 | title: { 9 | text: 'Statistics' 10 | }, 11 | xAxis: { 12 | type: 'datetime', 13 | tickPixelInterval: 150 14 | }, 15 | yAxis: { 16 | minPadding: 0.2, 17 | maxPadding: 0.2, 18 | title: { 19 | text: 'Value', 20 | margin: 80 21 | } 22 | } 23 | }); 24 | return c; 25 | } 26 | 27 | function deleteUnusedSeries(chart, seriesArray) 28 | { 29 | // seriesArray is an array that contains only the NAMES of the series that were updated this tick. 30 | 31 | //for (var j = 0; j < chart.series.length; j++) { 32 | // var series = chart.series[j]; 33 | // var isUsed = false; 34 | // for (var i = 0; i < seriesArray.length; i++) { 35 | // var usedSeriesName = seriesArray[i]; 36 | // if (series.name == usedSeriesName) { 37 | // isUsed = true; 38 | // } 39 | // } 40 | 41 | // if (!isUsed) { 42 | // //console.log("Removing: " + chart.series[j].name); 43 | // chart.series[j].remove(); 44 | // j = -1; 45 | // } 46 | 47 | //} 48 | } 49 | 50 | function addOrUpdateSeries(theChart, seriesName, value, valueName) 51 | { 52 | var series; 53 | var matchFound = false; 54 | if(theChart.series.length > 0) 55 | { 56 | for(var s = 0; s < theChart.series.length; s++) 57 | { 58 | if(theChart.series[s].name == seriesName) 59 | { 60 | series = theChart.series[s]; 61 | matchFound = true; 62 | s = theChart.series.length; // Stop looping 63 | } 64 | } 65 | } 66 | 67 | if(!matchFound) 68 | { 69 | //console.log("Adding series: " + seriesName); 70 | var seriesOptions = { 71 | id: seriesName, 72 | name: seriesName, 73 | data: [{ name: valueName, y: value}] 74 | }; 75 | series = theChart.addSeries(seriesOptions, false); 76 | } 77 | else 78 | { 79 | var shift = series.data.length > 20; 80 | series.addPoint(value, true, shift); 81 | } 82 | } 83 | 84 | function updateChart(vm, chart) 85 | { 86 | var updatedSeries = []; 87 | var districts = vm.Districts(); 88 | for(var i = 0; i < districts.length; i++) 89 | { 90 | var district = districts[i]; 91 | var districtName = district.DistrictName(); 92 | 93 | var seriesName = districtName + " - Population"; 94 | var population = district.TotalPopulationCount(); 95 | addOrUpdateSeries(chart, seriesName, population, vm.Time()); 96 | updatedSeries.push(seriesName); 97 | deleteUnusedSeries(chart, updatedSeries); 98 | } 99 | chart.redraw(); 100 | } -------------------------------------------------------------------------------- /CityWebServer.Extensibility/CityWebServer.Extensibility.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {DB96EFB4-FA45-4ACC-8D51-7ED37065CC79} 8 | Library 9 | Properties 10 | CityWebServer.Extensibility 11 | CityWebServer.Extensibility 12 | v3.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | ..\packages\JsonFx.2.0.1209.2802\lib\net35\JsonFx.dll 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 67 | -------------------------------------------------------------------------------- /SampleWebServerExtension/SampleWebServerExtension.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {7DF15DF6-C475-4866-9111-F5150C1336E1} 8 | Library 9 | Properties 10 | SampleWebServerExtension 11 | SampleWebServerExtension 12 | v3.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | ..\Assemblies\ICities.dll 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {db96efb4-fa45-4acc-8d51-7ed37065cc79} 51 | CityWebServer.Extensibility 52 | 53 | 54 | 55 | 56 | mkdir "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)_$(ProjectName)" 57 | del "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)_$(ProjectName)\$(TargetFileName)" 58 | xcopy /Y "$(TargetPath)" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)_$(ProjectName)" 59 | xcopy /Y "$(TargetDir)CityWebServer.Extensibility.dll" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)_$(ProjectName)\*.*" 60 | 61 | 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | [Rr]eleases/ 14 | x64/ 15 | x86/ 16 | build/ 17 | bld/ 18 | [Bb]in/ 19 | [Oo]bj/ 20 | 21 | # Roslyn cache directories 22 | *.ide/ 23 | 24 | # MSTest test Results 25 | [Tt]est[Rr]esult*/ 26 | [Bb]uild[Ll]og.* 27 | 28 | #NUNIT 29 | *.VisualState.xml 30 | TestResult.xml 31 | 32 | # Build Results of an ATL Project 33 | [Dd]ebugPS/ 34 | [Rr]eleasePS/ 35 | dlldata.c 36 | 37 | *_i.c 38 | *_p.c 39 | *_i.h 40 | *.ilk 41 | *.meta 42 | *.obj 43 | *.pch 44 | *.pdb 45 | *.pgc 46 | *.pgd 47 | *.rsp 48 | *.sbr 49 | *.tlb 50 | *.tli 51 | *.tlh 52 | *.tmp 53 | *.tmp_proj 54 | *.log 55 | *.vspscc 56 | *.vssscc 57 | .builds 58 | *.pidb 59 | *.svclog 60 | *.scc 61 | 62 | # Chutzpah Test files 63 | _Chutzpah* 64 | 65 | # Visual C++ cache files 66 | ipch/ 67 | *.aps 68 | *.ncb 69 | *.opensdf 70 | *.sdf 71 | *.cachefile 72 | 73 | # Visual Studio profiler 74 | *.psess 75 | *.vsp 76 | *.vspx 77 | 78 | # TFS 2012 Local Workspace 79 | $tf/ 80 | 81 | # Guidance Automation Toolkit 82 | *.gpState 83 | 84 | # ReSharper is a .NET coding add-in 85 | _ReSharper*/ 86 | *.[Rr]e[Ss]harper 87 | *.DotSettings.user 88 | 89 | # JustCode is a .NET coding addin-in 90 | .JustCode 91 | 92 | # TeamCity is a build add-in 93 | _TeamCity* 94 | 95 | # DotCover is a Code Coverage Tool 96 | *.dotCover 97 | 98 | # NCrunch 99 | _NCrunch_* 100 | .*crunch*.local.xml 101 | 102 | # MightyMoose 103 | *.mm.* 104 | AutoTest.Net/ 105 | 106 | # Web workbench (sass) 107 | .sass-cache/ 108 | 109 | # Installshield output folder 110 | [Ee]xpress/ 111 | 112 | # DocProject is a documentation generator add-in 113 | DocProject/buildhelp/ 114 | DocProject/Help/*.HxT 115 | DocProject/Help/*.HxC 116 | DocProject/Help/*.hhc 117 | DocProject/Help/*.hhk 118 | DocProject/Help/*.hhp 119 | DocProject/Help/Html2 120 | DocProject/Help/html 121 | 122 | # Click-Once directory 123 | publish/ 124 | 125 | # Publish Web Output 126 | *.[Pp]ublish.xml 127 | *.azurePubxml 128 | # TODO: Comment the next line if you want to checkin your web deploy settings 129 | # but database connection strings (with potential passwords) will be unencrypted 130 | *.pubxml 131 | *.publishproj 132 | 133 | # NuGet Packages 134 | *.nupkg 135 | # The packages folder can be ignored because of Package Restore 136 | **/packages/* 137 | # except build/, which is used as an MSBuild target. 138 | !**/packages/build/ 139 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 140 | #!**/packages/repositories.config 141 | 142 | # Windows Azure Build Output 143 | csx/ 144 | *.build.csdef 145 | 146 | # Windows Store app package directory 147 | AppPackages/ 148 | 149 | # Others 150 | sql/ 151 | *.Cache 152 | ClientBin/ 153 | [Ss]tyle[Cc]op.* 154 | ~$* 155 | *~ 156 | *.dbmdl 157 | *.dbproj.schemaview 158 | *.pfx 159 | *.publishsettings 160 | node_modules/ 161 | 162 | # RIA/Silverlight projects 163 | Generated_Code/ 164 | 165 | # Backup & report files from converting an old project file 166 | # to a newer Visual Studio version. Backup files are not needed, 167 | # because we have git ;-) 168 | _UpgradeReport_Files/ 169 | Backup*/ 170 | UpgradeLog*.XML 171 | UpgradeLog*.htm 172 | 173 | # SQL Server files 174 | *.mdf 175 | *.ldf 176 | 177 | # Business Intelligence projects 178 | *.rdl.data 179 | *.bim.layout 180 | *.bim_*.settings 181 | 182 | # Microsoft Fakes 183 | FakesAssemblies/ 184 | 185 | # Cities: Skylines Assemblies 186 | Assembly-CSharp.dll 187 | ColossalManaged.dll 188 | ICities.dll 189 | UnityEngine.dll 190 | UnityEngine.UI.dll 191 | -------------------------------------------------------------------------------- /CityWebServer/Helpers/TemplateHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using CityWebServer.Extensibility; 6 | using ColossalFramework.Plugins; 7 | 8 | namespace CityWebServer.Helpers 9 | { 10 | public static class TemplateHelper 11 | { 12 | /// 13 | /// Gets the full path of the directory that contains this assembly. 14 | /// 15 | public static String GetModPath() 16 | { 17 | var modPaths = PluginManager.instance.GetPluginsInfo().Select(obj => obj.modPath); 18 | 19 | foreach (var path in modPaths) 20 | { 21 | var indexPath = Path.Combine(path, "index.html"); 22 | if (File.Exists(indexPath)) 23 | { 24 | return indexPath; 25 | } 26 | } 27 | return null; 28 | } 29 | 30 | /// 31 | /// Gets the full content of a template. 32 | /// 33 | public static String GetTemplate(String template) 34 | { 35 | // Templates seem like something we shouldn't handle internally. 36 | // Perhaps we should force request handlers to implement their own templating if they so desire, and maintain a more "API" approach within the core. 37 | String modPath = GetModPath(); 38 | String templatePath = Path.Combine(modPath, "wwwroot"); 39 | String specifiedTemplatePath = String.Format("{0}{1}{2}.html", templatePath, Path.DirectorySeparatorChar, template); 40 | 41 | if (File.Exists(specifiedTemplatePath)) 42 | { 43 | String templateContents = File.ReadAllText(specifiedTemplatePath); 44 | return templateContents; 45 | } 46 | 47 | // All templates must at least have a #PAGEBODY# token. 48 | // If we can't find the specified template, just return a string that contains only that. 49 | return "#PAGEBODY#"; 50 | } 51 | 52 | /// 53 | /// Retrieves the template with the specified name, and returns the contents of the template after replacing instances of the dictionary keys from with their coorresponding values. 54 | /// 55 | /// The name of the template to populate. 56 | /// A dictionary containing key/value pairs for replacement. 57 | /// 58 | /// The value of should not include the file extension. 59 | /// 60 | public static String PopulateTemplate(String template, Dictionary tokenReplacements) 61 | { 62 | try 63 | { 64 | String templateContents = GetTemplate(template); 65 | foreach (var tokenReplacement in tokenReplacements) 66 | { 67 | templateContents = templateContents.Replace(tokenReplacement.Key, tokenReplacement.Value); 68 | } 69 | return templateContents; 70 | } 71 | catch (Exception ex) 72 | { 73 | IntegratedWebServer.LogMessage(ex.ToString()); 74 | return tokenReplacements["#PAGEBODY#"]; 75 | } 76 | } 77 | 78 | /// 79 | /// Gets a dictionary that contains standard replacement tokens using the specified values. 80 | /// 81 | public static Dictionary GetTokenReplacements(String cityName, String title, List handlers, String body) 82 | { 83 | var orderedHandlers = handlers.OrderBy(obj => obj.Priority).ThenBy(obj => obj.Name); 84 | var handlerLinks = orderedHandlers.Select(obj => String.Format("
  • {1}
  • ", obj.MainPath, obj.Name)).ToArray(); 85 | String nav = String.Join(Environment.NewLine, handlerLinks); 86 | 87 | return new Dictionary 88 | { 89 | { "#PAGETITLE#", title }, 90 | { "#NAV#", nav}, 91 | { "#CSS#", ""}, // Moved directly into the template. 92 | { "#PAGEBODY#", body}, 93 | { "#CITYNAME#", cityName}, 94 | { "#JS#", ""}, // Moved directly into the template. 95 | }; 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /CityWebServer.Extensibility/RequestHandlerBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using CityWebServer.Extensibility.Responses; 4 | 5 | namespace CityWebServer.Extensibility 6 | { 7 | public abstract class RequestHandlerBase : IRequestHandler, ILogAppender 8 | { 9 | #region ILogAppender Implementation 10 | 11 | public event EventHandler LogMessage; 12 | 13 | protected void OnLogMessage(String message) 14 | { 15 | var handler = LogMessage; 16 | if (handler != null) 17 | { 18 | handler(this, new LogAppenderEventArgs(message)); 19 | } 20 | } 21 | 22 | #endregion ILogAppender Implementation 23 | 24 | protected readonly IWebServer _server; 25 | protected Guid _handlerID; 26 | protected int _priority; 27 | protected String _name; 28 | protected String _author; 29 | protected String _mainPath; 30 | 31 | private RequestHandlerBase() 32 | { 33 | } 34 | 35 | protected RequestHandlerBase(IWebServer server, Guid handlerID, String name, String author, int priority, String mainPath) 36 | { 37 | _server = server; 38 | _handlerID = handlerID; 39 | _name = name; 40 | _author = author; 41 | _priority = priority; 42 | _mainPath = mainPath; 43 | } 44 | 45 | /// 46 | /// Gets the server that is currently servicing this instance. 47 | /// 48 | public virtual IWebServer Server { get { return _server; } } 49 | 50 | /// 51 | /// Gets a unique identifier for this handler. Only one handler can be loaded with a given identifier. 52 | /// 53 | public virtual Guid HandlerID { get { return _handlerID; } } 54 | 55 | /// 56 | /// Gets the priority of this request handler. A request will be handled by the request handler with the lowest priority. 57 | /// 58 | public virtual int Priority { get { return _priority; } } 59 | 60 | /// 61 | /// Gets the display name of this request handler. 62 | /// 63 | public virtual String Name { get { return _name; } } 64 | 65 | /// 66 | /// Gets the author of this request handler. 67 | /// 68 | public virtual String Author { get { return _author; } } 69 | 70 | /// 71 | /// Gets the absolute path to the main page for this request handler. Your class is responsible for handling requests at this path. 72 | /// 73 | /// 74 | /// When set to a value other than null, the Web Server will show this url as a link on the home page. 75 | /// 76 | public virtual String MainPath { get { return _mainPath; } } 77 | 78 | /// 79 | /// Returns a value that indicates whether this handler is capable of servicing the given request. 80 | /// 81 | public virtual Boolean ShouldHandle(HttpListenerRequest request) 82 | { 83 | return (request.Url.AbsolutePath.Equals(_mainPath, StringComparison.OrdinalIgnoreCase)); 84 | } 85 | 86 | /// 87 | /// Handles the specified request. The method should not close the stream. 88 | /// 89 | public abstract IResponseFormatter Handle(HttpListenerRequest request); 90 | 91 | /// 92 | /// Returns a response in JSON format. 93 | /// 94 | protected IResponseFormatter JsonResponse(T content, HttpStatusCode statusCode = HttpStatusCode.OK) 95 | { 96 | return new JsonResponseFormatter(content, statusCode); 97 | } 98 | 99 | /// 100 | /// Returns a response in HTML format. 101 | /// 102 | protected IResponseFormatter HtmlResponse(String content, HttpStatusCode statusCode = HttpStatusCode.OK) 103 | { 104 | return new HtmlResponseFormatter(content, statusCode); 105 | } 106 | 107 | /// 108 | /// Returns a response in plain text format. 109 | /// 110 | protected IResponseFormatter PlainTextResponse(String content, HttpStatusCode statusCode = HttpStatusCode.OK) 111 | { 112 | return new PlainTextResponseFormatter(content, statusCode); 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /CityWebServer/Models/DistrictInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using CityWebServer.Helpers; 5 | using ColossalFramework; 6 | 7 | namespace CityWebServer.Models 8 | { 9 | public class DistrictInfo 10 | { 11 | public int DistrictID { get; set; } 12 | 13 | public String DistrictName { get; set; } 14 | 15 | public PopulationGroup[] PopulationData { get; set; } 16 | 17 | public int TotalPopulationCount { get; set; } 18 | 19 | public int TotalBuildingCount { get; set; } 20 | 21 | public int TotalVehicleCount { get; set; } 22 | 23 | public int CurrentHouseholds { get; set; } 24 | 25 | public int AvailableHouseholds { get; set; } 26 | 27 | public int CurrentJobs { get; set; } 28 | 29 | public int AvailableJobs { get; set; } 30 | 31 | public int WeeklyTouristVisits { get; set; } 32 | 33 | public int AverageLandValue { get; set; } 34 | 35 | public Double Pollution { get; set; } 36 | 37 | public PolicyInfo[] Policies { get; set; } 38 | 39 | public static IEnumerable GetDistricts() 40 | { 41 | var districtManager = Singleton.instance; 42 | 43 | // This is the value used in Assembly-CSharp, so I presume that's the maximum number of districts allowed. 44 | const int count = 128; 45 | 46 | var districts = districtManager.m_districts.m_buffer; 47 | 48 | for (int i = 0; i < count; i++) 49 | { 50 | if (!districts[i].IsAlive()) { continue; } 51 | yield return i; 52 | } 53 | } 54 | 55 | public static DistrictInfo GetDistrictInfo(int districtID) 56 | { 57 | var districtManager = Singleton.instance; 58 | var district = GetDistrict(districtID); 59 | 60 | if (!district.IsValid()) { return null; } 61 | 62 | String districtName = String.Empty; 63 | 64 | if (districtID == 0) 65 | { 66 | // The district with ID 0 is always the global district. 67 | // It receives an auto-generated name by default, but the game always displays the city name instead. 68 | districtName = "City"; 69 | } 70 | else 71 | { 72 | districtName = districtManager.GetDistrictName(districtID); 73 | } 74 | 75 | var pollution = Math.Round((district.m_groundData.m_finalPollution / (Double) byte.MaxValue), 2); 76 | 77 | var model = new DistrictInfo 78 | { 79 | DistrictID = districtID, 80 | DistrictName = districtName, 81 | TotalPopulationCount = (int)district.m_populationData.m_finalCount, 82 | PopulationData = GetPopulationGroups(districtID), 83 | CurrentHouseholds = (int)district.m_residentialData.m_finalAliveCount, 84 | AvailableHouseholds = (int)district.m_residentialData.m_finalHomeOrWorkCount, 85 | CurrentJobs = (int)district.m_commercialData.m_finalAliveCount + (int)district.m_industrialData.m_finalAliveCount + (int)district.m_officeData.m_finalAliveCount + (int)district.m_playerData.m_finalAliveCount, 86 | AvailableJobs = (int)district.m_commercialData.m_finalHomeOrWorkCount + (int)district.m_industrialData.m_finalHomeOrWorkCount + (int)district.m_officeData.m_finalHomeOrWorkCount + (int)district.m_playerData.m_finalHomeOrWorkCount, 87 | AverageLandValue = district.GetLandValue(), 88 | Pollution = pollution, 89 | WeeklyTouristVisits = (int)district.m_tourist1Data.m_averageCount + (int)district.m_tourist2Data.m_averageCount + (int)district.m_tourist3Data.m_averageCount, 90 | Policies = GetPolicies().ToArray(), 91 | }; 92 | return model; 93 | } 94 | 95 | private static District GetDistrict(int? districtID = null) 96 | { 97 | if (districtID == null) { districtID = 0; } 98 | var districtManager = Singleton.instance; 99 | var district = districtManager.m_districts.m_buffer[districtID.Value]; 100 | return district; 101 | } 102 | 103 | private static PopulationGroup[] GetPopulationGroups(int? districtID = null) 104 | { 105 | var district = GetDistrict(districtID); 106 | return district.GetPopulation(); 107 | } 108 | 109 | private static IEnumerable GetPolicies() 110 | { 111 | var policies = EnumHelper.GetValues(); 112 | var districtManager = Singleton.instance; 113 | 114 | foreach (var policy in policies) 115 | { 116 | String policyName = Enum.GetName(typeof(DistrictPolicies.Policies), policy); 117 | Boolean isEnabled = districtManager.IsCityPolicySet(policy); 118 | yield return new PolicyInfo 119 | { 120 | Name = policyName, 121 | Enabled = isEnabled 122 | }; 123 | } 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /CityWebServer/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Cities: Skylines - Test 9 | 10 | 13 | 14 | 15 | 16 | 30 |
    31 |

    Cities: Skylines - Integrated Web Server

    32 |
    33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
    District IDNamePopulationBuildingsVehiclesHouseholds (Max)Jobs (Max)Weekly TouristsLand ValuePollution
    () ()
    Total () ()
    80 |
    81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 122 | 123 | -------------------------------------------------------------------------------- /CityWebServer/RequestHandlers/CityInfoRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using CityWebServer.Extensibility; 6 | using CityWebServer.Helpers; 7 | using CityWebServer.Models; 8 | using ColossalFramework; 9 | using JetBrains.Annotations; 10 | 11 | namespace CityWebServer.RequestHandlers 12 | { 13 | [UsedImplicitly] 14 | public class CityInfoRequestHandler : RequestHandlerBase 15 | { 16 | public CityInfoRequestHandler(IWebServer server) 17 | : base(server, new Guid("eeada0d0-f1d2-43b0-9595-2a6a4d917631"), "City Info", "Rychard", 100, "/CityInfo") 18 | { 19 | } 20 | 21 | public override IResponseFormatter Handle(HttpListenerRequest request) 22 | { 23 | if (request.QueryString.HasKey("showList")) 24 | { 25 | return HandleDistrictList(); 26 | } 27 | 28 | return HandleDistrict(request); 29 | } 30 | 31 | private IResponseFormatter HandleDistrictList() 32 | { 33 | var districtIDs = DistrictInfo.GetDistricts().ToArray(); 34 | 35 | return JsonResponse(districtIDs); 36 | } 37 | 38 | private IResponseFormatter HandleDistrict(HttpListenerRequest request) 39 | { 40 | var districtIDs = GetDistrictsFromRequest(request); 41 | 42 | DistrictInfo globalDistrictInfo = null; 43 | List districtInfoList = new List(); 44 | 45 | var buildings = GetBuildingBreakdownByDistrict(); 46 | var vehicles = GetVehicleBreakdownByDistrict(); 47 | 48 | foreach (var districtID in districtIDs) 49 | { 50 | var districtInfo = DistrictInfo.GetDistrictInfo(districtID); 51 | if (districtID == 0) 52 | { 53 | districtInfo.TotalBuildingCount = buildings.Sum(obj => obj.Value); 54 | districtInfo.TotalVehicleCount = vehicles.Sum(obj => obj.Value); 55 | globalDistrictInfo = districtInfo; 56 | } 57 | else 58 | { 59 | districtInfo.TotalBuildingCount = buildings.Where(obj => obj.Key == districtID).Sum(obj => obj.Value); 60 | districtInfo.TotalVehicleCount = vehicles.Where(obj => obj.Key == districtID).Sum(obj => obj.Value); 61 | districtInfoList.Add(districtInfo); 62 | } 63 | } 64 | 65 | var simulationManager = Singleton.instance; 66 | 67 | var cityInfo = new CityInfo 68 | { 69 | Name = simulationManager.m_metaData.m_CityName, 70 | Time = simulationManager.m_currentGameTime.Date, 71 | GlobalDistrict = globalDistrictInfo, 72 | Districts = districtInfoList.ToArray(), 73 | }; 74 | 75 | return JsonResponse(cityInfo); 76 | } 77 | 78 | private Dictionary GetBuildingBreakdownByDistrict() 79 | { 80 | var districtManager = Singleton.instance; 81 | 82 | Dictionary districtBuildings = new Dictionary(); 83 | BuildingManager instance = Singleton.instance; 84 | foreach (Building building in instance.m_buildings.m_buffer) 85 | { 86 | if (building.m_flags == Building.Flags.None) { continue; } 87 | var districtID = (int)districtManager.GetDistrict(building.m_position); 88 | if (districtBuildings.ContainsKey(districtID)) 89 | { 90 | districtBuildings[districtID]++; 91 | } 92 | else 93 | { 94 | districtBuildings.Add(districtID, 1); 95 | } 96 | } 97 | return districtBuildings; 98 | } 99 | 100 | private Dictionary GetVehicleBreakdownByDistrict() 101 | { 102 | var districtManager = Singleton.instance; 103 | 104 | Dictionary districtVehicles = new Dictionary(); 105 | VehicleManager vehicleManager = Singleton.instance; 106 | foreach (Vehicle vehicle in vehicleManager.m_vehicles.m_buffer) 107 | { 108 | if (vehicle.m_flags != Vehicle.Flags.None) 109 | { 110 | var districtID = (int)districtManager.GetDistrict(vehicle.GetLastFramePosition()); 111 | if (districtVehicles.ContainsKey(districtID)) 112 | { 113 | districtVehicles[districtID]++; 114 | } 115 | else 116 | { 117 | districtVehicles.Add(districtID, 1); 118 | } 119 | } 120 | } 121 | return districtVehicles; 122 | } 123 | 124 | private IEnumerable GetDistrictsFromRequest(HttpListenerRequest request) 125 | { 126 | IEnumerable districtIDs; 127 | if (request.QueryString.HasKey("districtID")) 128 | { 129 | List districtIDList = new List(); 130 | var districtID = request.QueryString.GetInteger("districtID"); 131 | if (districtID.HasValue) 132 | { 133 | districtIDList.Add(districtID.Value); 134 | } 135 | districtIDs = districtIDList; 136 | } 137 | else 138 | { 139 | districtIDs = DistrictInfo.GetDistricts(); 140 | } 141 | return districtIDs; 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /CityWebServer/WebsiteButton.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using CityWebServer.Helpers; 4 | using ColossalFramework.UI; 5 | using ICities; 6 | using UnityEngine; 7 | 8 | namespace CityWebServer 9 | { 10 | /// 11 | /// Adds a button to the UI for quick access to the website. 12 | /// 13 | /// 14 | /// Most/All of this code was sourced from here: 15 | /// https://github.com/AlexanderDzhoganov/Skylines-FPSCamera/blob/master/FPSCamera/Mod.cs 16 | /// 17 | public class WebsiteButton : LoadingExtensionBase 18 | { 19 | private const String keyButtonPositionX = "browserButtonPositionX"; 20 | private const String keyButtonPositionY = "browserButtonPositionY"; 21 | 22 | private UIDragHandle _browserButtonDragHandle; 23 | private UIButton _browserButton; 24 | private UILabel _browserButtonLabel; 25 | private Vector2 _buttonPosition; 26 | private Boolean _useSavedPosition; 27 | 28 | public override void OnLevelLoaded(LoadMode mode) 29 | { 30 | if (mode != LoadMode.LoadGame && mode != LoadMode.NewGame) 31 | { 32 | base.OnLevelLoaded(mode); 33 | return; 34 | } 35 | 36 | // Get a reference to the game's UI. 37 | var uiView = UnityEngine.Object.FindObjectOfType(); 38 | 39 | var button = uiView.AddUIComponent(typeof(UIButton)); 40 | _browserButton = button as UIButton; 41 | 42 | // The object should *never* be null. 43 | // We call this a "sanity check". 44 | if (_browserButton == null) { return; } 45 | 46 | // Create a drag handler and attach it to our button. 47 | _browserButtonDragHandle = button.AddUIComponent(); 48 | _browserButtonDragHandle.target = _browserButton; 49 | 50 | _browserButton.width = 36; 51 | _browserButton.height = 36; 52 | _browserButton.pressedBgSprite = "OptionBasePressed"; 53 | _browserButton.normalBgSprite = "OptionBase"; 54 | _browserButton.hoveredBgSprite = "OptionBaseHovered"; 55 | _browserButton.disabledBgSprite = "OptionBaseDisabled"; 56 | _browserButton.normalFgSprite = "ToolbarIconZoomOutGlobe"; 57 | _browserButton.foregroundSpriteMode = UIForegroundSpriteMode.Scale; 58 | _browserButton.scaleFactor = 1.0f; 59 | _browserButton.tooltip = "Open Browser (Hold Shift to drag)"; 60 | _browserButton.tooltipBox = uiView.defaultTooltipBox; 61 | 62 | // If the user has moved the button, load their saved position data. 63 | if (Configuration.HasSetting(keyButtonPositionX) && Configuration.HasSetting(keyButtonPositionY)) 64 | { 65 | var buttonPositionX = Configuration.GetFloat(keyButtonPositionX); 66 | var buttonPositionY = Configuration.GetFloat(keyButtonPositionY); 67 | _buttonPosition = new Vector2(buttonPositionX, buttonPositionY); 68 | _useSavedPosition = true; 69 | } 70 | else 71 | { 72 | _useSavedPosition = false; 73 | } 74 | 75 | // Since we're on another thread, we can pretty safely spin until our object has been created. 76 | //while (_browserButton == null) { System.Threading.Thread.Sleep(100); } 77 | 78 | if (!_useSavedPosition) 79 | { 80 | // Get a reference to the game's UI. 81 | //var uiView = UnityEngine.Object.FindObjectOfType(); 82 | 83 | // The default position of the button is the middle of the screen. 84 | var buttonPositionX = (uiView.fixedWidth / 2f) + (_browserButton.width / 2f); 85 | var buttonPositionY = (uiView.fixedHeight / 2f) + (_browserButton.height / 2f); 86 | _buttonPosition = new Vector2(buttonPositionX, buttonPositionY); 87 | } 88 | _browserButton.absolutePosition = _buttonPosition; 89 | 90 | var labelObject = new GameObject(); 91 | labelObject.transform.parent = uiView.transform; 92 | 93 | _browserButtonLabel = labelObject.AddComponent(); 94 | _browserButtonLabel.textColor = new Color32(255, 255, 255, 255); 95 | _browserButtonLabel.transformPosition = new Vector3(1.15f, 0.90f); 96 | _browserButtonLabel.Hide(); 97 | 98 | RegisterEvents(); 99 | 100 | base.OnLevelLoaded(mode); 101 | } 102 | 103 | private void RegisterEvents() 104 | { 105 | // Accept button clicks. 106 | _browserButton.eventClick += OnBrowserButtonClick; 107 | _browserButton.eventMouseLeave += OnBrowserButtonMouseLeave; 108 | } 109 | 110 | private void OnBrowserButtonMouseLeave(UIComponent component, UIMouseEventParameter eventParam) 111 | { 112 | _buttonPosition = component.absolutePosition; 113 | 114 | Configuration.SetFloat(keyButtonPositionX, _buttonPosition.x); 115 | Configuration.SetFloat(keyButtonPositionY, _buttonPosition.y); 116 | Configuration.SaveSettings(); 117 | } 118 | 119 | private void OnBrowserButtonClick(UIComponent component, UIMouseEventParameter args) 120 | { 121 | if (Input.GetKey(KeyCode.LeftControl)) 122 | { 123 | // If the left control key is pressed, open the folder where the configuration file is located. 124 | var filePath = Configuration.GetSettingsFilePath(); 125 | var directory = System.IO.Path.GetDirectoryName(filePath); 126 | if (!String.IsNullOrEmpty(directory) && System.IO.Directory.Exists(directory)) 127 | { 128 | Process.Start(directory); 129 | } 130 | } 131 | else if (!Input.GetKey(KeyCode.LeftShift)) 132 | { 133 | // Accept clicks only when shift isn't pressed. 134 | var endpoint = String.Format("{0}index.html", IntegratedWebServer.Endpoint); 135 | Process.Start(endpoint); 136 | } 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /CityWebServer/CityWebServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {61A80D3C-8A1E-44DA-A044-A16F1ABBA149} 8 | Library 9 | Properties 10 | CityWebServer 11 | CityWebServer 12 | v3.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | ..\Assemblies\Assembly-CSharp.dll 35 | 36 | 37 | ..\Assemblies\ColossalManaged.dll 38 | 39 | 40 | ..\Assemblies\ICities.dll 41 | 42 | 43 | ..\packages\JsonFx.2.0.1209.2802\lib\net35\JsonFx.dll 44 | 45 | 46 | 47 | 48 | True 49 | 50 | 51 | 52 | 53 | False 54 | 55 | 56 | 57 | ..\Assemblies\UnityEngine.dll 58 | 59 | 60 | ..\Assemblies\UnityEngine.UI.dll 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | PreserveNewest 94 | 95 | 96 | PreserveNewest 97 | 98 | 99 | PreserveNewest 100 | 101 | 102 | PreserveNewest 103 | 104 | 105 | PreserveNewest 106 | 107 | 108 | PreserveNewest 109 | 110 | 111 | PreserveNewest 112 | 113 | 114 | 115 | 116 | {db96efb4-fa45-4acc-8d51-7ed37065cc79} 117 | CityWebServer.Extensibility 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | mkdir "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)_$(ProjectName)" 126 | del "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)_$(ProjectName)\$(TargetFileName)" 127 | xcopy /Y "$(TargetPath)" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)_$(ProjectName)" 128 | xcopy /Y "$(TargetDir)CityWebServer.Extensibility.dll" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)_$(ProjectName)\*.*" 129 | xcopy /Y "$(TargetDir)Jsonfx.dll" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)_$(ProjectName)\*.*" 130 | xcopy /Y /E "$(TargetDir)wwwroot\*.*" "%25LOCALAPPDATA%25\Colossal Order\Cities_Skylines\Addons\Mods\$(SolutionName)_$(ProjectName)\wwwroot\*.*" 131 | 132 | 139 | -------------------------------------------------------------------------------- /CityWebServer/Helpers/ConfigurationHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | using ColossalFramework.IO; 7 | using JsonFx.Serialization; 8 | 9 | namespace CityWebServer.Helpers 10 | { 11 | public static class Configuration 12 | { 13 | private static readonly Object LockerObject = new Object(); 14 | private static readonly String _filePath; 15 | private static List _settings; 16 | 17 | static Configuration() 18 | { 19 | _filePath = GetSettingsFilePath(); 20 | LoadSettings(); 21 | } 22 | 23 | public static String GetSettingsFilePath() 24 | { 25 | var localApplicationDataRoot = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); 26 | var vendorRoot = System.IO.Path.Combine(localApplicationDataRoot, "Colossal Order"); 27 | var appRoot = System.IO.Path.Combine(vendorRoot, "Cities_Skylines"); 28 | var filePath = System.IO.Path.Combine(appRoot, "ModSettings.json"); 29 | 30 | // This works for Windows, but not for OSX. 31 | if (CanAccess(filePath)) 32 | { 33 | return filePath; 34 | } 35 | 36 | // If we just use a filename, it will exist in the root directory of the game's files. Not ideal, but it'll work. 37 | return "ModSettings.json"; 38 | } 39 | 40 | private static Boolean CanAccess(String filePath) 41 | { 42 | try 43 | { 44 | using (var fileStream = System.IO.File.Open(filePath, FileMode.OpenOrCreate, FileAccess.Read)) 45 | { 46 | return true; 47 | } 48 | } 49 | catch (Exception) 50 | { 51 | return false; 52 | } 53 | } 54 | 55 | private static void LoadSettings() 56 | { 57 | lock (LockerObject) 58 | { 59 | using (var fileStream = System.IO.File.Open(_filePath, FileMode.OpenOrCreate, FileAccess.Read)) 60 | { 61 | using (TextReader tr = new StreamReader(fileStream)) 62 | { 63 | var jsonReader = new JsonFx.Json.JsonReader(); 64 | var deserialized = jsonReader.Read(tr, typeof(List)); 65 | var settings = deserialized as List; 66 | _settings = settings ?? new List(); 67 | } 68 | } 69 | } 70 | } 71 | 72 | public static void SaveSettings() 73 | { 74 | lock (LockerObject) 75 | { 76 | // No settings to save? Don't save anything. 77 | if (_settings == null) { return; } 78 | 79 | var dataWriterSettings = new DataWriterSettings 80 | { 81 | PrettyPrint = true, 82 | }; 83 | 84 | if (System.IO.File.Exists(_filePath)) 85 | { 86 | System.IO.File.Delete(_filePath); 87 | } 88 | using (var fileStream = System.IO.File.Open(_filePath, FileMode.CreateNew, FileAccess.Write)) 89 | { 90 | using (TextWriter tw = new StreamWriter(fileStream)) 91 | { 92 | var jsonWriter = new JsonFx.Json.JsonWriter(dataWriterSettings); 93 | jsonWriter.Write(_settings, tw); 94 | } 95 | } 96 | } 97 | } 98 | 99 | private static String GetSettingRaw(String key) 100 | { 101 | lock (LockerObject) 102 | { 103 | String raw; 104 | var matches = _settings.Where(obj => obj.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)).ToList(); 105 | if (matches.Any()) 106 | { 107 | raw = matches.First().Value; 108 | } 109 | else 110 | { 111 | raw = null; 112 | } 113 | return raw; 114 | } 115 | } 116 | 117 | private static void SetSettingRaw(String key, String value, String type) 118 | { 119 | lock (LockerObject) 120 | { 121 | var matches = _settings.Where(obj => obj.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)).ToList(); 122 | if (matches.Any()) 123 | { 124 | matches.First().Value = value; 125 | } 126 | else 127 | { 128 | _settings.Add(new Setting 129 | { 130 | Key = key, 131 | Value = value, 132 | Type = type 133 | }); 134 | } 135 | } 136 | } 137 | 138 | public static Boolean HasSetting(String key) 139 | { 140 | lock (LockerObject) 141 | { 142 | if (_settings == null) { throw new Exception("Settings aren't loaded!"); } 143 | var matches = _settings.Where(obj => obj.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)).ToList(); 144 | return (matches.Any()); 145 | } 146 | } 147 | 148 | public static Type GetSettingType(String key) 149 | { 150 | lock (LockerObject) 151 | { 152 | var matches = _settings.Where(obj => obj.Key.Equals(key, StringComparison.InvariantCultureIgnoreCase)).ToList(); 153 | if (matches.Any()) 154 | { 155 | var t = matches.First().Type; 156 | switch (t) 157 | { 158 | case "string": 159 | return typeof(string); 160 | 161 | case "int": 162 | return typeof(int); 163 | 164 | case "float": 165 | return typeof(float); 166 | 167 | case "double": 168 | return typeof(double); 169 | 170 | default: 171 | return typeof(object); 172 | } 173 | } 174 | return null; 175 | } 176 | } 177 | 178 | #region String 179 | 180 | public static String GetString(String key) 181 | { 182 | var raw = GetSettingRaw(key); 183 | return raw; 184 | } 185 | 186 | public static void SetString(String key, String value) 187 | { 188 | SetSettingRaw(key, value, "string"); 189 | } 190 | 191 | #endregion String 192 | 193 | #region Integer 194 | 195 | public static int GetInt(String key) 196 | { 197 | var raw = GetSettingRaw(key); 198 | int i; 199 | if (int.TryParse(raw, out i)) 200 | { 201 | return i; 202 | } 203 | return default(int); 204 | } 205 | 206 | public static void SetInt(String key, int value) 207 | { 208 | SetSettingRaw(key, value.ToString(CultureInfo.InvariantCulture), "int"); 209 | } 210 | 211 | #endregion Integer 212 | 213 | #region Float 214 | 215 | public static float GetFloat(String key) 216 | { 217 | var raw = GetSettingRaw(key); 218 | float f; 219 | if (float.TryParse(raw, out f)) 220 | { 221 | return f; 222 | } 223 | return default(float); 224 | } 225 | 226 | public static void SetFloat(String key, float value) 227 | { 228 | SetSettingRaw(key, value.ToString(CultureInfo.InvariantCulture), "float"); 229 | } 230 | 231 | #endregion Float 232 | 233 | #region Double 234 | 235 | public static double GetDouble(String key) 236 | { 237 | var raw = GetSettingRaw(key); 238 | double d; 239 | if (double.TryParse(raw, out d)) 240 | { 241 | return d; 242 | } 243 | return default(double); 244 | } 245 | 246 | public static void SetDouble(String key, double value) 247 | { 248 | SetSettingRaw(key, value.ToString(CultureInfo.InvariantCulture), "double"); 249 | } 250 | 251 | #endregion Double 252 | } 253 | 254 | public class Setting 255 | { 256 | public String Key { get; set; } 257 | 258 | public String Value { get; set; } 259 | 260 | public String Type { get; set; } 261 | } 262 | } -------------------------------------------------------------------------------- /CityWebServer/wwwroot/knockout.mapping-latest.js: -------------------------------------------------------------------------------- 1 | /// Knockout Mapping plugin v2.4.1 2 | /// (c) 2013 Steven Sanderson, Roy Jacobs - http://knockoutjs.com/ 3 | /// License: MIT (http://www.opensource.org/licenses/mit-license.php) 4 | (function(e){"function"===typeof require&&"object"===typeof exports&&"object"===typeof module?e(require("knockout"),exports):"function"===typeof define&&define.amd?define(["knockout","exports"],e):e(ko,ko.mapping={})})(function(e,f){function y(b,c){var a,d;for(d in c)if(c.hasOwnProperty(d)&&c[d])if(a=f.getType(b[d]),d&&b[d]&&"array"!==a&&"string"!==a)y(b[d],c[d]);else if("array"===f.getType(b[d])&&"array"===f.getType(c[d])){a=b;for(var e=d,l=b[d],n=c[d],t={},g=l.length-1;0<=g;--g)t[l[g]]=l[g];for(g= 5 | n.length-1;0<=g;--g)t[n[g]]=n[g];l=[];n=void 0;for(n in t)l.push(t[n]);a[e]=l}else b[d]=c[d]}function E(b,c){var a={};y(a,b);y(a,c);return a}function z(b,c){for(var a=E({},b),e=L.length-1;0<=e;e--){var f=L[e];a[f]&&(a[""]instanceof Object||(a[""]={}),a[""][f]=a[f],delete a[f])}c&&(a.ignore=h(c.ignore,a.ignore),a.include=h(c.include,a.include),a.copy=h(c.copy,a.copy),a.observe=h(c.observe,a.observe));a.ignore=h(a.ignore,j.ignore);a.include=h(a.include,j.include);a.copy=h(a.copy,j.copy);a.observe=h(a.observe, 6 | j.observe);a.mappedProperties=a.mappedProperties||{};a.copiedProperties=a.copiedProperties||{};return a}function h(b,c){"array"!==f.getType(b)&&(b="undefined"===f.getType(b)?[]:[b]);"array"!==f.getType(c)&&(c="undefined"===f.getType(c)?[]:[c]);return e.utils.arrayGetDistinctValues(b.concat(c))}function F(b,c,a,d,k,l,n){var t="array"===f.getType(e.utils.unwrapObservable(c));l=l||"";if(f.isMapped(b)){var g=e.utils.unwrapObservable(b)[p];a=E(g,a)}var j=n||k,h=function(){return a[d]&&a[d].create instanceof 7 | Function},x=function(b){var f=G,g=e.dependentObservable;e.dependentObservable=function(a,b,c){c=c||{};a&&"object"==typeof a&&(c=a);var d=c.deferEvaluation,M=!1;c.deferEvaluation=!0;a=new H(a,b,c);if(!d){var g=a,d=e.dependentObservable;e.dependentObservable=H;a=e.isWriteableObservable(g);e.dependentObservable=d;d=H({read:function(){M||(e.utils.arrayRemoveItem(f,g),M=!0);return g.apply(g,arguments)},write:a&&function(a){return g(a)},deferEvaluation:!0});d.__DO=g;a=d;f.push(a)}return a};e.dependentObservable.fn= 8 | H.fn;e.computed=e.dependentObservable;b=e.utils.unwrapObservable(k)instanceof Array?a[d].create({data:b||c,parent:j,skip:N}):a[d].create({data:b||c,parent:j});e.dependentObservable=g;e.computed=e.dependentObservable;return b},u=function(){return a[d]&&a[d].update instanceof Function},v=function(b,f){var g={data:f||c,parent:j,target:e.utils.unwrapObservable(b)};e.isWriteableObservable(b)&&(g.observable=b);return a[d].update(g)};if(n=I.get(c))return n;d=d||"";if(t){var t=[],s=!1,m=function(a){return a}; 9 | a[d]&&a[d].key&&(m=a[d].key,s=!0);e.isObservable(b)||(b=e.observableArray([]),b.mappedRemove=function(a){var c="function"==typeof a?a:function(b){return b===m(a)};return b.remove(function(a){return c(m(a))})},b.mappedRemoveAll=function(a){var c=C(a,m);return b.remove(function(a){return-1!=e.utils.arrayIndexOf(c,m(a))})},b.mappedDestroy=function(a){var c="function"==typeof a?a:function(b){return b===m(a)};return b.destroy(function(a){return c(m(a))})},b.mappedDestroyAll=function(a){var c=C(a,m);return b.destroy(function(a){return-1!= 10 | e.utils.arrayIndexOf(c,m(a))})},b.mappedIndexOf=function(a){var c=C(b(),m);a=m(a);return e.utils.arrayIndexOf(c,a)},b.mappedGet=function(a){return b()[b.mappedIndexOf(a)]},b.mappedCreate=function(a){if(-1!==b.mappedIndexOf(a))throw Error("There already is an object with the key that you specified.");var c=h()?x(a):a;u()&&(a=v(c,a),e.isWriteableObservable(c)?c(a):c=a);b.push(c);return c});n=C(e.utils.unwrapObservable(b),m).sort();g=C(c,m);s&&g.sort();s=e.utils.compareArrays(n,g);n={};var J,A=e.utils.unwrapObservable(c), 11 | y={},z=!0,g=0;for(J=A.length;g _logLines; 25 | private static string _endpoint; 26 | 27 | private WebServer _server; 28 | private List _requestHandlers; 29 | private String _cityName = "CityName"; 30 | 31 | // Not required, but prevents a number of spurious entries from making it to the log file. 32 | private static readonly List IgnoredAssemblies = new List 33 | { 34 | "Anonymously Hosted DynamicMethods Assembly", 35 | "Assembly-CSharp", 36 | "Assembly-CSharp-firstpass", 37 | "Assembly-UnityScript-firstpass", 38 | "Boo.Lang", 39 | "ColossalManaged", 40 | "ICSharpCode.SharpZipLib", 41 | "ICities", 42 | "Mono.Security", 43 | "mscorlib", 44 | "System", 45 | "System.Configuration", 46 | "System.Core", 47 | "System.Xml", 48 | "UnityEngine", 49 | "UnityEngine.UI", 50 | }; 51 | 52 | /// 53 | /// Gets the root endpoint for which the server is configured to service HTTP requests. 54 | /// 55 | public static String Endpoint 56 | { 57 | get { return _endpoint; } 58 | } 59 | 60 | /// 61 | /// Gets the full path to the directory where static pages are served from. 62 | /// 63 | public static String GetWebRoot() 64 | { 65 | var modPaths = PluginManager.instance.GetPluginsInfo().Select(obj => obj.modPath); 66 | foreach (var path in modPaths) 67 | { 68 | var testPath = Path.Combine(path, "wwwroot"); 69 | 70 | if (Directory.Exists(testPath)) 71 | { 72 | return testPath; 73 | } 74 | } 75 | return null; 76 | } 77 | 78 | /// 79 | /// Gets an array containing all currently registered request handlers. 80 | /// 81 | public IRequestHandler[] RequestHandlers 82 | { 83 | get { return _requestHandlers.ToArray(); } 84 | } 85 | 86 | /// 87 | /// Initializes a new instance of the class. 88 | /// 89 | public IntegratedWebServer() 90 | { 91 | // For the entire lifetime of this instance, we'll preseve log messages. 92 | // After a certain point, it might be worth truncating them, but we'll cross that bridge when we get to it. 93 | _logLines = new List(); 94 | 95 | // We need a place to store all the request handlers that have been registered. 96 | _requestHandlers = new List(); 97 | } 98 | 99 | #region Create 100 | 101 | /// 102 | /// Called by the game after this instance is created. 103 | /// 104 | /// The threading. 105 | public override void OnCreated(IThreading threading) 106 | { 107 | InitializeServer(); 108 | 109 | base.OnCreated(threading); 110 | } 111 | 112 | private void InitializeServer() 113 | { 114 | if (_server != null) 115 | { 116 | _server.Stop(); 117 | _server = null; 118 | } 119 | 120 | LogMessage("Initializing Server..."); 121 | 122 | List bindings = new List(); 123 | 124 | int currentBinding = 1; 125 | String currentBindingKey = String.Format(WebServerHostKey, currentBinding); 126 | while (Configuration.HasSetting(currentBindingKey)) 127 | { 128 | bindings.Add(Configuration.GetString(currentBindingKey)); 129 | currentBinding++; 130 | currentBindingKey = String.Format(WebServerHostKey, currentBinding); 131 | } 132 | 133 | // If there are no bindings in the configuration file, we'll need to initialize those values. 134 | if (bindings.Count == 0) 135 | { 136 | const String defaultBinding = "http://localhost:8080/"; 137 | bindings.Add(defaultBinding); 138 | 139 | // If there aren't any bindings, the value of currentBindingKey will never have made it past 1. 140 | // As a result, we can just use that. 141 | Configuration.SetString(currentBindingKey, defaultBinding); 142 | Configuration.SaveSettings(); 143 | } 144 | 145 | // The endpoint used internally should always be the first binding in the configuration. 146 | // There's no need to use multiple bindings for internal references, we only need a single one. 147 | _endpoint = bindings.First(); 148 | 149 | WebServer ws = new WebServer(HandleRequest, bindings.ToArray()); 150 | _server = ws; 151 | _server.Run(); 152 | LogMessage("Server Initialized."); 153 | 154 | _requestHandlers = new List(); 155 | 156 | try 157 | { 158 | RegisterHandlers(); 159 | } 160 | catch (Exception ex) 161 | { 162 | UnityEngine.Debug.LogException(ex); 163 | } 164 | } 165 | 166 | #endregion Create 167 | 168 | #region Release 169 | 170 | /// 171 | /// Called by the game before this instance is about to be destroyed. 172 | /// 173 | public override void OnReleased() 174 | { 175 | ReleaseServer(); 176 | 177 | // TODO: Unregister from events (i.e. ILogAppender.LogMessage) 178 | _requestHandlers.Clear(); 179 | 180 | Configuration.SaveSettings(); 181 | 182 | base.OnReleased(); 183 | } 184 | 185 | private void ReleaseServer() 186 | { 187 | LogMessage("Checking for existing server..."); 188 | if (_server != null) 189 | { 190 | LogMessage("Server found; disposing..."); 191 | _server.Stop(); 192 | _server = null; 193 | LogMessage("Server Disposed."); 194 | } 195 | } 196 | 197 | #endregion Release 198 | 199 | /// ; 200 | /// Handles the specified request. 201 | /// 202 | /// 203 | /// Defers execution to an appropriate request handler, except for requests to the reserved endpoints: ~/ and ~/Log.
    204 | /// Returns a default error message if an appropriate request handler can not be found. 205 | ///
    206 | private void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) 207 | { 208 | LogMessage(String.Format("{0} {1}", request.HttpMethod, request.RawUrl)); 209 | 210 | var simulationManager = Singleton.instance; 211 | _cityName = simulationManager.m_metaData.m_CityName; 212 | 213 | // There are two reserved endpoints: "/" and "/Log". 214 | // These take precedence over all other request handlers. 215 | if (ServiceRoot(request, response)) 216 | { 217 | return; 218 | } 219 | 220 | if (ServiceLog(request, response)) 221 | { 222 | return; 223 | } 224 | 225 | // Get the request handler associated with the current request. 226 | var handler = _requestHandlers.FirstOrDefault(obj => obj.ShouldHandle(request)); 227 | if (handler != null) 228 | { 229 | try 230 | { 231 | IResponseFormatter responseFormatterWriter = handler.Handle(request); 232 | responseFormatterWriter.WriteContent(response); 233 | 234 | return; 235 | } 236 | catch (Exception ex) 237 | { 238 | String errorBody = String.Format("

    An error has occurred!

    {0}
    ", ex); 239 | var tokens = TemplateHelper.GetTokenReplacements(_cityName, "Error", _requestHandlers, errorBody); 240 | var template = TemplateHelper.PopulateTemplate("index", tokens); 241 | 242 | IResponseFormatter errorResponseFormatter = new HtmlResponseFormatter(template); 243 | errorResponseFormatter.WriteContent(response); 244 | 245 | return; 246 | } 247 | } 248 | 249 | var wwwroot = GetWebRoot(); 250 | 251 | // At this point, we can guarantee that we don't need any game data, so we can safely start a new thread to perform the remaining tasks. 252 | ServiceFileRequest(wwwroot, request, response); 253 | } 254 | 255 | private static void ServiceFileRequest(String wwwroot, HttpListenerRequest request, HttpListenerResponse response) 256 | { 257 | var relativePath = request.Url.AbsolutePath.Substring(1); 258 | relativePath = relativePath.Replace("/", Path.DirectorySeparatorChar.ToString()); 259 | var absolutePath = Path.Combine(wwwroot, relativePath); 260 | 261 | if (File.Exists(absolutePath)) 262 | { 263 | var extension = Path.GetExtension(absolutePath); 264 | response.ContentType = Apache.GetMime(extension); 265 | response.StatusCode = 200; // HTTP 200 - SUCCESS 266 | 267 | // Open file, read bytes into buffer and write them to the output stream. 268 | using (FileStream fileReader = File.OpenRead(absolutePath)) 269 | { 270 | byte[] buffer = new byte[4096]; 271 | int read; 272 | while ((read = fileReader.Read(buffer, 0, buffer.Length)) > 0) 273 | { 274 | response.OutputStream.Write(buffer, 0, read); 275 | } 276 | } 277 | } 278 | else 279 | { 280 | String body = String.Format("No resource is available at the specified filepath: {0}", absolutePath); 281 | 282 | IResponseFormatter notFoundResponseFormatter = new PlainTextResponseFormatter(body, HttpStatusCode.NotFound); 283 | notFoundResponseFormatter.WriteContent(response); 284 | } 285 | } 286 | 287 | /// 288 | /// Searches all the assemblies in the current AppDomain for class definitions that implement the interface. Those classes are instantiated and registered as request handlers. 289 | /// 290 | private void RegisterHandlers() 291 | { 292 | IEnumerable handlers = FindHandlersInLoadedAssemblies(); 293 | RegisterHandlers(handlers); 294 | } 295 | 296 | private void RegisterHandlers(IEnumerable handlers) 297 | { 298 | if (handlers == null) { return; } 299 | 300 | if (_requestHandlers == null) 301 | { 302 | _requestHandlers = new List(); 303 | } 304 | 305 | foreach (var handler in handlers) 306 | { 307 | // Only register handlers that we don't already have an instance of. 308 | if (_requestHandlers.Any(h => h.GetType() == handler)) 309 | { 310 | continue; 311 | } 312 | 313 | IRequestHandler handlerInstance = null; 314 | Boolean exists = false; 315 | 316 | try 317 | { 318 | if (typeof(RequestHandlerBase).IsAssignableFrom(handler)) 319 | { 320 | handlerInstance = (RequestHandlerBase)Activator.CreateInstance(handler, this); 321 | } 322 | else 323 | { 324 | handlerInstance = (IRequestHandler)Activator.CreateInstance(handler); 325 | } 326 | 327 | if (handlerInstance == null) 328 | { 329 | LogMessage(String.Format("Request Handler ({0}) could not be instantiated!", handler.Name)); 330 | continue; 331 | } 332 | 333 | // Duplicates handlers seem to pass the check above, so now we filter them based on their identifier values, which should work. 334 | exists = _requestHandlers.Any(obj => obj.HandlerID == handlerInstance.HandlerID); 335 | } 336 | catch (Exception ex) 337 | { 338 | LogMessage(ex.ToString()); 339 | } 340 | 341 | if (exists) 342 | { 343 | // TODO: Allow duplicate registrations to occur; previous registration is removed and replaced with a new one? 344 | LogMessage(String.Format("Supressing duplicate handler registration for '{0}'", handler.Name)); 345 | } 346 | else 347 | { 348 | _requestHandlers.Add(handlerInstance); 349 | if (handlerInstance is ILogAppender) 350 | { 351 | var logAppender = (handlerInstance as ILogAppender); 352 | logAppender.LogMessage += RequestHandlerLogAppender_OnLogMessage; 353 | } 354 | 355 | LogMessage(String.Format("Added Request Handler: {0}", handler.FullName)); 356 | } 357 | } 358 | } 359 | 360 | private void RequestHandlerLogAppender_OnLogMessage(object sender, LogAppenderEventArgs logAppenderEventArgs) 361 | { 362 | var senderTypeName = sender.GetType().Name; 363 | LogMessage(logAppenderEventArgs.LogLine, senderTypeName, false); 364 | } 365 | 366 | /// 367 | /// Searches all the assemblies in the current AppDomain, and returns a collection of those that implement the interface. 368 | /// 369 | private static IEnumerable FindHandlersInLoadedAssemblies() 370 | { 371 | var assemblies = AppDomain.CurrentDomain.GetAssemblies(); 372 | 373 | foreach (var assembly in assemblies) 374 | { 375 | var handlers = FetchHandlers(assembly); 376 | foreach (var handler in handlers) 377 | { 378 | yield return handler; 379 | } 380 | } 381 | } 382 | 383 | private static IEnumerable FetchHandlers(Assembly assembly) 384 | { 385 | var assemblyName = assembly.GetName().Name; 386 | 387 | // Skip any assemblies that we don't anticipate finding anything in. 388 | if (IgnoredAssemblies.Contains(assemblyName)) { yield break; } 389 | 390 | Type[] types = new Type[0]; 391 | try 392 | { 393 | types = assembly.GetTypes(); 394 | } 395 | catch { } 396 | 397 | foreach (var type in types) 398 | { 399 | Boolean isValid = false; 400 | try 401 | { 402 | isValid = typeof(IRequestHandler).IsAssignableFrom(type) && type.IsClass && !type.IsAbstract; 403 | } 404 | catch { } 405 | 406 | if (isValid) 407 | { 408 | yield return type; 409 | } 410 | } 411 | } 412 | 413 | #region Reserved Endpoint Handlers 414 | 415 | /// 416 | /// Services requests to ~/ 417 | /// 418 | private Boolean ServiceRoot(HttpListenerRequest request, HttpListenerResponse response) 419 | { 420 | if (request.Url.AbsolutePath.ToLower() == "/") 421 | { 422 | List links = new List(); 423 | foreach (var requestHandler in this._requestHandlers.OrderBy(obj => obj.Priority)) 424 | { 425 | links.Add(String.Format("
  • {0} by {2} (Priority: {3})
  • ", requestHandler.Name, requestHandler.MainPath, requestHandler.Author, requestHandler.Priority)); 426 | } 427 | 428 | String body = String.Format("

    Cities: Skylines - Integrated Web Server

      {0}
    ", String.Join("", links.ToArray())); 429 | var tokens = TemplateHelper.GetTokenReplacements(_cityName, "Home", _requestHandlers, body); 430 | var template = TemplateHelper.PopulateTemplate("index", tokens); 431 | 432 | IResponseFormatter htmlResponseFormatter = new HtmlResponseFormatter(template); 433 | htmlResponseFormatter.WriteContent(response); 434 | 435 | return true; 436 | } 437 | 438 | return false; 439 | } 440 | 441 | /// 442 | /// Services requests to ~/Log 443 | /// 444 | private Boolean ServiceLog(HttpListenerRequest request, HttpListenerResponse response) 445 | { 446 | if (request.Url.AbsolutePath.ToLower() == "/log") 447 | { 448 | { 449 | String body = String.Format("

    Server Log

    {0}
    ", String.Join("", _logLines.ToArray())); 450 | var tokens = TemplateHelper.GetTokenReplacements(_cityName, "Log", _requestHandlers, body); 451 | var template = TemplateHelper.PopulateTemplate("index", tokens); 452 | 453 | IResponseFormatter htmlResponseFormatter = new HtmlResponseFormatter(template); 454 | htmlResponseFormatter.WriteContent(response); 455 | 456 | return true; 457 | } 458 | } 459 | 460 | return false; 461 | } 462 | 463 | #endregion Reserved Endpoint Handlers 464 | 465 | #region Logging 466 | 467 | /// 468 | /// Adds a timestamp to the specified message, and appends it to the internal log. 469 | /// 470 | public static void LogMessage(String message, String label = null, Boolean showInDebugPanel = false) 471 | { 472 | var dt = DateTime.Now; 473 | String time = String.Format("{0} {1}", dt.ToShortDateString(), dt.ToShortTimeString()); 474 | String messageWithLabel = String.IsNullOrEmpty(label) ? message : String.Format("{0}: {1}", label, message); 475 | String line = String.Format("[{0}] {1}{2}", time, messageWithLabel, Environment.NewLine); 476 | _logLines.Add(line); 477 | if (showInDebugPanel) 478 | { 479 | DebugOutputPanel.AddMessage(PluginManager.MessageType.Message, line); 480 | } 481 | } 482 | 483 | /// 484 | /// Writes the value of . to the internal log. 485 | /// 486 | private void ServerOnLogMessage(object sender, LogAppenderEventArgs args) 487 | { 488 | LogMessage(args.LogLine); 489 | } 490 | 491 | #endregion Logging 492 | } 493 | } -------------------------------------------------------------------------------- /CityWebServer/wwwroot/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.2 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.2",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.2",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.2",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a(this.options.trigger).filter('[href="#'+b.id+'"], [data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.2",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0,trigger:'[data-toggle="collapse"]'},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":a.extend({},e.data(),{trigger:this});c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.2",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('