Initial commit

main
Nightshade System 4 weeks ago
commit adfaef2b1a

330
.gitignore vendored

@ -0,0 +1,330 @@
## 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
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
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/
**/Properties/launchSettings.json
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.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/
# 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
.cr/
# 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/

@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyubey", "Kyubey\Kyubey.csproj", "{4DCAE4EA-12E7-44D6-9C0A-F4F9773D20B9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4DCAE4EA-12E7-44D6-9C0A-F4F9773D20B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4DCAE4EA-12E7-44D6-9C0A-F4F9773D20B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4DCAE4EA-12E7-44D6-9C0A-F4F9773D20B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DCAE4EA-12E7-44D6-9C0A-F4F9773D20B9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

@ -0,0 +1,17 @@
using LiteDB;
namespace Kyubey.Database;
public class JewelDbContext
{
private static LiteDatabase _db = null!;
public static ILiteCollection<Score> Scores = null!;
public static ILiteCollection<User> Users = null!;
public static void Init()
{
_db = new LiteDatabase("kyubey.db");
Scores = _db.GetCollection<Score>("scores");
Users = _db.GetCollection<User>("users");
}
}

@ -0,0 +1,30 @@
using Kyubey.Interop;
// ReSharper disable PropertyCanBeMadeInitOnly.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
namespace Kyubey.Database;
public record Score
{
public required string Id { get; set; }
public required User User { get; set; }
public required JewelryMode GameMode { get; set; }
public required DateTime Timestamp { get; set; }
public required uint Level { get; set; }
public required uint Points { get; set; }
public required uint Time { get; set; }
public required uint Jewels { get; set; }
public required int Class { get; set; }
// base64'd binary data
public required string Replay { get; set; }
public override string ToString()
{
var timestampChunk = Timestamp.ToString("dd MMM yyyy HH:mm tt");
var utcOffset = Timestamp.ToString("zz");
var formattedTimestamp = $"{timestampChunk} UTC{utcOffset}";
// unsure on if this is actually where class goes but it's the only logical place it could go
return $"{Id}\n\n{User.RankingName}\n{Points}\n{Class}\n{Level}\n{Jewels}\n{Time}\n{formattedTimestamp}\na\n";
}
}

@ -0,0 +1,9 @@
namespace Kyubey.Database;
public class User
{
public string Username { get; set; } = null!;
// argon2 hashed even though the game sends passwords in plain-text over the querystring
public string Password { get; set; } = null!;
public string RankingName { get; set; } = null!;
}

@ -0,0 +1,8 @@
namespace Kyubey.Interop;
public enum JewelryMode
{
Normal = 0,
Hard = 1,
Death = 2
}

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EmbedIO" Version="3.5.2" />
<PackageReference Include="LiteDB" Version="5.0.21" />
</ItemGroup>
</Project>

@ -0,0 +1,27 @@
using EmbedIO;
using EmbedIO.WebApi;
using Kyubey.Database;
namespace Kyubey;
class Program
{
static async Task Main(string[] args)
{
JewelDbContext.Init();
var server = new WebServer(e =>
e.WithUrlPrefix("http://hg.arika.co.jp:8081")
.WithMode(HttpListenerMode.EmbedIO))
.WithWebApi("/JM_test/service", m =>
{
m.OnHttpException = (ctx, ex) => ExceptionHandler.EmptyResponse(ctx, (ex as Exception)!);
m.WithController<ServiceController>();
})
.WithWebApi("/api", m =>
{
m.OnHttpException = (ctx, ex) => ExceptionHandler.EmptyResponse(ctx, (ex as Exception)!);
m.WithController<ServerApiController>();
});
await server.RunAsync();
}
}

@ -0,0 +1,37 @@
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using Kyubey.Database;
namespace Kyubey;
public class ServerApiController : WebApiController
{
public record ApiCreateUserRequestData
{
public required string Username { get; set; }
public required string Password { get; set; }
public required string RankingName { get; set; }
}
[Route(HttpVerbs.Post, "/create_user")]
public async Task<string> Signup([JsonData] ApiCreateUserRequestData data)
{
if (JewelDbContext.Users.Exists(e => e.Username == data.Username || e.RankingName == data.RankingName))
{
throw HttpException.BadRequest("Username and rankingname must be unique");
}
var hashedPassword = Utils.ComputeHash(data.Password);
var userObject = new User()
{
Username = data.Username,
Password = hashedPassword,
RankingName = data.RankingName
};
var id = JewelDbContext.Users.Insert(userObject).AsObjectId;
return $"ok; id={id}";
}
}

@ -0,0 +1,198 @@
using System.Text;
using EmbedIO;
using EmbedIO.Routing;
using EmbedIO.WebApi;
using Kyubey.Database;
using Kyubey.Interop;
namespace Kyubey;
public class ServiceController : WebApiController
{
[Route(HttpVerbs.Get, "/GameEntry")]
public async Task GameEntry()
{
var maybeUsername = Request.QueryString.Get("id");
var maybePassword = Request.QueryString.Get("pass");
var maybeVersion = Request.QueryString.Get("ver");
if (maybeUsername is not { } username ||
maybePassword is not { } password ||
maybeVersion is not "1.32")
{
throw HttpException.BadRequest();
}
var hashedPass = Utils.ComputeHash(password);
var loginUser = JewelDbContext.Users.FindOne(e => e.Username == username && e.Password == hashedPass);
var response = loginUser is not null ? "0" : "1";
var buf = Encoding.ASCII.GetBytes(response);
Response.ContentType = "text/plain";
Response.ContentEncoding = Encoding.ASCII;
Response.ContentLength64 = buf.Length;
await Response.OutputStream.WriteAsync(buf, 0, buf.Length);
}
[Route(HttpVerbs.Get, "/GetName")]
public async Task GetName()
{
var maybeId = Request.QueryString.Get("id");
if (maybeId is not { } id)
{
throw HttpException.BadRequest();
}
var rankingUser = JewelDbContext.Users.FindOne(e => e.Username == id);
if (rankingUser is null) throw HttpException.NotFound("No such userid");
var response = rankingUser.RankingName;
var buf = Encoding.ASCII.GetBytes(response);
Response.ContentType = "text/plain";
Response.ContentEncoding = Encoding.ASCII;
Response.ContentLength64 = buf.Length;
await Response.OutputStream.WriteAsync(buf, 0, buf.Length);
}
[Route(HttpVerbs.Get, "/GetMessage")]
public async Task GetMessage()
{
var responses = new[]
{
"jewelry master lives, but why",
"did you know this game sends your password in cleartext? it's hashed server-side though!",
"this is not an entirely serious project",
"powered by IDA - The Interactive Disassembler",
$"powered by Microsoft .NET version {Environment.Version}",
"powered by anime music",
"greetz 2 mihara, your EULA can't stop me",
"did you know i can make this text say anything i want",
"i wonder how many of these scrolling messages there are"
};
var responseIdx = Random.Shared.Next(responses.Length);
var response = $"the server says: {responses[responseIdx]}";
var buf = Encoding.ASCII.GetBytes(response);
Response.ContentType = "text/plain";
Response.ContentEncoding = Encoding.ASCII;
Response.ContentLength64 = buf.Length;
await Response.OutputStream.WriteAsync(buf, 0, buf.Length);
}
[Route(HttpVerbs.Get, "/GetRanking")]
public async Task GetRanking()
{
// view = -1 means top 10, 0 and above mean pages in the global ranking
var view = int.Parse(Request.QueryString.Get("view")!);
var mode = (JewelryMode)int.Parse(Request.QueryString.Get("mode")!);
var userId = Request.QueryString.Get("id");
var response = "";
if (userId is not null && view == -1 || userId is null) // ?????? this game makes no sense dude
{
var factor = view == -1 ? 0 : view;
Console.WriteLine($"factor={factor}");
var globalRanking = JewelDbContext.Scores
.Find(e => e.GameMode == mode)
.OrderByDescending(e => e.Points)
.Skip(factor*view)
.Take(10)
.ToList();
if (globalRanking.Any())
{
response = $"{factor}\n";
}
foreach (var score in globalRanking) response += score.ToString();
} else // personal ranking
{
var personalRanking = JewelDbContext.Scores
.Find(e => e.User.Username == userId && e.GameMode == mode)
.OrderByDescending(e => e.Points)
.Take(10)
.ToList();
if (personalRanking.Any())
{
response = "0\n";
}
foreach (var score in personalRanking) response += score.ToString();
}
var buf = Encoding.ASCII.GetBytes(response);
Response.ContentType = "text/plain";
Response.ContentEncoding = Encoding.ASCII;
Response.ContentLength64 = buf.Length;
await Response.OutputStream.WriteAsync(buf, 0, buf.Length);
}
[Route(HttpVerbs.Post, "/ScoreEntry")]
public async Task ScoreEntry()
{
var id = Request.QueryString.Get("id")!;
var mode = (JewelryMode)int.Parse(Request.QueryString.Get("mode")!);
var score = uint.Parse(Request.QueryString.Get("score")!);
var jewel = uint.Parse(Request.QueryString.Get("jewel")!);
var rank = int.Parse(Request.QueryString.Get("class")!); // probably title
var time = uint.Parse(Request.QueryString.Get("time")!);
var level = uint.Parse(Request.QueryString.Get("level")!);
var maybeUser = JewelDbContext.Users.FindOne(e => e.Username == id);
if (maybeUser is not {} user)
{
throw HttpException.NotFound("No such userid");
}
// TODO: add compression to replay data
var rawReplay = await HttpContext.GetRequestBodyAsByteArrayAsync();
var replay = Convert.ToBase64String(rawReplay);
var scoreRecord = new Score()
{
Id = Utils.GenerateCheapGuid(),
User = user,
GameMode = mode,
Points = score,
Class = rank,
Time = time,
Jewels = jewel,
Level = level,
Replay = replay,
Timestamp = DateTime.Now
};
JewelDbContext.Scores.Insert(scoreRecord);
JewelDbContext.Scores.EnsureIndex(e => e.Points);
Response.ContentType = "text/plain";
Response.ContentEncoding = Encoding.ASCII;
}
[Route(HttpVerbs.Get, "/GetReplay")]
public async Task GetReplay()
{
Response.ContentType = "application/octet-stream";
var replayId = Request.QueryString.Get("id")!;
var score = JewelDbContext.Scores.FindOne(e => e.Id == replayId);
if (score is null)
{
throw HttpException.NotFound("No such replay");
}
// deconvert the replay
var replayBytes = Convert.FromBase64String(score.Replay);
// fire it down the byte hole
await Response.OutputStream.WriteAsync(replayBytes, 0, replayBytes.Length);
}
}

@ -0,0 +1,24 @@
using System.Security.Cryptography;
using System.Text;
namespace Kyubey;
public class Utils
{
public static string ComputeHash(string payload)
{
using var sha = SHA256.Create();
var hashedBytes = sha.ComputeHash(Encoding.UTF8.GetBytes(payload));
return Convert.ToHexString(hashedBytes);
}
// Not really ideal but using a full System.Guid overflows a buffer within the game which breaks replay downloading
// This game is wonderful and working on it is even more so
public static string GenerateCheapGuid()
{
var rng = new Random();
var bytes = new byte[8];
rng.NextBytes(bytes);
return Convert.ToHexStringLower(bytes);
}
}

@ -0,0 +1,11 @@
# Kyubey
Server reimplementation for 2006 Arika video game [Jewelry Master](https://tetris.wiki/Jewelry_Master). The game never came out of beta and now you too can see why.
## Dependencies
Nothing but a standard .NET install
## Why?
I (Lappland) got really bored one day and decided to reverse this game's netcode
## Why the name?
Jewelry, soul gems, something like that?
Loading…
Cancel
Save