From f9adc0bd6059f09505b7ab42390725e5473324a9 Mon Sep 17 00:00:00 2001 From: empathicqubit Date: Sun, 16 May 2021 10:43:37 -0400 Subject: [PATCH] Initial commit --- .gitignore | 351 ++++++++++++++++++ CMakeLists.txt | 137 +++++++ EmpathicQbt.ConsoleServer.sln | 30 ++ EmpathicQubit.ConsoleServer/App.config | 14 + EmpathicQubit.ConsoleServer/Configuration.cs | 85 +++++ EmpathicQubit.ConsoleServer/ConsoleInput.cs | 68 ++++ .../DragonbornSpeaksNaturally.SAMPLE.ini | 5 + .../EmpathicQbt.ConsoleServer.csproj | 157 ++++++++ .../ExternalInterop.cs | 87 +++++ EmpathicQubit.ConsoleServer/FavoritesList.cs | 173 +++++++++ EmpathicQubit.ConsoleServer/FodyWeavers.xml | 4 + EmpathicQubit.ConsoleServer/InputForwarder.cs | 87 +++++ EmpathicQubit.ConsoleServer/Log.cs | 31 ++ EmpathicQubit.ConsoleServer/Program.cs | 77 ++++ .../Properties/AssemblyInfo.cs | 36 ++ .../ServerBootstrapper.cs | 23 ++ EmpathicQubit.ConsoleServer/ServerModule.cs | 43 +++ EmpathicQubit.ConsoleServer/SkyrimInterop.cs | 130 +++++++ .../fomod/ModuleConfig.xml | 72 ++++ EmpathicQubit.ConsoleServer/fomod/info.xml | 6 + EmpathicQubit.ConsoleServer/packages.config | 11 + EmpathicQubit.ConsoleServer/static/index.css | 86 +++++ EmpathicQubit.ConsoleServer/static/index.html | 11 + EmpathicQubit.ConsoleServer/static/index.js | 112 ++++++ README.md | 11 + configure.bat | 84 +++++ disable_prefer_32bit.ps1 | 26 ++ 27 files changed, 1957 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 EmpathicQbt.ConsoleServer.sln create mode 100644 EmpathicQubit.ConsoleServer/App.config create mode 100644 EmpathicQubit.ConsoleServer/Configuration.cs create mode 100644 EmpathicQubit.ConsoleServer/ConsoleInput.cs create mode 100644 EmpathicQubit.ConsoleServer/DragonbornSpeaksNaturally.SAMPLE.ini create mode 100644 EmpathicQubit.ConsoleServer/EmpathicQbt.ConsoleServer.csproj create mode 100644 EmpathicQubit.ConsoleServer/ExternalInterop.cs create mode 100644 EmpathicQubit.ConsoleServer/FavoritesList.cs create mode 100644 EmpathicQubit.ConsoleServer/FodyWeavers.xml create mode 100644 EmpathicQubit.ConsoleServer/InputForwarder.cs create mode 100644 EmpathicQubit.ConsoleServer/Log.cs create mode 100644 EmpathicQubit.ConsoleServer/Program.cs create mode 100644 EmpathicQubit.ConsoleServer/Properties/AssemblyInfo.cs create mode 100644 EmpathicQubit.ConsoleServer/ServerBootstrapper.cs create mode 100644 EmpathicQubit.ConsoleServer/ServerModule.cs create mode 100644 EmpathicQubit.ConsoleServer/SkyrimInterop.cs create mode 100644 EmpathicQubit.ConsoleServer/fomod/ModuleConfig.xml create mode 100644 EmpathicQubit.ConsoleServer/fomod/info.xml create mode 100644 EmpathicQubit.ConsoleServer/packages.config create mode 100644 EmpathicQubit.ConsoleServer/static/index.css create mode 100644 EmpathicQubit.ConsoleServer/static/index.html create mode 100644 EmpathicQubit.ConsoleServer/static/index.js create mode 100644 README.md create mode 100644 configure.bat create mode 100644 disable_prefer_32bit.ps1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bdf0ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,351 @@ +# build dir +build +build*/ + +# options of configure.bat +install-path.ini + +!nuget.exe + +############################################################################ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ +# ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true +**/wwwroot/lib/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..087b265 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,137 @@ +cmake_minimum_required(VERSION 3.5) + +project(dsn_service LANGUAGES CSharp) + +include(CSharpUtilities) +set(CMAKE_CSharp_FLAGS "/langversion:6 /platform:anycpu /define:TRACE") + + +############### +# Source codes and Targets +############### + +file(GLOB_RECURSE DSN_SERVICE_SRC + dsn_service/*.cs + dsn_service/*.xml + dsn_service/*.config + dsn_service/*.txt +) + +add_executable(dsn_service ${DSN_SERVICE_SRC}) + + +############### +# NuGet package restore +############### + +add_custom_target(Please_Reinstall_NuGet_Packages_Manually COMMAND + echo ========================================================================================================================== && + echo For technical reasons, CMake can't automatically restore the NuGet package and update references for project dsn_service. && + echo If you met assembly missing, please restore NuGet packages manually and run this command in NuGet Package Manager Console: && + echo Update-Package -reinstall -projectname dsn_service && + echo ========================================================================================================================== +) + +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/dsn_service/packages.config + ${CMAKE_CURRENT_BINARY_DIR}/packages.config + COPYONLY +) + +configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/dsn_service/FodyWeavers.xml + ${CMAKE_CURRENT_BINARY_DIR}/FodyWeavers.xml + COPYONLY +) + +add_dependencies(dsn_service Please_Reinstall_NuGet_Packages_Manually) + + +############### +# Project Information +############### + +set(VS_DOTNET_REFERENCES + Microsoft.CSharp + System + System.Core + System.Data + System.Data.DataSetExtensions + System.Net.Http + System.Speech + System.Web.Extensions + System.Xml + System.Xml.Linq +) + +set_target_properties(dsn_service PROPERTIES + VS_GLOBAL_ProjectGuid "{DEA491EE-C426-4B79-A443-CF5B1D795288}" + VS_DOTNET_TARGET_FRAMEWORK_VERSION "v4.6.1" + VS_DOTNET_REFERENCES "${VS_DOTNET_REFERENCES}" + OUTPUT_NAME "DragonbornSpeaksNaturally" +) + + +############### +# Install and Package +############### + +if (SVR_DIR) + set(SVR_PLUGIN_DIR "${SVR_DIR}/Data/Plugins/Sumwunn") + message("-- SkyrimVR plugin install path: ${SVR_PLUGIN_DIR}/") + + install( + TARGETS dsn_service + COMPONENT SkyrimVR + RUNTIME DESTINATION ${SVR_PLUGIN_DIR} + ) + add_custom_command( + TARGET dsn_service POST_BUILD VERBATIM + COMMAND + ${CMAKE_COMMAND} -E copy "$" ${SVR_PLUGIN_DIR} && + echo file copied: "$ -> ${SVR_PLUGIN_DIR}" + ) +endif() + +if (SSE_DIR) + set(SSE_PLUGIN_DIR "${SSE_DIR}/Data/Plugins/Sumwunn") + message("-- SkyrimSE plugin install path: ${SSE_PLUGIN_DIR}/") + + add_custom_command( + TARGET dsn_service POST_BUILD VERBATIM + COMMAND + ${CMAKE_COMMAND} -E copy "$" ${SSE_PLUGIN_DIR} && + echo file copied: "$ -> ${SSE_PLUGIN_DIR}" + ) +endif() + +# +# ZIP Package +# +option(PACKAGE "Generate NMM/Vortex Compatible ZIP Package" ON) +if (PACKAGE) + if (NOT IS_SUB_PROJECT) + message("-- Generate NMM/Vortex Compatible ZIP Package: On (-DPACKAGE=ON)") + endif() + + set(CMAKE_INSTALL_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/package_tmp) + install( + TARGETS dsn_service + RUNTIME DESTINATION SkyrimVR/Data/Plugins/Sumwunn + ) + install( + TARGETS dsn_service + RUNTIME DESTINATION SkyrimSE/Data/Plugins/Sumwunn + ) + + set(CPACK_GENERATOR ZIP) + set(CPACK_INCLUDE_TOPLEVEL_DIRECTORY OFF) + if (NOT IS_SUB_PROJECT) + include(CPack) + endif() +else() + if (NOT IS_SUB_PROJECT) + message("-- Generate NMM/Vortex Compatible ZIP Package: Off (-DPACKAGE=OFF)") + endif() +endif() + diff --git a/EmpathicQbt.ConsoleServer.sln b/EmpathicQbt.ConsoleServer.sln new file mode 100644 index 0000000..e8466f5 --- /dev/null +++ b/EmpathicQbt.ConsoleServer.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31205.134 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EmpathicQbt.ConsoleServer", "EmpathicQubit.ConsoleServer\EmpathicQbt.ConsoleServer.csproj", "{DEA491EE-C426-4B79-A443-CF5B1D795288}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C0E8A622-ACEB-4328-93B5-C8FE43D10E02}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DEA491EE-C426-4B79-A443-CF5B1D795288}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DEA491EE-C426-4B79-A443-CF5B1D795288}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEA491EE-C426-4B79-A443-CF5B1D795288}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DEA491EE-C426-4B79-A443-CF5B1D795288}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {82D28A7C-FEF8-480B-A2F5-EA6F564C5917} + EndGlobalSection +EndGlobal diff --git a/EmpathicQubit.ConsoleServer/App.config b/EmpathicQubit.ConsoleServer/App.config new file mode 100644 index 0000000..e4933ea --- /dev/null +++ b/EmpathicQubit.ConsoleServer/App.config @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EmpathicQubit.ConsoleServer/Configuration.cs b/EmpathicQubit.ConsoleServer/Configuration.cs new file mode 100644 index 0000000..137dc9a --- /dev/null +++ b/EmpathicQubit.ConsoleServer/Configuration.cs @@ -0,0 +1,85 @@ +using IniParser; +using IniParser.Model; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EmpathicQbt.ConsoleServer { + + public class Configuration { + private readonly string CONFIG_FILE_NAME = "DragonbornSpeaksNaturally.ini"; + + // NOTE: Relative to SkyrimVR.exe + private static readonly string[] SEARCH_DIRECTORIES = { + "Data\\Plugins\\Sumwunn\\", + "..\\", + "" + }; + + private string iniFilePath = null; + + private IniData global = null; + private IniData local = null; + private IniData merged = null; + + public Configuration() { + iniFilePath = ResolveFilePath(CONFIG_FILE_NAME); + + loadLocal(); + loadGlobal(); + + merged = new IniData(); + merged.Merge(global); + merged.Merge(local); + } + + public string GetIniFilePath() { + return iniFilePath; + } + + public string Get(string section, string key, string def) { + string val = merged[section][key]; + if (val == null) + return def; + return val; + } + + private void loadGlobal() { + global = new IniData(); + } + + private void loadLocal() { + local = loadIniFromFilePath(iniFilePath); + if (local == null) + local = new IniData(); + } + + public static string ResolveFilePath(string filename) { + foreach (string directory in SEARCH_DIRECTORIES) { + string filepath = directory + filename; + if (File.Exists(filepath)) { + return Path.GetFullPath(filepath); ; + } + } + return null; + } + + private IniData loadIniFromFilePath(string filepath) { + if (filepath != null) { + Trace.TraceInformation("Loading ini from path " + filepath); + try { + var parser = new FileIniDataParser(); + return parser.ReadFile(filepath); + } catch (Exception ex) { + Trace.TraceError("Failed to load ini file at " + filepath); + Trace.TraceError(ex.ToString()); + } + } + return null; + } + } +} diff --git a/EmpathicQubit.ConsoleServer/ConsoleInput.cs b/EmpathicQubit.ConsoleServer/ConsoleInput.cs new file mode 100644 index 0000000..eebefde --- /dev/null +++ b/EmpathicQubit.ConsoleServer/ConsoleInput.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EmpathicQbt.ConsoleServer +{ + public class ConsoleInput + { + + private BlockingCollection inputQueue = new BlockingCollection(); + private Thread inputThread = null; + bool isInputTerminated = false; + + // Saved state, used to restore after reloading the configuration file. + public string currentFavoritesList = null; + + public void Start() + { + inputThread = new Thread(ReadLineFromConsole); + inputThread.Start(); + } + + private void ReadLineFromConsole() + { + while (true) + { + string input = Console.ReadLine(); + + // input will be null when Skyrim terminated (stdin closed) + if (input == null) + { + isInputTerminated = true; + Trace.TraceInformation("Skyrim is terminated, console server will quit."); + + // Notify the SkyrimInterop thread to exit + inputQueue.Add(null); + + break; + } + + inputQueue.Add(input); + } + } + + public bool IsInputTerminated() { + return isInputTerminated; + } + + public void WriteLine(string line) { + inputQueue.Add(line); + } + + public string ReadLine() { + return inputQueue.Take(); + } + + public void RestoreSavedState() { + if (currentFavoritesList != null) { + inputQueue.Add(currentFavoritesList); + } + } + } +} diff --git a/EmpathicQubit.ConsoleServer/DragonbornSpeaksNaturally.SAMPLE.ini b/EmpathicQubit.ConsoleServer/DragonbornSpeaksNaturally.SAMPLE.ini new file mode 100644 index 0000000..efd6b3b --- /dev/null +++ b/EmpathicQubit.ConsoleServer/DragonbornSpeaksNaturally.SAMPLE.ini @@ -0,0 +1,5 @@ +;;; Please do not edit, delete or move the next line (the ini section title), otherwise the options below will not take effect +[Server] + +;;; Specify the port to start the HTTP API on. Default is 12160 +;Port=12160 \ No newline at end of file diff --git a/EmpathicQubit.ConsoleServer/EmpathicQbt.ConsoleServer.csproj b/EmpathicQubit.ConsoleServer/EmpathicQbt.ConsoleServer.csproj new file mode 100644 index 0000000..a006d54 --- /dev/null +++ b/EmpathicQubit.ConsoleServer/EmpathicQbt.ConsoleServer.csproj @@ -0,0 +1,157 @@ + + + + + SkyrimVR\Data\Plugins\Sumwunn + + + Debug + AnyCPU + {DEA491EE-C426-4B79-A443-CF5B1D795288} + Exe + EmpathicQbt.ConsoleServer + EmpathicQbt.ConsoleServer + v4.6.1 + 512 + true + + + - + + + AnyCPU + false + true + full + false + bin\Debug\$(DragonBornModPath)\$(AssemblyName)\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + false + pdbonly + true + bin\Release\$(DragonBornModPath)\EmpathicQbt.ConsoleServer\ + TRACE + prompt + 4 + + + Always + + + + ..\packages\Costura.Fody.2.0.0\lib\net452\Costura.dll + + + ..\packages\ini-parser.2.5.2\lib\net20\INIFileParser.dll + + + ..\packages\Nancy.2.0.0\lib\net452\Nancy.dll + + + ..\packages\Nancy.Hosting.Self.2.0.0\lib\net452\Nancy.Hosting.Self.dll + + + ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(ProjectDir)\$(OutputPath)\..\..\..\..\.. + $(DragonBornPluginRoot)\SkyrimSE\Data\Plugins\Sumwunn\$(AssemblyName) + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/EmpathicQubit.ConsoleServer/ExternalInterop.cs b/EmpathicQubit.ConsoleServer/ExternalInterop.cs new file mode 100644 index 0000000..f40049d --- /dev/null +++ b/EmpathicQubit.ConsoleServer/ExternalInterop.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EmpathicQbt.ConsoleServer { + public class ExternalInterop { + + private Configuration config = null; + private SkyrimInterop skyrimInterop = null; + + private readonly HashSet BATCH_FILENAMES = new HashSet() { "wotv", "ivrqs" }; + private readonly long FILE_CHANGE_DEBOUNCE_TIME_TICKS = 10000 * 200; // 200 ms + + private string configFileName = null; + private FileSystemWatcher configFileWatcher; + private bool isConfigFileChanged = false; + + + public ExternalInterop(Configuration config, SkyrimInterop skyrimInterop) { + this.config = config; + this.skyrimInterop = skyrimInterop; + } + + public void Start() { + ListenForConfigFile(); + } + + public void Stop() { + if(configFileWatcher != null) + { + configFileWatcher.EnableRaisingEvents = false; + } + } + private void ListenForConfigFile() + { + try + { + string filePath = config.GetIniFilePath(); + if (filePath == null) { + throw new Exception("Configuration file does not exist."); + } + + configFileName = Path.GetFileName(filePath).ToLower(); + + configFileWatcher = new FileSystemWatcher(); + configFileWatcher.Path = Path.GetDirectoryName(filePath); + configFileWatcher.Changed += Watcher_ConfigFileChanged; + configFileWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.CreationTime; + Trace.TraceInformation("Watching for config file at {0}", configFileWatcher.Path); + configFileWatcher.EnableRaisingEvents = true; + } + catch (Exception ex) + { + Trace.TraceError("Failed to watch for config files: {0}", ex.ToString()); + } + } + + private void Watcher_ConfigFileChanged(object sender, FileSystemEventArgs e) + { + if (!isConfigFileChanged && + (e.ChangeType == WatcherChangeTypes.Changed || e.ChangeType == WatcherChangeTypes.Created)) + { + string filename = e.Name.ToLower(); + if (filename.Equals(configFileName)) + { + Trace.TraceInformation("Config file {0} changed", filename); + + // Wait for the configuration file to be saved + Thread.Sleep(1000); + + isConfigFileChanged = true; + Stop(); + skyrimInterop.Stop(); + } + } + } + + public bool IsConfigFileChanged() { + return isConfigFileChanged; + } + } +} diff --git a/EmpathicQubit.ConsoleServer/FavoritesList.cs b/EmpathicQubit.ConsoleServer/FavoritesList.cs new file mode 100644 index 0000000..bee6d5d --- /dev/null +++ b/EmpathicQubit.ConsoleServer/FavoritesList.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web.Script.Serialization; +using System.IO; + +namespace EmpathicQbt.ConsoleServer { + public class Favorite + { + public string ItemName { get; set; } + public long FormId { get; set; } + public long ItemId { get; set; } + public bool IsSingleHanded { get; set; } + public int TypeId { get; set; } + } + + public class FavoritesList { + + private Configuration config; + + public IList Favorites { get; protected set; } = new List() + { + new Favorite + { + ItemId = 666, + TypeId = 999, + FormId = 222, + IsSingleHanded = true, + ItemName = "Wabbajack", + }, + new Favorite + { + ItemId = 666, + TypeId = 999, + FormId = 222, + IsSingleHanded = true, + ItemName = "Wabbajack2", + }, + new Favorite + { + ItemId = 666, + TypeId = 999, + FormId = 222, + IsSingleHanded = true, + ItemName = "Wabbajack3", + }, + new Favorite + { + ItemId = 666, + TypeId = 999, + FormId = 222, + IsSingleHanded = true, + ItemName = "Wabbajack4", + }, + new Favorite + { + ItemId = 666, + TypeId = 999, + FormId = 222, + IsSingleHanded = true, + ItemName = "Wabbajack5", + }, + new Favorite + { + ItemId = 666, + TypeId = 999, + FormId = 222, + IsSingleHanded = true, + ItemName = "Wabbajack6", + }, + new Favorite + { + ItemId = 666, + TypeId = 999, + FormId = 222, + IsSingleHanded = true, + ItemName = "Wabbajack7", + }, + new Favorite + { + ItemId = 666, + TypeId = 999, + FormId = 222, + IsSingleHanded = true, + ItemName = "Wabbajack8", + }, + }; + + public FavoritesList(Configuration config) { + this.config = config; + } + + // Locates and loads item name replacement maps + // Returns dynamic map/dictionary or null when the replacement map files cannot be located + public dynamic LoadItemNameMap() + { + string filepath = Configuration.ResolveFilePath("item-name-map.json"); + if(File.Exists(filepath)) + { + return LoadItemNameMap(filepath); + } + return null; + } + + // Returns a map/dictionary or throws exception when the file cannot be opened/read + public dynamic LoadItemNameMap(string path) + { + var json = System.IO.File.ReadAllText(path); + JavaScriptSerializer jsonSerializer = new JavaScriptSerializer(); + return jsonSerializer.Deserialize(json); + } + + public string MaybeReplaceItemName(dynamic nameMap, string itemName) + { + if (nameMap == null) + return itemName; + + try + { + return nameMap[itemName]; + } + catch (KeyNotFoundException) + { + return itemName; + } + } + + public void Update(string input) { + dynamic itemNameMap = LoadItemNameMap(); + + Favorites.Clear(); + string[] itemTokens = input.Split('|'); + foreach(string itemStr in itemTokens) { + try + { + string[] tokens = itemStr.Split(','); + string itemName = tokens[0]; + long formId = long.Parse(tokens[1]); + long itemId = long.Parse(tokens[2]); + bool isSingleHanded = int.Parse(tokens[3]) > 0; + int typeId = int.Parse(tokens[4]); + + Favorites.Add(new Favorite + { + FormId = formId, + ItemName = itemName, + ItemId = itemId, + IsSingleHanded = isSingleHanded, + TypeId = typeId, + }); + + itemName = MaybeReplaceItemName(itemNameMap, itemName); + + // FIXME Store item info + } catch(Exception ex) { + Trace.TraceError("Failed to parse {0} due to exception:\n{1}", itemStr, ex.ToString()); + } + } + + PrintToTrace(); + } + + public void PrintToTrace() { + Trace.TraceInformation("Favorites List:"); + foreach (var favorite in Favorites) { + Trace.TraceInformation("{0}: {1}: {2}", favorite.ItemName, favorite.TypeId, favorite.IsSingleHanded); + } + } + } +} diff --git a/EmpathicQubit.ConsoleServer/FodyWeavers.xml b/EmpathicQubit.ConsoleServer/FodyWeavers.xml new file mode 100644 index 0000000..a3b687b --- /dev/null +++ b/EmpathicQubit.ConsoleServer/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/EmpathicQubit.ConsoleServer/InputForwarder.cs b/EmpathicQubit.ConsoleServer/InputForwarder.cs new file mode 100644 index 0000000..88ed5b0 --- /dev/null +++ b/EmpathicQubit.ConsoleServer/InputForwarder.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; + +namespace EmpathicQbt.ConsoleServer +{ + public class InputForwarder + { + private BlockingCollection outputQueue = new BlockingCollection(); + private Thread outputThread = null; + private StreamWriter inputWriter = null; + private Process subProcess = null; + bool isOutputTerminated = false; + + public void Start() + { + outputThread = new Thread(ReadLineFromProcess); + var filename = Configuration.ResolveFilePath("DragonbornSpeaksNaturally.Original.exe"); + + Trace.TraceInformation("Starting {0}", filename); + subProcess = Process.Start(new ProcessStartInfo() + { + FileName = filename, + Arguments = "\"" + String.Join("\" \"", System.Environment.GetCommandLineArgs().Skip(1)) + "\"", + CreateNoWindow = true, + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardInput = true, + RedirectStandardOutput = true, + StandardOutputEncoding = Console.OutputEncoding, + StandardErrorEncoding = Console.OutputEncoding, + }); + subProcess.Start(); + inputWriter = subProcess.StandardInput; + inputWriter.AutoFlush = true; + outputThread.Start(); + } + + private void ReadLineFromProcess() + { + while (true) + { + string input = subProcess.StandardOutput.ReadLine(); + + // input will be null when Skyrim terminated (stdin closed) + if (input == null) + { + isOutputTerminated = true; + Trace.TraceInformation("Skyrim is terminated, console server will quit."); + + // Notify the SkyrimInterop thread to exit + outputQueue.Add(null); + + break; + } + + outputQueue.Add(input); + } + } + + public bool IsInputTerminated() { + return isOutputTerminated; + } + + public void WriteLine(string line) { + inputWriter.WriteLine(line); + inputWriter.Flush(); + inputWriter.BaseStream.Flush(); + } + + public void Stop() + { + if(subProcess != null) + { + subProcess.Kill(); + } + } + + public string ReadLine() { + return outputQueue.Take(); + } + } +} \ No newline at end of file diff --git a/EmpathicQubit.ConsoleServer/Log.cs b/EmpathicQubit.ConsoleServer/Log.cs new file mode 100644 index 0000000..d9ee899 --- /dev/null +++ b/EmpathicQubit.ConsoleServer/Log.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EmpathicQbt.ConsoleServer { + public class Log { + + private static readonly string ERROR_LOG_FILE = "EmpathicQbt.ConsoleServer.log"; + + public static void Initialize() { + string logFilePath = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + logFilePath += "\\EmpathicQbt.ConsoleServer"; + System.IO.Directory.CreateDirectory(logFilePath); + logFilePath += "\\"+ERROR_LOG_FILE; + try { + // The compiler constant TRACE needs to be defined, otherwise logs will not be output to the file. + var listener = new TextWriterTraceListener(logFilePath); + listener.TraceOutputOptions = TraceOptions.DateTime; + Trace.AutoFlush = true; + Trace.Listeners.Add(listener); + } + catch(Exception ex) { + Console.Error.WriteLine("Failed to create log file at " + logFilePath + ": {0}", ex.ToString()); + } + + } + } +} diff --git a/EmpathicQubit.ConsoleServer/Program.cs b/EmpathicQubit.ConsoleServer/Program.cs new file mode 100644 index 0000000..6319ca2 --- /dev/null +++ b/EmpathicQubit.ConsoleServer/Program.cs @@ -0,0 +1,77 @@ +using System; +using System.Diagnostics; +using System.Threading; +using Nancy.Hosting.Self; + +namespace EmpathicQbt.ConsoleServer { + class Program { + private static readonly string VERSION = "0.19"; + + static void Main(string[] args) { + try + { + Log.Initialize(); + Trace.TraceInformation("ConsoleServer service started", VERSION); + + for (int i = 0; i < args.Length; i++) + { + if (args[i].Equals("--encoding") && args.Length >= i + 1) + { + string encode = args[i+1]; + + // Set encoding of stdin/stdout to the client specified. + // This can avoid non-ASCII characters (such as Chinese characters) garbled. + Console.InputEncoding = System.Text.Encoding.GetEncoding(encode); + Console.OutputEncoding = System.Text.Encoding.GetEncoding(encode); + + Trace.TraceInformation("Set encoding of stdin/stdout to {0}", encode); + } + } + + // Thread.Abort() cannot abort the calling of Console.ReadLine(). + // So the call is in a separate thread that does not need to be restarted + // after reloading the configuration file. + ConsoleInput consoleInput = new ConsoleInput(); + consoleInput.Start(); + + var inputForwarder = new InputForwarder(); + inputForwarder.Start(); + + bool reloadConfigFile = true; + while (reloadConfigFile) + { + Configuration config = new Configuration(); + SkyrimInterop skyrimInterop = new SkyrimInterop(config, consoleInput, inputForwarder); + ExternalInterop externalInterop = new ExternalInterop(config, skyrimInterop); + + var port = config.Get("Server", "Port", "12160"); + var host = new NancyHost(new ServerBootstrapper(skyrimInterop), new HostConfiguration() + { + RewriteLocalhost = false, + }, new Uri($"http://localhost:{port}")); + host.Start(); + + skyrimInterop.Start(); + externalInterop.Start(); + + // skyrimThread will terminate when Skyrim terminated (stdin closed) or config file updated + skyrimInterop.Join(); + + reloadConfigFile = externalInterop.IsConfigFileChanged(); + + if (!reloadConfigFile) + { + // Cleanup threads + externalInterop.Stop(); + skyrimInterop.Stop(); + inputForwarder.Stop(); + host.Stop(); + } + } + + } catch (Exception ex) { + Trace.TraceError(ex.ToString()); + } + } + } +} diff --git a/EmpathicQubit.ConsoleServer/Properties/AssemblyInfo.cs b/EmpathicQubit.ConsoleServer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..dccd5f4 --- /dev/null +++ b/EmpathicQubit.ConsoleServer/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("EmpathicQbt.ConsoleServer")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("EmpathicQbt.ConsoleServer")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("dea491ee-c426-4b79-a443-cf5b1d795288")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/EmpathicQubit.ConsoleServer/ServerBootstrapper.cs b/EmpathicQubit.ConsoleServer/ServerBootstrapper.cs new file mode 100644 index 0000000..8d7ffd0 --- /dev/null +++ b/EmpathicQubit.ConsoleServer/ServerBootstrapper.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using Nancy; +using Nancy.TinyIoc; + +namespace EmpathicQbt.ConsoleServer +{ + public class ServerBootstrapper : DefaultNancyBootstrapper + { + private SkyrimInterop skyrimInterop; + public ServerBootstrapper(SkyrimInterop skyrimInterop) + { + this.skyrimInterop = skyrimInterop; + } + + protected override void ConfigureRequestContainer(TinyIoCContainer container, NancyContext context) + { + base.ConfigureRequestContainer(container, context); + + container.Register(skyrimInterop); + } + } +} \ No newline at end of file diff --git a/EmpathicQubit.ConsoleServer/ServerModule.cs b/EmpathicQubit.ConsoleServer/ServerModule.cs new file mode 100644 index 0000000..9147942 --- /dev/null +++ b/EmpathicQubit.ConsoleServer/ServerModule.cs @@ -0,0 +1,43 @@ +using IniParser; +using IniParser.Model; +using Nancy; +using Nancy.ModelBinding; +using Nancy.Responses; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace EmpathicQbt.ConsoleServer { + public class CommandRequest + { + public string Command { get; set; } + } + + public class ServerModule : NancyModule { + private SkyrimInterop skyrimInterop; + + public ServerModule(SkyrimInterop skyrimInterop) + { + this.skyrimInterop = skyrimInterop; + Get("/api/ping", _ => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString()); + Get("/api/favorites", GetFavorites); + Post("/api/command", PostCommand); + Get("/{filename}", _ => Response.AsFile("static/" + (string)_.filename)); + } + + public object PostCommand(dynamic x) { + var cmd = this.Bind(); + skyrimInterop.SubmitCommand("COMMAND|" + cmd.Command); + return Response.AsJson(new { Status = "OK" }, HttpStatusCode.OK); + } + + public object GetFavorites(dynamic x) { + return Response.AsJson(skyrimInterop.GetFavorites(), HttpStatusCode.OK); + } + } +} diff --git a/EmpathicQubit.ConsoleServer/SkyrimInterop.cs b/EmpathicQubit.ConsoleServer/SkyrimInterop.cs new file mode 100644 index 0000000..a0e1c3f --- /dev/null +++ b/EmpathicQubit.ConsoleServer/SkyrimInterop.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EmpathicQbt.ConsoleServer { + public class SkyrimInterop { + + private Configuration config = null; + private ConsoleInput consoleInput = null; + private InputForwarder inputForwarder = null; + private FavoritesList favoritesList = null; + private Thread submissionThread; + private Thread listenThread; + private Thread forwardThread; + private BlockingCollection commandQueue; + + public SkyrimInterop(Configuration config, ConsoleInput consoleInput, InputForwarder inputForwarder) { + this.config = config; + this.consoleInput = consoleInput; + this.inputForwarder = inputForwarder; + } + + public void Start() { + try { + favoritesList = new FavoritesList(config); + commandQueue = new BlockingCollection(); + + listenThread = new Thread(ListenForInput); + submissionThread = new Thread(SubmitCommands); + forwardThread = new Thread(ListenForForward); + submissionThread.Start(); + listenThread.Start(); + forwardThread.Start(); + } + catch (Exception ex) { + Trace.TraceError("Failed to initialize due to error:"); + Trace.TraceError(ex.ToString()); + } + } + + public void Join() { + listenThread.Join(); + } + + public void Stop() { + // Notify threads to exit + consoleInput.WriteLine(null); + inputForwarder.WriteLine(null); + commandQueue.Add(null); + } + + public void SubmitCommand(string command) { + commandQueue.Add(sanitize(command)); + } + + private static string sanitize(string command) { + command = command.Trim(); + return command.Replace("\r", ""); + } + + private void SubmitCommands() { + while (true) { + string command = commandQueue.Take(); + + // Thread exit signal + if (command == null) { + break; + } + + Trace.TraceInformation("Sending command: {0}", command); + Console.Write(command + "\n"); + } + } + + public IList GetFavorites() => favoritesList.Favorites; + + private void ListenForForward() { + try { + while (true) { + string input = inputForwarder.ReadLine(); + + // input will be null when Skyrim terminated (stdin closed) + if (input == null) { + break; + } + + Trace.TraceInformation("Received command to forward: {0}", input); + + SubmitCommand(input); + } + } catch (Exception ex) { + Trace.TraceError(ex.ToString()); + } + } + + private void ListenForInput() { + try { + // try to restore saved state after reloading the configuration file. + consoleInput.RestoreSavedState(); + + while (true) { + string input = consoleInput.ReadLine(); + + // input will be null when Skyrim terminated (stdin closed) + if (input == null) { + break; + } + + Trace.TraceInformation("Received command: {0}", input); + + inputForwarder.WriteLine(input); + + string[] tokens = input.Split('|'); + string command = tokens[0]; + if (command.Equals("FAVORITES")) { + consoleInput.currentFavoritesList = input; + favoritesList.Update(string.Join("|", tokens, 1, tokens.Length - 1)); + } + } + } catch (Exception ex) { + Trace.TraceError(ex.ToString()); + } + } + } +} diff --git a/EmpathicQubit.ConsoleServer/fomod/ModuleConfig.xml b/EmpathicQubit.ConsoleServer/fomod/ModuleConfig.xml new file mode 100644 index 0000000..ea86451 --- /dev/null +++ b/EmpathicQubit.ConsoleServer/fomod/ModuleConfig.xml @@ -0,0 +1,72 @@ + + + + + Dragonborn Speaks Naturally + Weapon Select UI and HTTP API + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/EmpathicQubit.ConsoleServer/fomod/info.xml b/EmpathicQubit.ConsoleServer/fomod/info.xml new file mode 100644 index 0000000..1b91191 --- /dev/null +++ b/EmpathicQubit.ConsoleServer/fomod/info.xml @@ -0,0 +1,6 @@ + + + + Dragonborn Speaks Naturally + Weapon Select UI and HTTP API + https://www.nexusmods.com/skyrimspecialedition/mods/FIXME + diff --git a/EmpathicQubit.ConsoleServer/packages.config b/EmpathicQubit.ConsoleServer/packages.config new file mode 100644 index 0000000..9612e6c --- /dev/null +++ b/EmpathicQubit.ConsoleServer/packages.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/EmpathicQubit.ConsoleServer/static/index.css b/EmpathicQubit.ConsoleServer/static/index.css new file mode 100644 index 0000000..cecad88 --- /dev/null +++ b/EmpathicQubit.ConsoleServer/static/index.css @@ -0,0 +1,86 @@ +* { + box-sizing: border-box; + font-family: sans-serif; + font-size: 4.3vh; + font-weight: 700; + color: #fff; + margin: 0; + padding: 0; + text-shadow: rgb(0, 0, 0) 3px 0px 0px, rgb(0, 0, 0) 2.83487px 0.981584px 0px, rgb(0, 0, 0) 2.35766px 1.85511px 0px, rgb(0, 0, 0) 1.62091px 2.52441px 0px, rgb(0, 0, 0) 0.705713px 2.91581px 0px, rgb(0, 0, 0) -0.287171px 2.98622px 0px, rgb(0, 0, 0) -1.24844px 2.72789px 0px, rgb(0, 0, 0) -2.07227px 2.16926px 0px, rgb(0, 0, 0) -2.66798px 1.37182px 0px, rgb(0, 0, 0) -2.96998px 0.42336px 0px, rgb(0, 0, 0) -2.94502px -0.571704px 0px, rgb(0, 0, 0) -2.59586px -1.50383px 0px, rgb(0, 0, 0) -1.96093px -2.27041px 0px, rgb(0, 0, 0) -1.11013px -2.78704px 0px, rgb(0, 0, 0) -0.137119px -2.99686px 0px, rgb(0, 0, 0) 0.850987px -2.87677px 0px, rgb(0, 0, 0) 1.74541px -2.43999px 0px, rgb(0, 0, 0) 2.44769px -1.73459px 0px, rgb(0, 0, 0) 2.88051px -0.838247px 0px; +} + +html, body { + background-color: #0f0; +} + +h1 { + font-size: 1.2em; + margin: .5em 0; +} + +#content { + display: flex; +} + +.favorites { + padding: 1em; +} + +.favorites li { + display: block; + list-style-type: none; +} + +.favorites ul { + width: 18.1em; + flex-wrap: wrap; + display: flex; + margin: 0; + padding: 0; +} + +.favorites h1 { + text-align: center; + vertical-align: middle; +} + +.favorites button { + word-wrap: break-word; + width: 5.5em; + height: 5.5em; + margin: .25em; + border: .15em solid black; + border-radius: .25em; + opacity: 0.8; + overflow: hidden; +} + +.favorites__left ul { + flex-direction: row; +} + +.favorites__right ul { + flex-direction: row-reverse; +} + +.favorites__left button { + background-color: #800; +} + +.favorites button:hover { + border-color: #909; + text-shadow: #909 3px 0px 0px, #909 2.83487px 0.981584px 0px, #909 2.35766px 1.85511px 0px, #909 1.62091px 2.52441px 0px, #909 0.705713px 2.91581px 0px, #909 -0.287171px 2.98622px 0px, #909 -1.24844px 2.72789px 0px, #909 -2.07227px 2.16926px 0px, #909 -2.66798px 1.37182px 0px, #909 -2.96998px 0.42336px 0px, #909 -2.94502px -0.571704px 0px, #909 -2.59586px -1.50383px 0px, #909 -1.96093px -2.27041px 0px, #909 -1.11013px -2.78704px 0px, #909 -0.137119px -2.99686px 0px, #909 0.850987px -2.87677px 0px, #909 1.74541px -2.43999px 0px, #909 2.44769px -1.73459px 0px, #909 2.88051px -0.838247px 0px; + opacity: 1; +} + +.favorites__left button:hover { + background-color: #70d; +} + +.favorites__right button { + background-color: #008; +} + +.favorites__right button:hover { + background-color: #e4005f; +} diff --git a/EmpathicQubit.ConsoleServer/static/index.html b/EmpathicQubit.ConsoleServer/static/index.html new file mode 100644 index 0000000..bd1fc55 --- /dev/null +++ b/EmpathicQubit.ConsoleServer/static/index.html @@ -0,0 +1,11 @@ + + + + + +
+ + + \ No newline at end of file diff --git a/EmpathicQubit.ConsoleServer/static/index.js b/EmpathicQubit.ConsoleServer/static/index.js new file mode 100644 index 0000000..25b3c8b --- /dev/null +++ b/EmpathicQubit.ConsoleServer/static/index.js @@ -0,0 +1,112 @@ +const UPDATE_INTERVAL = 1000; + +const state = { + favorites: [], +} + +const processItem = (parent, item) => { + if (!item) { + return; + } + + if (item.nodeType !== undefined) { + parent.appendChild(item); + } + else if (typeof item != "object") { + const elem = document.createTextNode(item); + parent.appendChild(elem); + } + else { + for (const kid of item) { + processItem(parent, kid); + } + } +} + +const e = (name, attrs, kids) => { + const elem = document.createElement(name); + for (const a in attrs) { + if (a == 'style' || a == 'dataset') { + for (const s in attrs[a]) { + elem[a][s] = attrs[a][s]; + } + } + else { + elem[a] = attrs[a]; + } + } + + processItem(elem, kids); + + return elem; +} + +const equip = async (item, side) => { + side = side || 'right' + const res = await fetch('/api/command', { + method: 'POST', + body: JSON.stringify({ command: `player.equipitem ${item.formId.toString(16).padStart(8, '0')} 0 ${side}` }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + }); + const status = await res.json() +}; + +const reloadFavorites = async () => { + try { + const res = await fetch('/api/favorites', { headers: { 'Accept': 'application/json' } }); + const favorites = await res.json() + let different = false; + for (const f in favorites) { + const favorite = favorites[f]; + const orig = state.favorites[f]; + if (!orig || orig.formId != favorite.formId) { + different = true; + break; + } + } + + if (!different) { + return; + } + + state.favorites = favorites; + render(); + } + catch (e) { + console.error(e); + } + + setTimeout(reloadFavorites, UPDATE_INTERVAL) +}; + +const render = async () => { + try { + const content = e('div', { id: 'content' }, [ + e('div', { className: 'favorites favorites__left' }, [ + e('h1', null, 'Left Hand'), + e('ul', null, state.favorites.map(item => + e('li', null, + e('button', { onclick: () => equip(item, 'left') }, item.itemName), + ), + )), + ]), + e('div', { className: 'favorites favorites__right' }, [ + e('h1', null, 'Right Hand'), + e('ul', null, state.favorites.map(item => + e('li', null, + e('button', { onclick: () => equip(item, 'right') }, item.itemName), + ), + )), + ]), + ]); + document.getElementById("content").replaceWith(content); + } + catch (e) { + console.error(e); + } +}; + +setTimeout(reloadFavorites, UPDATE_INTERVAL) diff --git a/README.md b/README.md new file mode 100644 index 0000000..8f2856b --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# EmpathicQbt.ConsoleServer + +A proxy that intercepts Dragonborn Speaks Naturally to provide a weapon select interface. + +## Building + +1. Download the [latest DSN ZIP](https://www.nexusmods.com/skyrimspecialedition/mods/16514) +to your Downloads folder. +2. Build it +3. Copy all files in the `bin/$(Configuration)` folder into a ZIP. +4. Install in Vortex or NMM \ No newline at end of file diff --git a/configure.bat b/configure.bat new file mode 100644 index 0000000..6486695 --- /dev/null +++ b/configure.bat @@ -0,0 +1,84 @@ +@echo off & setlocal ENABLEDELAYEDEXPANSION + +:: jump to script's parent dir +cd /d %~dp0 + +:: load/create option files +SET InstallPathConfig=install-path.ini + +:: read or generate config files +if exist %InstallPathConfig% ( + call :load_ini %InstallPathConfig% + call :get_ini SkyrimVR InstallPath SkyrimVRInstallPath + call :get_ini SkyrimSE InstallPath SkyrimSEInstallPath +) else ( + echo Set plugin install directories after building: + set /p SkyrimVRInstallPath="SkyrimVR game root path (empty to disable installation): " + set /p SkyrimSEInstallPath="SkyrimSE game root path (empty to disable installation): " + + call :set_ini SkyrimVR InstallPath "!SkyrimVRInstallPath!" + call :set_ini SkyrimSE InstallPath "!SkyrimSEInstallPath!" + call :save_ini >%InstallPathConfig% +) + +:: create and enter build dir +md build +cd build + +:: run CMake +set CMakeFlags= +if defined SkyrimVRInstallPath ( + set CMakeFlags=!CMakeFlags! -DSVR_DIR="!SkyrimVRInstallPath!" +) +if defined SkyrimSEInstallPath ( + set CMakeFlags=!CMakeFlags! -DSSE_DIR="!SkyrimSEInstallPath!" +) +echo CMakeFlags:!CMakeFlags! + +cmake -A x64 !CMakeFlags! .. + +:: disable Prefer32Bit for C# project +type ..\disable_prefer_32bit.ps1 | powershell.exe -Command - + +:: load project +start dsn_service.sln + +:: pause for user's lookup +pause +goto :eof + + +:: ----------------- ini parse & edit functions ----------------- +:: From + +:load_ini [param#1=ini file path] +set "op=" +for /f " usebackq tokens=1* delims==" %%a in ("%~1") do ( + if "%%b"=="" ( + set "op=%%a" + ) else ( + set "##!op!#%%a=%%b" + ) +) +goto :eof + +:get_ini [param#1=Option] [param#2=Key] [param#3=StoredVar] +set %~3=!##[%~1]#%~2! +goto :eof + +:set_ini [param#1=Option] [param#2=Key] [param#3=Value, without it the key will be deleted] +set "##[%~1]#%~2=%~3" +goto :eof + +:save_ini [>ini path] +set "op=" +set "##=##" +for /f "tokens=1-3 delims=#=" %%a in ('set ##') do ( + if "%%a"=="!op!" ( + echo,%%b=%%c + ) else ( + echo,%%a + set "op=%%a" + echo,%%b=%%c + ) +) diff --git a/disable_prefer_32bit.ps1 b/disable_prefer_32bit.ps1 new file mode 100644 index 0000000..8f49fc2 --- /dev/null +++ b/disable_prefer_32bit.ps1 @@ -0,0 +1,26 @@ +# add Prefer32Bit=false to a csproj file + +$file = 'dsn_service.csproj' + +$doc = New-Object System.Xml.XmlDocument +$doc.Load($file) + +$pgroups = $doc.DocumentElement.PropertyGroup +$pgroupCount = 0 + +for ($i=0; $i -le $pgroups.Count; $i++) { + if ($pgroups[$i].PlatformTarget -eq "anycpu") { + if ($null -eq $pgroups[$i].Prefer32Bit) { + $child = $doc.CreateElement("Prefer32Bit", $doc.DocumentElement.xmlns) + $child.InnerText = "false" + $pgroups[$i].AppendChild($child) | out-null + } else { + $pgroups[$i].Prefer32Bit = "false" + } + + $pgroupCount++ + } +} + +$doc.Save($file) +"-- Set Prefer32Bit=false for $pgroupCount PropertyGroups"