Initial commit

This commit is contained in:
Empathic Qubit 2021-05-16 10:43:37 -04:00
commit f9adc0bd60
27 changed files with 1957 additions and 0 deletions

351
.gitignore vendored Normal file
View file

@ -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

137
CMakeLists.txt Normal file
View file

@ -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 "$<TARGET_FILE:dsn_service>" ${SVR_PLUGIN_DIR} &&
echo file copied: "$<TARGET_FILE:dsn_service> -> ${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 "$<TARGET_FILE:dsn_service>" ${SSE_PLUGIN_DIR} &&
echo file copied: "$<TARGET_FILE:dsn_service> -> ${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()

View file

@ -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

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-3.5.0.0" newVersion="3.5.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View file

@ -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;
}
}
}

View file

@ -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<string> inputQueue = new BlockingCollection<string>();
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);
}
}
}
}

View file

@ -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

View file

@ -0,0 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<DragonBornModPath>SkyrimVR\Data\Plugins\Sumwunn</DragonBornModPath>
</PropertyGroup>
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{DEA491EE-C426-4B79-A443-CF5B1D795288}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>EmpathicQbt.ConsoleServer</RootNamespace>
<AssemblyName>EmpathicQbt.ConsoleServer</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<NuGetPackageImportStamp>
</NuGetPackageImportStamp>
<AllowedReferenceRelatedFileExtensions>-</AllowedReferenceRelatedFileExtensions>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\$(DragonBornModPath)\$(AssemblyName)\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\$(DragonBornModPath)\EmpathicQbt.ConsoleServer\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<RunPostBuildEvent>Always</RunPostBuildEvent>
</PropertyGroup>
<ItemGroup>
<Reference Include="Costura, Version=2.0.0.0, Culture=neutral, PublicKeyToken=9919ef960d84173d, processorArchitecture=MSIL">
<HintPath>..\packages\Costura.Fody.2.0.0\lib\net452\Costura.dll</HintPath>
</Reference>
<Reference Include="INIFileParser, Version=2.5.2.0, Culture=neutral, PublicKeyToken=79af7b307b65cf3c, processorArchitecture=MSIL">
<HintPath>..\packages\ini-parser.2.5.2\lib\net20\INIFileParser.dll</HintPath>
</Reference>
<Reference Include="Nancy, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Nancy.2.0.0\lib\net452\Nancy.dll</HintPath>
</Reference>
<Reference Include="Nancy.Hosting.Self, Version=2.0.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Nancy.Hosting.Self.2.0.0\lib\net452\Nancy.Hosting.Self.dll</HintPath>
</Reference>
<Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Web.Extensions" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="InputForwarder.cs" />
<Compile Include="ServerBootstrapper.cs" />
<Compile Include="ServerModule.cs" />
<Compile Include="ConsoleInput.cs" />
<Compile Include="Configuration.cs" />
<Compile Include="ExternalInterop.cs" />
<Compile Include="FavoritesList.cs" />
<Compile Include="SkyrimInterop.cs" />
<Compile Include="Log.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Content Include="FodyWeavers.xml" />
<Content Include="static\index.css">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="static\index.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="static\index.js">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="..\packages\Fody.3.0.0\build\Fody.targets" Condition="Exists('..\packages\Fody.3.0.0\build\Fody.targets')" />
<ItemGroup>
<DragonBornZip Include="$(USERPROFILE)\Downloads\DragonbornSpeaksNaturally*.zip" />
</ItemGroup>
<ItemGroup>
<FoMod Include="fomod\info.xml" />
<FoMod Include="fomod\ModuleConfig.xml" />
</ItemGroup>
<ItemGroup>
<IniFile Include="DragonbornSpeaksNaturally.SAMPLE.ini" />
</ItemGroup>
<Target Name="ConcatIniFile">
<!-- List all the files you want to concatenate. -->
<ItemGroup>
<ConcatFiles Include="
$(ProjectDir)\$(OutputPath)\..\DragonbornSpeaksNaturally.SAMPLE.ini;
$(ProjectDir)\DragonbornSpeaksNaturally.SAMPLE.ini"/>
</ItemGroup>
<!-- Read the contents of the files (preserving tabs/spaces). -->
<ItemGroup>
<FileContents Include="$([System.IO.File]::ReadAllText(%(ConcatFiles.Identity)))"/>
</ItemGroup>
<!-- Write the result to a single file. -->
<WriteLinesToFile File="$(ProjectDir)\$(OutputPath)\..\DragonbornSpeaksNaturally.SAMPLE.ini" Lines="@(FileContents)" Overwrite="true" />
</Target>
<Target Name="CopyDragonbornNaturallySpeakingOriginal" AfterTargets="Build">
<ItemGroup>
<DragonBornOutputs Include="$(ProjectDir)\$(OutputPath)\**\*.*" Visible="false" />
</ItemGroup>
<PropertyGroup>
<DragonBornShimGen><![CDATA[
"$(SolutionDir)\packages\chocolatey.0.10.14\tools\chocolateyInstall\tools\shimgen.exe" --output="$(ProjectDir)\$(OutputPath)\..\DragonbornSpeaksNaturally.exe" -p="$(AssemblyName)\$(AssemblyName).exe"
]]></DragonBornShimGen>
<DragonBornPluginRoot>$(ProjectDir)\$(OutputPath)\..\..\..\..\..</DragonBornPluginRoot>
<DragonBornSEOutputPath>$(DragonBornPluginRoot)\SkyrimSE\Data\Plugins\Sumwunn\$(AssemblyName)</DragonBornSEOutputPath>
</PropertyGroup>
<Unzip SourceFiles="@(DragonBornZip)" DestinationFolder="$(ProjectDir)\$(OutputPath)\..\..\..\..\.." />
<Move SourceFiles="$(ProjectDir)\$(OutputPath)\..\DragonbornSpeaksNaturally.exe" DestinationFiles="$(ProjectDir)\$(OutputPath)\..\DragonbornSpeaksNaturally.Original.exe" />
<Exec Command="$(DragonBornShimGen)" />
<CallTarget Targets="ConcatIniFile" />
<Copy SkipUnchangedFiles="true" SourceFiles="@(DragonBornOutputs)" DestinationFolder="$(DragonBornSEOutputPath)" />
<Copy SkipUnchangedFiles="true" SourceFiles="$(ProjectDir)\$(OutputPath)\..\DragonbornSpeaksNaturally.exe" DestinationFolder="$(DragonBornSEOutputPath)\.." />
<Copy SkipUnchangedFiles="true" SourceFiles="$(ProjectDir)\$(OutputPath)\..\DragonbornSpeaksNaturally.Original.exe" DestinationFolder="$(DragonBornSEOutputPath)\.." />
<Copy SkipUnchangedFiles="true" SourceFiles="$(ProjectDir)\$(OutputPath)\..\DragonbornSpeaksNaturally.SAMPLE.ini" DestinationFolder="$(DragonBornSEOutputPath)\.." />
<Copy SkipUnchangedFiles="true" SourceFiles="@(FoMod)" DestinationFolder="$(DragonBornPluginRoot)\fomod" />
</Target>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>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}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\Fody.3.0.0\build\Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Fody.3.0.0\build\Fody.targets'))" />
<Error Condition="!Exists('..\packages\Costura.Fody.2.0.0\build\Costura.Fody.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Costura.Fody.2.0.0\build\Costura.Fody.targets'))" />
</Target>
<Import Project="..\packages\Costura.Fody.2.0.0\build\Costura.Fody.targets" Condition="Exists('..\packages\Costura.Fody.2.0.0\build\Costura.Fody.targets')" />
</Project>

View file

@ -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<string> BATCH_FILENAMES = new HashSet<string>() { "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;
}
}
}

View file

@ -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<Favorite> Favorites { get; protected set; } = new List<Favorite>()
{
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<dynamic>(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);
}
}
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers>
<!--<Costura />-->
</Weavers>

View file

@ -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<string> outputQueue = new BlockingCollection<string>();
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();
}
}
}

View file

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

View file

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

View file

@ -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")]

View file

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

View file

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

View file

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

View file

@ -0,0 +1,72 @@
<!-- FOMod Creation Tool [http://www.nexusmods.com/fallout4/mods/6821] -->
<!-- FOMod Quick Guide [https://media.readthedocs.org/pdf/fomod-docs/latest/fomod-docs.pdf] -->
<!-- FOMod Reference Manual [https://fomod-designer.readthedocs.io/en/stable/] -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://qconsulting.ca/fo3/ModConfig5.0.xsd">
<moduleName>Dragonborn Speaks Naturally + Weapon Select UI and HTTP API</moduleName>
<requiredInstallFiles>
<folder source="docs" destination="Docs" priority="0"/>
</requiredInstallFiles>
<installSteps order="Explicit">
<installStep name="Choose Option">
<optionalFileGroups order="Explicit">
<group name="Skyrim Edition:" type="SelectExactlyOne">
<plugins order="Explicit">
<plugin name="Skyrim VR">
<description><![CDATA[This plugin is compatible and only compatible with SkyrimVR 1.4.15.0.
To make the plugin working, you must manually install xSHADOWMANx's Dll Loader to the game root directory.
You cannot install the loader via any MOD manager (But DSN can be installed via MOD Manager).
You can find the loader here: https://www.nexusmods.com/skyrimspecialedition/mods/3619]]></description>
<files>
<folder source="SkyrimVR\Data" destination="" priority="0"/>
<file source="fomod/DSN-installed.md" destination="SKSE/DSN/DSN-for-SkyrimVR-installed.md" priority="0"/>
</files>
<typeDescriptor>
<dependencyType>
<defaultType name="Optional"/>
<patterns>
<pattern>
<dependencies operator="Or">
<fileDependency file="SkyrimVR.esp" state="Active"/>
<fileDependency file="SkyrimVR.esp" state="Inactive"/>
</dependencies>
<type name="Recommended"/>
</pattern>
</patterns>
</dependencyType>
</typeDescriptor>
</plugin>
<plugin name="Skyrim Special Edition">
<description><![CDATA[This plugin is compatible and only compatible with Skyrim Special Edition 1.5.97.0.
To make the plugin working, you must manually install xSHADOWMANx's Dll Loader to the game root directory.
You cannot install the loader via any MOD manager (But DSN can be installed via MOD Manager).
You can find the loader here: https://www.nexusmods.com/skyrimspecialedition/mods/3619]]></description>
<files>
<folder source="SkyrimSE\Data" destination="" priority="0"/>
<file source="fomod/DSN-installed.md" destination="SKSE/DSN/DSN-for-SkyrimSE-installed.md" priority="0"/>
</files>
<typeDescriptor>
<dependencyType>
<defaultType name="Optional"/>
<patterns>
<pattern>
<dependencies operator="Or">
<fileDependency file="SkyrimVR.esm" state="Missing"/>
</dependencies>
<type name="Recommended"/>
</pattern>
</patterns>
</dependencyType>
</typeDescriptor>
</plugin>
</plugins>
</group>
</optionalFileGroups>
</installStep>
</installSteps>
</config>

View file

@ -0,0 +1,6 @@
<!-- Created with FOMOD Creation Tool 1.7.0.37 [http://www.nexusmods.com/fallout4/mods/6821] -->
<!-- FOMod Document [https://media.readthedocs.org/pdf/fomod-docs/latest/fomod-docs.pdf] -->
<fomod>
<Name>Dragonborn Speaks Naturally + Weapon Select UI and HTTP API</Name>
<Website>https://www.nexusmods.com/skyrimspecialedition/mods/FIXME</Website>
</fomod>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="chocolatey" version="0.10.14" targetFramework="net461" />
<package id="Costura.Fody" version="2.0.0" targetFramework="net461" />
<package id="Fody" version="3.0.0" targetFramework="net461" developmentDependency="true" />
<package id="ini-parser" version="2.5.2" targetFramework="net461" />
<package id="KeystrokeAPI" version="1.0.6.4" targetFramework="net461" />
<package id="Nancy" version="2.0.0" targetFramework="net461" />
<package id="Nancy.Hosting.Self" version="2.0.0" targetFramework="net461" />
<package id="Newtonsoft.Json" version="9.0.1" targetFramework="net461" />
</packages>

View file

@ -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;
}

View file

@ -0,0 +1,11 @@
<html>
<head>
<link rel="stylesheet" type="text/css" href="/index.css" />
</head>
<body>
<div id="content"></div>
<script type="module">
import './index.js'
</script>
</body>
</html>

View file

@ -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)

11
README.md Normal file
View file

@ -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

84
configure.bat Normal file
View file

@ -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 <https://zhidao.baidu.com/question/982407720655882739.html>
: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
)
)

26
disable_prefer_32bit.ps1 Normal file
View file

@ -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"