First working version of the server
KCP gameserver with packet dispatch, scene streaming, combat, gacha and the shop, plus a small HTTP server for the login flow and the wish history page. Game data under assets is pulled out of the client instead of being typed by hand. The excel tables come from the packed bin container and the spawn points from the compiled scene scripts.
This commit is contained in:
parent
89e56a5c8e
commit
fc2ac1050f
559 changed files with 100045 additions and 1 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -477,3 +477,7 @@ fabric.properties
|
||||||
# Android studio 3.1+ serialized cache file
|
# Android studio 3.1+ serialized cache file
|
||||||
.idea/caches/build_file_checksums.ser
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
.idea/
|
||||||
|
NuGet.config
|
||||||
|
appsettings.Local.json
|
||||||
|
|
|
||||||
48
README.md
48
README.md
|
|
@ -1,3 +1,49 @@
|
||||||
# yuanshen
|
# yuanshen
|
||||||
|
|
||||||
A simple PS for GI v0.9.10 (Dev Build)
|
A private server for Genshin Impact 0.9.10 (CBT3), written from scratch in C#.
|
||||||
|
|
||||||
|
I started this to figure out how the client actually talks to its server, and it grew from
|
||||||
|
"can I get past the login screen" into something you can walk around in. You can log in, run
|
||||||
|
around Mondstadt, kill hilichurls, buy things and pull on a banner. Plenty is still missing.
|
||||||
|
|
||||||
|
## What's in here
|
||||||
|
|
||||||
|
* `Yuanshen.Gameserver` is the KCP server. Packet dispatch, scenes, combat, gacha, shop, GM commands.
|
||||||
|
* `Yuanshen.SDK` is a small HTTP server on 8899 for the login flow, plus a wish history page.
|
||||||
|
* `Yuanshen.Proto` holds the generated protobuf types and the CmdType table.
|
||||||
|
* `Yuanshen.Common` has excel loading and the shared console stuff.
|
||||||
|
* `Yuanshen.Kcp` is a vendored copy of KcpSharp. See THIRD-PARTY-NOTICES.md.
|
||||||
|
|
||||||
|
## Getting it running
|
||||||
|
|
||||||
|
.NET 10 and a PostgreSQL somewhere. Both servers read their connection string from
|
||||||
|
`appsettings.json` and default to postgres/postgres on localhost. That is a local dev default,
|
||||||
|
not a leaked password, but change it if your setup is different.
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet build Yuanshen.sln
|
||||||
|
```
|
||||||
|
|
||||||
|
Start the SDK first, then the gameserver, then a patched client. The gameserver console takes
|
||||||
|
GM commands directly, `/help` lists them.
|
||||||
|
|
||||||
|
## About the game data
|
||||||
|
|
||||||
|
Everything under `Yuanshen.Gameserver/assets` came out of the client rather than being typed
|
||||||
|
by hand. The excel tables live in a packed bin container that turns out to be a single byte
|
||||||
|
XOR, with the key derived from the version string, and the monster spawn points come out of
|
||||||
|
the compiled Lua scene scripts.
|
||||||
|
|
||||||
|
One thing you'll notice: items show up as ids and weapon types instead of names. The client
|
||||||
|
only ships text map hashes, and there is no text map in the build, so there is nothing to
|
||||||
|
resolve names against yet.
|
||||||
|
|
||||||
|
## State of things
|
||||||
|
|
||||||
|
Working: login, world streaming, movement, monsters and combat, gacha with pity and the
|
||||||
|
50/50, shop, wish history.
|
||||||
|
|
||||||
|
Not done: quests, dungeons, co-op, chests and world drops.
|
||||||
|
|
||||||
|
This is a reverse engineering project for learning. It is not affiliated with miHoYo and it
|
||||||
|
is not trying to be a playable replacement for the real game.
|
||||||
|
|
|
||||||
13
THIRD-PARTY-NOTICES.md
Normal file
13
THIRD-PARTY-NOTICES.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Third party code
|
||||||
|
|
||||||
|
## KcpSharp
|
||||||
|
|
||||||
|
`Yuanshen.Kcp` is a vendored copy of KcpSharp by yigolden, with the root namespace renamed
|
||||||
|
to `Yuanshen.Kcp`. Traces of the original name are still visible in the `using` aliases in
|
||||||
|
`KcpConversation.cs`, `KcpReceiveQueue.cs` and `KcpRawReceiveQueue.cs`, and in the XML docs.
|
||||||
|
|
||||||
|
Upstream: https://github.com/yigolden/KcpSharp
|
||||||
|
|
||||||
|
KcpSharp is distributed under the MIT license. Before publishing this repository, copy the
|
||||||
|
upstream `LICENSE` file into `Yuanshen.Kcp/LICENSE` so the original copyright notice ships
|
||||||
|
with the code. The MIT license requires that notice to be preserved in redistributions.
|
||||||
53
Yuanshen.Common/Console/ConsoleBanner.cs
Normal file
53
Yuanshen.Common/Console/ConsoleBanner.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
using Pastel;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Console;
|
||||||
|
|
||||||
|
public static class ConsoleBanner
|
||||||
|
{
|
||||||
|
private static readonly string[] Art =
|
||||||
|
{
|
||||||
|
@"██╗ ██╗██╗ ██╗ █████╗ ███╗ ██╗███████╗██╗ ██╗███████╗███╗ ██╗",
|
||||||
|
@"╚██╗ ██╔╝██║ ██║██╔══██╗████╗ ██║██╔════╝██║ ██║██╔════╝████╗ ██║",
|
||||||
|
@" ╚████╔╝ ██║ ██║███████║██╔██╗ ██║███████╗███████║█████╗ ██╔██╗ ██║",
|
||||||
|
@" ╚██╔╝ ██║ ██║██╔══██║██║╚██╗██║╚════██║██╔══██║██╔══╝ ██║╚██╗██║",
|
||||||
|
@" ██║ ╚██████╔╝██║ ██║██║ ╚████║███████║██║ ██║███████╗██║ ╚████║",
|
||||||
|
@" ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝"
|
||||||
|
};
|
||||||
|
|
||||||
|
private const int RuleWidth = 70;
|
||||||
|
|
||||||
|
public static void Print(string title, IReadOnlyList<(string Key, string Value)> rows)
|
||||||
|
{
|
||||||
|
ConsoleTheme.Enable();
|
||||||
|
System.Console.WriteLine();
|
||||||
|
|
||||||
|
for (int i = 0; i < Art.Length; i++)
|
||||||
|
{
|
||||||
|
double t = (double)i / Math.Max(Art.Length - 1, 1);
|
||||||
|
|
||||||
|
string rendered = ConsoleTheme.Gradient(
|
||||||
|
Art[i],
|
||||||
|
ConsoleTheme.Lerp(ConsoleTheme.GradientFrom, ConsoleTheme.GradientTo, t * 0.5),
|
||||||
|
ConsoleTheme.Lerp(ConsoleTheme.GradientFrom, ConsoleTheme.GradientTo, 0.5 + t * 0.5));
|
||||||
|
|
||||||
|
System.Console.WriteLine(ConsoleTheme.Center(Art[i], rendered));
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Console.WriteLine();
|
||||||
|
System.Console.WriteLine(ConsoleTheme.Center(title, ConsoleTheme.Gradient(title)));
|
||||||
|
System.Console.WriteLine();
|
||||||
|
|
||||||
|
string rule = new('─', RuleWidth);
|
||||||
|
System.Console.WriteLine(ConsoleTheme.Center(rule, rule.Pastel(ConsoleTheme.Muted)));
|
||||||
|
|
||||||
|
foreach ((string key, string value) in rows)
|
||||||
|
{
|
||||||
|
string plain = $"{key,-14}{value}";
|
||||||
|
string rendered = $"{key,-14}".Pastel(ConsoleTheme.Accent) + value.Pastel(ConsoleTheme.Text);
|
||||||
|
System.Console.WriteLine(ConsoleTheme.Center(plain, rendered));
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Console.WriteLine(ConsoleTheme.Center(rule, rule.Pastel(ConsoleTheme.Muted)));
|
||||||
|
System.Console.WriteLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
132
Yuanshen.Common/Console/ConsolePrompt.cs
Normal file
132
Yuanshen.Common/Console/ConsolePrompt.cs
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Text;
|
||||||
|
using Pastel;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Console;
|
||||||
|
|
||||||
|
public static class ConsolePrompt
|
||||||
|
{
|
||||||
|
private const string ClearLine = "\u001b[2K";
|
||||||
|
private const int PromptWidth = 60;
|
||||||
|
|
||||||
|
private static readonly object SyncRoot = new();
|
||||||
|
private static readonly StringBuilder Buffer = new();
|
||||||
|
|
||||||
|
private static string _label = ">";
|
||||||
|
private static bool _active;
|
||||||
|
private static Func<string, ValueTask>? _handler;
|
||||||
|
|
||||||
|
public static void Start(string label, Func<string, ValueTask> handler)
|
||||||
|
{
|
||||||
|
ConsoleTheme.Enable();
|
||||||
|
_label = label;
|
||||||
|
_handler = handler;
|
||||||
|
_active = true;
|
||||||
|
|
||||||
|
lock (SyncRoot)
|
||||||
|
Draw();
|
||||||
|
|
||||||
|
Thread thread = new(ReadLoop) { IsBackground = true, Name = "console-prompt" };
|
||||||
|
thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Write(string line)
|
||||||
|
{
|
||||||
|
lock (SyncRoot)
|
||||||
|
{
|
||||||
|
if (_active)
|
||||||
|
System.Console.Write("\r" + ClearLine);
|
||||||
|
|
||||||
|
foreach (string part in line.Split('\n'))
|
||||||
|
System.Console.WriteLine(ConsoleTheme.Center(part.TrimEnd('\r')));
|
||||||
|
|
||||||
|
if (_active)
|
||||||
|
Draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Reply(string line) => Write(line.Pastel(ConsoleTheme.Text));
|
||||||
|
|
||||||
|
public static void Warn(string line) => Write(line.Pastel(Color.FromArgb(0xFF, 0xB3, 0x4D)));
|
||||||
|
|
||||||
|
private static void Draw()
|
||||||
|
{
|
||||||
|
int pad = Math.Max((ConsoleTheme.Width - PromptWidth) / 2, 0);
|
||||||
|
|
||||||
|
System.Console.Write("\r" + ClearLine);
|
||||||
|
System.Console.Write(new string(' ', pad));
|
||||||
|
System.Console.Write(_label.Pastel(ConsoleTheme.Accent));
|
||||||
|
System.Console.Write(' ');
|
||||||
|
System.Console.Write(Buffer.ToString().Pastel(ConsoleTheme.Text));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReadLoop()
|
||||||
|
{
|
||||||
|
while (_active)
|
||||||
|
{
|
||||||
|
ConsoleKeyInfo key;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
key = System.Console.ReadKey(intercept: true);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
_active = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.Key == ConsoleKey.Enter)
|
||||||
|
{
|
||||||
|
string command;
|
||||||
|
|
||||||
|
lock (SyncRoot)
|
||||||
|
{
|
||||||
|
command = Buffer.ToString().Trim();
|
||||||
|
Buffer.Clear();
|
||||||
|
System.Console.Write("\r" + ClearLine);
|
||||||
|
|
||||||
|
if (command.Length != 0)
|
||||||
|
System.Console.WriteLine(ConsoleTheme.Center($"{_label} {command}".Pastel(ConsoleTheme.Muted)));
|
||||||
|
|
||||||
|
Draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.Length != 0 && _handler is not null)
|
||||||
|
Dispatch(command);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (SyncRoot)
|
||||||
|
{
|
||||||
|
if (key.Key == ConsoleKey.Backspace)
|
||||||
|
{
|
||||||
|
if (Buffer.Length != 0)
|
||||||
|
Buffer.Length--;
|
||||||
|
}
|
||||||
|
else if (!char.IsControl(key.KeyChar))
|
||||||
|
{
|
||||||
|
Buffer.Append(key.KeyChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
Draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Dispatch(string command)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _handler!(command);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Write($" console command failed: {exception.Message}".Pastel(Color.IndianRed));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
95
Yuanshen.Common/Console/ConsoleTheme.cs
Normal file
95
Yuanshen.Common/Console/ConsoleTheme.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Pastel;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Console;
|
||||||
|
|
||||||
|
public static class ConsoleTheme
|
||||||
|
{
|
||||||
|
public static readonly Color GradientFrom = Color.FromArgb(0x7B, 0x2F, 0xF7);
|
||||||
|
public static readonly Color GradientTo = Color.FromArgb(0xE0, 0x40, 0xFB);
|
||||||
|
public static readonly Color Accent = Color.FromArgb(0xC7, 0x7D, 0xFF);
|
||||||
|
public static readonly Color Muted = Color.FromArgb(0x6B, 0x5E, 0x7A);
|
||||||
|
public static readonly Color Text = Color.FromArgb(0xEC, 0xE4, 0xF5);
|
||||||
|
|
||||||
|
private static bool _enabled;
|
||||||
|
|
||||||
|
public static void Enable()
|
||||||
|
{
|
||||||
|
if (_enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_enabled = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsoleExtensions.Enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Color Lerp(Color from, Color to, double t)
|
||||||
|
{
|
||||||
|
t = Math.Clamp(t, 0d, 1d);
|
||||||
|
return Color.FromArgb(
|
||||||
|
(int)(from.R + (to.R - from.R) * t),
|
||||||
|
(int)(from.G + (to.G - from.G) * t),
|
||||||
|
(int)(from.B + (to.B - from.B) * t));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Gradient(string text) => Gradient(text, GradientFrom, GradientTo);
|
||||||
|
|
||||||
|
public static string Gradient(string text, Color from, Color to)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text))
|
||||||
|
return text;
|
||||||
|
|
||||||
|
System.Text.StringBuilder builder = new(text.Length * 12);
|
||||||
|
int steps = Math.Max(text.Length - 1, 1);
|
||||||
|
|
||||||
|
for (int i = 0; i < text.Length; i++)
|
||||||
|
{
|
||||||
|
char symbol = text[i];
|
||||||
|
if (symbol == ' ')
|
||||||
|
{
|
||||||
|
builder.Append(symbol);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(symbol.ToString().Pastel(Lerp(from, to, (double)i / steps)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int Width
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return System.Console.WindowWidth > 20 ? System.Console.WindowWidth : 100;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Center(string plain, string rendered)
|
||||||
|
{
|
||||||
|
int pad = Math.Max((Width - plain.Length) / 2, 0);
|
||||||
|
return new string(' ', pad) + rendered;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Center(string rendered) => Center(Strip(rendered), rendered);
|
||||||
|
|
||||||
|
public static string Strip(string text) => AnsiPattern.Replace(text, string.Empty);
|
||||||
|
|
||||||
|
private static readonly Regex AnsiPattern = new((char)27 + @"\[[0-9;]*m", RegexOptions.Compiled);
|
||||||
|
}
|
||||||
75
Yuanshen.Common/Console/ThemedLoggerProvider.cs
Normal file
75
Yuanshen.Common/Console/ThemedLoggerProvider.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
using System.Drawing;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Pastel;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Console;
|
||||||
|
|
||||||
|
public sealed class ThemedLoggerProvider : ILoggerProvider
|
||||||
|
{
|
||||||
|
private readonly LogLevel _minimum;
|
||||||
|
|
||||||
|
public ThemedLoggerProvider(LogLevel minimum = LogLevel.Information)
|
||||||
|
{
|
||||||
|
ConsoleTheme.Enable();
|
||||||
|
_minimum = minimum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ILogger CreateLogger(string categoryName) => new ThemedLogger(categoryName, _minimum);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ThemedLogger : ILogger
|
||||||
|
{
|
||||||
|
private static readonly Color Timestamp = Color.FromArgb(0x5A, 0x4E, 0x68);
|
||||||
|
private static readonly Color Category = Color.FromArgb(0x9A, 0x7F, 0xC0);
|
||||||
|
|
||||||
|
private readonly string _category;
|
||||||
|
private readonly LogLevel _minimum;
|
||||||
|
|
||||||
|
public ThemedLogger(string category, LogLevel minimum)
|
||||||
|
{
|
||||||
|
int lastDot = category.LastIndexOf('.');
|
||||||
|
_category = lastDot >= 0 ? category[(lastDot + 1)..] : category;
|
||||||
|
_minimum = minimum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||||
|
|
||||||
|
public bool IsEnabled(LogLevel logLevel) => logLevel >= _minimum;
|
||||||
|
|
||||||
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||||
|
Func<TState, Exception?, string> formatter)
|
||||||
|
{
|
||||||
|
if (!IsEnabled(logLevel))
|
||||||
|
return;
|
||||||
|
|
||||||
|
string message = formatter(state, exception);
|
||||||
|
if (string.IsNullOrEmpty(message) && exception is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
(Color color, string label) = logLevel switch
|
||||||
|
{
|
||||||
|
LogLevel.Trace => (Color.FromArgb(0x5A, 0x4E, 0x68), "trc"),
|
||||||
|
LogLevel.Debug => (Color.FromArgb(0x8B, 0x7D, 0x9B), "dbg"),
|
||||||
|
LogLevel.Information => (ConsoleTheme.Accent, "inf"),
|
||||||
|
LogLevel.Warning => (Color.FromArgb(0xFF, 0xB3, 0x4D), "wrn"),
|
||||||
|
LogLevel.Error => (Color.FromArgb(0xFF, 0x5C, 0x7A), "err"),
|
||||||
|
LogLevel.Critical => (Color.FromArgb(0xFF, 0x2D, 0x95), "crt"),
|
||||||
|
_ => (ConsoleTheme.Text, "log")
|
||||||
|
};
|
||||||
|
|
||||||
|
string line =
|
||||||
|
$"{DateTime.Now:HH:mm:ss}".Pastel(Timestamp) + " " +
|
||||||
|
label.Pastel(color) + " " +
|
||||||
|
_category.Pastel(Category) + " " +
|
||||||
|
message.Pastel(ConsoleTheme.Text);
|
||||||
|
|
||||||
|
ConsolePrompt.Write(line);
|
||||||
|
|
||||||
|
if (exception is not null)
|
||||||
|
ConsolePrompt.Write(exception.ToString().Pastel(Color.FromArgb(0xFF, 0x5C, 0x7A)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Yuanshen.Common/Data/Excel/Attributes/ExcelAttribute.cs
Normal file
14
Yuanshen.Common/Data/Excel/Attributes/ExcelAttribute.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
namespace Yuanshen.Common.Data.Excel.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||||
|
public sealed class ExcelAttribute : Attribute
|
||||||
|
{
|
||||||
|
public ExcelType Type { get; }
|
||||||
|
public string AssetName { get; }
|
||||||
|
|
||||||
|
public ExcelAttribute(ExcelType type, string assetName)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
AssetName = assetName;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Yuanshen.Common/Data/Excel/AvatarAbilitiesExcel.cs
Normal file
13
Yuanshen.Common/Data/Excel/AvatarAbilitiesExcel.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Yuanshen.Common.Data.Excel.Attributes;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
[Excel(ExcelType.AvatarAbilities, "AvatarAbilities.json")]
|
||||||
|
public sealed class AvatarAbilitiesExcel : ExcelItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")] public uint Id { get; set; }
|
||||||
|
[JsonPropertyName("abilities")] public List<string> Abilities { get; set; } = new();
|
||||||
|
|
||||||
|
public override uint ExcelId => Id;
|
||||||
|
}
|
||||||
23
Yuanshen.Common/Data/Excel/AvatarExcel.cs
Normal file
23
Yuanshen.Common/Data/Excel/AvatarExcel.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Yuanshen.Common.Data.Excel.Attributes;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
[Excel(ExcelType.Avatar, "AvatarExcelConfigData.json")]
|
||||||
|
public sealed class AvatarExcel : ExcelItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")] public uint Id { get; set; }
|
||||||
|
[JsonPropertyName("qualityType")] public string QualityType { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("iconName")] public string IconName { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("weaponType")] public string WeaponType { get; set; } = "WEAPON_SWORD_ONE_HAND";
|
||||||
|
[JsonPropertyName("skillDepotId")] public uint SkillDepotId { get; set; }
|
||||||
|
[JsonPropertyName("initialWeapon")] public uint InitialWeapon { get; set; }
|
||||||
|
[JsonPropertyName("hp_base")] public double HpBase { get; set; }
|
||||||
|
[JsonPropertyName("attack_base")] public double AttackBase { get; set; }
|
||||||
|
[JsonPropertyName("defense_base")] public double DefenseBase { get; set; }
|
||||||
|
[JsonPropertyName("critical")] public double Critical { get; set; }
|
||||||
|
[JsonPropertyName("criticalHurt")] public double CriticalHurt { get; set; }
|
||||||
|
[JsonPropertyName("chargeEfficiency")] public double ChargeEfficiency { get; set; }
|
||||||
|
|
||||||
|
public override uint ExcelId => Id;
|
||||||
|
}
|
||||||
17
Yuanshen.Common/Data/Excel/AvatarSkillDepotExcel.cs
Normal file
17
Yuanshen.Common/Data/Excel/AvatarSkillDepotExcel.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Yuanshen.Common.Data.Excel.Attributes;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
[Excel(ExcelType.AvatarSkillDepot, "AvatarSkillDepotExcelConfigData.json")]
|
||||||
|
public sealed class AvatarSkillDepotExcel : ExcelItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")] public uint Id { get; set; }
|
||||||
|
[JsonPropertyName("energySkill")] public uint EnergySkill { get; set; }
|
||||||
|
[JsonPropertyName("skills")] public List<uint> Skills { get; set; } = new();
|
||||||
|
[JsonPropertyName("subSkills")] public List<uint> SubSkills { get; set; } = new();
|
||||||
|
[JsonPropertyName("talents")] public List<uint> Talents { get; set; } = new();
|
||||||
|
[JsonPropertyName("inherentProudSkillOpens")] public List<uint> InherentProudSkillOpens { get; set; } = new();
|
||||||
|
|
||||||
|
public override uint ExcelId => Id;
|
||||||
|
}
|
||||||
6
Yuanshen.Common/Data/Excel/ExcelItem.cs
Normal file
6
Yuanshen.Common/Data/Excel/ExcelItem.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
public abstract class ExcelItem
|
||||||
|
{
|
||||||
|
public abstract uint ExcelId { get; }
|
||||||
|
}
|
||||||
31
Yuanshen.Common/Data/Excel/ExcelTable.cs
Normal file
31
Yuanshen.Common/Data/Excel/ExcelTable.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
public sealed class ExcelTable
|
||||||
|
{
|
||||||
|
private readonly ImmutableArray<ExcelItem> _items;
|
||||||
|
private readonly ImmutableDictionary<uint, ExcelItem> _itemsById;
|
||||||
|
|
||||||
|
public ExcelTable(JsonDocument document, Type type)
|
||||||
|
{
|
||||||
|
ImmutableArray<ExcelItem>.Builder items = ImmutableArray.CreateBuilder<ExcelItem>();
|
||||||
|
|
||||||
|
foreach (JsonElement element in document.RootElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (element.ValueKind != JsonValueKind.Object)
|
||||||
|
throw new ArgumentException("Excel table root must be an array of objects");
|
||||||
|
|
||||||
|
items.Add((element.Deserialize(type) as ExcelItem)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
_items = items.ToImmutable();
|
||||||
|
_itemsById = _items.ToImmutableDictionary(item => item.ExcelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<ExcelItem> Items => _items;
|
||||||
|
|
||||||
|
public TExcel? GetById<TExcel>(uint id) where TExcel : ExcelItem
|
||||||
|
=> _itemsById.TryGetValue(id, out ExcelItem? item) ? item as TExcel : null;
|
||||||
|
}
|
||||||
33
Yuanshen.Common/Data/Excel/ExcelTableCollection.cs
Normal file
33
Yuanshen.Common/Data/Excel/ExcelTableCollection.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Reflection;
|
||||||
|
using Yuanshen.Common.Data.Excel.Attributes;
|
||||||
|
using Yuanshen.Common.Data.Provider;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
public sealed class ExcelTableCollection
|
||||||
|
{
|
||||||
|
private readonly ImmutableDictionary<ExcelType, ExcelTable> _tables;
|
||||||
|
|
||||||
|
public ExcelTableCollection(IAssetProvider assetProvider)
|
||||||
|
{
|
||||||
|
ImmutableDictionary<ExcelType, ExcelTable>.Builder builder =
|
||||||
|
ImmutableDictionary.CreateBuilder<ExcelType, ExcelTable>();
|
||||||
|
|
||||||
|
IEnumerable<Type> types = Assembly.GetExecutingAssembly().GetTypes()
|
||||||
|
.Where(type => type.GetCustomAttribute<ExcelAttribute>() != null);
|
||||||
|
|
||||||
|
foreach (Type type in types)
|
||||||
|
{
|
||||||
|
ExcelAttribute attribute = type.GetCustomAttribute<ExcelAttribute>()!;
|
||||||
|
builder.Add(attribute.Type, new ExcelTable(assetProvider.GetExcelTableJson(attribute.AssetName), type));
|
||||||
|
}
|
||||||
|
|
||||||
|
_tables = builder.ToImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExcelTable GetTable(ExcelType type) => _tables[type];
|
||||||
|
|
||||||
|
public TExcel? GetExcel<TExcel>(ExcelType type, uint id) where TExcel : ExcelItem
|
||||||
|
=> _tables[type].GetById<TExcel>(id);
|
||||||
|
}
|
||||||
16
Yuanshen.Common/Data/Excel/ExcelType.cs
Normal file
16
Yuanshen.Common/Data/Excel/ExcelType.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
public enum ExcelType
|
||||||
|
{
|
||||||
|
Avatar,
|
||||||
|
Weapon,
|
||||||
|
Reliquary,
|
||||||
|
ReliquaryMainProp,
|
||||||
|
ReliquaryAffix,
|
||||||
|
AvatarSkillDepot,
|
||||||
|
AvatarAbilities,
|
||||||
|
Monster,
|
||||||
|
Material,
|
||||||
|
ShopGoods,
|
||||||
|
ShopmallEntrance
|
||||||
|
}
|
||||||
36
Yuanshen.Common/Data/Excel/MaterialExcel.cs
Normal file
36
Yuanshen.Common/Data/Excel/MaterialExcel.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Yuanshen.Common.Data.Excel.Attributes;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
public sealed class MaterialItemUse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("useOp")] public string UseOp { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("useParam")] public List<string> UseParam { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Excel(ExcelType.Material, "MaterialExcelConfigData.json")]
|
||||||
|
public sealed class MaterialExcel : ExcelItem
|
||||||
|
{
|
||||||
|
public const string GainAvatar = "ITEM_USE_GAIN_AVATAR";
|
||||||
|
|
||||||
|
[JsonPropertyName("id")] public uint Id { get; set; }
|
||||||
|
[JsonPropertyName("materialType")] public string MaterialType { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("itemUse")] public List<MaterialItemUse> ItemUse { get; set; } = new();
|
||||||
|
|
||||||
|
public override uint ExcelId => Id;
|
||||||
|
|
||||||
|
public uint GainedAvatarId
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
foreach (MaterialItemUse use in ItemUse)
|
||||||
|
{
|
||||||
|
if (use.UseOp == GainAvatar && use.UseParam.Count != 0 && uint.TryParse(use.UseParam[0], out uint avatarId))
|
||||||
|
return avatarId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Yuanshen.Common/Data/Excel/MonsterExcel.cs
Normal file
16
Yuanshen.Common/Data/Excel/MonsterExcel.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Yuanshen.Common.Data.Excel.Attributes;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
[Excel(ExcelType.Monster, "MonsterExcelConfigData.json")]
|
||||||
|
public sealed class MonsterExcel : ExcelItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")] public uint Id { get; set; }
|
||||||
|
[JsonPropertyName("monsterName")] public string MonsterName { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("hp_base")] public double HpBase { get; set; }
|
||||||
|
[JsonPropertyName("attack_base")] public double AttackBase { get; set; }
|
||||||
|
[JsonPropertyName("defense_base")] public double DefenseBase { get; set; }
|
||||||
|
|
||||||
|
public override uint ExcelId => Id;
|
||||||
|
}
|
||||||
35
Yuanshen.Common/Data/Excel/PackedBin.cs
Normal file
35
Yuanshen.Common/Data/Excel/PackedBin.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
public static class PackedBin
|
||||||
|
{
|
||||||
|
public const string ClientScriptVersion = "0.9.10";
|
||||||
|
|
||||||
|
public static byte GetKey(string scriptVersion)
|
||||||
|
{
|
||||||
|
int boundary = scriptVersion.LastIndexOf('.');
|
||||||
|
int key = 0;
|
||||||
|
|
||||||
|
for (int index = 0; index < boundary; index++)
|
||||||
|
key = (key + (scriptVersion[index] & 0xFF)) & 0xFF;
|
||||||
|
|
||||||
|
return key == 0 ? (byte)1 : (byte)key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Decode(Span<byte> bytes, byte key)
|
||||||
|
{
|
||||||
|
for (int index = 0; index < bytes.Length; index++)
|
||||||
|
{
|
||||||
|
bytes[index] ^= key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] Decode(ReadOnlySpan<byte> bytes, string scriptVersion = ClientScriptVersion)
|
||||||
|
{
|
||||||
|
byte[] decoded = bytes.ToArray();
|
||||||
|
Decode(decoded, GetKey(scriptVersion));
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] DecodeFile(string path, string scriptVersion = ClientScriptVersion)
|
||||||
|
=> Decode(File.ReadAllBytes(path), scriptVersion);
|
||||||
|
}
|
||||||
15
Yuanshen.Common/Data/Excel/ReliquaryAffixExcel.cs
Normal file
15
Yuanshen.Common/Data/Excel/ReliquaryAffixExcel.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Yuanshen.Common.Data.Excel.Attributes;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
[Excel(ExcelType.ReliquaryAffix, "ReliquaryAffixExcelConfigData.json")]
|
||||||
|
public sealed class ReliquaryAffixExcel : ExcelItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")] public uint Id { get; set; }
|
||||||
|
[JsonPropertyName("depotId")] public uint DepotId { get; set; }
|
||||||
|
[JsonPropertyName("propType")] public string PropType { get; set; } = string.Empty;
|
||||||
|
[JsonPropertyName("propValue")] public float PropValue { get; set; }
|
||||||
|
|
||||||
|
public override uint ExcelId => Id;
|
||||||
|
}
|
||||||
17
Yuanshen.Common/Data/Excel/ReliquaryExcel.cs
Normal file
17
Yuanshen.Common/Data/Excel/ReliquaryExcel.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Yuanshen.Common.Data.Excel.Attributes;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
[Excel(ExcelType.Reliquary, "ReliquaryExcelConfigData.json")]
|
||||||
|
public sealed class ReliquaryExcel : ExcelItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")] public uint Id { get; set; }
|
||||||
|
[JsonPropertyName("equipType")] public string EquipType { get; set; } = "EQUIP_BRACER";
|
||||||
|
[JsonPropertyName("rankLevel")] public uint RankLevel { get; set; }
|
||||||
|
[JsonPropertyName("mainPropDepotId")] public uint MainPropDepotId { get; set; }
|
||||||
|
[JsonPropertyName("appendPropDepotId")] public uint AppendPropDepotId { get; set; }
|
||||||
|
[JsonPropertyName("setId")] public uint SetId { get; set; }
|
||||||
|
|
||||||
|
public override uint ExcelId => Id;
|
||||||
|
}
|
||||||
14
Yuanshen.Common/Data/Excel/ReliquaryMainPropExcel.cs
Normal file
14
Yuanshen.Common/Data/Excel/ReliquaryMainPropExcel.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Yuanshen.Common.Data.Excel.Attributes;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
[Excel(ExcelType.ReliquaryMainProp, "ReliquaryMainPropExcelConfigData.json")]
|
||||||
|
public sealed class ReliquaryMainPropExcel : ExcelItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")] public uint Id { get; set; }
|
||||||
|
[JsonPropertyName("propDepotId")] public uint PropDepotId { get; set; }
|
||||||
|
[JsonPropertyName("propType")] public string PropType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public override uint ExcelId => Id;
|
||||||
|
}
|
||||||
28
Yuanshen.Common/Data/Excel/ShopGoodsExcel.cs
Normal file
28
Yuanshen.Common/Data/Excel/ShopGoodsExcel.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Yuanshen.Common.Data.Excel.Attributes;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
public sealed class ShopCostItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")] public uint Id { get; set; }
|
||||||
|
[JsonPropertyName("count")] public uint Count { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Excel(ExcelType.ShopGoods, "ShopGoodsExcelConfigData.json")]
|
||||||
|
public sealed class ShopGoodsExcel : ExcelItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("goodsId")] public uint GoodsId { get; set; }
|
||||||
|
[JsonPropertyName("shopType")] public uint ShopType { get; set; }
|
||||||
|
[JsonPropertyName("itemId")] public uint ItemId { get; set; }
|
||||||
|
[JsonPropertyName("itemCount")] public uint ItemCount { get; set; }
|
||||||
|
[JsonPropertyName("costHcoin")] public uint CostHcoin { get; set; }
|
||||||
|
[JsonPropertyName("costScoin")] public uint CostScoin { get; set; }
|
||||||
|
[JsonPropertyName("costItems")] public List<ShopCostItem> CostItems { get; set; } = new();
|
||||||
|
[JsonPropertyName("buyLimit")] public uint BuyLimit { get; set; }
|
||||||
|
[JsonPropertyName("minPlayerLevel")] public uint MinPlayerLevel { get; set; }
|
||||||
|
[JsonPropertyName("maxPlayerLevel")] public uint MaxPlayerLevel { get; set; }
|
||||||
|
[JsonPropertyName("sortLevel")] public uint SortLevel { get; set; }
|
||||||
|
|
||||||
|
public override uint ExcelId => GoodsId;
|
||||||
|
}
|
||||||
14
Yuanshen.Common/Data/Excel/ShopmallEntranceExcel.cs
Normal file
14
Yuanshen.Common/Data/Excel/ShopmallEntranceExcel.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Yuanshen.Common.Data.Excel.Attributes;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
[Excel(ExcelType.ShopmallEntrance, "ShopmallEntranceExcelConfigData.json")]
|
||||||
|
public sealed class ShopmallEntranceExcel : ExcelItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")] public uint Id { get; set; }
|
||||||
|
[JsonPropertyName("shopType")] public uint ShopType { get; set; }
|
||||||
|
[JsonPropertyName("order")] public uint Order { get; set; }
|
||||||
|
|
||||||
|
public override uint ExcelId => Id;
|
||||||
|
}
|
||||||
14
Yuanshen.Common/Data/Excel/WeaponExcel.cs
Normal file
14
Yuanshen.Common/Data/Excel/WeaponExcel.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Yuanshen.Common.Data.Excel.Attributes;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
[Excel(ExcelType.Weapon, "WeaponExcelConfigData.json")]
|
||||||
|
public sealed class WeaponExcel : ExcelItem
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")] public uint Id { get; set; }
|
||||||
|
[JsonPropertyName("weaponType")] public string WeaponType { get; set; } = "WEAPON_SWORD_ONE_HAND";
|
||||||
|
[JsonPropertyName("rankLevel")] public uint RankLevel { get; set; }
|
||||||
|
|
||||||
|
public override uint ExcelId => Id;
|
||||||
|
}
|
||||||
8
Yuanshen.Common/Data/Provider/IAssetProvider.cs
Normal file
8
Yuanshen.Common/Data/Provider/IAssetProvider.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Provider;
|
||||||
|
|
||||||
|
public interface IAssetProvider
|
||||||
|
{
|
||||||
|
JsonDocument GetExcelTableJson(string assetName);
|
||||||
|
}
|
||||||
15
Yuanshen.Common/Data/Provider/LocalAssetProvider.cs
Normal file
15
Yuanshen.Common/Data/Provider/LocalAssetProvider.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Provider;
|
||||||
|
|
||||||
|
internal sealed class LocalAssetProvider : IAssetProvider
|
||||||
|
{
|
||||||
|
private const string ExcelDirectory = "assets/excel/";
|
||||||
|
|
||||||
|
public JsonDocument GetExcelTableJson(string assetName)
|
||||||
|
{
|
||||||
|
string path = Path.Combine(AppContext.BaseDirectory, ExcelDirectory, assetName);
|
||||||
|
using FileStream stream = File.OpenRead(path);
|
||||||
|
return JsonDocument.Parse(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Yuanshen.Common/Data/Provider/ServiceCollectionExtensions.cs
Normal file
14
Yuanshen.Common/Data/Provider/ServiceCollectionExtensions.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Yuanshen.Common.Data.Excel;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Data.Provider;
|
||||||
|
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection UseLocalAssets(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IAssetProvider, LocalAssetProvider>();
|
||||||
|
services.AddSingleton<ExcelTableCollection>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Yuanshen.Common/Extensions/StringExtensions.cs
Normal file
16
Yuanshen.Common/Extensions/StringExtensions.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
namespace Yuanshen.Common.Extensions;
|
||||||
|
|
||||||
|
public static class StringExtensions
|
||||||
|
{
|
||||||
|
public static uint GetStableHash(this string value)
|
||||||
|
{
|
||||||
|
uint hash = 0;
|
||||||
|
|
||||||
|
foreach (char symbol in value)
|
||||||
|
{
|
||||||
|
hash = symbol + 131 * hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Yuanshen.Common/Security/MhySecurity.cs
Normal file
32
Yuanshen.Common/Security/MhySecurity.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using Yuanshen.Common.Security.Util;
|
||||||
|
|
||||||
|
namespace Yuanshen.Common.Security;
|
||||||
|
|
||||||
|
public static class MhySecurity
|
||||||
|
{
|
||||||
|
public static byte[] InitialKey { get; } = new byte[0x1000];
|
||||||
|
|
||||||
|
public static byte[] GenerateSecretKey(ulong seed)
|
||||||
|
{
|
||||||
|
byte[] key = GC.AllocateUninitializedArray<byte>(0x1000);
|
||||||
|
Span<byte> keySpan = key.AsSpan();
|
||||||
|
|
||||||
|
MT19937 mt = new(seed);
|
||||||
|
|
||||||
|
for (int i = 0; i < 0x1000; i += 8)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteUInt64BigEndian(keySpan[i..], mt.Int63());
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Xor(Span<byte> data, ReadOnlySpan<byte> key)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < data.Length; i++)
|
||||||
|
{
|
||||||
|
data[i] ^= key[i % key.Length];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
56
Yuanshen.Common/Security/Util/MT19937.cs
Normal file
56
Yuanshen.Common/Security/Util/MT19937.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
namespace Yuanshen.Common.Security.Util;
|
||||||
|
|
||||||
|
internal sealed class MT19937
|
||||||
|
{
|
||||||
|
private const ulong N = 312;
|
||||||
|
private const ulong M = 156;
|
||||||
|
private const ulong MATRIX_A = 0xB5026F5AA96619E9L;
|
||||||
|
private const ulong UPPER_MASK = 0xFFFFFFFF80000000;
|
||||||
|
private const ulong LOWER_MASK = 0X7FFFFFFFUL;
|
||||||
|
|
||||||
|
private readonly ulong[] _mt = new ulong[N + 1];
|
||||||
|
private ulong _mti = N + 1;
|
||||||
|
|
||||||
|
public MT19937(ulong seed) => Seed(seed);
|
||||||
|
|
||||||
|
public void Seed(ulong seed)
|
||||||
|
{
|
||||||
|
_mt[0] = seed;
|
||||||
|
for (_mti = 1; _mti < N; _mti++)
|
||||||
|
{
|
||||||
|
_mt[_mti] = 6364136223846793005L * (_mt[_mti - 1] ^ (_mt[_mti - 1] >> 62)) + _mti;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong Int63()
|
||||||
|
{
|
||||||
|
ulong x = 0;
|
||||||
|
ulong[] mag01 = { 0x0UL, MATRIX_A };
|
||||||
|
|
||||||
|
if (_mti >= N)
|
||||||
|
{
|
||||||
|
ulong kk;
|
||||||
|
if (_mti == N + 1) Seed(5489UL);
|
||||||
|
for (kk = 0; kk < N - M; kk++)
|
||||||
|
{
|
||||||
|
x = (_mt[kk] & UPPER_MASK) | (_mt[kk + 1] & LOWER_MASK);
|
||||||
|
_mt[kk] = _mt[kk + M] ^ (x >> 1) ^ mag01[x & 0x1UL];
|
||||||
|
}
|
||||||
|
for (; kk < N - 1; kk++)
|
||||||
|
{
|
||||||
|
x = (_mt[kk] & UPPER_MASK) | (_mt[kk + 1] & LOWER_MASK);
|
||||||
|
_mt[kk] = _mt[kk - M] ^ (x >> 1) ^ mag01[x & 0x1UL];
|
||||||
|
}
|
||||||
|
x = (_mt[N - 1] & UPPER_MASK) | (_mt[0] & LOWER_MASK);
|
||||||
|
_mt[N - 1] = _mt[M - 1] ^ (x >> 1) ^ mag01[x & 0x1UL];
|
||||||
|
_mti = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = _mt[_mti++];
|
||||||
|
x ^= (x >> 29) & 0x5555555555555555L;
|
||||||
|
x ^= (x << 17) & 0x71D67FFFEDA60000L;
|
||||||
|
x ^= (x << 37) & 0xFFF7EEE000000000L;
|
||||||
|
x ^= x >> 43;
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Yuanshen.Common/Yuanshen.Common.csproj
Normal file
15
Yuanshen.Common/Yuanshen.Common.csproj
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Pastel" Version="2.1.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
72
Yuanshen.Gameserver/Controllers/AbilityController.cs
Normal file
72
Yuanshen.Gameserver/Controllers/AbilityController.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers;
|
||||||
|
|
||||||
|
[NetController]
|
||||||
|
internal sealed class AbilityController : ControllerBase
|
||||||
|
{
|
||||||
|
[NetCommand(CmdType.AbilityInvocationsNotify)]
|
||||||
|
public async ValueTask<IResult> OnAbilityInvocationsNotify(NetSession session)
|
||||||
|
{
|
||||||
|
AbilityInvocationsNotify notify = Packet!.DecodeBody<AbilityInvocationsNotify>();
|
||||||
|
await ForwardAsync(session, notify.Invokes);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.ClientAbilityInitFinishNotify)]
|
||||||
|
public async ValueTask<IResult> OnClientAbilityInitFinishNotify(NetSession session, Player player, SceneManager scene, ILogger<AbilityController> logger)
|
||||||
|
{
|
||||||
|
ClientAbilityInitFinishNotify notify = Packet!.DecodeBody<ClientAbilityInitFinishNotify>();
|
||||||
|
|
||||||
|
foreach (AbilityInvokeEntry entry in notify.Invokes)
|
||||||
|
{
|
||||||
|
logger.LogDebug("AbilityInit entity={e} arg={arg} fwd={fwd} instAbility={ia} instModifier={im} localId={l} target={t}",
|
||||||
|
notify.EntityId, entry.ArgumentType, entry.ForwardType,
|
||||||
|
entry.Head?.InstancedAbilityId, entry.Head?.InstancedModifierId, entry.Head?.LocalId, entry.Head?.TargetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ForwardAsync(session, notify.Invokes);
|
||||||
|
|
||||||
|
GameMonster? monster = player.Monsters.FirstOrDefault(m => m.EntityId == notify.EntityId);
|
||||||
|
if (monster is not null)
|
||||||
|
await scene.SyncMonsterLevelAsync(monster);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.ClientAbilityChangeNotify)]
|
||||||
|
public async ValueTask<IResult> OnClientAbilityChangeNotify(NetSession session)
|
||||||
|
{
|
||||||
|
ClientAbilityChangeNotify notify = Packet!.DecodeBody<ClientAbilityChangeNotify>();
|
||||||
|
await ForwardAsync(session, notify.Invokes);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EvtDoSkillSuccNotify)]
|
||||||
|
public ValueTask<IResult> OnEvtDoSkillSuccNotify(ILogger<AbilityController> logger)
|
||||||
|
{
|
||||||
|
logger.LogDebug("DoSkillSucc raw={body}", Convert.ToHexString(Packet!.Body.Span));
|
||||||
|
return ValueTask.FromResult(Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async ValueTask ForwardAsync(NetSession session, IEnumerable<AbilityInvokeEntry> invokes)
|
||||||
|
{
|
||||||
|
AbilityInvocationsNotify forward = new();
|
||||||
|
|
||||||
|
foreach (AbilityInvokeEntry entry in invokes)
|
||||||
|
{
|
||||||
|
if (entry.ForwardType is ForwardType.ForwardLocal)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
forward.Invokes.Add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forward.Invokes.Count != 0)
|
||||||
|
await session.NotifyAsync(CmdType.AbilityInvocationsNotify, forward);
|
||||||
|
}
|
||||||
|
}
|
||||||
164
Yuanshen.Gameserver/Controllers/AccountController.cs
Normal file
164
Yuanshen.Gameserver/Controllers/AccountController.cs
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
using Yuanshen.Gameserver.Database;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers;
|
||||||
|
|
||||||
|
[NetController]
|
||||||
|
internal sealed class AccountController : ControllerBase
|
||||||
|
{
|
||||||
|
[NetCommand(CmdType.GetPlayerTokenReq)]
|
||||||
|
public async ValueTask<IResult> OnGetPlayerTokenReq(Player player, IAccountRepository accounts)
|
||||||
|
{
|
||||||
|
GetPlayerTokenReq req = Packet!.DecodeBody<GetPlayerTokenReq>();
|
||||||
|
|
||||||
|
uint uid = await accounts.GetOrCreateUidAsync(req.AccountUid);
|
||||||
|
player.AssignUid(uid);
|
||||||
|
|
||||||
|
GetPlayerTokenRsp rsp = new()
|
||||||
|
{
|
||||||
|
PlayerUid = player.Uid,
|
||||||
|
AccountToken = req.AccountToken,
|
||||||
|
AccountType = (int)req.AccountType,
|
||||||
|
SecretKey = 1337
|
||||||
|
};
|
||||||
|
|
||||||
|
return Response(CmdType.GetPlayerTokenRsp, rsp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.PingReq)]
|
||||||
|
public async ValueTask<IResult> OnPingReq(SceneManager scene)
|
||||||
|
{
|
||||||
|
PingReq req = Packet!.DecodeBody<PingReq>();
|
||||||
|
|
||||||
|
await scene.SyncPendingMonsterLevelsAsync();
|
||||||
|
|
||||||
|
PingRsp rsp = new()
|
||||||
|
{
|
||||||
|
Seq = (uint)req.Seq,
|
||||||
|
ClientTime = (uint)req.ClientTime
|
||||||
|
};
|
||||||
|
|
||||||
|
return Response(CmdType.PingRsp, rsp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.PlayerLoginReq)]
|
||||||
|
public async ValueTask<IResult> OnPlayerLoginReq(NetSession session, Player player, GameState state, PlayerPersistence persistence, OpenStateTable openStates, ILogger<AccountController> logger)
|
||||||
|
{
|
||||||
|
player.InitDefault();
|
||||||
|
persistence.Acquire();
|
||||||
|
|
||||||
|
PlayerRecord? record = await persistence.LoadAsync();
|
||||||
|
if (record is not null)
|
||||||
|
{
|
||||||
|
player.ApplyRecord(record);
|
||||||
|
|
||||||
|
if (record.PosX != 0f || record.PosY != 0f || record.PosZ != 0f)
|
||||||
|
state.UpdatePosition(new Vector { X = record.PosX, Y = record.PosY, Z = record.PosZ });
|
||||||
|
|
||||||
|
logger.LogInformation("Loaded player {uid}: pos=({x}, {y}, {z}), team={team}, killed={killed}",
|
||||||
|
player.Uid, state.CurrentPos.X, state.CurrentPos.Y, state.CurrentPos.Z,
|
||||||
|
player.TeamAvatarGuids.Count, record.KilledNodes.Count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.LogInformation("No saved record for player {uid}, starting fresh", player.Uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
await session.NotifyAsync(CmdType.PlayerDataNotify, new PlayerDataNotify
|
||||||
|
{
|
||||||
|
NickName = player.Nickname,
|
||||||
|
PropMap =
|
||||||
|
{
|
||||||
|
{ 4001u, new PropValue { Type = 4001, Ival = 1, Val = 1 } },
|
||||||
|
{ 10006u, new PropValue { Type = 10006, Ival = 1, Val = 1 } },
|
||||||
|
{ 10009u, new PropValue { Type = 10009, Ival = 1, Val = 1 } },
|
||||||
|
{ 10010u, new PropValue { Type = 10010, Ival = 24000, Val = 24000 } },
|
||||||
|
{ 10011u, new PropValue { Type = 10011, Ival = 24000, Val = 24000 } },
|
||||||
|
{ 10013u, new PropValue { Type = 10013, Ival = 60, Val = 60 } },
|
||||||
|
{ 10015u, new PropValue { Type = 10015, Ival = player.Hcoin, Val = player.Hcoin } },
|
||||||
|
{ 10016u, new PropValue { Type = 10016, Ival = player.Scoin, Val = player.Scoin } },
|
||||||
|
{ 10019u, new PropValue { Type = 10019, Ival = 8, Val = 8 } },
|
||||||
|
{ 10020u, new PropValue { Type = 10020, Ival = 160, Val = 160 } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await session.NotifyAsync(CmdType.AvatarDataNotify, BuildAvatarData(player));
|
||||||
|
await session.NotifyAsync(CmdType.PlayerStoreNotify, BuildStore(player));
|
||||||
|
|
||||||
|
OpenStateUpdateNotify openStateNotify = new();
|
||||||
|
foreach (uint openStateId in openStates.Ids)
|
||||||
|
openStateNotify.OpenStateMap[openStateId] = 1;
|
||||||
|
|
||||||
|
await session.NotifyAsync(CmdType.OpenStateUpdateNotify, openStateNotify);
|
||||||
|
|
||||||
|
await session.NotifyAsync(CmdType.PlayerEnterSceneNotify, new PlayerEnterSceneNotify
|
||||||
|
{
|
||||||
|
SceneId = GameState.SceneId,
|
||||||
|
Pos = state.CurrentPos,
|
||||||
|
SceneBeginTime = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||||
|
Type = EnterType.EnterSelf,
|
||||||
|
TargetUid = player.Uid,
|
||||||
|
EnterSceneToken = state.NextEnterSceneToken(),
|
||||||
|
IsFirstLoginEnterScene = true,
|
||||||
|
WorldLevel = 0,
|
||||||
|
EnterReason = 1
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response(CmdType.PlayerLoginRsp, new PlayerLoginRsp
|
||||||
|
{
|
||||||
|
Retcode = 0,
|
||||||
|
GameBiz = "hk4e_global"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AvatarDataNotify BuildAvatarData(Player player)
|
||||||
|
{
|
||||||
|
AvatarDataNotify data = new()
|
||||||
|
{
|
||||||
|
CurAvatarTeamId = player.CurrentTeamId,
|
||||||
|
ChooseAvatarGuid = player.CurrentAvatarGuid
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (uint flycloak in Player.Flycloaks)
|
||||||
|
data.OwnedFlycloakList.Add(flycloak);
|
||||||
|
|
||||||
|
foreach (GameAvatar avatar in player.Avatars)
|
||||||
|
data.AvatarList.Add(avatar.AsAvatarInfo());
|
||||||
|
|
||||||
|
AvatarTeam team = new() { TeamName = "Team " + player.CurrentTeamId };
|
||||||
|
team.AvatarGuidList.AddRange(player.TeamAvatarGuids);
|
||||||
|
data.AvatarTeamMap.Add(player.CurrentTeamId, team);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayerStoreNotify BuildStore(Player player)
|
||||||
|
{
|
||||||
|
PlayerStoreNotify store = new()
|
||||||
|
{
|
||||||
|
StoreType = StoreType.StorePack,
|
||||||
|
WeightLimit = 30000
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (GameWeapon weapon in player.Weapons)
|
||||||
|
store.ItemList.Add(weapon.AsItem());
|
||||||
|
|
||||||
|
foreach (GameReliquary reliquary in player.Reliquaries)
|
||||||
|
store.ItemList.Add(reliquary.AsItem());
|
||||||
|
|
||||||
|
foreach ((uint itemId, uint count) in player.Materials)
|
||||||
|
store.ItemList.Add(new Item
|
||||||
|
{
|
||||||
|
ItemId = itemId,
|
||||||
|
Guid = ((ulong)player.Uid << 32) | itemId,
|
||||||
|
Material = new Material { Count = count }
|
||||||
|
});
|
||||||
|
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||||
|
internal class NetCommandAttribute : Attribute
|
||||||
|
{
|
||||||
|
public CmdType CmdType { get; }
|
||||||
|
|
||||||
|
public NetCommandAttribute(CmdType cmdType)
|
||||||
|
{
|
||||||
|
CmdType = cmdType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||||
|
internal class NetControllerAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
137
Yuanshen.Gameserver/Controllers/AvatarController.cs
Normal file
137
Yuanshen.Gameserver/Controllers/AvatarController.cs
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Gameserver.Database;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers;
|
||||||
|
|
||||||
|
[NetController]
|
||||||
|
internal sealed class AvatarController : ControllerBase
|
||||||
|
{
|
||||||
|
[NetCommand(CmdType.SetUpAvatarTeamReq)]
|
||||||
|
public async ValueTask<IResult> OnSetUpAvatarTeamReq(Player player, SceneManager sceneManager, PlayerPersistence persistence)
|
||||||
|
{
|
||||||
|
SetUpAvatarTeamReq req = Packet!.DecodeBody<SetUpAvatarTeamReq>();
|
||||||
|
|
||||||
|
if (req.AvatarTeamGuidList.Count == 0)
|
||||||
|
return Response(CmdType.SetUpAvatarTeamRsp, new SetUpAvatarTeamRsp { Retcode = -1 });
|
||||||
|
|
||||||
|
await sceneManager.SetupTeamAsync(req.TeamId, req.AvatarTeamGuidList, req.CurAvatarGuid);
|
||||||
|
await persistence.SaveAsync();
|
||||||
|
|
||||||
|
SetUpAvatarTeamRsp rsp = new()
|
||||||
|
{
|
||||||
|
Retcode = 0,
|
||||||
|
TeamId = req.TeamId,
|
||||||
|
CurAvatarGuid = player.CurrentAvatarGuid
|
||||||
|
};
|
||||||
|
rsp.AvatarTeamGuidList.AddRange(req.AvatarTeamGuidList);
|
||||||
|
|
||||||
|
return Response(CmdType.SetUpAvatarTeamRsp, rsp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.ChangeAvatarReq)]
|
||||||
|
public async ValueTask<IResult> OnChangeAvatarReq(SceneManager sceneManager, PlayerPersistence persistence)
|
||||||
|
{
|
||||||
|
ChangeAvatarReq req = Packet!.DecodeBody<ChangeAvatarReq>();
|
||||||
|
|
||||||
|
await sceneManager.ChangeAvatarAsync(req.Guid);
|
||||||
|
await persistence.SaveAsync();
|
||||||
|
|
||||||
|
return Response(CmdType.ChangeAvatarRsp, new ChangeAvatarRsp
|
||||||
|
{
|
||||||
|
Retcode = 0,
|
||||||
|
CurrGuid = req.Guid,
|
||||||
|
SkillId = req.SkillId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool OnScene(Player player, GameAvatar avatar)
|
||||||
|
=> player.TeamAvatarGuids.Contains(avatar.Guid);
|
||||||
|
|
||||||
|
[NetCommand(CmdType.WearEquipReq)]
|
||||||
|
public async ValueTask<IResult> OnWearEquipReq(NetSession session, Player player, SceneManager sceneManager, PlayerPersistence persistence, ILogger<AvatarController> logger)
|
||||||
|
{
|
||||||
|
WearEquipReq req = Packet!.DecodeBody<WearEquipReq>();
|
||||||
|
|
||||||
|
GameAvatar? avatar = player.FindAvatar(req.AvatarGuid);
|
||||||
|
GameWeapon? weapon = player.FindWeapon(req.EquipGuid);
|
||||||
|
GameReliquary? reliquary = player.FindReliquary(req.EquipGuid);
|
||||||
|
|
||||||
|
logger.LogDebug("WearEquip avatarGuid={a} equipGuid={e} -> avatar={av} weapon={w} reliquary={r}",
|
||||||
|
req.AvatarGuid, req.EquipGuid,
|
||||||
|
avatar is null ? "NOT FOUND" : avatar.AvatarId.ToString(),
|
||||||
|
weapon is null ? "-" : weapon.ItemId.ToString(),
|
||||||
|
reliquary is null ? "-" : reliquary.ItemId.ToString());
|
||||||
|
|
||||||
|
if (avatar is not null && weapon is not null)
|
||||||
|
{
|
||||||
|
avatar.WeaponGuid = weapon.Guid;
|
||||||
|
|
||||||
|
await session.NotifyAsync(CmdType.AvatarEquipChangeNotify, new AvatarEquipChangeNotify
|
||||||
|
{
|
||||||
|
AvatarGuid = avatar.Guid,
|
||||||
|
EquipType = 6,
|
||||||
|
ItemId = weapon.ItemId,
|
||||||
|
EquipGuid = weapon.Guid,
|
||||||
|
Weapon = weapon.AsSceneWeapon(OnScene(player, avatar) ? avatar.WeaponEntityId : 0)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (avatar is not null && reliquary is not null)
|
||||||
|
{
|
||||||
|
avatar.ReliquaryGuids[reliquary.EquipType] = reliquary.Guid;
|
||||||
|
|
||||||
|
await session.NotifyAsync(CmdType.AvatarEquipChangeNotify, new AvatarEquipChangeNotify
|
||||||
|
{
|
||||||
|
AvatarGuid = avatar.Guid,
|
||||||
|
EquipType = reliquary.EquipType,
|
||||||
|
ItemId = reliquary.ItemId,
|
||||||
|
EquipGuid = reliquary.Guid,
|
||||||
|
Reliquary = reliquary.AsSceneReliquary()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatar is not null && player.TeamAvatarGuids.Contains(avatar.Guid))
|
||||||
|
await session.NotifyAsync(CmdType.SceneTeamUpdateNotify, sceneManager.BuildSceneTeamUpdate());
|
||||||
|
|
||||||
|
if (avatar is not null)
|
||||||
|
await persistence.SaveAsync();
|
||||||
|
|
||||||
|
return Response(CmdType.WearEquipRsp, new WearEquipRsp
|
||||||
|
{
|
||||||
|
Retcode = 0,
|
||||||
|
AvatarGuid = req.AvatarGuid,
|
||||||
|
EquipGuid = req.EquipGuid
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.AvatarWearFlycloakReq)]
|
||||||
|
public async ValueTask<IResult> OnAvatarWearFlycloakReq(Player player, NetSession session, PlayerPersistence persistence)
|
||||||
|
{
|
||||||
|
AvatarWearFlycloakReq req = Packet!.DecodeBody<AvatarWearFlycloakReq>();
|
||||||
|
|
||||||
|
GameAvatar? avatar = player.Avatars.FirstOrDefault(candidate => candidate.Guid == req.AvatarGuid);
|
||||||
|
if (avatar is null)
|
||||||
|
return Response(CmdType.AvatarWearFlycloakRsp, new AvatarWearFlycloakRsp { Retcode = Retcode.RET_AVATAR_ID_ERROR });
|
||||||
|
|
||||||
|
avatar.FlycloakId = req.FlycloakId;
|
||||||
|
|
||||||
|
await session.NotifyAsync(CmdType.AvatarFlycloakChangeNotify, new AvatarFlycloakChangeNotify
|
||||||
|
{
|
||||||
|
AvatarGuid = req.AvatarGuid,
|
||||||
|
FlycloakId = req.FlycloakId
|
||||||
|
});
|
||||||
|
|
||||||
|
await persistence.SaveAsync();
|
||||||
|
|
||||||
|
return Response(CmdType.AvatarWearFlycloakRsp, new AvatarWearFlycloakRsp
|
||||||
|
{
|
||||||
|
Retcode = Retcode.RET_SUCC,
|
||||||
|
AvatarGuid = req.AvatarGuid,
|
||||||
|
FlycloakId = req.FlycloakId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
44
Yuanshen.Gameserver/Controllers/ChatController.cs
Normal file
44
Yuanshen.Gameserver/Controllers/ChatController.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers;
|
||||||
|
|
||||||
|
[NetController]
|
||||||
|
internal sealed class ChatController : ControllerBase
|
||||||
|
{
|
||||||
|
[NetCommand(CmdType.PlayerChatReq)]
|
||||||
|
public async ValueTask<IResult> OnPlayerChatReq(NetSession session, GmCommandService gm, Player player)
|
||||||
|
{
|
||||||
|
PlayerChatReq req = Packet!.DecodeBody<PlayerChatReq>();
|
||||||
|
string text = req.ChatInfo?.Text ?? string.Empty;
|
||||||
|
|
||||||
|
if (text.StartsWith('/'))
|
||||||
|
{
|
||||||
|
foreach (string line in await gm.ExecuteAsync(text))
|
||||||
|
await ReplyAsync(session, req.ChannelId, player.Uid, line);
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(text))
|
||||||
|
{
|
||||||
|
await ReplyAsync(session, req.ChannelId, player.Uid, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(CmdType.PlayerChatRsp, new PlayerChatRsp { Retcode = 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async ValueTask ReplyAsync(NetSession session, uint channelId, uint uid, string text)
|
||||||
|
{
|
||||||
|
await session.NotifyAsync(CmdType.PlayerChatNotify, new PlayerChatNotify
|
||||||
|
{
|
||||||
|
ChannelId = channelId,
|
||||||
|
ChatInfo = new ChatInfo
|
||||||
|
{
|
||||||
|
Time = (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||||
|
Uid = uid,
|
||||||
|
Text = text
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
99
Yuanshen.Gameserver/Controllers/CombatController.cs
Normal file
99
Yuanshen.Gameserver/Controllers/CombatController.cs
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
using Google.Protobuf;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers;
|
||||||
|
|
||||||
|
[NetController]
|
||||||
|
internal sealed class CombatController : ControllerBase
|
||||||
|
{
|
||||||
|
[NetCommand(CmdType.UnionCmdNotify)]
|
||||||
|
public async ValueTask<IResult> OnUnionCmdNotify(GameState state, Player player, NetSession session, CombatManager combat, WorldSpawner worldSpawner, ILogger<CombatController> logger)
|
||||||
|
{
|
||||||
|
UnionCmdNotify notify = Packet!.DecodeBody<UnionCmdNotify>();
|
||||||
|
|
||||||
|
foreach (UnionCmd cmd in notify.CmdList)
|
||||||
|
{
|
||||||
|
if (cmd.MessageId == (uint)CmdType.CombatInvocationsNotify)
|
||||||
|
await ProcessCombatInvocationsAsync(CombatInvocationsNotify.Parser.ParseFrom(cmd.Body), state, player, combat, worldSpawner, logger, session);
|
||||||
|
else if (cmd.MessageId == (uint)CmdType.AbilityInvocationsNotify)
|
||||||
|
await AbilityController.ForwardAsync(session, AbilityInvocationsNotify.Parser.ParseFrom(cmd.Body).Invokes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.CombatInvocationsNotify)]
|
||||||
|
public async ValueTask<IResult> OnCombatInvocationsNotify(GameState state, Player player, CombatManager combat, WorldSpawner worldSpawner, ILogger<CombatController> logger, NetSession session)
|
||||||
|
{
|
||||||
|
await ProcessCombatInvocationsAsync(Packet!.DecodeBody<CombatInvocationsNotify>(), state, player, combat, worldSpawner, logger, session);
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EvtBeingHitNotify)]
|
||||||
|
public async ValueTask<IResult> OnEvtBeingHitNotify(CombatManager combat)
|
||||||
|
{
|
||||||
|
EvtBeingHitNotify notify = Packet!.DecodeBody<EvtBeingHitNotify>();
|
||||||
|
if (notify.BeingHitInfo?.AttackResult is not null)
|
||||||
|
await combat.HandleAttackAsync(notify.BeingHitInfo.AttackResult);
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EvtBeingHitsCombineNotify)]
|
||||||
|
public async ValueTask<IResult> OnEvtBeingHitsCombineNotify(CombatManager combat)
|
||||||
|
{
|
||||||
|
EvtBeingHitsCombineNotify notify = Packet!.DecodeBody<EvtBeingHitsCombineNotify>();
|
||||||
|
|
||||||
|
foreach (EvtBeingHitInfo hit in notify.EvtBeingHitInfoList)
|
||||||
|
{
|
||||||
|
if (hit.AttackResult is not null)
|
||||||
|
await combat.HandleAttackAsync(hit.AttackResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async ValueTask ProcessCombatInvocationsAsync(CombatInvocationsNotify notify, GameState state, Player player, CombatManager combat, WorldSpawner worldSpawner, ILogger logger, NetSession session)
|
||||||
|
{
|
||||||
|
uint activeEntityId = player.ActiveAvatar?.EntityId ?? 0;
|
||||||
|
CombatInvocationsNotify forward = new();
|
||||||
|
|
||||||
|
foreach (CombatInvokeEntry entry in notify.InvokeList)
|
||||||
|
{
|
||||||
|
switch (entry.ArgumentType)
|
||||||
|
{
|
||||||
|
case CombatTypeArgument.EntityMove:
|
||||||
|
EntityMoveInfo move = EntityMoveInfo.Parser.ParseFrom(entry.CombatData);
|
||||||
|
if (move.EntityId == activeEntityId && move.MotionInfo?.Pos is not null)
|
||||||
|
{
|
||||||
|
state.UpdatePosition(move.MotionInfo.Pos);
|
||||||
|
await worldSpawner.StreamAsync(move.MotionInfo.Pos);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CombatTypeArgument.CombatEvtBeingHit:
|
||||||
|
EvtBeingHitInfo hit = EvtBeingHitInfo.Parser.ParseFrom(entry.CombatData);
|
||||||
|
logger.LogInformation("HIT defense={d} damage={dmg} forward={f}",
|
||||||
|
hit.AttackResult?.DefenseId, hit.AttackResult?.Damage, entry.ForwardType);
|
||||||
|
if (hit.AttackResult is not null)
|
||||||
|
await combat.HandleAttackAsync(hit.AttackResult);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.LogDebug("combat arg={arg} fwd={f}", entry.ArgumentType, entry.ForwardType);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.ForwardType is ForwardType.ForwardToAll or ForwardType.ForwardToHost or ForwardType.ForwardToPeer)
|
||||||
|
forward.InvokeList.Add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forward.InvokeList.Count != 0)
|
||||||
|
await session.NotifyAsync(CmdType.CombatInvocationsNotify, forward);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
Yuanshen.Gameserver/Controllers/ControllerBase.cs
Normal file
25
Yuanshen.Gameserver/Controllers/ControllerBase.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Gameserver.Network;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
using Google.Protobuf;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers;
|
||||||
|
internal abstract class ControllerBase
|
||||||
|
{
|
||||||
|
public NetPacket? Packet { get; set; }
|
||||||
|
|
||||||
|
protected IResult Ok()
|
||||||
|
{
|
||||||
|
return new SinglePacketResult(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected IResult Response<TMessage>(CmdType cmdType, TMessage message) where TMessage : IMessage
|
||||||
|
{
|
||||||
|
return new SinglePacketResult(new()
|
||||||
|
{
|
||||||
|
CmdType = cmdType,
|
||||||
|
Head = Memory<byte>.Empty,
|
||||||
|
Body = message.ToByteArray()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Gameserver.Network;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers.Dispatching;
|
||||||
|
internal class NetCommandDispatcher
|
||||||
|
{
|
||||||
|
private static readonly ImmutableDictionary<CmdType, Func<IServiceProvider, NetPacket, ValueTask<IResult>>> _handlers;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
static NetCommandDispatcher()
|
||||||
|
{
|
||||||
|
_handlers = InitializeHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public NetCommandDispatcher(IServiceProvider serviceProvider, ILogger<NetCommandDispatcher> logger)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<IResult?> InvokeHandler(NetPacket packet)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(packet, nameof(packet));
|
||||||
|
|
||||||
|
if (_handlers.TryGetValue(packet.CmdType, out Func<IServiceProvider, NetPacket, ValueTask<IResult>>? handler))
|
||||||
|
{
|
||||||
|
return await handler(_serviceProvider, packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("No handler for cmdId {cmdId} (headLen={headLen} bodyLen={bodyLen}) body={body}",
|
||||||
|
(ushort)packet.CmdType, packet.Head.Length, packet.Body.Length,
|
||||||
|
Convert.ToHexString(packet.Body.Span[..Math.Min(48, packet.Body.Length)]));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImmutableDictionary<CmdType, Func<IServiceProvider, NetPacket, ValueTask<IResult>>> InitializeHandlers()
|
||||||
|
{
|
||||||
|
ImmutableDictionary<CmdType, Func<IServiceProvider, NetPacket, ValueTask<IResult>>>.Builder builder
|
||||||
|
= ImmutableDictionary.CreateBuilder<CmdType, Func<IServiceProvider, NetPacket, ValueTask<IResult>>>();
|
||||||
|
|
||||||
|
IEnumerable<Type> types = Assembly.GetExecutingAssembly().GetTypes()
|
||||||
|
.Where(type => type.GetCustomAttribute<NetControllerAttribute>() != null);
|
||||||
|
|
||||||
|
foreach (Type type in types)
|
||||||
|
{
|
||||||
|
IEnumerable<MethodInfo> methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public)
|
||||||
|
.Where(method => method.GetCustomAttribute<NetCommandAttribute>() != null);
|
||||||
|
|
||||||
|
foreach (MethodInfo method in methods)
|
||||||
|
{
|
||||||
|
NetCommandAttribute attribute = method.GetCustomAttribute<NetCommandAttribute>()!;
|
||||||
|
if (builder.TryGetKey(attribute.CmdType, out _))
|
||||||
|
throw new Exception($"Handler for command {attribute.CmdType} defined twice!");
|
||||||
|
|
||||||
|
builder[attribute.CmdType] = CreateHandlerDelegate(type, method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Func<IServiceProvider, NetPacket, ValueTask<IResult>> CreateHandlerDelegate(Type controllerType, MethodInfo method)
|
||||||
|
{
|
||||||
|
ParameterExpression serviceProviderParameter = Expression.Parameter(typeof(IServiceProvider), "serviceProvider");
|
||||||
|
ParameterExpression netPacketParameter = Expression.Parameter(typeof(NetPacket), "packet");
|
||||||
|
|
||||||
|
ConstantExpression controllerTypeConstant = Expression.Constant(controllerType);
|
||||||
|
ParameterExpression controllerVariable = Expression.Variable(controllerType, "controller");
|
||||||
|
PropertyInfo packetProperty = typeof(ControllerBase).GetProperty("Packet")!;
|
||||||
|
|
||||||
|
MethodInfo createInstanceMethod = typeof(ActivatorUtilities).GetMethod("CreateInstance", new Type[] { typeof(IServiceProvider), typeof(Type), typeof(object[]) })!;
|
||||||
|
|
||||||
|
List<Expression> expressionBlock = new()
|
||||||
|
{
|
||||||
|
Expression.Assign(controllerVariable, Expression.Convert(
|
||||||
|
Expression.Call(null, createInstanceMethod, serviceProviderParameter, controllerTypeConstant, Expression.Constant(Array.Empty<object>())),
|
||||||
|
controllerType)),
|
||||||
|
Expression.Assign(Expression.Property(controllerVariable, packetProperty), netPacketParameter)
|
||||||
|
};
|
||||||
|
|
||||||
|
List<Expression> parameterExpressions = new();
|
||||||
|
foreach (ParameterInfo parameter in method.GetParameters())
|
||||||
|
{
|
||||||
|
MethodInfo getServiceMethod = typeof(ServiceProviderServiceExtensions)
|
||||||
|
.GetMethod("GetRequiredService", new Type[] { typeof(IServiceProvider) })!
|
||||||
|
.MakeGenericMethod(parameter.ParameterType);
|
||||||
|
|
||||||
|
parameterExpressions.Add(Expression.Call(getServiceMethod, serviceProviderParameter));
|
||||||
|
}
|
||||||
|
|
||||||
|
expressionBlock.Add(Expression.Call(controllerVariable, method, parameterExpressions));
|
||||||
|
|
||||||
|
return Expression.Lambda<Func<IServiceProvider, NetPacket, ValueTask<IResult>>>(
|
||||||
|
Expression.Block(new[] { controllerVariable }, expressionBlock),
|
||||||
|
serviceProviderParameter,
|
||||||
|
netPacketParameter)
|
||||||
|
.Compile();
|
||||||
|
}
|
||||||
|
}
|
||||||
56
Yuanshen.Gameserver/Controllers/EventController.cs
Normal file
56
Yuanshen.Gameserver/Controllers/EventController.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers;
|
||||||
|
|
||||||
|
[NetController]
|
||||||
|
internal sealed class EventController : ControllerBase
|
||||||
|
{
|
||||||
|
[NetCommand(CmdType.SeeMonsterReq)]
|
||||||
|
public ValueTask<IResult> OnSeeMonsterReq()
|
||||||
|
{
|
||||||
|
SeeMonsterReq req = Packet!.DecodeBody<SeeMonsterReq>();
|
||||||
|
return ValueTask.FromResult(Response(CmdType.SeeMonsterRsp, new SeeMonsterRsp { Retcode = Retcode.RET_SUCC }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EvtAvatarEnterFocusNotify)]
|
||||||
|
public ValueTask<IResult> OnEvtAvatarEnterFocusNotify() => Handled<EvtAvatarEnterFocusNotify>();
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EvtAvatarUpdateFocusNotify)]
|
||||||
|
public ValueTask<IResult> OnEvtAvatarUpdateFocusNotify() => Handled<EvtAvatarUpdateFocusNotify>();
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EvtAvatarExitFocusNotify)]
|
||||||
|
public ValueTask<IResult> OnEvtAvatarExitFocusNotify() => Handled<EvtAvatarExitFocusNotify>();
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EvtCreateGadgetNotify)]
|
||||||
|
public ValueTask<IResult> OnEvtCreateGadgetNotify(Player player, ILogger<EventController> logger)
|
||||||
|
{
|
||||||
|
EvtCreateGadgetNotify notify = Packet!.DecodeBody<EvtCreateGadgetNotify>();
|
||||||
|
player.ClientGadgets[notify.EntityId] = notify.ConfigId;
|
||||||
|
|
||||||
|
logger.LogDebug("Gadget created entity={entity} config={config} owner={owner}",
|
||||||
|
notify.EntityId, notify.ConfigId, notify.OwnerEntityId);
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EvtDestroyGadgetNotify)]
|
||||||
|
public ValueTask<IResult> OnEvtDestroyGadgetNotify(Player player, ILogger<EventController> logger)
|
||||||
|
{
|
||||||
|
EvtDestroyGadgetNotify notify = Packet!.DecodeBody<EvtDestroyGadgetNotify>();
|
||||||
|
player.ClientGadgets.Remove(notify.EntityId);
|
||||||
|
|
||||||
|
logger.LogDebug("Gadget destroyed entity={entity}", notify.EntityId);
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask<IResult> Handled<TMessage>() where TMessage : Google.Protobuf.IMessage<TMessage>, new()
|
||||||
|
{
|
||||||
|
Packet!.DecodeBody<TMessage>();
|
||||||
|
return ValueTask.FromResult(Ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
279
Yuanshen.Gameserver/Controllers/GachaController.cs
Normal file
279
Yuanshen.Gameserver/Controllers/GachaController.cs
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
using Yuanshen.Common.Data.Excel;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Gameserver.Database;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers;
|
||||||
|
|
||||||
|
[NetController]
|
||||||
|
internal sealed class GachaController : ControllerBase
|
||||||
|
{
|
||||||
|
private const uint MaxConstellation = 6;
|
||||||
|
private const uint StarglitterItemId = 221;
|
||||||
|
|
||||||
|
private static readonly Random Random = new();
|
||||||
|
|
||||||
|
[NetCommand(CmdType.GetGachaInfoReq)]
|
||||||
|
public ValueTask<IResult> OnGetGachaInfoReq(GachaBannerTable banners)
|
||||||
|
{
|
||||||
|
GetGachaInfoRsp rsp = new()
|
||||||
|
{
|
||||||
|
Retcode = Retcode.RET_SUCC,
|
||||||
|
GachaRandom = (uint)Random.Next(1, int.MaxValue)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (GachaBanner banner in banners.Banners)
|
||||||
|
rsp.GachaInfoList.Add(banner.Info.Clone());
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Response(CmdType.GetGachaInfoRsp, rsp));
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.DoGachaReq)]
|
||||||
|
public async ValueTask<IResult> OnDoGachaReq(NetSession session, Player player, SceneManager scene,
|
||||||
|
PlayerPersistence persistence, GachaBannerTable banners, ExcelTableCollection excel,
|
||||||
|
ILogger<GachaController> logger)
|
||||||
|
{
|
||||||
|
DoGachaReq req = Packet!.DecodeBody<DoGachaReq>();
|
||||||
|
|
||||||
|
GachaBanner? banner = banners.Find(req.GachaType, req.GachaScheduleId);
|
||||||
|
if (banner is null)
|
||||||
|
return Response(CmdType.DoGachaRsp, new DoGachaRsp { Retcode = Retcode.RET_GACHA_SCHEDULE_NOT_MATCH });
|
||||||
|
|
||||||
|
uint times = req.GachaTimes == 10 ? 10u : 1u;
|
||||||
|
uint costItemId = banner.Info.CostItemId;
|
||||||
|
uint costItemNum = times == 10 ? banner.Info.TenCostItemNum : banner.Info.CostItemNum;
|
||||||
|
|
||||||
|
if (player.Materials.GetValueOrDefault(costItemId) < costItemNum)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Gacha refused: need {num}x {item}", costItemNum, costItemId);
|
||||||
|
return Response(CmdType.DoGachaRsp, new DoGachaRsp { Retcode = Retcode.RET_GACHA_COST_ITEM_NOT_ENOUGH });
|
||||||
|
}
|
||||||
|
|
||||||
|
player.TakeMaterial(costItemId, costItemNum);
|
||||||
|
|
||||||
|
StoreItemChangeNotify store = new() { StoreType = StoreType.StorePack };
|
||||||
|
store.ItemList.Add(BuildMaterial(player, costItemId));
|
||||||
|
|
||||||
|
List<GameAvatar> added = new();
|
||||||
|
List<GameAvatar> upgraded = new();
|
||||||
|
DoGachaRsp rsp = new()
|
||||||
|
{
|
||||||
|
Retcode = Retcode.RET_SUCC,
|
||||||
|
GachaType = req.GachaType,
|
||||||
|
GachaTimes = times,
|
||||||
|
GachaScheduleId = req.GachaScheduleId,
|
||||||
|
NewGachaRandom = (uint)Random.Next(1, int.MaxValue),
|
||||||
|
CostItemId = costItemId,
|
||||||
|
CostItemNum = banner.Info.CostItemNum,
|
||||||
|
TenCostItemId = banner.Info.TenCostItemId,
|
||||||
|
TenCostItemNum = banner.Info.TenCostItemNum,
|
||||||
|
LeftGachaTimes = banner.Info.LeftGachaTimes,
|
||||||
|
GachaTimesLimit = banner.Info.GachaTimesLimit
|
||||||
|
};
|
||||||
|
|
||||||
|
for (uint pull = 0; pull < times; pull++)
|
||||||
|
{
|
||||||
|
uint itemId = Draw(banner, player);
|
||||||
|
bool isNew = await AwardAsync(player, excel, itemId, store, added, upgraded);
|
||||||
|
|
||||||
|
(string label, string kind) = Describe(excel, itemId);
|
||||||
|
|
||||||
|
player.GachaLog.Add(new GachaLogEntry
|
||||||
|
{
|
||||||
|
GachaType = req.GachaType,
|
||||||
|
ItemId = itemId,
|
||||||
|
RankLevel = RankOf(banner, itemId),
|
||||||
|
Name = label,
|
||||||
|
Kind = kind,
|
||||||
|
Time = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
||||||
|
});
|
||||||
|
|
||||||
|
rsp.GachaItemList.Add(new GachaItem
|
||||||
|
{
|
||||||
|
GachaItem_ = new ItemParam { ItemId = itemId, Count = 1 },
|
||||||
|
IsGachaItemNew = isNew
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await session.NotifyAsync(CmdType.StoreItemChangeNotify, store);
|
||||||
|
|
||||||
|
foreach (GameAvatar avatar in added)
|
||||||
|
await session.NotifyAsync(CmdType.AvatarAddNotify, new AvatarAddNotify
|
||||||
|
{
|
||||||
|
Avatar = avatar.AsAvatarInfo(),
|
||||||
|
IsInTeam = false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (added.Count != 0 || upgraded.Count != 0)
|
||||||
|
await scene.RefreshAvatarDataAsync();
|
||||||
|
|
||||||
|
if (upgraded.Count != 0)
|
||||||
|
await scene.RefreshTeamAsync();
|
||||||
|
|
||||||
|
await persistence.SaveAsync();
|
||||||
|
|
||||||
|
logger.LogInformation("Gacha {type} x{times} -> {items}", req.GachaType, times,
|
||||||
|
string.Join(", ", rsp.GachaItemList.Select(item => item.GachaItem_.ItemId)));
|
||||||
|
|
||||||
|
return Response(CmdType.DoGachaRsp, rsp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint Draw(GachaBanner banner, Player player)
|
||||||
|
{
|
||||||
|
uint gachaType = banner.Info.GachaType;
|
||||||
|
uint counter = player.GachaPity.GetValueOrDefault(gachaType) + 1;
|
||||||
|
bool forced = banner.PityCount != 0 && counter >= banner.PityCount;
|
||||||
|
|
||||||
|
GachaBucket bucket = forced
|
||||||
|
? banner.Buckets.FirstOrDefault(candidate => banner.PityTypes.Contains(candidate.Type)) ?? PickBucket(banner)
|
||||||
|
: PickBucket(banner);
|
||||||
|
|
||||||
|
player.GachaPity[gachaType] = banner.PityTypes.Contains(bucket.Type) ? 0 : counter;
|
||||||
|
|
||||||
|
if (!banner.FeaturedPools.TryGetValue(bucket.Type, out IReadOnlyList<uint>? featured) || featured.Count == 0)
|
||||||
|
return bucket.ItemIds[Random.Next(bucket.ItemIds.Count)];
|
||||||
|
|
||||||
|
bool carriesGuarantee = banner.GuaranteeTypes.Contains(bucket.Type);
|
||||||
|
bool takeFeatured = (carriesGuarantee && player.GachaGuarantee.GetValueOrDefault(gachaType) != 0)
|
||||||
|
|| Random.Next(100) < banner.FeaturedRate;
|
||||||
|
|
||||||
|
if (carriesGuarantee)
|
||||||
|
player.GachaGuarantee[gachaType] = takeFeatured ? 0u : 1u;
|
||||||
|
|
||||||
|
return takeFeatured
|
||||||
|
? featured[Random.Next(featured.Count)]
|
||||||
|
: bucket.ItemIds[Random.Next(bucket.ItemIds.Count)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string Label, string Kind) Describe(ExcelTableCollection excel, uint itemId)
|
||||||
|
{
|
||||||
|
WeaponExcel? weapon = excel.GetTable(ExcelType.Weapon).Items.OfType<WeaponExcel>()
|
||||||
|
.FirstOrDefault(candidate => candidate.Id == itemId);
|
||||||
|
|
||||||
|
if (weapon is not null)
|
||||||
|
return (FriendlyWeapon(weapon.WeaponType), "weapon");
|
||||||
|
|
||||||
|
MaterialExcel? material = excel.GetTable(ExcelType.Material).Items.OfType<MaterialExcel>()
|
||||||
|
.FirstOrDefault(candidate => candidate.Id == itemId);
|
||||||
|
uint avatarId = material?.GainedAvatarId ?? 0;
|
||||||
|
|
||||||
|
if (avatarId == 0)
|
||||||
|
return ($"Item {itemId}", "item");
|
||||||
|
|
||||||
|
AvatarExcel? avatar = excel.GetTable(ExcelType.Avatar).Items.OfType<AvatarExcel>()
|
||||||
|
.FirstOrDefault(candidate => candidate.Id == avatarId);
|
||||||
|
|
||||||
|
string icon = avatar?.IconName ?? string.Empty;
|
||||||
|
int cut = icon.LastIndexOf('_');
|
||||||
|
string name = cut >= 0 && cut + 1 < icon.Length ? icon[(cut + 1)..] : $"Avatar {avatarId}";
|
||||||
|
|
||||||
|
return (name, "avatar");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FriendlyWeapon(string weaponType) => weaponType switch
|
||||||
|
{
|
||||||
|
"WEAPON_SWORD_ONE_HAND" => "Sword",
|
||||||
|
"WEAPON_CLAYMORE" => "Claymore",
|
||||||
|
"WEAPON_POLE" => "Polearm",
|
||||||
|
"WEAPON_CATALYST" => "Catalyst",
|
||||||
|
"WEAPON_BOW" => "Bow",
|
||||||
|
_ => "Weapon"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static uint RankOf(GachaBanner banner, uint itemId)
|
||||||
|
{
|
||||||
|
foreach (GachaBucket bucket in banner.Buckets)
|
||||||
|
{
|
||||||
|
if (bucket.ItemIds.Contains(itemId))
|
||||||
|
return bucket.Type.EndsWith("S5") ? 5u : bucket.Type.EndsWith("S4") ? 4u : 3u;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ((string type, IReadOnlyList<uint> pool) in banner.FeaturedPools)
|
||||||
|
{
|
||||||
|
if (pool.Contains(itemId))
|
||||||
|
return type.EndsWith("S5") ? 5u : type.EndsWith("S4") ? 4u : 3u;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GachaBucket PickBucket(GachaBanner banner)
|
||||||
|
{
|
||||||
|
uint roll = (uint)Random.Next(10000);
|
||||||
|
uint accumulated = 0;
|
||||||
|
|
||||||
|
foreach (GachaBucket bucket in banner.Buckets)
|
||||||
|
{
|
||||||
|
accumulated += bucket.Weight;
|
||||||
|
if (roll < accumulated)
|
||||||
|
return bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
return banner.Buckets[^1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async ValueTask<bool> AwardAsync(Player player, ExcelTableCollection excel, uint itemId,
|
||||||
|
StoreItemChangeNotify store, List<GameAvatar> added, List<GameAvatar> upgraded)
|
||||||
|
{
|
||||||
|
bool isWeapon = excel.GetTable(ExcelType.Weapon).Items.OfType<WeaponExcel>().Any(weapon => weapon.Id == itemId);
|
||||||
|
|
||||||
|
if (isWeapon)
|
||||||
|
{
|
||||||
|
store.ItemList.Add(player.CreateWeapon(itemId).AsItem());
|
||||||
|
return await ValueTask.FromResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialExcel? material = excel.GetTable(ExcelType.Material).Items.OfType<MaterialExcel>()
|
||||||
|
.FirstOrDefault(candidate => candidate.Id == itemId);
|
||||||
|
uint avatarId = material?.GainedAvatarId ?? 0;
|
||||||
|
|
||||||
|
if (avatarId == 0)
|
||||||
|
{
|
||||||
|
player.GiveMaterial(itemId, 1);
|
||||||
|
store.ItemList.Add(BuildMaterial(player, itemId));
|
||||||
|
return await ValueTask.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
GameAvatar? owned = player.Avatars.FirstOrDefault(avatar => avatar.AvatarId == avatarId);
|
||||||
|
|
||||||
|
if (owned is null)
|
||||||
|
{
|
||||||
|
GameAvatar? granted = player.GrantAvatar(avatarId);
|
||||||
|
if (granted is not null)
|
||||||
|
{
|
||||||
|
added.Add(granted);
|
||||||
|
return await ValueTask.FromResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
player.GiveMaterial(itemId, 1);
|
||||||
|
store.ItemList.Add(BuildMaterial(player, itemId));
|
||||||
|
return await ValueTask.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (owned.Constellation < MaxConstellation)
|
||||||
|
{
|
||||||
|
owned.Constellation++;
|
||||||
|
upgraded.Add(owned);
|
||||||
|
return await ValueTask.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
AvatarExcel? avatarExcel = excel.GetTable(ExcelType.Avatar).Items.OfType<AvatarExcel>()
|
||||||
|
.FirstOrDefault(candidate => candidate.Id == avatarId);
|
||||||
|
uint starglitter = avatarExcel?.QualityType == "QUALITY_ORANGE" ? 25u : 5u;
|
||||||
|
|
||||||
|
player.GiveMaterial(StarglitterItemId, starglitter);
|
||||||
|
store.ItemList.Add(BuildMaterial(player, StarglitterItemId));
|
||||||
|
return await ValueTask.FromResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Item BuildMaterial(Player player, uint itemId) => new()
|
||||||
|
{
|
||||||
|
ItemId = itemId,
|
||||||
|
Guid = player.MaterialGuid(itemId),
|
||||||
|
Material = new Material { Count = player.Materials.GetValueOrDefault(itemId) }
|
||||||
|
};
|
||||||
|
}
|
||||||
33
Yuanshen.Gameserver/Controllers/GmTalkController.cs
Normal file
33
Yuanshen.Gameserver/Controllers/GmTalkController.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers;
|
||||||
|
|
||||||
|
[NetController]
|
||||||
|
internal sealed class GmTalkController : ControllerBase
|
||||||
|
{
|
||||||
|
private static readonly char[] Separators = { ';', '\n', '\r' };
|
||||||
|
|
||||||
|
[NetCommand(CmdType.GmTalkReq)]
|
||||||
|
public async ValueTask<IResult> OnGmTalkReq(GmCommandService gm, ILogger<GmTalkController> logger)
|
||||||
|
{
|
||||||
|
GmTalkReq req = Packet!.DecodeBody<GmTalkReq>();
|
||||||
|
string message = req.Msg ?? string.Empty;
|
||||||
|
|
||||||
|
logger.LogInformation("GmTalk: {message}", message);
|
||||||
|
|
||||||
|
List<string> reply = new();
|
||||||
|
foreach (string part in message.Split(Separators, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
|
reply.AddRange(await gm.ExecuteAsync(part));
|
||||||
|
|
||||||
|
return Response(CmdType.GmTalkRsp, new GmTalkRsp
|
||||||
|
{
|
||||||
|
Retcode = 0,
|
||||||
|
Msg = message,
|
||||||
|
Retmsg = string.Join('\n', reply)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
128
Yuanshen.Gameserver/Controllers/MiscController.cs
Normal file
128
Yuanshen.Gameserver/Controllers/MiscController.cs
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers;
|
||||||
|
|
||||||
|
[NetController]
|
||||||
|
internal sealed class MiscController : ControllerBase
|
||||||
|
{
|
||||||
|
[NetCommand(CmdType.EnterWorldAreaReq)]
|
||||||
|
public ValueTask<IResult> OnEnterWorldAreaReq()
|
||||||
|
{
|
||||||
|
EnterWorldAreaReq req = Packet!.DecodeBody<EnterWorldAreaReq>();
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Response(CmdType.EnterWorldAreaRsp, new EnterWorldAreaRsp
|
||||||
|
{
|
||||||
|
AreaType = req.AreaType,
|
||||||
|
AreaId = req.AreaId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.GetScenePointReq)]
|
||||||
|
public ValueTask<IResult> OnGetScenePointReq(Player player)
|
||||||
|
{
|
||||||
|
GetScenePointReq req = Packet!.DecodeBody<GetScenePointReq>();
|
||||||
|
|
||||||
|
GetScenePointRsp rsp = new()
|
||||||
|
{
|
||||||
|
Retcode = 0,
|
||||||
|
SceneId = req.SceneId,
|
||||||
|
BelongUid = player.Uid
|
||||||
|
};
|
||||||
|
|
||||||
|
for (uint pointId = 1; pointId <= 1000; pointId++)
|
||||||
|
rsp.UnlockedPointList.Add(pointId);
|
||||||
|
|
||||||
|
for (uint areaId = 1; areaId <= 100; areaId++)
|
||||||
|
rsp.UnlockAreaList.Add(areaId);
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Response(CmdType.GetScenePointRsp, rsp));
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.GetSceneAreaReq)]
|
||||||
|
public ValueTask<IResult> OnGetSceneAreaReq()
|
||||||
|
{
|
||||||
|
GetSceneAreaReq req = Packet!.DecodeBody<GetSceneAreaReq>();
|
||||||
|
|
||||||
|
GetSceneAreaRsp rsp = new()
|
||||||
|
{
|
||||||
|
Retcode = 0,
|
||||||
|
SceneId = req.SceneId
|
||||||
|
};
|
||||||
|
|
||||||
|
for (uint areaId = 1; areaId <= 100; areaId++)
|
||||||
|
rsp.AreaIdList.Add(areaId);
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Response(CmdType.GetSceneAreaRsp, rsp));
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.GetPlayerSocialDetailReq)]
|
||||||
|
public ValueTask<IResult> OnGetPlayerSocialDetailReq(Player player)
|
||||||
|
=> ValueTask.FromResult(Response(CmdType.GetPlayerSocialDetailRsp, new GetPlayerSocialDetailRsp
|
||||||
|
{
|
||||||
|
Retcode = 0,
|
||||||
|
DetailData = new SocialDetail
|
||||||
|
{
|
||||||
|
Uid = player.Uid,
|
||||||
|
Nickname = "Traveler",
|
||||||
|
Level = 1,
|
||||||
|
WorldLevel = 0,
|
||||||
|
IsFriend = false
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
[NetCommand(CmdType.GetPlayerBlacklistReq)]
|
||||||
|
public ValueTask<IResult> OnGetPlayerBlacklistReq()
|
||||||
|
=> ValueTask.FromResult(Response(CmdType.GetPlayerBlacklistRsp, new GetPlayerBlacklistRsp { Retcode = 0 }));
|
||||||
|
|
||||||
|
[NetCommand(CmdType.SceneTransToPointReq)]
|
||||||
|
public async ValueTask<IResult> OnSceneTransToPointReq(NetSession session, GameState state, Player player, ScenePointTable points, ILogger<MiscController> logger)
|
||||||
|
{
|
||||||
|
SceneTransToPointReq req = Packet!.DecodeBody<SceneTransToPointReq>();
|
||||||
|
|
||||||
|
uint sceneId = req.SceneId != 0 ? req.SceneId : GameState.SceneId;
|
||||||
|
if (points.TryGet(sceneId, req.PointId, out Vector target))
|
||||||
|
state.UpdatePosition(target);
|
||||||
|
|
||||||
|
logger.LogInformation("Teleport scene={s} point={p} -> ({x}, {y}, {z})",
|
||||||
|
sceneId, req.PointId, state.CurrentPos.X, state.CurrentPos.Y, state.CurrentPos.Z);
|
||||||
|
|
||||||
|
await session.NotifyAsync(CmdType.PlayerEnterSceneNotify, new PlayerEnterSceneNotify
|
||||||
|
{
|
||||||
|
SceneId = sceneId,
|
||||||
|
Pos = state.CurrentPos,
|
||||||
|
SceneBeginTime = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||||
|
Type = EnterType.EnterGoto,
|
||||||
|
TargetUid = player.Uid,
|
||||||
|
EnterSceneToken = state.NextEnterSceneToken(),
|
||||||
|
WorldLevel = 0,
|
||||||
|
EnterReason = 4
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response(CmdType.SceneTransToPointRsp, new SceneTransToPointRsp
|
||||||
|
{
|
||||||
|
Retcode = 0,
|
||||||
|
SceneId = req.SceneId,
|
||||||
|
PointId = req.PointId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.MarkMapReq)]
|
||||||
|
public ValueTask<IResult> OnMarkMapReq()
|
||||||
|
{
|
||||||
|
MarkMapReq req = Packet!.DecodeBody<MarkMapReq>();
|
||||||
|
MarkMapRsp rsp = new() { Retcode = 0 };
|
||||||
|
if (req.Mark is not null)
|
||||||
|
rsp.MarkList.Add(req.Mark);
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Response(CmdType.MarkMapRsp, rsp));
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.TowerAllDataReq)]
|
||||||
|
public ValueTask<IResult> OnTowerAllDataReq()
|
||||||
|
=> ValueTask.FromResult(Response(CmdType.TowerAllDataRsp, new TowerAllDataRsp()));
|
||||||
|
}
|
||||||
8
Yuanshen.Gameserver/Controllers/Result/IResult.cs
Normal file
8
Yuanshen.Gameserver/Controllers/Result/IResult.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Yuanshen.Gameserver.Network;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
internal interface IResult
|
||||||
|
{
|
||||||
|
bool NextPacket([MaybeNullWhen(false)] out NetPacket packet);
|
||||||
|
}
|
||||||
21
Yuanshen.Gameserver/Controllers/Result/SinglePacketResult.cs
Normal file
21
Yuanshen.Gameserver/Controllers/Result/SinglePacketResult.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Yuanshen.Gameserver.Network;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
internal class SinglePacketResult : IResult
|
||||||
|
{
|
||||||
|
private NetPacket? _packet;
|
||||||
|
|
||||||
|
public SinglePacketResult(NetPacket? packet)
|
||||||
|
{
|
||||||
|
_packet = packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool NextPacket([MaybeNullWhen(false)] out NetPacket packet)
|
||||||
|
{
|
||||||
|
packet = _packet;
|
||||||
|
_packet = null;
|
||||||
|
|
||||||
|
return packet != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
86
Yuanshen.Gameserver/Controllers/SceneController.cs
Normal file
86
Yuanshen.Gameserver/Controllers/SceneController.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers;
|
||||||
|
|
||||||
|
[NetController]
|
||||||
|
internal sealed class SceneController : ControllerBase
|
||||||
|
{
|
||||||
|
[NetCommand(CmdType.EnterSceneReadyReq)]
|
||||||
|
public async ValueTask<IResult> OnEnterSceneReadyReq(NetSession session, GameState state)
|
||||||
|
{
|
||||||
|
await session.NotifyAsync(CmdType.EnterScenePeerNotify, new EnterScenePeerNotify
|
||||||
|
{
|
||||||
|
DestSceneId = GameState.SceneId,
|
||||||
|
PeerId = Player.PeerId,
|
||||||
|
HostPeerId = Player.PeerId,
|
||||||
|
EnterSceneToken = state.EnterSceneToken
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response(CmdType.EnterSceneReadyRsp, new EnterSceneReadyRsp
|
||||||
|
{
|
||||||
|
EnterSceneToken = state.EnterSceneToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.SceneInitFinishReq)]
|
||||||
|
public async ValueTask<IResult> OnSceneInitFinishReq(NetSession session, GameState state, SceneManager sceneManager, Player player)
|
||||||
|
{
|
||||||
|
await session.NotifyAsync(CmdType.WorldDataNotify, new WorldDataNotify
|
||||||
|
{
|
||||||
|
WorldPropMap =
|
||||||
|
{
|
||||||
|
{ 1u, new PropValue { Type = 1, Ival = 0, Val = 0 } },
|
||||||
|
{ 2u, new PropValue { Type = 2, Ival = 0, Val = 0 } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await session.NotifyAsync(CmdType.WorldPlayerInfoNotify, new WorldPlayerInfoNotify
|
||||||
|
{
|
||||||
|
PlayerInfoList =
|
||||||
|
{
|
||||||
|
new OnlinePlayerInfo
|
||||||
|
{
|
||||||
|
Uid = player.Uid,
|
||||||
|
Nickname = "Traveler",
|
||||||
|
PlayerLevel = 1,
|
||||||
|
CurPlayerNumInWorld = 1,
|
||||||
|
WorldLevel = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PlayerUidList = { player.Uid }
|
||||||
|
});
|
||||||
|
|
||||||
|
await session.NotifyAsync(CmdType.PlayerEnterSceneInfoNotify, sceneManager.BuildEnterSceneInfo());
|
||||||
|
await session.NotifyAsync(CmdType.SceneTeamUpdateNotify, sceneManager.BuildSceneTeamUpdate());
|
||||||
|
|
||||||
|
return Response(CmdType.SceneInitFinishRsp, new SceneInitFinishRsp
|
||||||
|
{
|
||||||
|
EnterSceneToken = state.EnterSceneToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EnterSceneDoneReq)]
|
||||||
|
public async ValueTask<IResult> OnEnterSceneDoneReq(GameState state, SceneManager sceneManager, WorldSpawner worldSpawner)
|
||||||
|
{
|
||||||
|
await sceneManager.SpawnActiveAvatarAsync(VisionType.VisionMeet);
|
||||||
|
await worldSpawner.EnterSceneAsync(state.CurrentPos);
|
||||||
|
|
||||||
|
return Response(CmdType.EnterSceneDoneRsp, new EnterSceneDoneRsp
|
||||||
|
{
|
||||||
|
EnterSceneToken = state.EnterSceneToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.PostEnterSceneReq)]
|
||||||
|
public ValueTask<IResult> OnPostEnterSceneReq(GameState state)
|
||||||
|
{
|
||||||
|
return ValueTask.FromResult(Response(CmdType.PostEnterSceneRsp, new PostEnterSceneRsp
|
||||||
|
{
|
||||||
|
EnterSceneToken = state.EnterSceneToken
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
158
Yuanshen.Gameserver/Controllers/ShopController.cs
Normal file
158
Yuanshen.Gameserver/Controllers/ShopController.cs
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
using Yuanshen.Common.Data.Excel;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Gameserver.Database;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers;
|
||||||
|
|
||||||
|
[NetController]
|
||||||
|
internal sealed class ShopController : ControllerBase
|
||||||
|
{
|
||||||
|
private const uint ActionReasonShop = 4;
|
||||||
|
|
||||||
|
[NetCommand(CmdType.GetShopmallDataReq)]
|
||||||
|
public ValueTask<IResult> OnGetShopmallDataReq(ExcelTableCollection excel)
|
||||||
|
{
|
||||||
|
GetShopmallDataRsp rsp = new() { Retcode = Retcode.RET_SUCC };
|
||||||
|
|
||||||
|
foreach (ShopmallEntranceExcel entrance in excel.GetTable(ExcelType.ShopmallEntrance)
|
||||||
|
.Items.OfType<ShopmallEntranceExcel>().OrderBy(entrance => entrance.Order))
|
||||||
|
{
|
||||||
|
rsp.ShopTypeList.Add(entrance.ShopType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Response(CmdType.GetShopmallDataRsp, rsp));
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.GetShopReq)]
|
||||||
|
public ValueTask<IResult> OnGetShopReq(Player player, ExcelTableCollection excel)
|
||||||
|
{
|
||||||
|
GetShopReq req = Packet!.DecodeBody<GetShopReq>();
|
||||||
|
|
||||||
|
Shop shop = new() { ShopType = req.ShopType };
|
||||||
|
|
||||||
|
foreach (ShopGoodsExcel goods in Goods(excel).Where(goods => goods.ShopType == req.ShopType)
|
||||||
|
.OrderBy(goods => goods.SortLevel).ThenBy(goods => goods.GoodsId))
|
||||||
|
{
|
||||||
|
shop.GoodsList.Add(BuildGoods(player, goods));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Response(CmdType.GetShopRsp, new GetShopRsp { Retcode = Retcode.RET_SUCC, Shop = shop }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.BuyGoodsReq)]
|
||||||
|
public async ValueTask<IResult> OnBuyGoodsReq(NetSession session, Player player, ExcelTableCollection excel,
|
||||||
|
PlayerPersistence persistence, ILogger<ShopController> logger)
|
||||||
|
{
|
||||||
|
BuyGoodsReq req = Packet!.DecodeBody<BuyGoodsReq>();
|
||||||
|
|
||||||
|
uint goodsId = req.Goods?.GoodsId ?? 0;
|
||||||
|
uint count = Math.Max(req.BuyCount, 1u);
|
||||||
|
|
||||||
|
ShopGoodsExcel? goods = Goods(excel).FirstOrDefault(candidate => candidate.GoodsId == goodsId);
|
||||||
|
if (goods is null)
|
||||||
|
return Response(CmdType.BuyGoodsRsp, new BuyGoodsRsp { Retcode = Retcode.RET_SHOP_CONTENT_NOT_MATCH, ShopType = req.ShopType });
|
||||||
|
|
||||||
|
uint bought = player.BoughtGoods.GetValueOrDefault(goodsId);
|
||||||
|
if (goods.BuyLimit != 0 && bought + count > goods.BuyLimit)
|
||||||
|
return Response(CmdType.BuyGoodsRsp, new BuyGoodsRsp { Retcode = Retcode.RET_SHOP_CONTENT_NOT_MATCH, ShopType = req.ShopType });
|
||||||
|
|
||||||
|
if (player.Hcoin < goods.CostHcoin * count || player.Scoin < goods.CostScoin * count)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Shop refused {goods}: not enough currency", goodsId);
|
||||||
|
return Response(CmdType.BuyGoodsRsp, new BuyGoodsRsp { Retcode = Retcode.RET_SHOP_CONTENT_NOT_MATCH, ShopType = req.ShopType });
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (ShopCostItem cost in goods.CostItems.Where(cost => cost.Id != 0))
|
||||||
|
{
|
||||||
|
if (player.Materials.GetValueOrDefault(cost.Id) < cost.Count * count)
|
||||||
|
{
|
||||||
|
logger.LogInformation("Shop refused {goods}: need {n}x {item}", goodsId, cost.Count * count, cost.Id);
|
||||||
|
return Response(CmdType.BuyGoodsRsp, new BuyGoodsRsp { Retcode = Retcode.RET_SHOP_CONTENT_NOT_MATCH, ShopType = req.ShopType });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
player.Hcoin -= goods.CostHcoin * count;
|
||||||
|
player.Scoin -= goods.CostScoin * count;
|
||||||
|
|
||||||
|
StoreItemChangeNotify store = new() { StoreType = StoreType.StorePack };
|
||||||
|
|
||||||
|
foreach (ShopCostItem cost in goods.CostItems.Where(cost => cost.Id != 0))
|
||||||
|
{
|
||||||
|
player.TakeMaterial(cost.Id, cost.Count * count);
|
||||||
|
store.ItemList.Add(BuildMaterial(player, cost.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint gained = goods.ItemCount * count;
|
||||||
|
bool isWeapon = excel.GetTable(ExcelType.Weapon).Items.OfType<WeaponExcel>().Any(weapon => weapon.Id == goods.ItemId);
|
||||||
|
|
||||||
|
if (isWeapon)
|
||||||
|
{
|
||||||
|
for (uint made = 0; made < Math.Min(gained, 50u); made++)
|
||||||
|
store.ItemList.Add(player.CreateWeapon(goods.ItemId).AsItem());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
player.GiveMaterial(goods.ItemId, gained);
|
||||||
|
store.ItemList.Add(BuildMaterial(player, goods.ItemId));
|
||||||
|
}
|
||||||
|
|
||||||
|
player.BoughtGoods[goodsId] = bought + count;
|
||||||
|
|
||||||
|
await session.NotifyAsync(CmdType.StoreItemChangeNotify, store);
|
||||||
|
await session.NotifyAsync(CmdType.PlayerPropNotify, player.BuildCurrencyProps());
|
||||||
|
|
||||||
|
ItemAddHintNotify hint = new() { Reason = ActionReasonShop };
|
||||||
|
hint.ItemList.Add(new ItemHint { ItemId = goods.ItemId, Count = gained, IsNew = false });
|
||||||
|
await session.NotifyAsync(CmdType.ItemAddHintNotify, hint);
|
||||||
|
|
||||||
|
await persistence.SaveAsync();
|
||||||
|
|
||||||
|
logger.LogInformation("Shop {shop}: bought {count}x goods {goods} -> {n}x item {item}",
|
||||||
|
req.ShopType, count, goodsId, gained, goods.ItemId);
|
||||||
|
|
||||||
|
return Response(CmdType.BuyGoodsRsp, new BuyGoodsRsp
|
||||||
|
{
|
||||||
|
Retcode = Retcode.RET_SUCC,
|
||||||
|
ShopType = req.ShopType,
|
||||||
|
BuyCount = count,
|
||||||
|
Goods = BuildGoods(player, goods)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<ShopGoodsExcel> Goods(ExcelTableCollection excel)
|
||||||
|
=> excel.GetTable(ExcelType.ShopGoods).Items.OfType<ShopGoodsExcel>();
|
||||||
|
|
||||||
|
private static ShopGoods BuildGoods(Player player, ShopGoodsExcel goods)
|
||||||
|
{
|
||||||
|
ShopGoods result = new()
|
||||||
|
{
|
||||||
|
GoodsId = goods.GoodsId,
|
||||||
|
GoodsItem = new ItemParam { ItemId = goods.ItemId, Count = goods.ItemCount },
|
||||||
|
Scoin = goods.CostScoin,
|
||||||
|
Hcoin = goods.CostHcoin,
|
||||||
|
BoughtNum = player.BoughtGoods.GetValueOrDefault(goods.GoodsId),
|
||||||
|
BuyLimit = goods.BuyLimit,
|
||||||
|
MinLevel = goods.MinPlayerLevel,
|
||||||
|
MaxLevel = goods.MaxPlayerLevel == 0 ? 99u : goods.MaxPlayerLevel,
|
||||||
|
BeginTime = 0,
|
||||||
|
EndTime = uint.MaxValue
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (ShopCostItem cost in goods.CostItems.Where(cost => cost.Id != 0))
|
||||||
|
result.CostItemList.Add(new ItemParam { ItemId = cost.Id, Count = cost.Count });
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Item BuildMaterial(Player player, uint itemId) => new()
|
||||||
|
{
|
||||||
|
ItemId = itemId,
|
||||||
|
Guid = player.MaterialGuid(itemId),
|
||||||
|
Material = new Material { Count = player.Materials.GetValueOrDefault(itemId) }
|
||||||
|
};
|
||||||
|
}
|
||||||
92
Yuanshen.Gameserver/Controllers/StubController.cs
Normal file
92
Yuanshen.Gameserver/Controllers/StubController.cs
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
using Yuanshen.Gameserver.Controllers.Attributes;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Controllers;
|
||||||
|
|
||||||
|
[NetController]
|
||||||
|
internal sealed class StubController : ControllerBase
|
||||||
|
{
|
||||||
|
[NetCommand(CmdType.ClientFpsStatusNotify)]
|
||||||
|
public ValueTask<IResult> OnClientFpsStatusNotify() => ValueTask.FromResult(Ok());
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EntityAbilityConfigHashNotify)]
|
||||||
|
public ValueTask<IResult> OnEntityAbilityConfigHashNotify() => ValueTask.FromResult(Ok());
|
||||||
|
|
||||||
|
[NetCommand(CmdType.MonsterAIConfigHashNotify)]
|
||||||
|
public ValueTask<IResult> OnMonsterAIConfigHashNotify() => ValueTask.FromResult(Ok());
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EvtEntityRenderersChangedNotify)]
|
||||||
|
public ValueTask<IResult> OnEvtEntityRenderersChangedNotify() => ValueTask.FromResult(Ok());
|
||||||
|
|
||||||
|
[NetCommand(CmdType.EvtAiSyncCombatThreatInfoNotify)]
|
||||||
|
public ValueTask<IResult> OnEvtAiSyncCombatThreatInfoNotify() => ValueTask.FromResult(Ok());
|
||||||
|
|
||||||
|
[NetCommand(CmdType.PlayerSetPauseReq)]
|
||||||
|
public ValueTask<IResult> OnPlayerSetPauseReq()
|
||||||
|
=> ValueTask.FromResult(Response(CmdType.PlayerSetPauseRsp, new PlayerSetPauseRsp { Retcode = Retcode.RET_SUCC }));
|
||||||
|
|
||||||
|
[NetCommand(CmdType.PathfindingEnterSceneReq)]
|
||||||
|
public ValueTask<IResult> OnPathfindingEnterSceneReq()
|
||||||
|
=> ValueTask.FromResult(Response(CmdType.PathfindingEnterSceneRsp,
|
||||||
|
new PathfindingEnterSceneRsp { Retcode = Retcode.RET_SUCC }));
|
||||||
|
|
||||||
|
[NetCommand(CmdType.PersonalLineAllDataReq)]
|
||||||
|
public ValueTask<IResult> OnPersonalLineAllDataReq()
|
||||||
|
=> ValueTask.FromResult(Response(CmdType.PersonalLineAllDataRsp,
|
||||||
|
new PersonalLineAllDataRsp { Retcode = Retcode.RET_SUCC }));
|
||||||
|
|
||||||
|
[NetCommand(CmdType.GetAllActivatedBargainDataReq)]
|
||||||
|
public ValueTask<IResult> OnGetAllActivatedBargainDataReq()
|
||||||
|
=> ValueTask.FromResult(Response(CmdType.GetAllActivatedBargainDataRsp,
|
||||||
|
new GetAllActivatedBargainDataRsp { Retcode = Retcode.RET_SUCC }));
|
||||||
|
|
||||||
|
[NetCommand(CmdType.GetInvestigationMonsterReq)]
|
||||||
|
public ValueTask<IResult> OnGetInvestigationMonsterReq()
|
||||||
|
=> ValueTask.FromResult(Response(CmdType.GetInvestigationMonsterRsp,
|
||||||
|
new GetInvestigationMonsterRsp { Retcode = Retcode.RET_SUCC }));
|
||||||
|
|
||||||
|
[NetCommand(CmdType.GetPlayerFriendListReq)]
|
||||||
|
public ValueTask<IResult> OnGetPlayerFriendListReq()
|
||||||
|
=> ValueTask.FromResult(Response(CmdType.GetPlayerFriendListRsp,
|
||||||
|
new GetPlayerFriendListRsp { Retcode = Retcode.RET_SUCC }));
|
||||||
|
|
||||||
|
[NetCommand(CmdType.SceneGetAreaExplorePercentReq)]
|
||||||
|
public ValueTask<IResult> OnSceneGetAreaExplorePercentReq()
|
||||||
|
{
|
||||||
|
SceneGetAreaExplorePercentReq req = Packet!.DecodeBody<SceneGetAreaExplorePercentReq>();
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Response(CmdType.SceneGetAreaExplorePercentRsp, new SceneGetAreaExplorePercentRsp
|
||||||
|
{
|
||||||
|
Retcode = Retcode.RET_SUCC,
|
||||||
|
AreaId = req.AreaId,
|
||||||
|
ExplorePercent = 0
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.AvatarFetterLevelRewardReq)]
|
||||||
|
public ValueTask<IResult> OnAvatarFetterLevelRewardReq()
|
||||||
|
{
|
||||||
|
AvatarFetterLevelRewardReq req = Packet!.DecodeBody<AvatarFetterLevelRewardReq>();
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Response(CmdType.AvatarFetterLevelRewardRsp, new AvatarFetterLevelRewardRsp
|
||||||
|
{
|
||||||
|
Retcode = Retcode.RET_SUCC,
|
||||||
|
AvatarGuid = req.AvatarGuid,
|
||||||
|
FetterLevel = req.FetterLevel
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[NetCommand(CmdType.QueryPathReq)]
|
||||||
|
public ValueTask<IResult> OnQueryPathReq()
|
||||||
|
{
|
||||||
|
QueryPathReq req = Packet!.DecodeBody<QueryPathReq>();
|
||||||
|
|
||||||
|
return ValueTask.FromResult(Response(CmdType.QueryPathRsp, new QueryPathRsp
|
||||||
|
{
|
||||||
|
Retcode = Retcode.RET_SUCC,
|
||||||
|
QueryId = req.QueryId,
|
||||||
|
QueryStatus = 1
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Yuanshen.Gameserver/Database/IAccountRepository.cs
Normal file
6
Yuanshen.Gameserver/Database/IAccountRepository.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Yuanshen.Gameserver.Database;
|
||||||
|
|
||||||
|
internal interface IAccountRepository
|
||||||
|
{
|
||||||
|
ValueTask<uint> GetOrCreateUidAsync(string accountKey);
|
||||||
|
}
|
||||||
7
Yuanshen.Gameserver/Database/IPlayerRepository.cs
Normal file
7
Yuanshen.Gameserver/Database/IPlayerRepository.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Yuanshen.Gameserver.Database;
|
||||||
|
|
||||||
|
internal interface IPlayerRepository
|
||||||
|
{
|
||||||
|
ValueTask<PlayerRecord?> LoadAsync(uint uid);
|
||||||
|
ValueTask<bool> SaveAsync(PlayerRecord record);
|
||||||
|
}
|
||||||
61
Yuanshen.Gameserver/Database/PlayerPersistence.cs
Normal file
61
Yuanshen.Gameserver/Database/PlayerPersistence.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Database;
|
||||||
|
|
||||||
|
internal sealed class PlayerPersistence
|
||||||
|
{
|
||||||
|
private readonly Player _player;
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly IPlayerRepository _repository;
|
||||||
|
private readonly PlayerSessionRegistry _registry;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
private long _sessionId;
|
||||||
|
|
||||||
|
public PlayerPersistence(Player player, GameState state, IPlayerRepository repository,
|
||||||
|
PlayerSessionRegistry registry, ILogger<PlayerPersistence> logger)
|
||||||
|
{
|
||||||
|
_player = player;
|
||||||
|
_state = state;
|
||||||
|
_repository = repository;
|
||||||
|
_registry = registry;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint PlayerUid => _player.Uid;
|
||||||
|
|
||||||
|
public void Bind(long sessionId) => _sessionId = sessionId;
|
||||||
|
|
||||||
|
public void Acquire() => _registry.Acquire(_player.Uid, _sessionId);
|
||||||
|
|
||||||
|
public void Release() => _registry.Release(_player.Uid, _sessionId);
|
||||||
|
|
||||||
|
public ValueTask<PlayerRecord?> LoadAsync() => _repository.LoadAsync(_player.Uid);
|
||||||
|
|
||||||
|
public async ValueTask<bool> SaveAsync()
|
||||||
|
{
|
||||||
|
if (!_player.IsInitialized)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!_registry.IsOwner(_player.Uid, _sessionId))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Skipped save for player {uid}: session {sessionId} is stale", _player.Uid, _sessionId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool saved = await _repository.SaveAsync(_player.ToRecord(_state.CurrentPos));
|
||||||
|
if (saved)
|
||||||
|
_logger.LogDebug("Persisted player {uid} at ({x}, {y}, {z}), killed={killed}",
|
||||||
|
_player.Uid, _state.CurrentPos.X, _state.CurrentPos.Y, _state.CurrentPos.Z, _player.KilledNodeIds.Count);
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogError("Failed to persist player {uid}: {message}", _player.Uid, exception.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
Yuanshen.Gameserver/Database/PlayerRecord.cs
Normal file
48
Yuanshen.Gameserver/Database/PlayerRecord.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
namespace Yuanshen.Gameserver.Database;
|
||||||
|
|
||||||
|
internal sealed class PlayerRecord
|
||||||
|
{
|
||||||
|
public uint Uid { get; set; }
|
||||||
|
public string Nickname { get; set; } = "Traveler";
|
||||||
|
public uint CurrentTeamId { get; set; } = 1;
|
||||||
|
public ulong CurrentAvatarGuid { get; set; }
|
||||||
|
public List<ulong> TeamAvatarGuids { get; set; } = new();
|
||||||
|
public List<uint> TeamAvatarIds { get; set; } = new();
|
||||||
|
public uint CurrentAvatarId { get; set; }
|
||||||
|
public Dictionary<uint, ulong> EquippedWeapons { get; set; } = new();
|
||||||
|
public Dictionary<uint, Dictionary<uint, ulong>> EquippedReliquaries { get; set; } = new();
|
||||||
|
public Dictionary<uint, AvatarProgress> AvatarProgress { get; set; } = new();
|
||||||
|
public Dictionary<uint, uint> Materials { get; set; } = new();
|
||||||
|
public List<uint> KilledNodes { get; set; } = new();
|
||||||
|
public uint SpawnDataVersion { get; set; }
|
||||||
|
public Dictionary<uint, uint> GachaPity { get; set; } = new();
|
||||||
|
public Dictionary<uint, uint> GachaGuarantee { get; set; } = new();
|
||||||
|
public List<GachaLogEntry> GachaLog { get; set; } = new();
|
||||||
|
public Dictionary<uint, uint> BoughtGoods { get; set; } = new();
|
||||||
|
public uint Hcoin { get; set; }
|
||||||
|
public uint Scoin { get; set; }
|
||||||
|
public float PosX { get; set; }
|
||||||
|
public float PosY { get; set; }
|
||||||
|
public float PosZ { get; set; }
|
||||||
|
public DateTimeOffset LastSeen { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class AvatarProgress
|
||||||
|
{
|
||||||
|
public uint Level { get; set; } = 90;
|
||||||
|
public uint PromoteLevel { get; set; } = 6;
|
||||||
|
public uint TalentLevel { get; set; } = 10;
|
||||||
|
public uint Constellation { get; set; }
|
||||||
|
public uint WeaponLevel { get; set; } = 90;
|
||||||
|
public uint WeaponPromote { get; set; } = 6;
|
||||||
|
public uint WeaponRefinement { get; set; }
|
||||||
|
}
|
||||||
|
internal sealed class GachaLogEntry
|
||||||
|
{
|
||||||
|
public uint GachaType { get; set; }
|
||||||
|
public uint ItemId { get; set; }
|
||||||
|
public uint RankLevel { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Kind { get; set; } = string.Empty;
|
||||||
|
public long Time { get; set; }
|
||||||
|
}
|
||||||
16
Yuanshen.Gameserver/Database/PlayerSessionRegistry.cs
Normal file
16
Yuanshen.Gameserver/Database/PlayerSessionRegistry.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Database;
|
||||||
|
|
||||||
|
internal sealed class PlayerSessionRegistry
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<uint, long> _owners = new();
|
||||||
|
|
||||||
|
public void Acquire(uint uid, long sessionId) => _owners[uid] = sessionId;
|
||||||
|
|
||||||
|
public bool IsOwner(uint uid, long sessionId)
|
||||||
|
=> _owners.TryGetValue(uid, out long owner) && owner == sessionId;
|
||||||
|
|
||||||
|
public void Release(uint uid, long sessionId)
|
||||||
|
=> _owners.TryRemove(new KeyValuePair<uint, long>(uid, sessionId));
|
||||||
|
}
|
||||||
87
Yuanshen.Gameserver/Database/PostgresAccountRepository.cs
Normal file
87
Yuanshen.Gameserver/Database/PostgresAccountRepository.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Npgsql;
|
||||||
|
using Yuanshen.Gameserver.Options;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Database;
|
||||||
|
|
||||||
|
internal sealed class PostgresAccountRepository : IAccountRepository, IAsyncDisposable
|
||||||
|
{
|
||||||
|
private const string CreateTableSql =
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
|
uid integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||||
|
account_key text UNIQUE NOT NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string GetOrCreateSql =
|
||||||
|
"""
|
||||||
|
INSERT INTO accounts (account_key) VALUES (@key)
|
||||||
|
ON CONFLICT (account_key) DO UPDATE SET account_key = EXCLUDED.account_key
|
||||||
|
RETURNING uid
|
||||||
|
""";
|
||||||
|
|
||||||
|
private readonly NpgsqlDataSource _dataSource;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly SemaphoreSlim _schemaLock = new(1, 1);
|
||||||
|
private volatile bool _schemaReady;
|
||||||
|
|
||||||
|
public PostgresAccountRepository(IOptions<DatabaseOptions> options, ILogger<PostgresAccountRepository> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_dataSource = new NpgsqlDataSourceBuilder(options.Value.ConnectionString).Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<uint> GetOrCreateUidAsync(string accountKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(accountKey))
|
||||||
|
accountKey = "default";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await EnsureSchemaAsync();
|
||||||
|
|
||||||
|
await using NpgsqlCommand command = _dataSource.CreateCommand(GetOrCreateSql);
|
||||||
|
command.Parameters.AddWithValue("key", accountKey);
|
||||||
|
|
||||||
|
object? result = await command.ExecuteScalarAsync();
|
||||||
|
uint uid = Convert.ToUInt32(result);
|
||||||
|
_logger.LogDebug("Account '{key}' resolved to uid {uid}", accountKey, uid);
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogError("Failed to resolve account '{key}': {message}", accountKey, exception.Message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask EnsureSchemaAsync()
|
||||||
|
{
|
||||||
|
if (_schemaReady)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _schemaLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_schemaReady)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await using NpgsqlCommand command = _dataSource.CreateCommand(CreateTableSql);
|
||||||
|
await command.ExecuteNonQueryAsync();
|
||||||
|
_schemaReady = true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_schemaLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
_schemaLock.Dispose();
|
||||||
|
return _dataSource.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
131
Yuanshen.Gameserver/Database/PostgresPlayerRepository.cs
Normal file
131
Yuanshen.Gameserver/Database/PostgresPlayerRepository.cs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Npgsql;
|
||||||
|
using NpgsqlTypes;
|
||||||
|
using Yuanshen.Gameserver.Options;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Database;
|
||||||
|
|
||||||
|
internal sealed class PostgresPlayerRepository : IPlayerRepository, IAsyncDisposable
|
||||||
|
{
|
||||||
|
private const string CreateTableSql =
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS players (
|
||||||
|
uid bigint PRIMARY KEY,
|
||||||
|
data jsonb NOT NULL,
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string UpsertSql =
|
||||||
|
"""
|
||||||
|
INSERT INTO players (uid, data, updated_at)
|
||||||
|
VALUES (@uid, @data, now())
|
||||||
|
ON CONFLICT (uid) DO UPDATE
|
||||||
|
SET data = EXCLUDED.data, updated_at = now()
|
||||||
|
""";
|
||||||
|
|
||||||
|
private const string SelectSql = "SELECT data FROM players WHERE uid = @uid";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly NpgsqlDataSource _dataSource;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly SemaphoreSlim _schemaLock = new(1, 1);
|
||||||
|
private volatile bool _schemaReady;
|
||||||
|
|
||||||
|
public PostgresPlayerRepository(IOptions<DatabaseOptions> options, ILogger<PostgresPlayerRepository> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_dataSource = new NpgsqlDataSourceBuilder(options.Value.ConnectionString).Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<bool> EnsureSchemaAsync()
|
||||||
|
{
|
||||||
|
if (_schemaReady)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
await _schemaLock.WaitAsync();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_schemaReady)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
await using NpgsqlCommand command = _dataSource.CreateCommand(CreateTableSql);
|
||||||
|
await command.ExecuteNonQueryAsync();
|
||||||
|
|
||||||
|
_schemaReady = true;
|
||||||
|
_logger.LogInformation("PostgreSQL connected, player storage ready");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("PostgreSQL unavailable, persistence disabled for now: {message}", exception.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_schemaLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<PlayerRecord?> LoadAsync(uint uid)
|
||||||
|
{
|
||||||
|
if (!await EnsureSchemaAsync())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using NpgsqlCommand command = _dataSource.CreateCommand(SelectSql);
|
||||||
|
command.Parameters.AddWithValue("uid", (long)uid);
|
||||||
|
|
||||||
|
object? result = await command.ExecuteScalarAsync();
|
||||||
|
if (result is not string json)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<PlayerRecord>(json, SerializerOptions);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogError("Failed to load player {uid}: {message}", uid, exception.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<bool> SaveAsync(PlayerRecord record)
|
||||||
|
{
|
||||||
|
if (!await EnsureSchemaAsync())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
record.LastSeen = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using NpgsqlCommand command = _dataSource.CreateCommand(UpsertSql);
|
||||||
|
command.Parameters.AddWithValue("uid", (long)record.Uid);
|
||||||
|
command.Parameters.Add(new NpgsqlParameter("data", NpgsqlDbType.Jsonb)
|
||||||
|
{
|
||||||
|
Value = JsonSerializer.Serialize(record, SerializerOptions)
|
||||||
|
});
|
||||||
|
|
||||||
|
await command.ExecuteNonQueryAsync();
|
||||||
|
_logger.LogDebug("Saved player {uid}", record.Uid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogError("Failed to save player {uid}: {message}", record.Uid, exception.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
_schemaLock.Dispose();
|
||||||
|
return _dataSource.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
169
Yuanshen.Gameserver/Game/CombatManager.cs
Normal file
169
Yuanshen.Gameserver/Game/CombatManager.cs
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Yuanshen.Gameserver.Database;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed class CombatManager
|
||||||
|
{
|
||||||
|
private const uint FightPropCurHp = 1010;
|
||||||
|
private const uint FightPropMaxHp = 2000;
|
||||||
|
private const uint ActionReasonMonsterDie = 37;
|
||||||
|
private const uint ActionReasonMonsterEnvAnimal = 8;
|
||||||
|
private const uint MoraItemId = 202;
|
||||||
|
private const uint PrimogemItemId = 201;
|
||||||
|
|
||||||
|
private static readonly Random Random = new();
|
||||||
|
|
||||||
|
private readonly NetSession _session;
|
||||||
|
private readonly Player _player;
|
||||||
|
private readonly PlayerPersistence _persistence;
|
||||||
|
private readonly DropTable _drops;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public CombatManager(NetSession session, Player player, PlayerPersistence persistence, DropTable drops,
|
||||||
|
ILogger<CombatManager> logger)
|
||||||
|
{
|
||||||
|
_session = session;
|
||||||
|
_player = player;
|
||||||
|
_persistence = persistence;
|
||||||
|
_drops = drops;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask HandleAttackAsync(AttackResult attack)
|
||||||
|
{
|
||||||
|
GameMonster? monster = _player.Monsters.FirstOrDefault(m => m.EntityId == attack.DefenseId);
|
||||||
|
if (monster is null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("hit on non-monster entity {id}", attack.DefenseId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!monster.IsAlive || attack.Damage <= 0f)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("hit ignored on {id}: alive={alive} damage={dmg}", attack.DefenseId, monster.IsAlive, attack.Damage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("monster {mid} takes {dmg} damage ({hp} -> {left})",
|
||||||
|
monster.MonsterId, attack.Damage, monster.CurrentHp, monster.CurrentHp - attack.Damage);
|
||||||
|
|
||||||
|
float remaining = monster.ApplyDamage(attack.Damage);
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.EntityFightPropUpdateNotify, new EntityFightPropUpdateNotify
|
||||||
|
{
|
||||||
|
EntityId = monster.EntityId,
|
||||||
|
FightPropMap =
|
||||||
|
{
|
||||||
|
{ FightPropCurHp, remaining }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (remaining > 0f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_logger.LogInformation("Monster {monsterId} killed by entity {attacker}", monster.MonsterId, attack.AttackerId);
|
||||||
|
|
||||||
|
if (monster.NodeId != 0)
|
||||||
|
_player.MarkNodeKilled(monster.NodeId);
|
||||||
|
|
||||||
|
await GrantDropsAsync(monster);
|
||||||
|
|
||||||
|
if (monster.NodeId != 0)
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.LifeStateChangeNotify, new LifeStateChangeNotify
|
||||||
|
{
|
||||||
|
EntityId = monster.EntityId,
|
||||||
|
LifeState = 2,
|
||||||
|
SourceEntityId = attack.AttackerId,
|
||||||
|
DieType = PlayerDieType.PlayerDieKillByMonster
|
||||||
|
});
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.SceneEntityDisappearNotify, new SceneEntityDisappearNotify
|
||||||
|
{
|
||||||
|
DisappearType = VisionType.VisionDie,
|
||||||
|
EntityList = { monster.EntityId }
|
||||||
|
});
|
||||||
|
|
||||||
|
_player.Monsters.Remove(monster);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask GrantDropsAsync(GameMonster monster)
|
||||||
|
{
|
||||||
|
uint dropId = _drops.DropIdFor(monster.MonsterId, monster.Level);
|
||||||
|
if (dropId == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Dictionary<uint, uint> rolled = _drops.Roll(dropId, Random);
|
||||||
|
if (rolled.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
StoreItemChangeNotify store = new() { StoreType = StoreType.StorePack };
|
||||||
|
ItemAddHintNotify hint = new()
|
||||||
|
{
|
||||||
|
Reason = monster.IsWildlife ? ActionReasonMonsterEnvAnimal : ActionReasonMonsterDie,
|
||||||
|
Position = monster.Pos,
|
||||||
|
IsPositionValid = true
|
||||||
|
};
|
||||||
|
|
||||||
|
List<string> granted = new();
|
||||||
|
bool currencyChanged = false;
|
||||||
|
|
||||||
|
foreach ((uint itemId, uint count) in rolled)
|
||||||
|
{
|
||||||
|
switch (_drops.TypeOf(itemId))
|
||||||
|
{
|
||||||
|
case "weapon":
|
||||||
|
for (uint made = 0; made < Math.Min(count, 10u); made++)
|
||||||
|
store.ItemList.Add(_player.CreateWeapon(itemId).AsItem());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "virtual" when itemId == MoraItemId:
|
||||||
|
_player.Scoin += count;
|
||||||
|
currencyChanged = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "virtual" when itemId == PrimogemItemId:
|
||||||
|
_player.Hcoin += count;
|
||||||
|
currencyChanged = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "virtual":
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case "reliquary":
|
||||||
|
continue;
|
||||||
|
|
||||||
|
default:
|
||||||
|
_player.GiveMaterial(itemId, count);
|
||||||
|
store.ItemList.Add(new Item
|
||||||
|
{
|
||||||
|
ItemId = itemId,
|
||||||
|
Guid = _player.MaterialGuid(itemId),
|
||||||
|
Material = new Material { Count = _player.Materials.GetValueOrDefault(itemId) }
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
hint.ItemList.Add(new ItemHint { ItemId = itemId, Count = count, IsNew = false });
|
||||||
|
granted.Add($"{itemId}x{count}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hint.ItemList.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (store.ItemList.Count != 0)
|
||||||
|
await _session.NotifyAsync(CmdType.StoreItemChangeNotify, store);
|
||||||
|
|
||||||
|
if (currencyChanged)
|
||||||
|
await _session.NotifyAsync(CmdType.PlayerPropNotify, _player.BuildCurrencyProps());
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.ItemAddHintNotify, hint);
|
||||||
|
|
||||||
|
_logger.LogInformation("Monster {monsterId} lvl {level} dropped {items}",
|
||||||
|
monster.MonsterId, monster.Level, string.Join(", ", granted));
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Yuanshen.Gameserver/Game/ConsoleBridge.cs
Normal file
16
Yuanshen.Gameserver/Game/ConsoleBridge.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed class ConsoleBridge
|
||||||
|
{
|
||||||
|
private IServiceProvider? _session;
|
||||||
|
|
||||||
|
public void Attach(IServiceProvider session) => _session = session;
|
||||||
|
|
||||||
|
public void Detach(IServiceProvider session)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_session, session))
|
||||||
|
_session = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IServiceProvider? Session => _session;
|
||||||
|
}
|
||||||
150
Yuanshen.Gameserver/Game/DropTable.cs
Normal file
150
Yuanshen.Gameserver/Game/DropTable.cs
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal readonly record struct DropEntry(uint ItemId, uint MinCount, uint MaxCount, double Chance, int Rolls, int Pool);
|
||||||
|
|
||||||
|
internal sealed class DropTable
|
||||||
|
{
|
||||||
|
private readonly Dictionary<uint, Dictionary<uint, uint>> _monsters = new();
|
||||||
|
private readonly Dictionary<uint, List<DropEntry>> _drops = new();
|
||||||
|
private readonly Dictionary<uint, string> _itemTypes = new();
|
||||||
|
|
||||||
|
public DropTable()
|
||||||
|
{
|
||||||
|
Load("monster_drops.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEmpty => _drops.Count == 0;
|
||||||
|
|
||||||
|
public int MonsterCount => _monsters.Count;
|
||||||
|
|
||||||
|
public string TypeOf(uint itemId) => _itemTypes.GetValueOrDefault(itemId, "material");
|
||||||
|
|
||||||
|
public uint DropIdFor(uint monsterId, uint level)
|
||||||
|
{
|
||||||
|
if (!_monsters.TryGetValue(monsterId, out Dictionary<uint, uint>? byLevel) || byLevel.Count == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (byLevel.TryGetValue(level, out uint exact))
|
||||||
|
return exact;
|
||||||
|
|
||||||
|
uint best = 0;
|
||||||
|
uint bestLevel = 0;
|
||||||
|
|
||||||
|
foreach ((uint candidate, uint dropId) in byLevel)
|
||||||
|
{
|
||||||
|
if (candidate <= level && candidate >= bestLevel)
|
||||||
|
{
|
||||||
|
bestLevel = candidate;
|
||||||
|
best = dropId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best != 0 ? best : byLevel.OrderBy(pair => pair.Key).First().Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<uint, uint> Roll(uint dropId, Random random)
|
||||||
|
{
|
||||||
|
Dictionary<uint, uint> result = new();
|
||||||
|
|
||||||
|
if (!_drops.TryGetValue(dropId, out List<DropEntry>? entries))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
foreach (DropEntry entry in entries.Where(entry => entry.Pool == 0))
|
||||||
|
{
|
||||||
|
for (int attempt = 0; attempt < entry.Rolls; attempt++)
|
||||||
|
{
|
||||||
|
if (random.NextDouble() < entry.Chance)
|
||||||
|
Add(result, entry, random);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (IGrouping<int, DropEntry> group in entries.Where(entry => entry.Pool != 0).GroupBy(entry => entry.Pool))
|
||||||
|
{
|
||||||
|
List<DropEntry> members = group.ToList();
|
||||||
|
|
||||||
|
for (int attempt = 0; attempt < members[0].Rolls; attempt++)
|
||||||
|
{
|
||||||
|
double roll = random.NextDouble();
|
||||||
|
double accumulated = 0;
|
||||||
|
|
||||||
|
foreach (DropEntry member in members)
|
||||||
|
{
|
||||||
|
accumulated += member.Chance;
|
||||||
|
if (roll >= accumulated)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Add(result, member, random);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Add(Dictionary<uint, uint> result, DropEntry entry, Random random)
|
||||||
|
{
|
||||||
|
uint count = entry.MaxCount > entry.MinCount
|
||||||
|
? (uint)random.Next((int)entry.MinCount, (int)entry.MaxCount + 1)
|
||||||
|
: entry.MinCount;
|
||||||
|
|
||||||
|
if (count != 0)
|
||||||
|
result[entry.ItemId] = result.GetValueOrDefault(entry.ItemId) + count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Load(string fileName)
|
||||||
|
{
|
||||||
|
string path = Path.Combine(AppContext.BaseDirectory, "assets", "drops", fileName);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return;
|
||||||
|
|
||||||
|
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
|
JsonElement root = document.RootElement;
|
||||||
|
|
||||||
|
foreach (JsonProperty monster in root.GetProperty("monsters").EnumerateObject())
|
||||||
|
{
|
||||||
|
if (!uint.TryParse(monster.Name, out uint monsterId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Dictionary<uint, uint> byLevel = new();
|
||||||
|
|
||||||
|
foreach (JsonProperty level in monster.Value.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (uint.TryParse(level.Name, out uint parsedLevel))
|
||||||
|
byLevel[parsedLevel] = level.Value.GetUInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (byLevel.Count != 0)
|
||||||
|
_monsters[monsterId] = byLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (JsonProperty drop in root.GetProperty("drops").EnumerateObject())
|
||||||
|
{
|
||||||
|
if (!uint.TryParse(drop.Name, out uint dropId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
List<DropEntry> entries = new();
|
||||||
|
|
||||||
|
foreach (JsonElement entry in drop.Value.EnumerateArray())
|
||||||
|
{
|
||||||
|
entries.Add(new DropEntry(
|
||||||
|
entry.GetProperty("itemId").GetUInt32(),
|
||||||
|
entry.GetProperty("minCount").GetUInt32(),
|
||||||
|
entry.GetProperty("maxCount").GetUInt32(),
|
||||||
|
entry.GetProperty("chance").GetDouble(),
|
||||||
|
entry.TryGetProperty("rolls", out JsonElement rolls) ? Math.Max(rolls.GetInt32(), 1) : 1,
|
||||||
|
entry.TryGetProperty("pool", out JsonElement pool) ? pool.GetInt32() : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
_drops[dropId] = entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (JsonProperty item in root.GetProperty("items").EnumerateObject())
|
||||||
|
{
|
||||||
|
if (uint.TryParse(item.Name, out uint itemId))
|
||||||
|
_itemTypes[itemId] = item.Value.GetProperty("type").GetString() ?? "material";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
125
Yuanshen.Gameserver/Game/GachaBannerTable.cs
Normal file
125
Yuanshen.Gameserver/Game/GachaBannerTable.cs
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed record GachaBucket(string Type, uint Weight, IReadOnlyList<uint> ItemIds);
|
||||||
|
|
||||||
|
internal sealed class GachaBanner
|
||||||
|
{
|
||||||
|
public required string Id { get; init; }
|
||||||
|
public required GachaInfo Info { get; init; }
|
||||||
|
public required IReadOnlyList<GachaBucket> Buckets { get; init; }
|
||||||
|
public uint PityCount { get; init; }
|
||||||
|
public IReadOnlyList<string> PityTypes { get; init; } = Array.Empty<string>();
|
||||||
|
public uint FeaturedRate { get; init; }
|
||||||
|
public IReadOnlyList<string> GuaranteeTypes { get; init; } = Array.Empty<string>();
|
||||||
|
public IReadOnlyDictionary<string, IReadOnlyList<uint>> FeaturedPools { get; init; }
|
||||||
|
= new Dictionary<string, IReadOnlyList<uint>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class GachaBannerTable
|
||||||
|
{
|
||||||
|
public IReadOnlyList<GachaBanner> Banners { get; }
|
||||||
|
|
||||||
|
public GachaBannerTable()
|
||||||
|
{
|
||||||
|
Banners = Load("banners.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public GachaBanner? Find(uint gachaType, uint scheduleId)
|
||||||
|
=> Banners.FirstOrDefault(banner => banner.Info.GachaType == gachaType && banner.Info.ScheduleId == scheduleId)
|
||||||
|
?? Banners.FirstOrDefault(banner => banner.Info.GachaType == gachaType);
|
||||||
|
|
||||||
|
private static List<GachaBanner> Load(string fileName)
|
||||||
|
{
|
||||||
|
List<GachaBanner> banners = new();
|
||||||
|
string path = Path.Combine(AppContext.BaseDirectory, "assets", "gacha", fileName);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return banners;
|
||||||
|
|
||||||
|
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
|
|
||||||
|
foreach (JsonElement entry in document.RootElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (!entry.GetProperty("ship").GetBoolean())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
JsonElement info = entry.GetProperty("gachaInfo");
|
||||||
|
List<GachaBucket> buckets = new();
|
||||||
|
|
||||||
|
foreach (JsonElement bucket in entry.GetProperty("buckets").EnumerateArray())
|
||||||
|
{
|
||||||
|
List<uint> ids = new();
|
||||||
|
foreach (JsonElement id in bucket.GetProperty("itemIds").EnumerateArray())
|
||||||
|
ids.Add(id.GetUInt32());
|
||||||
|
|
||||||
|
buckets.Add(new GachaBucket(bucket.GetProperty("type").GetString()!, bucket.GetProperty("weight").GetUInt32(), ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint pityCount = 0;
|
||||||
|
List<string> pityTypes = new();
|
||||||
|
if (entry.TryGetProperty("pity", out JsonElement pity))
|
||||||
|
{
|
||||||
|
pityCount = pity.GetProperty("count").GetUInt32();
|
||||||
|
foreach (JsonElement type in pity.GetProperty("types").EnumerateArray())
|
||||||
|
pityTypes.Add(type.GetString()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint featuredRate = 0;
|
||||||
|
List<string> guaranteeTypes = new();
|
||||||
|
Dictionary<string, IReadOnlyList<uint>> featuredPools = new();
|
||||||
|
|
||||||
|
if (entry.TryGetProperty("featured", out JsonElement featured))
|
||||||
|
{
|
||||||
|
featuredRate = featured.GetProperty("rate").GetUInt32();
|
||||||
|
|
||||||
|
foreach (JsonElement type in featured.GetProperty("guaranteeTypes").EnumerateArray())
|
||||||
|
guaranteeTypes.Add(type.GetString()!);
|
||||||
|
|
||||||
|
foreach (JsonProperty poolEntry in featured.GetProperty("pools").EnumerateObject())
|
||||||
|
{
|
||||||
|
List<uint> ids = new();
|
||||||
|
foreach (JsonElement id in poolEntry.Value.EnumerateArray())
|
||||||
|
ids.Add(id.GetUInt32());
|
||||||
|
|
||||||
|
if (ids.Count != 0)
|
||||||
|
featuredPools[poolEntry.Name] = ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
banners.Add(new GachaBanner
|
||||||
|
{
|
||||||
|
Id = entry.GetProperty("id").GetString()!,
|
||||||
|
Info = BuildInfo(info),
|
||||||
|
Buckets = buckets,
|
||||||
|
PityCount = pityCount,
|
||||||
|
PityTypes = pityTypes,
|
||||||
|
FeaturedRate = featuredRate,
|
||||||
|
GuaranteeTypes = guaranteeTypes,
|
||||||
|
FeaturedPools = featuredPools
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return banners;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GachaInfo BuildInfo(JsonElement info) => new()
|
||||||
|
{
|
||||||
|
GachaType = info.GetProperty("gachaType").GetUInt32(),
|
||||||
|
ScheduleId = info.GetProperty("scheduleId").GetUInt32(),
|
||||||
|
BeginTime = info.GetProperty("beginTime").GetUInt32(),
|
||||||
|
EndTime = info.GetProperty("endTime").GetUInt32(),
|
||||||
|
CostItemId = info.GetProperty("costItemId").GetUInt32(),
|
||||||
|
CostItemNum = info.GetProperty("costItemNum").GetUInt32(),
|
||||||
|
GachaPrefabPath = info.GetProperty("gachaPrefabPath").GetString(),
|
||||||
|
GachaProbUrl = info.GetProperty("gachaProbUrl").GetString(),
|
||||||
|
GachaRecordUrl = info.GetProperty("gachaRecordUrl").GetString(),
|
||||||
|
GachaPreviewPrefabPath = info.GetProperty("gachaPreviewPrefabPath").GetString(),
|
||||||
|
TenCostItemId = info.GetProperty("tenCostItemId").GetUInt32(),
|
||||||
|
TenCostItemNum = info.GetProperty("tenCostItemNum").GetUInt32(),
|
||||||
|
LeftGachaTimes = info.GetProperty("leftGachaTimes").GetUInt32(),
|
||||||
|
GachaTimesLimit = info.GetProperty("gachaTimesLimit").GetUInt32(),
|
||||||
|
GachaSortId = info.GetProperty("gachaSortId").GetUInt32()
|
||||||
|
};
|
||||||
|
}
|
||||||
201
Yuanshen.Gameserver/Game/GameAvatar.cs
Normal file
201
Yuanshen.Gameserver/Game/GameAvatar.cs
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
using Yuanshen.Common.Data.Excel;
|
||||||
|
using Yuanshen.Common.Extensions;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed class GameAvatar
|
||||||
|
{
|
||||||
|
public uint AvatarId { get; }
|
||||||
|
public ulong Guid { get; }
|
||||||
|
public uint SkillDepotId { get; }
|
||||||
|
public uint EntityId { get; }
|
||||||
|
public uint WeaponEntityId { get; }
|
||||||
|
public ulong WeaponGuid { get; set; }
|
||||||
|
public uint OwnerUid { get; set; }
|
||||||
|
|
||||||
|
public uint Level { get; set; } = 90;
|
||||||
|
public uint PromoteLevel { get; set; } = 6;
|
||||||
|
public uint TalentLevel { get; set; } = 10;
|
||||||
|
public uint Constellation { get; set; }
|
||||||
|
public uint FlycloakId { get; set; } = 140001;
|
||||||
|
|
||||||
|
public uint CoreProudSkillLevel => (uint)((PromoteLevel >= 1 ? 1 : 0) + (PromoteLevel >= 4 ? 1 : 0));
|
||||||
|
|
||||||
|
public uint BornTime { get; } = 1600000000;
|
||||||
|
public Dictionary<uint, ulong> ReliquaryGuids { get; } = new();
|
||||||
|
|
||||||
|
private readonly AvatarExcel _excel;
|
||||||
|
private readonly AvatarSkillDepotExcel? _depot;
|
||||||
|
private readonly AvatarAbilitiesExcel? _abilities;
|
||||||
|
|
||||||
|
public GameAvatar(AvatarExcel excel, ulong guid, uint entityId, uint weaponEntityId, AvatarSkillDepotExcel? depot = null, AvatarAbilitiesExcel? abilities = null)
|
||||||
|
{
|
||||||
|
_excel = excel;
|
||||||
|
_depot = depot;
|
||||||
|
_abilities = abilities;
|
||||||
|
AvatarId = excel.Id;
|
||||||
|
SkillDepotId = excel.SkillDepotId;
|
||||||
|
Guid = guid;
|
||||||
|
EntityId = entityId;
|
||||||
|
WeaponEntityId = weaponEntityId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AvatarInfo AsAvatarInfo()
|
||||||
|
{
|
||||||
|
AvatarInfo info = new()
|
||||||
|
{
|
||||||
|
AvatarId = AvatarId,
|
||||||
|
Guid = Guid,
|
||||||
|
LifeState = 1,
|
||||||
|
SkillDepotId = SkillDepotId,
|
||||||
|
BornTime = BornTime,
|
||||||
|
AvatarType = 1,
|
||||||
|
WearingFlycloakId = 140001,
|
||||||
|
CoreProudSkillLevel = CoreProudSkillLevel,
|
||||||
|
FetterInfo = new AvatarFetterInfo { ExpLevel = 10 }
|
||||||
|
};
|
||||||
|
|
||||||
|
info.PropMap.Add(4001u, new PropValue { Type = 4001, Ival = Level, Val = Level });
|
||||||
|
info.PropMap.Add(1002u, new PropValue { Type = 1002, Ival = PromoteLevel, Val = PromoteLevel });
|
||||||
|
info.PropMap.Add(1001u, new PropValue { Type = 1001, Ival = 0, Val = 0 });
|
||||||
|
info.PropMap.Add(4002u, new PropValue { Type = 4002, Ival = 0, Val = 0 });
|
||||||
|
|
||||||
|
foreach ((uint type, float value) in FightProps())
|
||||||
|
info.FightPropMap.Add(type, value);
|
||||||
|
|
||||||
|
foreach (uint skillId in SkillIds())
|
||||||
|
info.SkillLevelMap.Add(skillId, TalentLevel);
|
||||||
|
foreach (uint proud in _depot?.InherentProudSkillOpens ?? new List<uint>())
|
||||||
|
info.InherentProudSkillList.Add(proud);
|
||||||
|
foreach (uint talentId in TalentIds())
|
||||||
|
info.TalentIdList.Add(talentId);
|
||||||
|
|
||||||
|
info.EquipGuidList.Add(WeaponGuid);
|
||||||
|
foreach (ulong guid in ReliquaryGuids.Values)
|
||||||
|
info.EquipGuidList.Add(guid);
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SceneEntityInfo AsSceneEntity(Vector pos, GameWeapon weapon, IEnumerable<GameReliquary>? reliquaries = null)
|
||||||
|
{
|
||||||
|
SceneEntityInfo entity = new()
|
||||||
|
{
|
||||||
|
EntityType = ProtEntityType.ProtEntityAvatar,
|
||||||
|
EntityId = EntityId,
|
||||||
|
LifeState = 1,
|
||||||
|
MotionInfo = new MotionInfo
|
||||||
|
{
|
||||||
|
Pos = pos,
|
||||||
|
Rot = new Vector(),
|
||||||
|
Speed = new Vector(),
|
||||||
|
State = MotionState.MotionStandby
|
||||||
|
},
|
||||||
|
EntityClientData = new EntityClientData(),
|
||||||
|
EntityAuthorityInfo = new EntityAuthorityInfo
|
||||||
|
{
|
||||||
|
AbilityInfo = new AbilitySyncStateInfo { IsInited = true },
|
||||||
|
RendererChangedInfo = new EntityRendererChangedInfo(),
|
||||||
|
AiInfo = new SceneEntityAiInfo { IsAiOpen = true, BornPos = pos },
|
||||||
|
BornPos = pos
|
||||||
|
},
|
||||||
|
Avatar = new SceneAvatarInfo
|
||||||
|
{
|
||||||
|
PlayerId = OwnerUid,
|
||||||
|
AvatarId = AvatarId,
|
||||||
|
Guid = Guid,
|
||||||
|
PeerId = Player.PeerId,
|
||||||
|
SkillDepotId = SkillDepotId,
|
||||||
|
BornTime = BornTime,
|
||||||
|
WearingFlycloakId = 140001,
|
||||||
|
Weapon = weapon.AsSceneWeapon(WeaponEntityId)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
entity.PropList.Add(new PropPair
|
||||||
|
{
|
||||||
|
Type = 4001,
|
||||||
|
PropValue = new PropValue { Type = 4001, Ival = Level, Val = Level }
|
||||||
|
});
|
||||||
|
entity.PropList.Add(new PropPair
|
||||||
|
{
|
||||||
|
Type = 1002,
|
||||||
|
PropValue = new PropValue { Type = 1002, Ival = PromoteLevel, Val = PromoteLevel }
|
||||||
|
});
|
||||||
|
|
||||||
|
entity.Avatar.CoreProudSkillLevel = CoreProudSkillLevel;
|
||||||
|
foreach (uint skillId in SkillIds())
|
||||||
|
entity.Avatar.SkillLevelMap.Add(skillId, TalentLevel);
|
||||||
|
foreach (uint proud in _depot?.InherentProudSkillOpens ?? new List<uint>())
|
||||||
|
entity.Avatar.InherentProudSkillList.Add(proud);
|
||||||
|
foreach (uint talentId in TalentIds())
|
||||||
|
entity.Avatar.TalentIdList.Add(talentId);
|
||||||
|
|
||||||
|
foreach ((uint type, float value) in FightProps())
|
||||||
|
entity.FightPropList.Add(new FightPropPair { Type = type, PropValue = value });
|
||||||
|
|
||||||
|
if (reliquaries is not null)
|
||||||
|
{
|
||||||
|
foreach (GameReliquary reliquary in reliquaries)
|
||||||
|
entity.Avatar.ReliquaryList.Add(reliquary.AsSceneReliquary());
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AbilityControlBlock BuildAbilityControlBlock()
|
||||||
|
{
|
||||||
|
AbilityControlBlock block = new();
|
||||||
|
|
||||||
|
foreach (string ability in _abilities?.Abilities ?? new List<string>())
|
||||||
|
{
|
||||||
|
block.AbilityEmbryoList.Add(new AbilityEmbryo
|
||||||
|
{
|
||||||
|
AbilityId = (uint)(block.AbilityEmbryoList.Count + 1),
|
||||||
|
AbilityNameHash = ability.GetStableHash(),
|
||||||
|
AbilityOverrideNameHash = "Default".GetStableHash()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<uint> TalentIds()
|
||||||
|
{
|
||||||
|
for (uint i = 0; i < Constellation && i < 6; i++)
|
||||||
|
yield return AvatarId * 10 + (i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<uint> SkillIds()
|
||||||
|
{
|
||||||
|
if (_depot is null)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
foreach (uint id in _depot.Skills)
|
||||||
|
yield return id;
|
||||||
|
|
||||||
|
if (_depot.EnergySkill != 0)
|
||||||
|
yield return _depot.EnergySkill;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<(uint Type, float Value)> FightProps()
|
||||||
|
{
|
||||||
|
yield return (1u, (float)_excel.HpBase);
|
||||||
|
yield return (4u, (float)_excel.AttackBase);
|
||||||
|
yield return (7u, (float)_excel.DefenseBase);
|
||||||
|
yield return (1010u, (float)_excel.HpBase);
|
||||||
|
yield return (2000u, (float)_excel.HpBase);
|
||||||
|
yield return (2001u, (float)_excel.AttackBase);
|
||||||
|
yield return (2002u, (float)_excel.DefenseBase);
|
||||||
|
yield return (20u, (float)_excel.Critical);
|
||||||
|
yield return (22u, (float)_excel.CriticalHurt);
|
||||||
|
yield return (23u, (float)_excel.ChargeEfficiency);
|
||||||
|
|
||||||
|
for (uint element = 0; element < 7; element++)
|
||||||
|
{
|
||||||
|
yield return (70u + element, 100f);
|
||||||
|
yield return (1000u + element, 100f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
95
Yuanshen.Gameserver/Game/GameMonster.cs
Normal file
95
Yuanshen.Gameserver/Game/GameMonster.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed class GameMonster
|
||||||
|
{
|
||||||
|
public uint MonsterId { get; }
|
||||||
|
public uint EntityId { get; }
|
||||||
|
public uint NodeId { get; set; }
|
||||||
|
public uint ConfigId { get; set; }
|
||||||
|
public uint GroupId { get; set; }
|
||||||
|
public bool IsWildlife => MonsterId is >= 28000000 and < 29000000;
|
||||||
|
public Vector Pos { get; }
|
||||||
|
public uint Level { get; }
|
||||||
|
public float MaxHp { get; }
|
||||||
|
public float CurrentHp { get; private set; }
|
||||||
|
public int LevelSyncCount { get; set; }
|
||||||
|
public bool IsAlive => CurrentHp > 0;
|
||||||
|
|
||||||
|
private readonly float _attack;
|
||||||
|
private readonly float _defense;
|
||||||
|
|
||||||
|
public GameMonster(uint monsterId, uint entityId, Vector pos, uint level, float maxHp, float attack, float defense)
|
||||||
|
{
|
||||||
|
MonsterId = monsterId;
|
||||||
|
EntityId = entityId;
|
||||||
|
Pos = pos;
|
||||||
|
Level = level;
|
||||||
|
MaxHp = Math.Max(maxHp, 1f);
|
||||||
|
CurrentHp = MaxHp;
|
||||||
|
_attack = attack;
|
||||||
|
_defense = defense;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float ApplyDamage(float damage)
|
||||||
|
{
|
||||||
|
CurrentHp = Math.Max(0f, CurrentHp - damage);
|
||||||
|
return CurrentHp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SceneEntityInfo AsSceneEntity()
|
||||||
|
{
|
||||||
|
SceneEntityInfo entity = new()
|
||||||
|
{
|
||||||
|
EntityType = ProtEntityType.ProtEntityMonster,
|
||||||
|
EntityId = EntityId,
|
||||||
|
LifeState = 1,
|
||||||
|
MotionInfo = new MotionInfo
|
||||||
|
{
|
||||||
|
Pos = Pos,
|
||||||
|
Rot = new Vector(),
|
||||||
|
Speed = new Vector(),
|
||||||
|
State = MotionState.MotionStandby
|
||||||
|
},
|
||||||
|
EntityClientData = new EntityClientData(),
|
||||||
|
EntityAuthorityInfo = new EntityAuthorityInfo
|
||||||
|
{
|
||||||
|
AbilityInfo = new AbilitySyncStateInfo { IsInited = true },
|
||||||
|
RendererChangedInfo = new EntityRendererChangedInfo(),
|
||||||
|
AiInfo = new SceneEntityAiInfo { IsAiOpen = true, BornPos = Pos },
|
||||||
|
BornPos = Pos
|
||||||
|
},
|
||||||
|
Monster = new SceneMonsterInfo
|
||||||
|
{
|
||||||
|
MonsterId = MonsterId,
|
||||||
|
AuthorityPeerId = Player.PeerId,
|
||||||
|
BornType = MonsterBornType.MonsterBornDefault,
|
||||||
|
GroupId = GroupId,
|
||||||
|
ConfigId = ConfigId != 0 ? ConfigId : EntityId
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
entity.PropList.Add(new PropPair
|
||||||
|
{
|
||||||
|
Type = 4001,
|
||||||
|
PropValue = new PropValue { Type = 4001, Ival = Level, Val = Level }
|
||||||
|
});
|
||||||
|
|
||||||
|
entity.FightPropList.Add(new FightPropPair { Type = 1, PropValue = MaxHp });
|
||||||
|
entity.FightPropList.Add(new FightPropPair { Type = 1010, PropValue = CurrentHp });
|
||||||
|
entity.FightPropList.Add(new FightPropPair { Type = 2000, PropValue = MaxHp });
|
||||||
|
entity.FightPropList.Add(new FightPropPair { Type = 4, PropValue = _attack });
|
||||||
|
entity.FightPropList.Add(new FightPropPair { Type = 2001, PropValue = _attack });
|
||||||
|
entity.FightPropList.Add(new FightPropPair { Type = 7, PropValue = _defense });
|
||||||
|
entity.FightPropList.Add(new FightPropPair { Type = 2002, PropValue = _defense });
|
||||||
|
entity.FightPropList.Add(new FightPropPair { Type = 5, PropValue = _attack });
|
||||||
|
entity.FightPropList.Add(new FightPropPair { Type = 8, PropValue = _defense });
|
||||||
|
entity.FightPropList.Add(new FightPropPair { Type = 29, PropValue = 0f });
|
||||||
|
|
||||||
|
for (uint resist = 50; resist <= 56; resist++)
|
||||||
|
entity.FightPropList.Add(new FightPropPair { Type = resist, PropValue = 0f });
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
Yuanshen.Gameserver/Game/GameReliquary.cs
Normal file
48
Yuanshen.Gameserver/Game/GameReliquary.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed class GameReliquary
|
||||||
|
{
|
||||||
|
public uint ItemId { get; }
|
||||||
|
public ulong Guid { get; }
|
||||||
|
public uint EquipType { get; }
|
||||||
|
public uint MainPropId { get; }
|
||||||
|
public uint Level { get; set; } = 21;
|
||||||
|
public uint PromoteLevel { get; set; } = 5;
|
||||||
|
public List<uint> AppendPropIds { get; } = new();
|
||||||
|
|
||||||
|
public GameReliquary(uint itemId, ulong guid, uint equipType, uint mainPropId)
|
||||||
|
{
|
||||||
|
ItemId = itemId;
|
||||||
|
Guid = guid;
|
||||||
|
EquipType = equipType;
|
||||||
|
MainPropId = mainPropId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Item AsItem()
|
||||||
|
{
|
||||||
|
Reliquary reliquary = new()
|
||||||
|
{
|
||||||
|
Level = Level,
|
||||||
|
PromoteLevel = PromoteLevel,
|
||||||
|
MainPropId = MainPropId
|
||||||
|
};
|
||||||
|
reliquary.AppendPropIdList.AddRange(AppendPropIds);
|
||||||
|
|
||||||
|
return new Item
|
||||||
|
{
|
||||||
|
ItemId = ItemId,
|
||||||
|
Guid = Guid,
|
||||||
|
Equip = new Equip { Reliquary = reliquary }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public SceneReliquaryInfo AsSceneReliquary() => new()
|
||||||
|
{
|
||||||
|
ItemId = ItemId,
|
||||||
|
Guid = Guid,
|
||||||
|
Level = Level,
|
||||||
|
PromoteLevel = PromoteLevel
|
||||||
|
};
|
||||||
|
}
|
||||||
23
Yuanshen.Gameserver/Game/GameState.cs
Normal file
23
Yuanshen.Gameserver/Game/GameState.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed class GameState
|
||||||
|
{
|
||||||
|
public const uint SceneId = 3;
|
||||||
|
|
||||||
|
public uint EnterSceneToken { get; private set; } = 1000;
|
||||||
|
|
||||||
|
public Vector CurrentPos { get; private set; } = SpawnPos;
|
||||||
|
|
||||||
|
public static Vector SpawnPos => new()
|
||||||
|
{
|
||||||
|
X = 2191.16357421875f,
|
||||||
|
Y = 214.65115356445312f,
|
||||||
|
Z = -1120.633056640625f
|
||||||
|
};
|
||||||
|
|
||||||
|
public uint NextEnterSceneToken() => ++EnterSceneToken;
|
||||||
|
|
||||||
|
public void UpdatePosition(Vector pos) => CurrentPos = pos;
|
||||||
|
}
|
||||||
47
Yuanshen.Gameserver/Game/GameWeapon.cs
Normal file
47
Yuanshen.Gameserver/Game/GameWeapon.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed class GameWeapon
|
||||||
|
{
|
||||||
|
public uint ItemId { get; }
|
||||||
|
public ulong Guid { get; }
|
||||||
|
public uint Level { get; set; } = 90;
|
||||||
|
public uint PromoteLevel { get; set; } = 6;
|
||||||
|
public uint Refinement { get; set; }
|
||||||
|
|
||||||
|
public GameWeapon(uint itemId, ulong guid)
|
||||||
|
{
|
||||||
|
ItemId = itemId;
|
||||||
|
Guid = guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Item AsItem() => new()
|
||||||
|
{
|
||||||
|
ItemId = ItemId,
|
||||||
|
Guid = Guid,
|
||||||
|
Equip = new Equip
|
||||||
|
{
|
||||||
|
Weapon = new Weapon { Level = Level, PromoteLevel = PromoteLevel }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public SceneWeaponInfo AsSceneWeapon(uint entityId)
|
||||||
|
{
|
||||||
|
SceneWeaponInfo weapon = new()
|
||||||
|
{
|
||||||
|
EntityId = entityId,
|
||||||
|
GadgetId = 50000000 + ItemId,
|
||||||
|
ItemId = ItemId,
|
||||||
|
Guid = Guid,
|
||||||
|
Level = Level,
|
||||||
|
PromoteLevel = PromoteLevel,
|
||||||
|
AbilityInfo = new AbilitySyncStateInfo { IsInited = true }
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Refinement > 0)
|
||||||
|
weapon.AffixMap.Add(111000 + ItemId % 1000, Math.Min(Refinement, 4u));
|
||||||
|
|
||||||
|
return weapon;
|
||||||
|
}
|
||||||
|
}
|
||||||
592
Yuanshen.Gameserver/Game/GmCommandService.cs
Normal file
592
Yuanshen.Gameserver/Game/GmCommandService.cs
Normal file
|
|
@ -0,0 +1,592 @@
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Yuanshen.Common.Data.Excel;
|
||||||
|
using Yuanshen.Gameserver.Database;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed class GmCommandService
|
||||||
|
{
|
||||||
|
private const int TeamSize = 4;
|
||||||
|
private const uint MoraItemId = 202;
|
||||||
|
private const uint ActionReasonTrifle = 3;
|
||||||
|
private const uint MaxConstellation = 6;
|
||||||
|
|
||||||
|
private readonly Player _player;
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly SceneManager _scene;
|
||||||
|
private readonly NetSession _session;
|
||||||
|
private readonly PlayerPersistence _persistence;
|
||||||
|
private readonly ExcelTableCollection _excel;
|
||||||
|
private readonly WorldSpawner _worldSpawner;
|
||||||
|
private readonly GachaBannerTable _banners;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
public GmCommandService(Player player, GameState state, SceneManager scene, NetSession session,
|
||||||
|
PlayerPersistence persistence, ExcelTableCollection excel, WorldSpawner worldSpawner,
|
||||||
|
GachaBannerTable banners, ILogger<GmCommandService> logger)
|
||||||
|
{
|
||||||
|
_player = player;
|
||||||
|
_state = state;
|
||||||
|
_scene = scene;
|
||||||
|
_session = session;
|
||||||
|
_persistence = persistence;
|
||||||
|
_excel = excel;
|
||||||
|
_worldSpawner = worldSpawner;
|
||||||
|
_banners = banners;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<IReadOnlyList<string>> ExecuteAsync(string input)
|
||||||
|
{
|
||||||
|
string[] parts = input.TrimStart('/').Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
if (parts.Length == 0)
|
||||||
|
return Help();
|
||||||
|
|
||||||
|
string command = parts[0].ToLowerInvariant();
|
||||||
|
string[] args = parts[1..];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> reply = command switch
|
||||||
|
{
|
||||||
|
"help" => Help(),
|
||||||
|
"tp" => await TeleportAsync(args),
|
||||||
|
"spawn" => await SpawnAsync(args),
|
||||||
|
"clear" => await ClearAsync(),
|
||||||
|
"respawn" => await RespawnAsync(),
|
||||||
|
"give" => await GiveAsync(args),
|
||||||
|
"heal" => await HealAsync(),
|
||||||
|
"level" => await LevelAsync(args),
|
||||||
|
"asc" => await AscendAsync(args),
|
||||||
|
"talent" => await TalentAsync(args),
|
||||||
|
"const" => await ConstellationAsync(args),
|
||||||
|
"wlevel" => await WeaponLevelAsync(args),
|
||||||
|
"refine" => await RefineAsync(args),
|
||||||
|
"wish" => await WishAsync(args),
|
||||||
|
"avatar" => await AvatarAsync(args),
|
||||||
|
"team" => await TeamAsync(args),
|
||||||
|
"monster" => await SpawnAsync(args),
|
||||||
|
"trifle" => await GiveAsync(args),
|
||||||
|
"kill" => await KillAsync(args),
|
||||||
|
"item" => await ItemAsync(args),
|
||||||
|
"equip" => await EquipAsync(args),
|
||||||
|
"break" => await AscendAsync(args),
|
||||||
|
"skill" => await SkillAsync(args),
|
||||||
|
"goto" => await TeleportAsync(args),
|
||||||
|
"scoin" => await ScoinAsync(args),
|
||||||
|
"hcoin" => await HcoinAsync(args),
|
||||||
|
"player" => await PlayerAsync(args),
|
||||||
|
"quest" => Unsupported("quest"),
|
||||||
|
"dungeon" => Unsupported("dungeon"),
|
||||||
|
"jump" => Unsupported("jump"),
|
||||||
|
_ => new List<string> { $"unknown command /{command} - try /help" }
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("GM /{command} -> {result}", command, string.Join(" | ", reply));
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("GM /{command} failed: {message}", command, exception.Message);
|
||||||
|
return new List<string> { $"/{command} failed: {exception.Message}" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> Help() => new()
|
||||||
|
{
|
||||||
|
"commands: /tp x y z, /spawn id [n], /clear, /respawn, /give id [n], /heal,",
|
||||||
|
"/level n, /asc n, /talent n, /const n, /wlevel n, /refine n, /wish [n] (grants fates),",
|
||||||
|
"team: avatar add id, team add id, team remove id, team list",
|
||||||
|
"mihoyo syntax: item add id [n], equip add id [lvl] [promote], level n, break n,",
|
||||||
|
"skill slot n, talent unlock all, goto x y z, scoin n, hcoin n, player level n"
|
||||||
|
};
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> AvatarAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length >= 2 && args[0].Equals("add", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return await TeamAddAsync(args[1]);
|
||||||
|
|
||||||
|
return new List<string> { "usage: avatar add avatarId" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> TeamAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length == 0)
|
||||||
|
return TeamList();
|
||||||
|
|
||||||
|
string action = args[0].ToLowerInvariant();
|
||||||
|
|
||||||
|
if (action == "list")
|
||||||
|
return TeamList();
|
||||||
|
|
||||||
|
if (action == "reset")
|
||||||
|
return await TeamResetAsync();
|
||||||
|
|
||||||
|
if (args.Length < 2)
|
||||||
|
return new List<string> { "usage: team add|remove avatarId, team list, team reset" };
|
||||||
|
|
||||||
|
return action switch
|
||||||
|
{
|
||||||
|
"add" => await TeamAddAsync(args[1]),
|
||||||
|
"remove" => await TeamRemoveAsync(args[1]),
|
||||||
|
_ => new List<string> { "usage: team add|remove avatarId, team list, team reset" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> TeamList()
|
||||||
|
=> _player.TeamAvatars
|
||||||
|
.Select(avatar => $"{avatar.AvatarId} guid={avatar.Guid}")
|
||||||
|
.DefaultIfEmpty("team is empty")
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> TeamAddAsync(string rawId)
|
||||||
|
{
|
||||||
|
if (!uint.TryParse(rawId, out uint avatarId))
|
||||||
|
return new List<string> { "usage: team add avatarId" };
|
||||||
|
|
||||||
|
GameAvatar? avatar = _player.Avatars.FirstOrDefault(candidate => candidate.AvatarId == avatarId);
|
||||||
|
|
||||||
|
if (avatar is null)
|
||||||
|
{
|
||||||
|
List<string> granted = await GrantAvatarAsync(avatarId, avatarId);
|
||||||
|
avatar = _player.Avatars.FirstOrDefault(candidate => candidate.AvatarId == avatarId);
|
||||||
|
if (avatar is null)
|
||||||
|
return granted;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_player.TeamAvatarGuids.Contains(avatar.Guid))
|
||||||
|
return new List<string> { $"avatar {avatarId} is already in the team" };
|
||||||
|
|
||||||
|
if (_player.TeamAvatarGuids.Count >= TeamSize)
|
||||||
|
return new List<string> { $"team is full, remove one first (max {TeamSize})" };
|
||||||
|
|
||||||
|
List<ulong> team = _player.TeamAvatarGuids.Append(avatar.Guid).ToList();
|
||||||
|
await _scene.SetupTeamAsync(_player.CurrentTeamId, team, _player.CurrentAvatarGuid);
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
|
||||||
|
return new List<string> { $"added avatar {avatarId} to the team" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> TeamResetAsync()
|
||||||
|
{
|
||||||
|
_player.ResetTeam();
|
||||||
|
await _scene.SetupTeamAsync(_player.CurrentTeamId, _player.TeamAvatarGuids.ToList(), _player.CurrentAvatarGuid);
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
|
||||||
|
return new List<string> { "team reset to the default avatar" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> TeamRemoveAsync(string rawId)
|
||||||
|
{
|
||||||
|
if (!uint.TryParse(rawId, out uint avatarId))
|
||||||
|
return new List<string> { "usage: team remove avatarId" };
|
||||||
|
|
||||||
|
GameAvatar? avatar = _player.Avatars.FirstOrDefault(candidate => candidate.AvatarId == avatarId);
|
||||||
|
if (avatar is null || !_player.TeamAvatarGuids.Contains(avatar.Guid))
|
||||||
|
return new List<string> { $"avatar {avatarId} is not in the team" };
|
||||||
|
|
||||||
|
if (_player.TeamAvatarGuids.Count <= 1)
|
||||||
|
return new List<string> { "cannot remove the last team member" };
|
||||||
|
|
||||||
|
List<ulong> team = _player.TeamAvatarGuids.Where(guid => guid != avatar.Guid).ToList();
|
||||||
|
ulong active = _player.CurrentAvatarGuid == avatar.Guid ? team[0] : _player.CurrentAvatarGuid;
|
||||||
|
|
||||||
|
await _scene.SetupTeamAsync(_player.CurrentTeamId, team, active);
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
|
||||||
|
return new List<string> { $"removed avatar {avatarId} from the team" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> Unsupported(string verb)
|
||||||
|
=> new() { $"{verb} is not implemented on this server yet" };
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> ItemAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length >= 2 && args[0].Equals("add", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return await GiveAsync(args[1..]);
|
||||||
|
|
||||||
|
return new List<string> { "usage: item add itemId [count]" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> ScoinAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length < 1 || !uint.TryParse(args[0], out uint amount))
|
||||||
|
return new List<string> { "usage: scoin amount" };
|
||||||
|
|
||||||
|
_player.Scoin += amount;
|
||||||
|
await _session.NotifyAsync(CmdType.PlayerPropNotify, _player.BuildCurrencyProps());
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
|
||||||
|
return new List<string> { $"mora = {_player.Scoin}" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> HcoinAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length < 1 || !uint.TryParse(args[0], out uint amount))
|
||||||
|
return new List<string> { "usage: hcoin amount" };
|
||||||
|
|
||||||
|
_player.Hcoin += amount;
|
||||||
|
await _session.NotifyAsync(CmdType.PlayerPropNotify, _player.BuildCurrencyProps());
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
|
||||||
|
return new List<string> { $"primogems = {_player.Hcoin}" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> PlayerAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length < 2 || !args[0].Equals("level", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| !uint.TryParse(args[1], out uint level))
|
||||||
|
{
|
||||||
|
return new List<string> { "usage: player level n" };
|
||||||
|
}
|
||||||
|
|
||||||
|
level = Math.Clamp(level, 1u, 60u);
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.PlayerPropNotify, new PlayerPropNotify
|
||||||
|
{
|
||||||
|
PropMap = { { 4001u, new PropValue { Type = 4001, Ival = level, Val = level } } }
|
||||||
|
});
|
||||||
|
|
||||||
|
return new List<string> { $"player level = {level} (display only, not persisted)" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> SkillAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length < 2 || !uint.TryParse(args[1], out uint level))
|
||||||
|
return new List<string> { "usage: skill slot level" };
|
||||||
|
|
||||||
|
return await TalentAsync(new[] { level.ToString() });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> EquipAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length < 2 || !args[0].Equals("add", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| !uint.TryParse(args[1], out uint itemId))
|
||||||
|
{
|
||||||
|
return new List<string> { "usage: equip add itemId [level] [promote]" };
|
||||||
|
}
|
||||||
|
|
||||||
|
GameAvatar? avatar = _player.ActiveAvatar;
|
||||||
|
if (avatar is null)
|
||||||
|
return new List<string> { "no active avatar" };
|
||||||
|
|
||||||
|
uint level = args.Length > 2 && uint.TryParse(args[2], out uint parsedLevel) ? parsedLevel : 90u;
|
||||||
|
uint promote = args.Length > 3 && uint.TryParse(args[3], out uint parsedPromote) ? parsedPromote : 0u;
|
||||||
|
|
||||||
|
bool isWeapon = _excel.GetTable(ExcelType.Weapon).Items.OfType<WeaponExcel>().Any(weapon => weapon.Id == itemId);
|
||||||
|
|
||||||
|
if (isWeapon)
|
||||||
|
{
|
||||||
|
GameWeapon weapon = _player.CreateWeapon(itemId);
|
||||||
|
weapon.Level = Math.Clamp(level, 1u, 90u);
|
||||||
|
weapon.PromoteLevel = Math.Clamp(promote, 0u, 6u);
|
||||||
|
avatar.WeaponGuid = weapon.Guid;
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.StoreItemChangeNotify, new StoreItemChangeNotify
|
||||||
|
{
|
||||||
|
StoreType = StoreType.StorePack,
|
||||||
|
ItemList = { weapon.AsItem() }
|
||||||
|
});
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.AvatarEquipChangeNotify, new AvatarEquipChangeNotify
|
||||||
|
{
|
||||||
|
AvatarGuid = avatar.Guid,
|
||||||
|
EquipType = 6,
|
||||||
|
ItemId = weapon.ItemId,
|
||||||
|
EquipGuid = weapon.Guid,
|
||||||
|
Weapon = weapon.AsSceneWeapon(avatar.WeaponEntityId)
|
||||||
|
});
|
||||||
|
|
||||||
|
await _scene.RefreshTeamAsync();
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
|
||||||
|
return new List<string> { $"equipped weapon {itemId} lv{weapon.Level} promote {weapon.PromoteLevel}" };
|
||||||
|
}
|
||||||
|
|
||||||
|
GameReliquary? reliquary = _player.FindReliquaryByItemId(itemId);
|
||||||
|
if (reliquary is null)
|
||||||
|
return new List<string> { $"item {itemId} is neither a known weapon nor a reliquary" };
|
||||||
|
|
||||||
|
reliquary.Level = Math.Max(level, 1u);
|
||||||
|
avatar.ReliquaryGuids[reliquary.EquipType] = reliquary.Guid;
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.AvatarEquipChangeNotify, new AvatarEquipChangeNotify
|
||||||
|
{
|
||||||
|
AvatarGuid = avatar.Guid,
|
||||||
|
EquipType = reliquary.EquipType,
|
||||||
|
ItemId = reliquary.ItemId,
|
||||||
|
EquipGuid = reliquary.Guid,
|
||||||
|
Reliquary = reliquary.AsSceneReliquary()
|
||||||
|
});
|
||||||
|
|
||||||
|
await _scene.RefreshTeamAsync();
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
|
||||||
|
return new List<string> { $"equipped reliquary {itemId} lv{reliquary.Level}" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> KillAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length >= 1 && args[0].Equals("monster", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return await ClearAsync();
|
||||||
|
|
||||||
|
return new List<string> { "usage: kill monster all" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> TeleportAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length < 3 || !float.TryParse(args[0], out float x) || !float.TryParse(args[1], out float y) || !float.TryParse(args[2], out float z))
|
||||||
|
return new List<string> { "usage: /tp x y z" };
|
||||||
|
|
||||||
|
await _scene.TeleportAsync(new Vector { X = x, Y = y, Z = z });
|
||||||
|
return new List<string> { $"teleported to ({x}, {y}, {z})" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> SpawnAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length < 1 || !uint.TryParse(args[0], out uint monsterId))
|
||||||
|
return new List<string> { "usage: /spawn monsterId [count]" };
|
||||||
|
|
||||||
|
uint count = args.Length > 1 && uint.TryParse(args[1], out uint parsed) ? Math.Min(parsed, 50u) : 1u;
|
||||||
|
uint spawned = 0;
|
||||||
|
|
||||||
|
for (uint i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
double angle = Math.PI * 2 * i / Math.Max(count, 1u);
|
||||||
|
Vector pos = new()
|
||||||
|
{
|
||||||
|
X = _state.CurrentPos.X + (float)(Math.Cos(angle) * 4),
|
||||||
|
Y = _state.CurrentPos.Y,
|
||||||
|
Z = _state.CurrentPos.Z + (float)(Math.Sin(angle) * 4)
|
||||||
|
};
|
||||||
|
|
||||||
|
GameMonster monster = _player.SpawnMonster(monsterId, pos, 30, 1000f, 100f, 500f);
|
||||||
|
await _scene.AppearMonsterAsync(monster);
|
||||||
|
spawned++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new List<string> { $"spawned {spawned}x monster {monsterId}" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> ClearAsync()
|
||||||
|
{
|
||||||
|
await _scene.ClearMonstersAsync();
|
||||||
|
return new List<string> { "cleared all monsters" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> RespawnAsync()
|
||||||
|
{
|
||||||
|
await _scene.ClearMonstersAsync();
|
||||||
|
await _worldSpawner.RespawnAsync(_state.CurrentPos);
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
return new List<string> { "respawned all monsters around you" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> GiveAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length < 1 || !uint.TryParse(args[0], out uint itemId))
|
||||||
|
return new List<string> { "usage: /give itemId [count]" };
|
||||||
|
|
||||||
|
uint count = args.Length > 1 && uint.TryParse(args[1], out uint parsed) ? Math.Max(parsed, 1u) : 1u;
|
||||||
|
|
||||||
|
MaterialExcel? material = _excel.GetTable(ExcelType.Material).Items.OfType<MaterialExcel>()
|
||||||
|
.FirstOrDefault(candidate => candidate.Id == itemId);
|
||||||
|
|
||||||
|
if (material is not null && material.GainedAvatarId != 0)
|
||||||
|
return await GrantAvatarAsync(material.GainedAvatarId, itemId);
|
||||||
|
|
||||||
|
bool isWeapon = _excel.GetTable(ExcelType.Weapon).Items.OfType<WeaponExcel>().Any(weapon => weapon.Id == itemId);
|
||||||
|
|
||||||
|
StoreItemChangeNotify notify = new() { StoreType = StoreType.StorePack };
|
||||||
|
|
||||||
|
if (isWeapon)
|
||||||
|
{
|
||||||
|
for (uint i = 0; i < Math.Min(count, 50u); i++)
|
||||||
|
notify.ItemList.Add(_player.CreateWeapon(itemId).AsItem());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_player.GiveMaterial(itemId, count);
|
||||||
|
notify.ItemList.Add(new Item
|
||||||
|
{
|
||||||
|
ItemId = itemId,
|
||||||
|
Guid = _player.MaterialGuid(itemId),
|
||||||
|
Material = new Material { Count = _player.Materials[itemId] }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.StoreItemChangeNotify, notify);
|
||||||
|
|
||||||
|
ItemAddHintNotify hint = new() { Reason = ActionReasonTrifle };
|
||||||
|
hint.ItemList.Add(new ItemHint { ItemId = itemId, Count = count, IsNew = false });
|
||||||
|
await _session.NotifyAsync(CmdType.ItemAddHintNotify, hint);
|
||||||
|
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
return new List<string> { $"gave {count}x {itemId}" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> GrantAvatarAsync(uint avatarId, uint cardItemId)
|
||||||
|
{
|
||||||
|
if (_player.Avatars.Any(avatar => avatar.AvatarId == avatarId))
|
||||||
|
return new List<string> { $"{cardItemId} is the card for avatar {avatarId}, already owned" };
|
||||||
|
|
||||||
|
if (!_player.HasAvatarData(avatarId))
|
||||||
|
return new List<string> { $"avatar {avatarId} is not in this build" };
|
||||||
|
|
||||||
|
GameAvatar? granted = _player.GrantAvatar(avatarId);
|
||||||
|
if (granted is null)
|
||||||
|
return new List<string> { $"avatar {avatarId} could not be created" };
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.AvatarAddNotify, new AvatarAddNotify
|
||||||
|
{
|
||||||
|
Avatar = granted.AsAvatarInfo(),
|
||||||
|
IsInTeam = false
|
||||||
|
});
|
||||||
|
|
||||||
|
await _scene.RefreshAvatarDataAsync();
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
|
||||||
|
List<string> reply = new() { $"granted avatar {avatarId} - add it with: team add {avatarId}" };
|
||||||
|
if (!_player.HasAbilities(avatarId))
|
||||||
|
reply.Add("warning: no ability data, it will have no skills and may break the UI - use 'team reset' to recover");
|
||||||
|
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> HealAsync()
|
||||||
|
{
|
||||||
|
await _scene.HealTeamAsync();
|
||||||
|
return new List<string> { "team healed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> LevelAsync(string[] args)
|
||||||
|
{
|
||||||
|
GameAvatar? avatar = _player.ActiveAvatar;
|
||||||
|
if (avatar is null || args.Length < 1 || !uint.TryParse(args[0], out uint level))
|
||||||
|
return new List<string> { "usage: /level n" };
|
||||||
|
|
||||||
|
avatar.Level = Math.Clamp(level, 1u, 90u);
|
||||||
|
await _session.NotifyAsync(CmdType.AvatarPropNotify, new AvatarPropNotify
|
||||||
|
{
|
||||||
|
AvatarGuid = avatar.Guid,
|
||||||
|
PropMap = { { 4001u, avatar.Level } }
|
||||||
|
});
|
||||||
|
await _scene.RefreshTeamAsync();
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
return new List<string> { $"avatar {avatar.AvatarId} level = {avatar.Level}" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> AscendAsync(string[] args)
|
||||||
|
{
|
||||||
|
GameAvatar? avatar = _player.ActiveAvatar;
|
||||||
|
if (avatar is null || args.Length < 1 || !uint.TryParse(args[0], out uint promote))
|
||||||
|
return new List<string> { "usage: /asc n" };
|
||||||
|
|
||||||
|
avatar.PromoteLevel = Math.Clamp(promote, 0u, 6u);
|
||||||
|
await _session.NotifyAsync(CmdType.AvatarPropNotify, new AvatarPropNotify
|
||||||
|
{
|
||||||
|
AvatarGuid = avatar.Guid,
|
||||||
|
PropMap = { { 1002u, avatar.PromoteLevel } }
|
||||||
|
});
|
||||||
|
await _scene.RefreshTeamAsync();
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
return new List<string> { $"avatar {avatar.AvatarId} ascension = {avatar.PromoteLevel}" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> TalentAsync(string[] args)
|
||||||
|
{
|
||||||
|
if (args.Length >= 1 && args[0].Equals("unlock", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return await ConstellationAsync(new[] { MaxConstellation.ToString() });
|
||||||
|
|
||||||
|
GameAvatar? avatar = _player.ActiveAvatar;
|
||||||
|
if (avatar is null || args.Length < 1 || !uint.TryParse(args[0], out uint talent))
|
||||||
|
return new List<string> { "usage: /talent n, or talent unlock all" };
|
||||||
|
|
||||||
|
avatar.TalentLevel = Math.Clamp(talent, 1u, 15u);
|
||||||
|
await _scene.RefreshTeamAsync();
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
return new List<string> { $"avatar {avatar.AvatarId} talents = {avatar.TalentLevel} (reopen character screen to see)" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> ConstellationAsync(string[] args)
|
||||||
|
{
|
||||||
|
GameAvatar? avatar = _player.ActiveAvatar;
|
||||||
|
if (avatar is null || args.Length < 1 || !uint.TryParse(args[0], out uint constellation))
|
||||||
|
return new List<string> { "usage: /const n" };
|
||||||
|
|
||||||
|
avatar.Constellation = Math.Clamp(constellation, 0u, 6u);
|
||||||
|
await _scene.RefreshTeamAsync();
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
return new List<string> { $"avatar {avatar.AvatarId} constellation = {avatar.Constellation} (reopen character screen to see)" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> WeaponLevelAsync(string[] args)
|
||||||
|
{
|
||||||
|
GameAvatar? avatar = _player.ActiveAvatar;
|
||||||
|
GameWeapon? weapon = avatar is null ? null : _player.FindWeapon(avatar.WeaponGuid);
|
||||||
|
if (weapon is null || args.Length < 1 || !uint.TryParse(args[0], out uint level))
|
||||||
|
return new List<string> { "usage: /wlevel n" };
|
||||||
|
|
||||||
|
weapon.Level = Math.Clamp(level, 1u, 90u);
|
||||||
|
await _scene.RefreshTeamAsync();
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
return new List<string> { $"weapon {weapon.ItemId} level = {weapon.Level}" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> RefineAsync(string[] args)
|
||||||
|
{
|
||||||
|
GameAvatar? avatar = _player.ActiveAvatar;
|
||||||
|
GameWeapon? weapon = avatar is null ? null : _player.FindWeapon(avatar.WeaponGuid);
|
||||||
|
if (weapon is null || args.Length < 1 || !uint.TryParse(args[0], out uint refine))
|
||||||
|
return new List<string> { "usage: /refine n" };
|
||||||
|
|
||||||
|
weapon.Refinement = Math.Clamp(refine, 0u, 4u);
|
||||||
|
await _scene.RefreshTeamAsync();
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
return new List<string> { $"weapon {weapon.ItemId} refinement = {weapon.Refinement + 1}" };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask<List<string>> WishAsync(string[] args)
|
||||||
|
{
|
||||||
|
uint amount = args.Length > 0 && uint.TryParse(args[0], out uint parsed) ? Math.Clamp(parsed, 1u, 9999u) : 10u;
|
||||||
|
|
||||||
|
HashSet<uint> costItems = new();
|
||||||
|
foreach (GachaBanner banner in _banners.Banners)
|
||||||
|
{
|
||||||
|
if (banner.Info.CostItemId != 0)
|
||||||
|
costItems.Add(banner.Info.CostItemId);
|
||||||
|
|
||||||
|
if (banner.Info.TenCostItemId != 0)
|
||||||
|
costItems.Add(banner.Info.TenCostItemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (costItems.Count == 0)
|
||||||
|
return new List<string> { "no banners are shipped, nothing to grant" };
|
||||||
|
|
||||||
|
StoreItemChangeNotify notify = new() { StoreType = StoreType.StorePack };
|
||||||
|
List<string> results = new();
|
||||||
|
|
||||||
|
foreach (uint itemId in costItems.OrderBy(id => id))
|
||||||
|
{
|
||||||
|
_player.GiveMaterial(itemId, amount);
|
||||||
|
uint total = _player.Materials.GetValueOrDefault(itemId);
|
||||||
|
|
||||||
|
notify.ItemList.Add(new Item
|
||||||
|
{
|
||||||
|
ItemId = itemId,
|
||||||
|
Guid = _player.MaterialGuid(itemId),
|
||||||
|
Material = new Material { Count = total }
|
||||||
|
});
|
||||||
|
|
||||||
|
results.Add($"+{amount} x {itemId} (now {total})");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.StoreItemChangeNotify, notify);
|
||||||
|
await _persistence.SaveAsync();
|
||||||
|
|
||||||
|
return new List<string> { "granted wish currency - open the wish page and pull for the real animation" }
|
||||||
|
.Concat(results).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
59
Yuanshen.Gameserver/Game/MonsterSpawnTable.cs
Normal file
59
Yuanshen.Gameserver/Game/MonsterSpawnTable.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal readonly record struct SpawnNode(uint NodeId, uint ConfigId, uint GroupId, uint MonsterId, uint Level, Vector Pos, float Hp, float Attack, float Defense)
|
||||||
|
{
|
||||||
|
public bool IsWildlife => MonsterId is >= 28000000 and < 29000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class MonsterSpawnTable
|
||||||
|
{
|
||||||
|
public IReadOnlyList<SpawnNode> Nodes { get; }
|
||||||
|
|
||||||
|
public MonsterSpawnTable()
|
||||||
|
{
|
||||||
|
Nodes = Load("scene3_spawns.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<SpawnNode> Load(string fileName)
|
||||||
|
{
|
||||||
|
List<SpawnNode> nodes = new();
|
||||||
|
string path = Path.Combine(AppContext.BaseDirectory, "assets", "scene", fileName);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return nodes;
|
||||||
|
|
||||||
|
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
|
foreach (JsonElement entry in document.RootElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
uint configId = entry.GetProperty("c").GetUInt32();
|
||||||
|
if (configId == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (entry.TryGetProperty("a", out JsonElement active) && active.GetUInt32() != 1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
uint nodeId = entry.TryGetProperty("n", out JsonElement node) ? node.GetUInt32() : configId;
|
||||||
|
uint groupId = entry.TryGetProperty("g", out JsonElement group) ? group.GetUInt32() : 0;
|
||||||
|
|
||||||
|
nodes.Add(new SpawnNode(
|
||||||
|
nodeId,
|
||||||
|
configId,
|
||||||
|
groupId,
|
||||||
|
entry.GetProperty("m").GetUInt32(),
|
||||||
|
entry.GetProperty("l").GetUInt32(),
|
||||||
|
new Vector
|
||||||
|
{
|
||||||
|
X = entry.GetProperty("x").GetSingle(),
|
||||||
|
Y = entry.GetProperty("y").GetSingle(),
|
||||||
|
Z = entry.GetProperty("z").GetSingle()
|
||||||
|
},
|
||||||
|
entry.GetProperty("hp").GetSingle(),
|
||||||
|
entry.GetProperty("atk").GetSingle(),
|
||||||
|
entry.GetProperty("def").GetSingle()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Yuanshen.Gameserver/Game/OpenStateTable.cs
Normal file
33
Yuanshen.Gameserver/Game/OpenStateTable.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed class OpenStateTable
|
||||||
|
{
|
||||||
|
private static readonly uint[] Fallback = { 1, 2, 3, 4, 5, 31, 32 };
|
||||||
|
|
||||||
|
public IReadOnlyList<uint> Ids { get; }
|
||||||
|
|
||||||
|
public OpenStateTable()
|
||||||
|
{
|
||||||
|
List<uint> ids = Load("OpenStateConfigData.json");
|
||||||
|
Ids = ids.Count != 0 ? ids : Fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<uint> Load(string fileName)
|
||||||
|
{
|
||||||
|
List<uint> ids = new();
|
||||||
|
string path = Path.Combine(AppContext.BaseDirectory, "assets", "excel", fileName);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return ids;
|
||||||
|
|
||||||
|
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
|
foreach (JsonElement entry in document.RootElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (entry.TryGetProperty("id", out JsonElement id) && id.TryGetUInt32(out uint value) && value != 0)
|
||||||
|
ids.Add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
355
Yuanshen.Gameserver/Game/Player.cs
Normal file
355
Yuanshen.Gameserver/Game/Player.cs
Normal file
|
|
@ -0,0 +1,355 @@
|
||||||
|
using Yuanshen.Common.Data.Excel;
|
||||||
|
using Yuanshen.Gameserver.Database;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed class Player
|
||||||
|
{
|
||||||
|
public uint Uid { get; private set; } = 1;
|
||||||
|
public const uint PeerId = 1;
|
||||||
|
public const uint TeamEntityId = 0x09000001;
|
||||||
|
public const uint MpLevelEntityId = 0x0B000001;
|
||||||
|
|
||||||
|
public static readonly uint[] Flycloaks = { 140001, 140002, 140003, 140004, 140005 };
|
||||||
|
|
||||||
|
public string Nickname { get; } = "Traveler";
|
||||||
|
|
||||||
|
public List<GameAvatar> Avatars { get; } = new();
|
||||||
|
public List<GameWeapon> Weapons { get; } = new();
|
||||||
|
public List<GameReliquary> Reliquaries { get; } = new();
|
||||||
|
public List<GameMonster> Monsters { get; } = new();
|
||||||
|
public HashSet<uint> KilledNodeIds { get; } = new();
|
||||||
|
public Dictionary<uint, uint> Materials { get; } = new();
|
||||||
|
|
||||||
|
public bool IsInitialized { get; private set; }
|
||||||
|
|
||||||
|
public uint CurrentTeamId { get; set; } = 1;
|
||||||
|
public List<ulong> TeamAvatarGuids { get; } = new();
|
||||||
|
public ulong CurrentAvatarGuid { get; set; }
|
||||||
|
|
||||||
|
private readonly ExcelTableCollection _excel;
|
||||||
|
private uint _guidSeq;
|
||||||
|
private uint _avatarEntitySeq;
|
||||||
|
private uint _weaponEntitySeq;
|
||||||
|
private uint _monsterEntitySeq;
|
||||||
|
|
||||||
|
public Player(ExcelTableCollection excel) => _excel = excel;
|
||||||
|
|
||||||
|
public void AssignUid(uint uid) => Uid = uid;
|
||||||
|
|
||||||
|
public const uint StartingHcoin = 1_600_000;
|
||||||
|
public const uint StartingScoin = 9_999_999;
|
||||||
|
|
||||||
|
public const uint CurrentSpawnDataVersion = 2;
|
||||||
|
|
||||||
|
public ulong NextGuid() => ((ulong)Uid << 32) | ++_guidSeq;
|
||||||
|
|
||||||
|
public GameAvatar? ActiveAvatar => FindAvatar(CurrentAvatarGuid);
|
||||||
|
public GameAvatar? FindAvatar(ulong guid) => Avatars.FirstOrDefault(avatar => avatar.Guid == guid);
|
||||||
|
public GameWeapon? FindWeapon(ulong guid) => Weapons.FirstOrDefault(weapon => weapon.Guid == guid);
|
||||||
|
public GameReliquary? FindReliquary(ulong guid) => Reliquaries.FirstOrDefault(item => item.Guid == guid);
|
||||||
|
|
||||||
|
public GameReliquary? FindReliquaryByItemId(uint itemId) => Reliquaries.FirstOrDefault(item => item.ItemId == itemId);
|
||||||
|
|
||||||
|
public IEnumerable<GameAvatar> TeamAvatars
|
||||||
|
=> TeamAvatarGuids.Select(FindAvatar).OfType<GameAvatar>();
|
||||||
|
|
||||||
|
public void InitDefault()
|
||||||
|
{
|
||||||
|
if (IsInitialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
List<AvatarSkillDepotExcel> depots = _excel.GetTable(ExcelType.AvatarSkillDepot)
|
||||||
|
.Items.OfType<AvatarSkillDepotExcel>().ToList();
|
||||||
|
List<AvatarAbilitiesExcel> abilities = _excel.GetTable(ExcelType.AvatarAbilities)
|
||||||
|
.Items.OfType<AvatarAbilitiesExcel>().ToList();
|
||||||
|
|
||||||
|
foreach (AvatarExcel excel in _excel.GetTable(ExcelType.Avatar).Items.OfType<AvatarExcel>())
|
||||||
|
{
|
||||||
|
AvatarAbilitiesExcel? avatarAbilities = abilities.FirstOrDefault(a => a.Id == excel.Id);
|
||||||
|
if (avatarAbilities is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
AvatarSkillDepotExcel? depot = depots.FirstOrDefault(d => d.Id == excel.SkillDepotId);
|
||||||
|
GameAvatar avatar = new(excel, NextGuid(), 0x01000000 + ++_avatarEntitySeq, 0x06000000 + ++_weaponEntitySeq, depot,
|
||||||
|
avatarAbilities);
|
||||||
|
GameWeapon weapon = CreateWeapon(excel.InitialWeapon);
|
||||||
|
avatar.WeaponGuid = weapon.Guid;
|
||||||
|
avatar.OwnerUid = Uid;
|
||||||
|
Avatars.Add(avatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (WeaponExcel excel in _excel.GetTable(ExcelType.Weapon).Items.OfType<WeaponExcel>())
|
||||||
|
CreateWeapon(excel.Id);
|
||||||
|
|
||||||
|
CreateReliquaries();
|
||||||
|
|
||||||
|
CurrentAvatarGuid = Avatars[0].Guid;
|
||||||
|
TeamAvatarGuids.Add(Avatars[0].Guid);
|
||||||
|
IsInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameAvatar? GrantAvatar(uint avatarId)
|
||||||
|
{
|
||||||
|
if (Avatars.Any(existing => existing.AvatarId == avatarId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
AvatarExcel? excel = _excel.GetTable(ExcelType.Avatar).Items.OfType<AvatarExcel>()
|
||||||
|
.FirstOrDefault(candidate => candidate.Id == avatarId);
|
||||||
|
AvatarAbilitiesExcel? abilities = _excel.GetTable(ExcelType.AvatarAbilities).Items.OfType<AvatarAbilitiesExcel>()
|
||||||
|
.FirstOrDefault(candidate => candidate.Id == avatarId);
|
||||||
|
|
||||||
|
if (excel is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
AvatarSkillDepotExcel? depot = _excel.GetTable(ExcelType.AvatarSkillDepot).Items.OfType<AvatarSkillDepotExcel>()
|
||||||
|
.FirstOrDefault(candidate => candidate.Id == excel.SkillDepotId);
|
||||||
|
|
||||||
|
GameAvatar avatar = new(excel, NextGuid(), 0x01000000 + ++_avatarEntitySeq, 0x06000000 + ++_weaponEntitySeq, depot, abilities);
|
||||||
|
GameWeapon weapon = CreateWeapon(excel.InitialWeapon);
|
||||||
|
avatar.WeaponGuid = weapon.Guid;
|
||||||
|
avatar.OwnerUid = Uid;
|
||||||
|
Avatars.Add(avatar);
|
||||||
|
|
||||||
|
return avatar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasAvatarData(uint avatarId)
|
||||||
|
=> _excel.GetTable(ExcelType.Avatar).Items.OfType<AvatarExcel>().Any(candidate => candidate.Id == avatarId);
|
||||||
|
|
||||||
|
public bool HasAbilities(uint avatarId)
|
||||||
|
=> _excel.GetTable(ExcelType.AvatarAbilities).Items.OfType<AvatarAbilitiesExcel>().Any(candidate => candidate.Id == avatarId);
|
||||||
|
|
||||||
|
public void ResetTeam()
|
||||||
|
{
|
||||||
|
TeamAvatarGuids.Clear();
|
||||||
|
TeamAvatarGuids.Add(Avatars[0].Guid);
|
||||||
|
CurrentAvatarGuid = Avatars[0].Guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ulong MaterialGuid(uint itemId) => ((ulong)Uid << 32) | (0x80000000u | itemId);
|
||||||
|
|
||||||
|
public void MarkNodeKilled(uint nodeId) => KilledNodeIds.Add(nodeId);
|
||||||
|
|
||||||
|
public Dictionary<uint, uint> GachaPity { get; } = new();
|
||||||
|
public Dictionary<uint, uint> GachaGuarantee { get; } = new();
|
||||||
|
public List<GachaLogEntry> GachaLog { get; } = new();
|
||||||
|
public Dictionary<uint, uint> BoughtGoods { get; } = new();
|
||||||
|
public Dictionary<uint, uint> ClientGadgets { get; } = new();
|
||||||
|
|
||||||
|
public uint Hcoin { get; set; } = StartingHcoin;
|
||||||
|
public uint Scoin { get; set; } = StartingScoin;
|
||||||
|
|
||||||
|
public Yuanshen.Protocol.PlayerPropNotify BuildCurrencyProps()
|
||||||
|
{
|
||||||
|
Yuanshen.Protocol.PlayerPropNotify notify = new();
|
||||||
|
notify.PropMap.Add(10015u, new Yuanshen.Protocol.PropValue { Type = 10015, Ival = Hcoin, Val = Hcoin });
|
||||||
|
notify.PropMap.Add(10016u, new Yuanshen.Protocol.PropValue { Type = 10016, Ival = Scoin, Val = Scoin });
|
||||||
|
return notify;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TakeMaterial(uint itemId, uint count)
|
||||||
|
{
|
||||||
|
uint left = Materials.GetValueOrDefault(itemId);
|
||||||
|
Materials[itemId] = left > count ? left - count : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GiveMaterial(uint itemId, uint count)
|
||||||
|
=> Materials[itemId] = Materials.GetValueOrDefault(itemId) + count;
|
||||||
|
|
||||||
|
public GameMonster SpawnMonster(uint monsterId, Yuanshen.Protocol.Vector pos, uint level, float maxHp, float attack, float defense, uint nodeId = 0, uint configId = 0, uint groupId = 0)
|
||||||
|
{
|
||||||
|
GameMonster monster = new(monsterId, 0x02000000 + ++_monsterEntitySeq, pos, level, maxHp, attack, defense)
|
||||||
|
{
|
||||||
|
NodeId = nodeId,
|
||||||
|
ConfigId = configId,
|
||||||
|
GroupId = groupId
|
||||||
|
};
|
||||||
|
Monsters.Add(monster);
|
||||||
|
return monster;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerRecord ToRecord(Yuanshen.Protocol.Vector pos) => new()
|
||||||
|
{
|
||||||
|
Uid = Uid,
|
||||||
|
Nickname = Nickname,
|
||||||
|
CurrentTeamId = CurrentTeamId,
|
||||||
|
CurrentAvatarGuid = CurrentAvatarGuid,
|
||||||
|
TeamAvatarGuids = TeamAvatarGuids.ToList(),
|
||||||
|
TeamAvatarIds = TeamAvatars.Select(avatar => avatar.AvatarId).ToList(),
|
||||||
|
CurrentAvatarId = ActiveAvatar?.AvatarId ?? 0,
|
||||||
|
EquippedWeapons = Avatars
|
||||||
|
.GroupBy(a => a.AvatarId)
|
||||||
|
.ToDictionary(group => group.Key, group => FindWeapon(group.First().WeaponGuid)?.ItemId ?? 0UL),
|
||||||
|
EquippedReliquaries = Avatars
|
||||||
|
.Where(a => a.ReliquaryGuids.Count != 0)
|
||||||
|
.GroupBy(a => a.AvatarId)
|
||||||
|
.ToDictionary(group => group.Key, group => group.First().ReliquaryGuids
|
||||||
|
.ToDictionary(slot => slot.Key, slot => (ulong)(FindReliquary(slot.Value)?.ItemId ?? 0))),
|
||||||
|
AvatarProgress = Avatars
|
||||||
|
.GroupBy(a => a.AvatarId)
|
||||||
|
.ToDictionary(group => group.Key, group => BuildProgress(group.First())),
|
||||||
|
Materials = new Dictionary<uint, uint>(Materials),
|
||||||
|
KilledNodes = KilledNodeIds.ToList(),
|
||||||
|
SpawnDataVersion = CurrentSpawnDataVersion,
|
||||||
|
GachaPity = GachaPity.ToDictionary(pair => pair.Key, pair => pair.Value),
|
||||||
|
GachaGuarantee = GachaGuarantee.ToDictionary(pair => pair.Key, pair => pair.Value),
|
||||||
|
GachaLog = GachaLog.ToList(),
|
||||||
|
BoughtGoods = BoughtGoods.ToDictionary(pair => pair.Key, pair => pair.Value),
|
||||||
|
Hcoin = Hcoin,
|
||||||
|
Scoin = Scoin,
|
||||||
|
PosX = pos.X,
|
||||||
|
PosY = pos.Y,
|
||||||
|
PosZ = pos.Z
|
||||||
|
};
|
||||||
|
|
||||||
|
public void ApplyRecord(PlayerRecord record)
|
||||||
|
{
|
||||||
|
CurrentTeamId = record.CurrentTeamId;
|
||||||
|
|
||||||
|
Hcoin = record.Hcoin != 0 || record.Scoin != 0 ? record.Hcoin : StartingHcoin;
|
||||||
|
Scoin = record.Hcoin != 0 || record.Scoin != 0 ? record.Scoin : StartingScoin;
|
||||||
|
|
||||||
|
BoughtGoods.Clear();
|
||||||
|
foreach ((uint goodsId, uint bought) in record.BoughtGoods)
|
||||||
|
BoughtGoods[goodsId] = bought;
|
||||||
|
|
||||||
|
GachaPity.Clear();
|
||||||
|
foreach ((uint gachaType, uint counter) in record.GachaPity)
|
||||||
|
GachaPity[gachaType] = counter;
|
||||||
|
|
||||||
|
GachaGuarantee.Clear();
|
||||||
|
foreach ((uint gachaType, uint flag) in record.GachaGuarantee)
|
||||||
|
GachaGuarantee[gachaType] = flag;
|
||||||
|
|
||||||
|
GachaLog.Clear();
|
||||||
|
GachaLog.AddRange(record.GachaLog);
|
||||||
|
|
||||||
|
KilledNodeIds.Clear();
|
||||||
|
foreach (uint nodeId in record.SpawnDataVersion == CurrentSpawnDataVersion ? record.KilledNodes : new List<uint>())
|
||||||
|
KilledNodeIds.Add(nodeId);
|
||||||
|
|
||||||
|
HashSet<ulong> takenWeapons = new();
|
||||||
|
foreach ((uint avatarId, ulong itemId) in record.EquippedWeapons)
|
||||||
|
{
|
||||||
|
GameAvatar? avatar = Avatars.FirstOrDefault(a => a.AvatarId == avatarId);
|
||||||
|
GameWeapon? weapon = Weapons.FirstOrDefault(w => w.ItemId == itemId && !takenWeapons.Contains(w.Guid));
|
||||||
|
if (avatar is null || weapon is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
avatar.WeaponGuid = weapon.Guid;
|
||||||
|
takenWeapons.Add(weapon.Guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<ulong> takenReliquaries = new();
|
||||||
|
foreach ((uint avatarId, Dictionary<uint, ulong> slots) in record.EquippedReliquaries)
|
||||||
|
{
|
||||||
|
GameAvatar? avatar = Avatars.FirstOrDefault(a => a.AvatarId == avatarId);
|
||||||
|
if (avatar is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
foreach ((uint slot, ulong itemId) in slots)
|
||||||
|
{
|
||||||
|
GameReliquary? reliquary = Reliquaries
|
||||||
|
.FirstOrDefault(r => r.ItemId == itemId && !takenReliquaries.Contains(r.Guid));
|
||||||
|
if (reliquary is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
avatar.ReliquaryGuids[slot] = reliquary.Guid;
|
||||||
|
takenReliquaries.Add(reliquary.Guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ((uint avatarId, AvatarProgress progress) in record.AvatarProgress)
|
||||||
|
{
|
||||||
|
GameAvatar? avatar = Avatars.FirstOrDefault(a => a.AvatarId == avatarId);
|
||||||
|
if (avatar is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
avatar.Level = progress.Level;
|
||||||
|
avatar.PromoteLevel = progress.PromoteLevel;
|
||||||
|
avatar.TalentLevel = progress.TalentLevel;
|
||||||
|
avatar.Constellation = progress.Constellation;
|
||||||
|
|
||||||
|
GameWeapon? weapon = FindWeapon(avatar.WeaponGuid);
|
||||||
|
if (weapon is not null)
|
||||||
|
{
|
||||||
|
weapon.Level = progress.WeaponLevel;
|
||||||
|
weapon.PromoteLevel = progress.WeaponPromote;
|
||||||
|
weapon.Refinement = progress.WeaponRefinement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Materials.Clear();
|
||||||
|
foreach ((uint itemId, uint count) in record.Materials)
|
||||||
|
Materials[itemId] = count;
|
||||||
|
|
||||||
|
List<ulong> team = record.TeamAvatarIds
|
||||||
|
.Select(avatarId => Avatars.FirstOrDefault(a => a.AvatarId == avatarId)?.Guid ?? 0)
|
||||||
|
.Where(guid => guid != 0)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (team.Count != 0)
|
||||||
|
{
|
||||||
|
TeamAvatarGuids.Clear();
|
||||||
|
TeamAvatarGuids.AddRange(team);
|
||||||
|
|
||||||
|
ulong active = Avatars.FirstOrDefault(a => a.AvatarId == record.CurrentAvatarId)?.Guid ?? 0;
|
||||||
|
CurrentAvatarGuid = team.Contains(active) ? active : team[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AvatarProgress BuildProgress(GameAvatar avatar)
|
||||||
|
{
|
||||||
|
GameWeapon? weapon = FindWeapon(avatar.WeaponGuid);
|
||||||
|
return new AvatarProgress
|
||||||
|
{
|
||||||
|
Level = avatar.Level,
|
||||||
|
PromoteLevel = avatar.PromoteLevel,
|
||||||
|
TalentLevel = avatar.TalentLevel,
|
||||||
|
Constellation = avatar.Constellation,
|
||||||
|
WeaponLevel = weapon?.Level ?? 90,
|
||||||
|
WeaponPromote = weapon?.PromoteLevel ?? 6,
|
||||||
|
WeaponRefinement = weapon?.Refinement ?? 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameWeapon CreateWeapon(uint itemId)
|
||||||
|
{
|
||||||
|
GameWeapon weapon = new(itemId, NextGuid());
|
||||||
|
Weapons.Add(weapon);
|
||||||
|
return weapon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateReliquaries()
|
||||||
|
{
|
||||||
|
List<ReliquaryMainPropExcel> mainProps = _excel.GetTable(ExcelType.ReliquaryMainProp)
|
||||||
|
.Items.OfType<ReliquaryMainPropExcel>().ToList();
|
||||||
|
List<ReliquaryAffixExcel> affixes = _excel.GetTable(ExcelType.ReliquaryAffix)
|
||||||
|
.Items.OfType<ReliquaryAffixExcel>().ToList();
|
||||||
|
|
||||||
|
foreach (ReliquaryExcel excel in _excel.GetTable(ExcelType.Reliquary).Items.OfType<ReliquaryExcel>())
|
||||||
|
{
|
||||||
|
ReliquaryMainPropExcel? main = mainProps.FirstOrDefault(p => p.PropDepotId == excel.MainPropDepotId);
|
||||||
|
if (main is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
GameReliquary reliquary = new(excel.Id, NextGuid(), EquipTypeId(excel.EquipType), main.Id);
|
||||||
|
|
||||||
|
foreach (ReliquaryAffixExcel affix in affixes.Where(a => a.DepotId == excel.AppendPropDepotId).Take(4))
|
||||||
|
reliquary.AppendPropIds.Add(affix.Id);
|
||||||
|
|
||||||
|
Reliquaries.Add(reliquary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static uint EquipTypeId(string equipType) => equipType switch
|
||||||
|
{
|
||||||
|
"EQUIP_BRACER" => 1,
|
||||||
|
"EQUIP_NECKLACE" => 2,
|
||||||
|
"EQUIP_SHOES" => 3,
|
||||||
|
"EQUIP_RING" => 4,
|
||||||
|
"EQUIP_DRESS" => 5,
|
||||||
|
_ => 0
|
||||||
|
};
|
||||||
|
}
|
||||||
247
Yuanshen.Gameserver/Game/SceneManager.cs
Normal file
247
Yuanshen.Gameserver/Game/SceneManager.cs
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed class SceneManager
|
||||||
|
{
|
||||||
|
private const int LevelSyncAttempts = 3;
|
||||||
|
|
||||||
|
private readonly NetSession _session;
|
||||||
|
private readonly Player _player;
|
||||||
|
private readonly GameState _state;
|
||||||
|
|
||||||
|
public SceneManager(NetSession session, Player player, GameState state)
|
||||||
|
{
|
||||||
|
_session = session;
|
||||||
|
_player = player;
|
||||||
|
_state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SceneTeamUpdateNotify BuildSceneTeamUpdate()
|
||||||
|
{
|
||||||
|
SceneTeamUpdateNotify notify = new();
|
||||||
|
|
||||||
|
foreach (GameAvatar avatar in _player.TeamAvatars)
|
||||||
|
{
|
||||||
|
GameWeapon weapon = _player.FindWeapon(avatar.WeaponGuid)!;
|
||||||
|
|
||||||
|
notify.SceneTeamAvatarList.Add(new SceneTeamAvatar
|
||||||
|
{
|
||||||
|
PlayerId = _player.Uid,
|
||||||
|
AvatarGuid = avatar.Guid,
|
||||||
|
SceneId = GameState.SceneId,
|
||||||
|
EntityId = avatar.EntityId,
|
||||||
|
WeaponGuid = avatar.WeaponGuid,
|
||||||
|
WeaponEntityId = avatar.WeaponEntityId,
|
||||||
|
IsPlayerCurAvatar = avatar.Guid == _player.CurrentAvatarGuid,
|
||||||
|
IsOnScene = avatar.Guid == _player.CurrentAvatarGuid,
|
||||||
|
AbilityControlBlock = avatar.BuildAbilityControlBlock(),
|
||||||
|
AvatarAbilityInfo = new AbilitySyncStateInfo { IsInited = true },
|
||||||
|
WeaponAbilityInfo = new AbilitySyncStateInfo { IsInited = true },
|
||||||
|
SceneEntityInfo = avatar.AsSceneEntity(_state.CurrentPos, weapon, EquippedReliquaries(avatar))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return notify;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerEnterSceneInfoNotify BuildEnterSceneInfo()
|
||||||
|
{
|
||||||
|
GameAvatar active = _player.ActiveAvatar!;
|
||||||
|
|
||||||
|
PlayerEnterSceneInfoNotify notify = new()
|
||||||
|
{
|
||||||
|
CurAvatarEntityId = active.EntityId,
|
||||||
|
EnterSceneToken = _state.EnterSceneToken,
|
||||||
|
MpLevelEntityInfo = new MPLevelEntityInfo
|
||||||
|
{
|
||||||
|
EntityId = Player.MpLevelEntityId,
|
||||||
|
AuthorityPeerId = Player.PeerId,
|
||||||
|
AbilityInfo = new AbilitySyncStateInfo { IsInited = true }
|
||||||
|
},
|
||||||
|
TeamEnterInfo = new TeamEnterSceneInfo
|
||||||
|
{
|
||||||
|
TeamEntityId = Player.TeamEntityId,
|
||||||
|
TeamAbilityInfo = new AbilitySyncStateInfo { IsInited = true }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (GameAvatar avatar in _player.TeamAvatars)
|
||||||
|
{
|
||||||
|
notify.AvatarEnterInfo.Add(new AvatarEnterSceneInfo
|
||||||
|
{
|
||||||
|
AvatarGuid = avatar.Guid,
|
||||||
|
AvatarEntityId = avatar.EntityId,
|
||||||
|
WeaponGuid = avatar.WeaponGuid,
|
||||||
|
WeaponEntityId = avatar.WeaponEntityId,
|
||||||
|
AvatarAbilityInfo = new AbilitySyncStateInfo { IsInited = true },
|
||||||
|
WeaponAbilityInfo = new AbilitySyncStateInfo { IsInited = true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return notify;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask SpawnActiveAvatarAsync(VisionType visionType)
|
||||||
|
{
|
||||||
|
GameAvatar active = _player.ActiveAvatar!;
|
||||||
|
GameWeapon weapon = _player.FindWeapon(active.WeaponGuid)!;
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.SceneEntityAppearNotify, new SceneEntityAppearNotify
|
||||||
|
{
|
||||||
|
AppearType = visionType,
|
||||||
|
EntityList = { active.AsSceneEntity(_state.CurrentPos, weapon, EquippedReliquaries(active)) }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DespawnMonsterAsync(GameMonster monster)
|
||||||
|
{
|
||||||
|
_player.Monsters.Remove(monster);
|
||||||
|
|
||||||
|
SceneEntityDisappearNotify notify = new() { DisappearType = VisionType.VisionRemove };
|
||||||
|
notify.EntityList.Add(monster.EntityId);
|
||||||
|
await _session.NotifyAsync(CmdType.SceneEntityDisappearNotify, notify);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask AppearMonsterAsync(GameMonster monster)
|
||||||
|
{
|
||||||
|
SceneEntityAppearNotify notify = new() { AppearType = VisionType.VisionBorn };
|
||||||
|
notify.EntityList.Add(monster.AsSceneEntity());
|
||||||
|
await _session.NotifyAsync(CmdType.SceneEntityAppearNotify, notify);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask SyncMonsterLevelAsync(GameMonster monster)
|
||||||
|
{
|
||||||
|
await _session.NotifyAsync(CmdType.EntityPropNotify, new EntityPropNotify
|
||||||
|
{
|
||||||
|
EntityId = monster.EntityId,
|
||||||
|
PropMap = { { 4001u, new PropValue { Type = 4001, Ival = monster.Level, Val = monster.Level } } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask SyncPendingMonsterLevelsAsync()
|
||||||
|
{
|
||||||
|
foreach (GameMonster monster in _player.Monsters.Where(m => m.IsAlive && m.LevelSyncCount < LevelSyncAttempts).ToList())
|
||||||
|
{
|
||||||
|
monster.LevelSyncCount++;
|
||||||
|
await SyncMonsterLevelAsync(monster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask ClearMonstersAsync()
|
||||||
|
{
|
||||||
|
if (_player.Monsters.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SceneEntityDisappearNotify notify = new() { DisappearType = VisionType.VisionRemove };
|
||||||
|
foreach (GameMonster monster in _player.Monsters)
|
||||||
|
notify.EntityList.Add(monster.EntityId);
|
||||||
|
|
||||||
|
_player.Monsters.Clear();
|
||||||
|
await _session.NotifyAsync(CmdType.SceneEntityDisappearNotify, notify);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask TeleportAsync(Vector pos)
|
||||||
|
{
|
||||||
|
_state.UpdatePosition(pos);
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.PlayerEnterSceneNotify, new PlayerEnterSceneNotify
|
||||||
|
{
|
||||||
|
SceneId = GameState.SceneId,
|
||||||
|
Pos = pos,
|
||||||
|
SceneBeginTime = (ulong)DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||||
|
Type = EnterType.EnterGoto,
|
||||||
|
TargetUid = _player.Uid,
|
||||||
|
EnterSceneToken = _state.NextEnterSceneToken(),
|
||||||
|
WorldLevel = 0,
|
||||||
|
EnterReason = 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask HealTeamAsync()
|
||||||
|
{
|
||||||
|
foreach (GameAvatar avatar in _player.TeamAvatars)
|
||||||
|
await _session.NotifyAsync(CmdType.EntityFightPropUpdateNotify, new EntityFightPropUpdateNotify
|
||||||
|
{
|
||||||
|
EntityId = avatar.EntityId,
|
||||||
|
FightPropMap = { { 1010u, 999999f }, { 2000u, 999999f } }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask RefreshAvatarDataAsync()
|
||||||
|
{
|
||||||
|
AvatarDataNotify data = new()
|
||||||
|
{
|
||||||
|
CurAvatarTeamId = _player.CurrentTeamId,
|
||||||
|
ChooseAvatarGuid = _player.CurrentAvatarGuid
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (uint flycloak in Player.Flycloaks)
|
||||||
|
data.OwnedFlycloakList.Add(flycloak);
|
||||||
|
|
||||||
|
foreach (GameAvatar avatar in _player.Avatars)
|
||||||
|
data.AvatarList.Add(avatar.AsAvatarInfo());
|
||||||
|
|
||||||
|
AvatarTeam team = new() { TeamName = "Team " + _player.CurrentTeamId };
|
||||||
|
team.AvatarGuidList.AddRange(_player.TeamAvatarGuids);
|
||||||
|
data.AvatarTeamMap.Add(_player.CurrentTeamId, team);
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.AvatarDataNotify, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask RefreshTeamAsync()
|
||||||
|
=> await _session.NotifyAsync(CmdType.SceneTeamUpdateNotify, BuildSceneTeamUpdate());
|
||||||
|
|
||||||
|
public async ValueTask SetupTeamAsync(uint teamId, IReadOnlyList<ulong> guids, ulong curAvatarGuid)
|
||||||
|
{
|
||||||
|
ulong previousActiveGuid = _player.CurrentAvatarGuid;
|
||||||
|
|
||||||
|
_player.CurrentTeamId = teamId;
|
||||||
|
_player.TeamAvatarGuids.Clear();
|
||||||
|
_player.TeamAvatarGuids.AddRange(guids);
|
||||||
|
_player.CurrentAvatarGuid = curAvatarGuid != 0 ? curAvatarGuid : guids[0];
|
||||||
|
|
||||||
|
AvatarTeam team = new() { TeamName = "Team " + teamId };
|
||||||
|
team.AvatarGuidList.AddRange(guids);
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.AvatarTeamUpdateNotify, new AvatarTeamUpdateNotify
|
||||||
|
{
|
||||||
|
AvatarTeamMap = { { teamId, team } }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (previousActiveGuid != _player.CurrentAvatarGuid)
|
||||||
|
await DisappearAsync(previousActiveGuid, VisionType.VisionReplace);
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.SceneTeamUpdateNotify, BuildSceneTeamUpdate());
|
||||||
|
|
||||||
|
if (previousActiveGuid != _player.CurrentAvatarGuid)
|
||||||
|
await SpawnActiveAvatarAsync(VisionType.VisionReplace);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask ChangeAvatarAsync(ulong newGuid)
|
||||||
|
{
|
||||||
|
if (_player.CurrentAvatarGuid == newGuid)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await DisappearAsync(_player.CurrentAvatarGuid, VisionType.VisionReplace);
|
||||||
|
_player.CurrentAvatarGuid = newGuid;
|
||||||
|
await SpawnActiveAvatarAsync(VisionType.VisionReplace);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<GameReliquary> EquippedReliquaries(GameAvatar avatar)
|
||||||
|
=> avatar.ReliquaryGuids.Values.Select(_player.FindReliquary).OfType<GameReliquary>();
|
||||||
|
|
||||||
|
private async ValueTask DisappearAsync(ulong avatarGuid, VisionType visionType)
|
||||||
|
{
|
||||||
|
GameAvatar? avatar = _player.FindAvatar(avatarGuid);
|
||||||
|
if (avatar is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _session.NotifyAsync(CmdType.SceneEntityDisappearNotify, new SceneEntityDisappearNotify
|
||||||
|
{
|
||||||
|
DisappearType = visionType,
|
||||||
|
EntityList = { avatar.EntityId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
47
Yuanshen.Gameserver/Game/ScenePointTable.cs
Normal file
47
Yuanshen.Gameserver/Game/ScenePointTable.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed class ScenePointTable
|
||||||
|
{
|
||||||
|
private readonly Dictionary<uint, Dictionary<uint, Vector>> _points = new();
|
||||||
|
|
||||||
|
public ScenePointTable()
|
||||||
|
{
|
||||||
|
Load(3, "scene3_points.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGet(uint sceneId, uint pointId, out Vector position)
|
||||||
|
{
|
||||||
|
position = new Vector();
|
||||||
|
return _points.TryGetValue(sceneId, out Dictionary<uint, Vector>? points)
|
||||||
|
&& points.TryGetValue(pointId, out position!);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Load(uint sceneId, string fileName)
|
||||||
|
{
|
||||||
|
string path = Path.Combine(AppContext.BaseDirectory, "assets", "scene", fileName);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
return;
|
||||||
|
|
||||||
|
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(path));
|
||||||
|
Dictionary<uint, Vector> points = new();
|
||||||
|
|
||||||
|
foreach (JsonProperty property in document.RootElement.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (!uint.TryParse(property.Name, out uint pointId))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
JsonElement value = property.Value;
|
||||||
|
points[pointId] = new Vector
|
||||||
|
{
|
||||||
|
X = value.GetProperty("x").GetSingle(),
|
||||||
|
Y = value.GetProperty("y").GetSingle(),
|
||||||
|
Z = value.GetProperty("z").GetSingle()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_points[sceneId] = points;
|
||||||
|
}
|
||||||
|
}
|
||||||
93
Yuanshen.Gameserver/Game/WorldSpawner.cs
Normal file
93
Yuanshen.Gameserver/Game/WorldSpawner.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Game;
|
||||||
|
|
||||||
|
internal sealed class WorldSpawner
|
||||||
|
{
|
||||||
|
private const float SpawnRadiusSq = 120f * 120f;
|
||||||
|
private const float DespawnRadiusSq = 150f * 150f;
|
||||||
|
private const float StreamStepSq = 20f * 20f;
|
||||||
|
private const int ConcurrentCap = 25;
|
||||||
|
private const int WildlifeCap = 6;
|
||||||
|
|
||||||
|
private readonly Player _player;
|
||||||
|
private readonly SceneManager _scene;
|
||||||
|
private readonly MonsterSpawnTable _table;
|
||||||
|
|
||||||
|
private Vector? _lastStream;
|
||||||
|
|
||||||
|
public WorldSpawner(Player player, SceneManager scene, MonsterSpawnTable table)
|
||||||
|
{
|
||||||
|
_player = player;
|
||||||
|
_scene = scene;
|
||||||
|
_table = table;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask EnterSceneAsync(Vector pos)
|
||||||
|
{
|
||||||
|
_player.Monsters.Clear();
|
||||||
|
_lastStream = null;
|
||||||
|
await StreamAsync(pos, force: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask StreamAsync(Vector pos)
|
||||||
|
{
|
||||||
|
await StreamAsync(pos, force: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask RespawnAsync(Vector pos)
|
||||||
|
{
|
||||||
|
_player.KilledNodeIds.Clear();
|
||||||
|
await EnterSceneAsync(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask StreamAsync(Vector pos, bool force)
|
||||||
|
{
|
||||||
|
if (!force && _lastStream is { } last && HorizontalSq(pos, last) < StreamStepSq)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_lastStream = pos;
|
||||||
|
|
||||||
|
HashSet<uint> active = new();
|
||||||
|
foreach (GameMonster monster in _player.Monsters)
|
||||||
|
if (monster.NodeId != 0)
|
||||||
|
active.Add(monster.NodeId);
|
||||||
|
|
||||||
|
foreach (GameMonster monster in _player.Monsters
|
||||||
|
.Where(monster => monster.NodeId != 0 && HorizontalSq(pos, monster.Pos) > DespawnRadiusSq)
|
||||||
|
.ToList())
|
||||||
|
{
|
||||||
|
active.Remove(monster.NodeId);
|
||||||
|
await _scene.DespawnMonsterAsync(monster);
|
||||||
|
}
|
||||||
|
|
||||||
|
int wildlife = _player.Monsters.Count(monster => monster.IsWildlife);
|
||||||
|
|
||||||
|
foreach (SpawnNode node in _table.Nodes.OrderBy(node => node.IsWildlife))
|
||||||
|
{
|
||||||
|
if (active.Count >= ConcurrentCap)
|
||||||
|
break;
|
||||||
|
if (active.Contains(node.NodeId) || _player.KilledNodeIds.Contains(node.NodeId))
|
||||||
|
continue;
|
||||||
|
if (HorizontalSq(pos, node.Pos) > SpawnRadiusSq)
|
||||||
|
continue;
|
||||||
|
if (node.IsWildlife && wildlife >= WildlifeCap)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (node.IsWildlife)
|
||||||
|
wildlife++;
|
||||||
|
|
||||||
|
GameMonster monster = _player.SpawnMonster(node.MonsterId, node.Pos, node.Level, node.Hp, node.Attack, node.Defense, node.NodeId, node.ConfigId, node.GroupId);
|
||||||
|
|
||||||
|
active.Add(node.NodeId);
|
||||||
|
await _scene.AppearMonsterAsync(monster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float HorizontalSq(Vector a, Vector b)
|
||||||
|
{
|
||||||
|
float dx = a.X - b.X;
|
||||||
|
float dz = a.Z - b.Z;
|
||||||
|
return dx * dx + dz * dz;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Yuanshen.Gameserver/GameServer.cs
Normal file
10
Yuanshen.Gameserver/GameServer.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Yuanshen.Gameserver.Network;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver;
|
||||||
|
|
||||||
|
internal sealed class GameServer(IGateway gateway) : IHostedService
|
||||||
|
{
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => gateway.Start();
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => gateway.Stop();
|
||||||
|
}
|
||||||
6
Yuanshen.Gameserver/Network/IGateway.cs
Normal file
6
Yuanshen.Gameserver/Network/IGateway.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Yuanshen.Gameserver.Network;
|
||||||
|
internal interface IGateway
|
||||||
|
{
|
||||||
|
Task Start();
|
||||||
|
Task Stop();
|
||||||
|
}
|
||||||
10
Yuanshen.Gameserver/Network/INetworkUnit.cs
Normal file
10
Yuanshen.Gameserver/Network/INetworkUnit.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Network;
|
||||||
|
internal interface INetworkUnit : IDisposable
|
||||||
|
{
|
||||||
|
IPEndPoint RemoteEndPoint { get; }
|
||||||
|
|
||||||
|
ValueTask<int> ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken);
|
||||||
|
ValueTask SendAsync(Memory<byte> buffer, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
105
Yuanshen.Gameserver/Network/Kcp/KcpGateway.cs
Normal file
105
Yuanshen.Gameserver/Network/Kcp/KcpGateway.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using Yuanshen.Gameserver.Options;
|
||||||
|
using Yuanshen.Kcp;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Network.Kcp;
|
||||||
|
internal sealed class KcpGateway : IGateway
|
||||||
|
{
|
||||||
|
private readonly Random _random;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly IOptions<GatewayOptions> _options;
|
||||||
|
private readonly NetSessionManager _sessionManager;
|
||||||
|
|
||||||
|
private uint _sessionCounter;
|
||||||
|
private IKcpTransport<IKcpMultiplexConnection>? _transport;
|
||||||
|
|
||||||
|
public KcpGateway(ILogger<KcpGateway> logger, IOptions<GatewayOptions> options, NetSessionManager sessionManager)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_options = options;
|
||||||
|
_sessionManager = sessionManager;
|
||||||
|
|
||||||
|
_random = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Start()
|
||||||
|
{
|
||||||
|
IPEndPoint bindEndPoint = _options.Value.EndPoint;
|
||||||
|
|
||||||
|
_transport = KcpSocketTransport.CreateMultiplexConnection(new(bindEndPoint), 1400);
|
||||||
|
_transport.SetCallbacks(20, HandleKcpHandshake);
|
||||||
|
_transport.Start();
|
||||||
|
|
||||||
|
_logger.LogInformation("KCP Gateway is up at {endPoint}", bindEndPoint);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask HandleKcpHandshake(UdpReceiveResult receiveResult)
|
||||||
|
{
|
||||||
|
KcpHandshake handshake = KcpHandshake.ReadFrom(receiveResult.Buffer);
|
||||||
|
|
||||||
|
_logger.LogDebug("KCP control datagram from {endPoint}: len={len} head=0x{head:X} tail=0x{tail:X}",
|
||||||
|
receiveResult.RemoteEndPoint, receiveResult.Buffer.Length, handshake.Head, handshake.Tail);
|
||||||
|
|
||||||
|
switch ((handshake.Head, handshake.Tail))
|
||||||
|
{
|
||||||
|
case (KcpHandshake.StartConversationHead, KcpHandshake.StartConversationTail):
|
||||||
|
await OnStartConversationRequest(receiveResult.RemoteEndPoint);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask OnStartConversationRequest(IPEndPoint clientEndPoint)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
uint convId = Interlocked.Increment(ref _sessionCounter);
|
||||||
|
uint token = (uint)_random.Next();
|
||||||
|
|
||||||
|
long convId64 = (long)convId << 32 | token;
|
||||||
|
KcpConversation conversation = _transport!.Connection.CreateConversation(convId64, clientEndPoint);
|
||||||
|
_ = _sessionManager.RunSessionAsync(convId64, new KcpNetworkUnit(conversation, clientEndPoint));
|
||||||
|
|
||||||
|
await SendConversationCreatedPacket(clientEndPoint, convId, token);
|
||||||
|
_logger.LogInformation("StartConversation accepted from {endPoint}, conv={conv}", clientEndPoint, convId64);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogError("StartConversation from {endPoint} failed: {exception}", clientEndPoint, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask SendConversationCreatedPacket(IPEndPoint clientEndPoint, uint convId, uint token)
|
||||||
|
{
|
||||||
|
KcpHandshake handshakeResponse = new()
|
||||||
|
{
|
||||||
|
Head = KcpHandshake.ConversationCreatedHead,
|
||||||
|
Param1 = convId,
|
||||||
|
Param2 = token,
|
||||||
|
Data = 1234567890,
|
||||||
|
Tail = KcpHandshake.ConversationCreatedTail
|
||||||
|
};
|
||||||
|
|
||||||
|
byte[] buffer = ArrayPool<byte>.Shared.Rent(20);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Memory<byte> bufferMemory = buffer.AsMemory();
|
||||||
|
|
||||||
|
handshakeResponse.WriteTo(buffer);
|
||||||
|
await _transport!.SendPacketAsync(bufferMemory[..20], clientEndPoint, CancellationToken.None);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Stop()
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Yuanshen.Gameserver/Network/Kcp/KcpHandshake.cs
Normal file
39
Yuanshen.Gameserver/Network/Kcp/KcpHandshake.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Network.Kcp;
|
||||||
|
|
||||||
|
internal struct KcpHandshake
|
||||||
|
{
|
||||||
|
public const uint StartConversationHead = 0xFF;
|
||||||
|
public const uint StartConversationTail = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
public const uint ConversationCreatedHead = 0x00000145;
|
||||||
|
public const uint ConversationCreatedTail = 0x14514545;
|
||||||
|
|
||||||
|
public const uint ConversationEndHead = 0x194;
|
||||||
|
public const uint ConversationEndTail = 0x19419494;
|
||||||
|
|
||||||
|
public uint Head { get; set; }
|
||||||
|
public uint Param1 { get; set; }
|
||||||
|
public uint Param2 { get; set; }
|
||||||
|
public uint Data { get; set; }
|
||||||
|
public uint Tail { get; set; }
|
||||||
|
|
||||||
|
public readonly void WriteTo(Span<byte> buffer)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer[0..4], Head);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer[4..8], Param1);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer[8..12], Param2);
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer[12..16], Data);
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer[16..20], Tail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KcpHandshake ReadFrom(ReadOnlySpan<byte> buffer) => new()
|
||||||
|
{
|
||||||
|
Head = BinaryPrimitives.ReadUInt32BigEndian(buffer[0..4]),
|
||||||
|
Param1 = BinaryPrimitives.ReadUInt32LittleEndian(buffer[4..8]),
|
||||||
|
Param2 = BinaryPrimitives.ReadUInt32LittleEndian(buffer[8..12]),
|
||||||
|
Data = BinaryPrimitives.ReadUInt32BigEndian(buffer[12..16]),
|
||||||
|
Tail = BinaryPrimitives.ReadUInt32BigEndian(buffer[16..20])
|
||||||
|
};
|
||||||
|
}
|
||||||
35
Yuanshen.Gameserver/Network/Kcp/KcpNetworkUnit.cs
Normal file
35
Yuanshen.Gameserver/Network/Kcp/KcpNetworkUnit.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
using System.Net;
|
||||||
|
using Yuanshen.Kcp;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Network.Kcp;
|
||||||
|
internal class KcpNetworkUnit : INetworkUnit
|
||||||
|
{
|
||||||
|
public IPEndPoint RemoteEndPoint { get; }
|
||||||
|
|
||||||
|
private readonly KcpConversation _conversation;
|
||||||
|
|
||||||
|
public KcpNetworkUnit(KcpConversation conversation, IPEndPoint remoteEndPoint)
|
||||||
|
{
|
||||||
|
_conversation = conversation;
|
||||||
|
RemoteEndPoint = remoteEndPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<int> ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
KcpConversationReceiveResult result = await _conversation.ReceiveAsync(buffer, cancellationToken);
|
||||||
|
if (result.TransportClosed)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
return result.BytesReceived;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask SendAsync(Memory<byte> buffer, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _conversation.SendAsync(buffer, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_conversation.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
48
Yuanshen.Gameserver/Network/Kcp/KcpSession.cs
Normal file
48
Yuanshen.Gameserver/Network/Kcp/KcpSession.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
using Yuanshen.Common.Security;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Dispatching;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Network.Kcp;
|
||||||
|
internal class KcpSession : NetSession
|
||||||
|
{
|
||||||
|
private const int MaxPacketSize = 32768;
|
||||||
|
private const int ReadTimeout = 30;
|
||||||
|
private const int WriteTimeout = 30;
|
||||||
|
|
||||||
|
private readonly byte[] _recvBuffer;
|
||||||
|
private readonly byte[] _sendBuffer;
|
||||||
|
|
||||||
|
public KcpSession(ILogger<NetSession> logger, NetSessionManager sessionManager, NetCommandDispatcher commandDispatcher) : base(logger, sessionManager, commandDispatcher)
|
||||||
|
{
|
||||||
|
_recvBuffer = GC.AllocateUninitializedArray<byte>(MaxPacketSize);
|
||||||
|
_sendBuffer = GC.AllocateUninitializedArray<byte>(MaxPacketSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask RunAsync()
|
||||||
|
{
|
||||||
|
Memory<byte> buffer = _recvBuffer.AsMemory();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int readAmount = await ReadWithTimeoutAsync(buffer, ReadTimeout);
|
||||||
|
if (readAmount <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
MhySecurity.Xor(buffer[..readAmount].Span, EncryptionKey);
|
||||||
|
int consumedBytes = await ConsumePacketsAsync(buffer[..readAmount]);
|
||||||
|
if (consumedBytes == -1)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask SendAsync(NetPacket packet)
|
||||||
|
{
|
||||||
|
Memory<byte> buffer = _sendBuffer.AsMemory();
|
||||||
|
|
||||||
|
int length = packet.EncodeTo(buffer);
|
||||||
|
MhySecurity.Xor(buffer[..length].Span, EncryptionKey);
|
||||||
|
|
||||||
|
await WriteWithTimeoutAsync(buffer[..length], WriteTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
Yuanshen.Gameserver/Network/NetPacket.cs
Normal file
67
Yuanshen.Gameserver/Network/NetPacket.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
using Google.Protobuf;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Network;
|
||||||
|
internal class NetPacket
|
||||||
|
{
|
||||||
|
private const ushort HeadMagic = 0x4567;
|
||||||
|
private const ushort TailMagic = 0x89AB;
|
||||||
|
|
||||||
|
public CmdType CmdType { get; set; }
|
||||||
|
public Memory<byte> Head { get; set; }
|
||||||
|
public Memory<byte> Body { get; set; }
|
||||||
|
|
||||||
|
public TBody DecodeBody<TBody>() where TBody : IMessage<TBody>, new()
|
||||||
|
{
|
||||||
|
return new MessageParser<TBody>(() => new()).ParseFrom(Body.Span);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int EncodeTo(Memory<byte> buffer)
|
||||||
|
{
|
||||||
|
Span<byte> span = buffer.Span;
|
||||||
|
|
||||||
|
BinaryPrimitives.WriteUInt16BigEndian(span[0..2], HeadMagic);
|
||||||
|
BinaryPrimitives.WriteUInt16BigEndian(span[2..4], (ushort)CmdType);
|
||||||
|
BinaryPrimitives.WriteUInt16BigEndian(span[4..6], (ushort)Head.Length);
|
||||||
|
BinaryPrimitives.WriteInt32BigEndian(span[6..10], Body.Length);
|
||||||
|
Head.CopyTo(buffer[10..]);
|
||||||
|
Body.CopyTo(buffer[(10 + Head.Length)..]);
|
||||||
|
BinaryPrimitives.WriteUInt16BigEndian(span[(10 + Head.Length + Body.Length)..], TailMagic);
|
||||||
|
|
||||||
|
return 12 + Head.Length + Body.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (NetPacket?, int) DecodeFrom(Memory<byte> data)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<byte> span = data.Span;
|
||||||
|
|
||||||
|
ushort headMagic = BinaryPrimitives.ReadUInt16BigEndian(span[0..2]);
|
||||||
|
if (headMagic != HeadMagic)
|
||||||
|
return (null, 0);
|
||||||
|
|
||||||
|
ushort cmdType = BinaryPrimitives.ReadUInt16BigEndian(span[2..4]);
|
||||||
|
|
||||||
|
int headLength = BinaryPrimitives.ReadUInt16BigEndian(span[4..6]);
|
||||||
|
int bodyLength = BinaryPrimitives.ReadInt32BigEndian(span[6..10]);
|
||||||
|
|
||||||
|
if (data.Length < 12 + headLength + bodyLength)
|
||||||
|
return (null, 0);
|
||||||
|
|
||||||
|
Memory<byte> head = data.Slice(10, headLength);
|
||||||
|
Memory<byte> body = data.Slice(10 + headLength, bodyLength);
|
||||||
|
|
||||||
|
ushort tailMagic = BinaryPrimitives.ReadUInt16BigEndian(span[(10 + headLength + bodyLength)..]);
|
||||||
|
if (tailMagic != TailMagic)
|
||||||
|
return (null, 0);
|
||||||
|
|
||||||
|
NetPacket netPacket = new()
|
||||||
|
{
|
||||||
|
CmdType = (CmdType)cmdType,
|
||||||
|
Head = head,
|
||||||
|
Body = body
|
||||||
|
};
|
||||||
|
|
||||||
|
return (netPacket, 12 + headLength + bodyLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
100
Yuanshen.Gameserver/Network/NetSessionManager.cs
Normal file
100
Yuanshen.Gameserver/Network/NetSessionManager.cs
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Yuanshen.Gameserver.Database;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Network;
|
||||||
|
internal class NetSessionManager
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan AutoSaveInterval = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<long, NetSession> _sessions;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
|
private readonly Yuanshen.Gameserver.Game.ConsoleBridge _consoleBridge;
|
||||||
|
|
||||||
|
public NetSessionManager(ILogger<NetSessionManager> logger, IServiceScopeFactory serviceScopeFactory, Yuanshen.Gameserver.Game.ConsoleBridge consoleBridge)
|
||||||
|
{
|
||||||
|
_sessions = new();
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
|
_consoleBridge = consoleBridge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RunSessionAsync(long sessionId, INetworkUnit networkUnit)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using AsyncServiceScope serviceScope = _serviceScopeFactory.CreateAsyncScope();
|
||||||
|
NetSession session = serviceScope.ServiceProvider.GetRequiredService<NetSession>();
|
||||||
|
_consoleBridge.Attach(serviceScope.ServiceProvider);
|
||||||
|
PlayerPersistence persistence = serviceScope.ServiceProvider.GetRequiredService<PlayerPersistence>();
|
||||||
|
persistence.Bind(sessionId);
|
||||||
|
|
||||||
|
using CancellationTokenSource sessionEnded = new();
|
||||||
|
Task autoSave = RunAutoSaveAsync(persistence, sessionEnded.Token);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
session.Establish(sessionId, networkUnit);
|
||||||
|
await session.RunAsync();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogError("Exception occurred during handling a session, trace: {exception}", exception);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await sessionEnded.CancelAsync();
|
||||||
|
await autoSave;
|
||||||
|
|
||||||
|
if (await persistence.SaveAsync())
|
||||||
|
_logger.LogInformation("Player {uid} state persisted on disconnect", persistence.PlayerUid);
|
||||||
|
|
||||||
|
persistence.Release();
|
||||||
|
_consoleBridge.Detach(serviceScope.ServiceProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogError("Failed to start session {sessionId}: {exception}", sessionId, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task RunAutoSaveAsync(PlayerPersistence persistence, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using PeriodicTimer timer = new(AutoSaveInterval);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (await timer.WaitForNextTickAsync(cancellationToken))
|
||||||
|
await persistence.SaveAsync();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(NetSession session)
|
||||||
|
{
|
||||||
|
_sessions[session.SessionId] = session;
|
||||||
|
_logger.LogInformation("New connection from {endPoint}", session.EndPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRemove(NetSession session)
|
||||||
|
{
|
||||||
|
bool removed = _sessions.TryRemove(session.SessionId, out _);
|
||||||
|
if (removed)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Client from {endPoint} disconnected", session.EndPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
Yuanshen.Gameserver/Network/Session/NetSession.cs
Normal file
114
Yuanshen.Gameserver/Network/Session/NetSession.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
using System.Net;
|
||||||
|
using Yuanshen.Common.Security;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Dispatching;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Result;
|
||||||
|
using Yuanshen.Protocol;
|
||||||
|
using Google.Protobuf;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Network.Session;
|
||||||
|
internal abstract class NetSession : IDisposable
|
||||||
|
{
|
||||||
|
public IPEndPoint EndPoint => _networkUnit!.RemoteEndPoint;
|
||||||
|
|
||||||
|
public long SessionId { get; private set; }
|
||||||
|
private INetworkUnit? _networkUnit;
|
||||||
|
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly NetSessionManager _sessionManager;
|
||||||
|
private readonly NetCommandDispatcher _commandDispatcher;
|
||||||
|
|
||||||
|
protected byte[] EncryptionKey { get; private set; }
|
||||||
|
|
||||||
|
public NetSession(ILogger<NetSession> logger, NetSessionManager sessionManager, NetCommandDispatcher commandDispatcher)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_sessionManager = sessionManager;
|
||||||
|
_commandDispatcher = commandDispatcher;
|
||||||
|
|
||||||
|
EncryptionKey = MhySecurity.InitialKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract ValueTask RunAsync();
|
||||||
|
public abstract ValueTask SendAsync(NetPacket packet);
|
||||||
|
|
||||||
|
public void Establish(long sessionId, INetworkUnit networkUnit)
|
||||||
|
{
|
||||||
|
SessionId = sessionId;
|
||||||
|
_networkUnit = networkUnit;
|
||||||
|
|
||||||
|
_sessionManager.Add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task NotifyAsync<TNotify>(CmdType cmdType, TNotify notify) where TNotify : IMessage<TNotify>
|
||||||
|
{
|
||||||
|
await SendAsync(new()
|
||||||
|
{
|
||||||
|
CmdType = cmdType,
|
||||||
|
Head = Memory<byte>.Empty,
|
||||||
|
Body = notify.ToByteArray()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async ValueTask<int> ConsumePacketsAsync(Memory<byte> buffer)
|
||||||
|
{
|
||||||
|
if (buffer.Length < 12)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int consumed = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
(NetPacket? packet, int bytesConsumed) = NetPacket.DecodeFrom(buffer[consumed..]);
|
||||||
|
consumed += bytesConsumed;
|
||||||
|
|
||||||
|
if (packet == null)
|
||||||
|
return consumed;
|
||||||
|
|
||||||
|
IResult? result = await _commandDispatcher.InvokeHandler(packet);
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
while (result.NextPacket(out NetPacket? serverPacket))
|
||||||
|
{
|
||||||
|
await SendAsync(serverPacket);
|
||||||
|
|
||||||
|
if (serverPacket.CmdType == CmdType.GetPlayerTokenRsp)
|
||||||
|
{
|
||||||
|
InitializeEncryption(1337);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (_unhandled.Add(packet.CmdType))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("No handler for command {cmdType} ({cmdId})", packet.CmdType, (int)packet.CmdType);
|
||||||
|
}
|
||||||
|
} while (buffer.Length - consumed >= 12);
|
||||||
|
|
||||||
|
return consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly HashSet<CmdType> _unhandled = new();
|
||||||
|
|
||||||
|
private void InitializeEncryption(ulong seed)
|
||||||
|
{
|
||||||
|
EncryptionKey = MhySecurity.GenerateSecretKey(seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async ValueTask<int> ReadWithTimeoutAsync(Memory<byte> buffer, int timeoutSeconds)
|
||||||
|
{
|
||||||
|
using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromSeconds(timeoutSeconds));
|
||||||
|
return await _networkUnit!.ReceiveAsync(buffer, cancellationTokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async ValueTask WriteWithTimeoutAsync(Memory<byte> buffer, int timeoutSeconds)
|
||||||
|
{
|
||||||
|
using CancellationTokenSource cancellationTokenSource = new(TimeSpan.FromSeconds(timeoutSeconds));
|
||||||
|
await _networkUnit!.SendAsync(buffer, cancellationTokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Dispose()
|
||||||
|
{
|
||||||
|
_networkUnit?.Dispose();
|
||||||
|
_ = _sessionManager.TryRemove(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Yuanshen.Gameserver/Options/DatabaseOptions.cs
Normal file
8
Yuanshen.Gameserver/Options/DatabaseOptions.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace Yuanshen.Gameserver.Options;
|
||||||
|
|
||||||
|
internal record DatabaseOptions
|
||||||
|
{
|
||||||
|
public const string Section = "Database";
|
||||||
|
|
||||||
|
public required string ConnectionString { get; set; }
|
||||||
|
}
|
||||||
13
Yuanshen.Gameserver/Options/GatewayOptions.cs
Normal file
13
Yuanshen.Gameserver/Options/GatewayOptions.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace Yuanshen.Gameserver.Options;
|
||||||
|
|
||||||
|
internal record GatewayOptions
|
||||||
|
{
|
||||||
|
public const string Section = "Gateway";
|
||||||
|
|
||||||
|
public required string Host { get; set; }
|
||||||
|
public required int Port { get; set; }
|
||||||
|
|
||||||
|
public IPEndPoint EndPoint => new(IPAddress.Parse(Host), Port);
|
||||||
|
}
|
||||||
75
Yuanshen.Gameserver/Program.cs
Normal file
75
Yuanshen.Gameserver/Program.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Yuanshen.Gameserver;
|
||||||
|
using Yuanshen.Gameserver.Controllers.Dispatching;
|
||||||
|
using Yuanshen.Gameserver.Game;
|
||||||
|
using Yuanshen.Gameserver.Network;
|
||||||
|
using Yuanshen.Gameserver.Network.Kcp;
|
||||||
|
using Yuanshen.Gameserver.Network.Session;
|
||||||
|
using Yuanshen.Gameserver.Options;
|
||||||
|
using Yuanshen.Common.Console;
|
||||||
|
using Yuanshen.Common.Data.Provider;
|
||||||
|
|
||||||
|
Console.Title = "Yuanshen | Game Server";
|
||||||
|
Directory.SetCurrentDirectory(AppContext.BaseDirectory);
|
||||||
|
|
||||||
|
ConsoleBanner.Print("GAME SERVER", new[]
|
||||||
|
{
|
||||||
|
("listen", "0.0.0.0:22102"),
|
||||||
|
("protocol", "kcp / 0.9.10"),
|
||||||
|
("console", "type a GM command, ? for help")
|
||||||
|
});
|
||||||
|
|
||||||
|
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
|
||||||
|
builder.Logging.ClearProviders();
|
||||||
|
builder.Logging.AddProvider(new ThemedLoggerProvider());
|
||||||
|
|
||||||
|
builder.Services.Configure<GatewayOptions>(builder.Configuration.GetSection(GatewayOptions.Section));
|
||||||
|
builder.Services.Configure<DatabaseOptions>(builder.Configuration.GetSection(DatabaseOptions.Section));
|
||||||
|
|
||||||
|
builder.Services.UseLocalAssets();
|
||||||
|
|
||||||
|
builder.Services.AddScoped<NetCommandDispatcher>();
|
||||||
|
builder.Services.AddScoped<NetSession, KcpSession>();
|
||||||
|
builder.Services.AddScoped<GameState>();
|
||||||
|
builder.Services.AddScoped<Player>();
|
||||||
|
builder.Services.AddScoped<SceneManager>();
|
||||||
|
builder.Services.AddScoped<CombatManager>();
|
||||||
|
builder.Services.AddScoped<GmCommandService>();
|
||||||
|
builder.Services.AddSingleton<MonsterSpawnTable>();
|
||||||
|
builder.Services.AddSingleton<DropTable>();
|
||||||
|
builder.Services.AddScoped<WorldSpawner>();
|
||||||
|
builder.Services.AddScoped<Yuanshen.Gameserver.Database.PlayerPersistence>();
|
||||||
|
builder.Services.AddSingleton<Yuanshen.Gameserver.Database.PlayerSessionRegistry>();
|
||||||
|
builder.Services.AddSingleton<Yuanshen.Gameserver.Database.IPlayerRepository, Yuanshen.Gameserver.Database.PostgresPlayerRepository>();
|
||||||
|
builder.Services.AddSingleton<Yuanshen.Gameserver.Database.IAccountRepository, Yuanshen.Gameserver.Database.PostgresAccountRepository>();
|
||||||
|
builder.Services.AddSingleton<IGateway, KcpGateway>();
|
||||||
|
builder.Services.AddSingleton<ScenePointTable>();
|
||||||
|
builder.Services.AddSingleton<OpenStateTable>();
|
||||||
|
builder.Services.AddSingleton<GachaBannerTable>();
|
||||||
|
builder.Services.AddSingleton<ConsoleBridge>();
|
||||||
|
builder.Services.AddSingleton<NetSessionManager>();
|
||||||
|
|
||||||
|
builder.Services.AddHostedService<GameServer>();
|
||||||
|
|
||||||
|
IHost host = builder.Build();
|
||||||
|
ConsoleBridge bridge = host.Services.GetRequiredService<ConsoleBridge>();
|
||||||
|
|
||||||
|
ConsolePrompt.Start("yuanshen >", async command =>
|
||||||
|
{
|
||||||
|
IServiceProvider? session = bridge.Session;
|
||||||
|
|
||||||
|
if (session is null)
|
||||||
|
{
|
||||||
|
ConsolePrompt.Warn("no player is connected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GmCommandService gm = session.GetRequiredService<GmCommandService>();
|
||||||
|
|
||||||
|
foreach (string line in await gm.ExecuteAsync(command))
|
||||||
|
ConsolePrompt.Reply(line);
|
||||||
|
});
|
||||||
|
|
||||||
|
await host.RunAsync();
|
||||||
39
Yuanshen.Gameserver/Yuanshen.Gameserver.csproj
Normal file
39
Yuanshen.Gameserver/Yuanshen.Gameserver.csproj
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
||||||
|
<PackageReference Include="Npgsql" Version="10.0.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Yuanshen.Proto\Yuanshen.Proto.csproj" />
|
||||||
|
<ProjectReference Include="..\Yuanshen.Kcp\Yuanshen.Kcp.csproj" />
|
||||||
|
<ProjectReference Include="..\Yuanshen.Common\Yuanshen.Common.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="appsettings.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="assets\excel\*.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="assets\gacha\*.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="assets\drops\*.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="assets\scene\*.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
9
Yuanshen.Gameserver/appsettings.json
Normal file
9
Yuanshen.Gameserver/appsettings.json
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Gateway": {
|
||||||
|
"Host": "0.0.0.0",
|
||||||
|
"Port": 22102
|
||||||
|
},
|
||||||
|
"Database": {
|
||||||
|
"ConnectionString": "Host=localhost;Port=5432;Database=yuanshen;Username=postgres;Password=postgres;Timeout=5"
|
||||||
|
}
|
||||||
|
}
|
||||||
830
Yuanshen.Gameserver/assets/commands.json
Normal file
830
Yuanshen.Gameserver/assets/commands.json
Normal file
|
|
@ -0,0 +1,830 @@
|
||||||
|
{
|
||||||
|
"播放cutscene": {
|
||||||
|
"登陆女神岛": [
|
||||||
|
"quest clear all",
|
||||||
|
"quest accept 35204",
|
||||||
|
"quest finish 35204",
|
||||||
|
"goto 2560 216 -1334"
|
||||||
|
],
|
||||||
|
"森林遇龙": [
|
||||||
|
"quest clear all",
|
||||||
|
"quest accept 35401",
|
||||||
|
"quest finish 35401",
|
||||||
|
"goto 2365 206 -1140"
|
||||||
|
],
|
||||||
|
"安柏入队": [
|
||||||
|
"quest clear all",
|
||||||
|
"quest finish father 351 362 352",
|
||||||
|
"quest accept 35307",
|
||||||
|
"quest finish 35307",
|
||||||
|
"goto 2300 209 -1135"
|
||||||
|
],
|
||||||
|
"初入蒙德": [
|
||||||
|
"quest clear all",
|
||||||
|
"quest accept 36005",
|
||||||
|
"quest finish 36005",
|
||||||
|
"goto 2225 210 -944"
|
||||||
|
],
|
||||||
|
"风龙来袭": [
|
||||||
|
"quest clear all",
|
||||||
|
"quest finish father 351 352 353 355 354 360 ",
|
||||||
|
"quest accept 35606",
|
||||||
|
"quest finish 35606",
|
||||||
|
"goto 2262 216 -880"
|
||||||
|
],
|
||||||
|
"凯亚登场": [
|
||||||
|
"quest clear all",
|
||||||
|
"quest accept 35722",
|
||||||
|
"quest finish 35722",
|
||||||
|
"goto 2310 250 -767"
|
||||||
|
],
|
||||||
|
"凯亚地城": [
|
||||||
|
"quest clear all",
|
||||||
|
"dungeon 1",
|
||||||
|
"quest accept 30713",
|
||||||
|
"quest finish 30713"
|
||||||
|
],
|
||||||
|
"播片+动画": [
|
||||||
|
"quest clear all",
|
||||||
|
"player level 20",
|
||||||
|
"quest accept 37113",
|
||||||
|
"quest finish 37113",
|
||||||
|
"goto 2350 260 -689"
|
||||||
|
],
|
||||||
|
"地下偷琴": [
|
||||||
|
"quest clear all",
|
||||||
|
"jump 1005",
|
||||||
|
"player level 20",
|
||||||
|
"quest accept 37505"
|
||||||
|
],
|
||||||
|
"温迪逃跑": [
|
||||||
|
"quest clear all",
|
||||||
|
"player level 20",
|
||||||
|
"quest accept 37505",
|
||||||
|
"goto 2350 260 -689"
|
||||||
|
],
|
||||||
|
"摘星之崖": [
|
||||||
|
"quest clear all",
|
||||||
|
"player level 20",
|
||||||
|
"quest accept 38402",
|
||||||
|
"quest finish 38402",
|
||||||
|
"goto 2629 380 -1730"
|
||||||
|
],
|
||||||
|
"开启风障": [
|
||||||
|
"quest clear all",
|
||||||
|
"player level 20",
|
||||||
|
"quest accept 38806",
|
||||||
|
"quest finish 38806",
|
||||||
|
"goto 2360 223 -64"
|
||||||
|
],
|
||||||
|
"温迪告白": [
|
||||||
|
"quest clear all",
|
||||||
|
"dungeon 2002",
|
||||||
|
"player level 20",
|
||||||
|
"quest accept 39403",
|
||||||
|
"quest finish 39403"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"常用指令": {
|
||||||
|
"获得御三家": [
|
||||||
|
"avatar add 10000021",
|
||||||
|
"avatar add 10000015",
|
||||||
|
"avatar add 10000006"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"快速测试方法": {
|
||||||
|
"1阶段(执行后要F12)": [
|
||||||
|
"quest finish father 351 352 353 361 362 355 354 360 356 357 358 306 307 308 309 311",
|
||||||
|
"item add 15201",
|
||||||
|
"item add 14201",
|
||||||
|
"item add 20004",
|
||||||
|
"item add 20002",
|
||||||
|
"item add 104012 100"
|
||||||
|
],
|
||||||
|
"2阶段(执行后要F12)": [
|
||||||
|
"player level 10",
|
||||||
|
"quest finish father 351 352 353 361 362 355 354 360 356 357 358 306 307 308 309 311 370 371 372 373 374 375 376 377 379 380 381 382 383 384",
|
||||||
|
"item add 15201",
|
||||||
|
"item add 14201",
|
||||||
|
"item add 20004",
|
||||||
|
"item add 20002",
|
||||||
|
"item add 104012 200",
|
||||||
|
"item add 59111",
|
||||||
|
"item add 59121",
|
||||||
|
"item add 59131",
|
||||||
|
"item add 59141",
|
||||||
|
"item add 59151",
|
||||||
|
"item add 60111",
|
||||||
|
"item add 60121",
|
||||||
|
"item add 60131",
|
||||||
|
"item add 60141",
|
||||||
|
"item add 60151",
|
||||||
|
"item add 15301",
|
||||||
|
"item add 11302",
|
||||||
|
"item add 12201"
|
||||||
|
],
|
||||||
|
"3阶段(执行后要F12)": [
|
||||||
|
"player level 18",
|
||||||
|
"quest finish father 351 352 353 361 362 355 354 360 356 357 358 306 307 308 309 311 370 371 372 373 374 375 376 377 379 380 381 382 383 384 397 388 389 390 393 394 396",
|
||||||
|
"item add 15201",
|
||||||
|
"item add 14201",
|
||||||
|
"item add 20004",
|
||||||
|
"item add 20002",
|
||||||
|
"item add 59111",
|
||||||
|
"item add 59121",
|
||||||
|
"item add 59131",
|
||||||
|
"item add 59141",
|
||||||
|
"item add 59151",
|
||||||
|
"item add 60111",
|
||||||
|
"item add 60121",
|
||||||
|
"item add 60131",
|
||||||
|
"item add 60141",
|
||||||
|
"item add 60151",
|
||||||
|
"item add 15301",
|
||||||
|
"item add 12201",
|
||||||
|
"item add 11302",
|
||||||
|
"item add 104012 300",
|
||||||
|
"item add 15401",
|
||||||
|
"item add 12301",
|
||||||
|
"item add 14301",
|
||||||
|
"item add 54212",
|
||||||
|
"item add 54222",
|
||||||
|
"item add 54232",
|
||||||
|
"item add 54242",
|
||||||
|
"item add 60212",
|
||||||
|
"item add 60222",
|
||||||
|
"item add 60232",
|
||||||
|
"item add 60242"
|
||||||
|
],
|
||||||
|
"解除蒙德风龙天气": [
|
||||||
|
"quest accept 30904"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"角色成长": {
|
||||||
|
"当前角色13级": [
|
||||||
|
"level 13",
|
||||||
|
"skill 3 1",
|
||||||
|
"Equip Add 11302 13 0",
|
||||||
|
"Equip Add 12302 13 0",
|
||||||
|
"Equip Add 13302 13 0",
|
||||||
|
"Equip Add 14303 13 0",
|
||||||
|
"Equip Add 15302 13 0",
|
||||||
|
"Equip Add 51140 5 1",
|
||||||
|
"Equip Add 51120 5 1"
|
||||||
|
],
|
||||||
|
"当前角色20级": [
|
||||||
|
"level 20",
|
||||||
|
"skill 3 1",
|
||||||
|
"Equip Add 11302 20 0",
|
||||||
|
"Equip Add 12302 20 0",
|
||||||
|
"Equip Add 13302 20 0",
|
||||||
|
"Equip Add 14303 20 0",
|
||||||
|
"Equip Add 15302 20 0",
|
||||||
|
"Equip Add 51151 5 1",
|
||||||
|
"Equip Add 51141 5 1",
|
||||||
|
"Equip Add 51131 5 1",
|
||||||
|
"Equip Add 51121 5 1",
|
||||||
|
"Equip Add 51111 5 1"
|
||||||
|
],
|
||||||
|
"当前角色30级": [
|
||||||
|
"break 1",
|
||||||
|
"level 30",
|
||||||
|
"skill 3 2",
|
||||||
|
"Equip Add 11302 30 1",
|
||||||
|
"Equip Add 12302 30 1",
|
||||||
|
"Equip Add 13302 30 1",
|
||||||
|
"Equip Add 14303 30 1",
|
||||||
|
"Equip Add 15302 30 1",
|
||||||
|
"Equip Add 56252 9 1",
|
||||||
|
"Equip Add 56242 9 1",
|
||||||
|
"Equip Add 56232 9 1",
|
||||||
|
"Equip Add 56222 9 1",
|
||||||
|
"Equip Add 56212 9 1"
|
||||||
|
],
|
||||||
|
"当前角色40级": [
|
||||||
|
"break 1",
|
||||||
|
"level 40",
|
||||||
|
"skill 3 2",
|
||||||
|
"Equip Add 11409 40 1",
|
||||||
|
"Equip Add 12409 40 1",
|
||||||
|
"Equip Add 13405 40 1",
|
||||||
|
"Equip Add 14409 40 1",
|
||||||
|
"Equip Add 15409 40 1",
|
||||||
|
"Equip Add 23486 9 1",
|
||||||
|
"Equip Add 23487 9 1",
|
||||||
|
"Equip Add 23488 9 1",
|
||||||
|
"Equip Add 23489 9 1",
|
||||||
|
"Equip Add 23490 9 1"
|
||||||
|
],
|
||||||
|
"当前角色50级": [
|
||||||
|
"break 2",
|
||||||
|
"level 50",
|
||||||
|
"skill 3 3",
|
||||||
|
"Equip Add 11409 50 2",
|
||||||
|
"Equip Add 12409 50 2",
|
||||||
|
"Equip Add 13405 50 2",
|
||||||
|
"Equip Add 14409 50 2",
|
||||||
|
"Equip Add 15409 50 2",
|
||||||
|
"Equip Add 23486 9 1",
|
||||||
|
"Equip Add 23487 9 1",
|
||||||
|
"Equip Add 23488 9 1",
|
||||||
|
"Equip Add 23489 9 1",
|
||||||
|
"Equip Add 23490 9 1"
|
||||||
|
],
|
||||||
|
"当前角色60级": [
|
||||||
|
"break 3",
|
||||||
|
"level 60",
|
||||||
|
"skill 3 4",
|
||||||
|
"Equip Add 11409 60 3",
|
||||||
|
"Equip Add 12409 60 3",
|
||||||
|
"Equip Add 13405 60 3",
|
||||||
|
"Equip Add 14409 60 3",
|
||||||
|
"Equip Add 15409 60 3",
|
||||||
|
"Equip Add 23486 13 1",
|
||||||
|
"Equip Add 23487 13 1",
|
||||||
|
"Equip Add 23488 13 1",
|
||||||
|
"Equip Add 23489 13 1",
|
||||||
|
"Equip Add 23490 13 1"
|
||||||
|
],
|
||||||
|
"当前角色70级": [
|
||||||
|
"break 4",
|
||||||
|
"level 70",
|
||||||
|
"skill 3 5",
|
||||||
|
"Equip Add 11409 70 4",
|
||||||
|
"Equip Add 12409 70 4",
|
||||||
|
"Equip Add 13405 70 4",
|
||||||
|
"Equip Add 14409 70 4",
|
||||||
|
"Equip Add 15409 70 4",
|
||||||
|
"Equip Add 23486 17 1",
|
||||||
|
"Equip Add 23487 17 1",
|
||||||
|
"Equip Add 23488 17 1",
|
||||||
|
"Equip Add 23489 17 1",
|
||||||
|
"Equip Add 23490 17 1"
|
||||||
|
],
|
||||||
|
"当前角色80级": [
|
||||||
|
"break 5",
|
||||||
|
"level 80",
|
||||||
|
"skill 3 6",
|
||||||
|
"Equip Add 11409 80 5",
|
||||||
|
"Equip Add 12409 80 5",
|
||||||
|
"Equip Add 13405 80 5",
|
||||||
|
"Equip Add 14409 80 5",
|
||||||
|
"Equip Add 15409 80 5",
|
||||||
|
"Equip Add 23416 17 1",
|
||||||
|
"Equip Add 23417 17 1",
|
||||||
|
"Equip Add 23488 17 1",
|
||||||
|
"Equip Add 23489 17 1",
|
||||||
|
"Equip Add 23490 17 1"
|
||||||
|
],
|
||||||
|
"当前角色90级": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 7",
|
||||||
|
"Equip Add 11401 90 6",
|
||||||
|
"Equip Add 12401 90 6",
|
||||||
|
"Equip Add 13401 90 6",
|
||||||
|
"Equip Add 14401 90 6",
|
||||||
|
"Equip Add 15401 90 6",
|
||||||
|
"Equip Add 23481 21 1",
|
||||||
|
"Equip Add 23482 21 1",
|
||||||
|
"Equip Add 23483 21 1",
|
||||||
|
"Equip Add 23484 21 1",
|
||||||
|
"Equip Add 23485 21 1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"材料补给": {
|
||||||
|
"补充角色经验道具": [
|
||||||
|
"item add 104001 100",
|
||||||
|
"item add 104002 100",
|
||||||
|
"item add 104003 100"
|
||||||
|
],
|
||||||
|
"摩拉100w": [
|
||||||
|
"scoin 1000000"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"武器圣遗物": {
|
||||||
|
"获取10级武器A套": [
|
||||||
|
"item add 11301 1",
|
||||||
|
"item add 12301 1",
|
||||||
|
"item add 13301 1",
|
||||||
|
"item add 14301 1",
|
||||||
|
"item add 15301 1"
|
||||||
|
],
|
||||||
|
"获取10级武器B套": [
|
||||||
|
"item add 11302 1",
|
||||||
|
"item add 12302 1",
|
||||||
|
"item add 13302 1",
|
||||||
|
"item add 14302 1",
|
||||||
|
"item add 15302 1"
|
||||||
|
],
|
||||||
|
"获取15级武器A套": [
|
||||||
|
"item add 11401 1",
|
||||||
|
"item add 12401 1",
|
||||||
|
"item add 13302 1",
|
||||||
|
"item add 14402 1",
|
||||||
|
"item add 15302 1"
|
||||||
|
],
|
||||||
|
"获取15级武器B套": [
|
||||||
|
"item add 11403 1",
|
||||||
|
"item add 12403 1",
|
||||||
|
"item add 13301 1",
|
||||||
|
"item add 14401 1",
|
||||||
|
"item add 15401 1"
|
||||||
|
],
|
||||||
|
"获取20级武器A套": [
|
||||||
|
"item add 11402 1",
|
||||||
|
"item add 12403 1",
|
||||||
|
"item add 13401 1",
|
||||||
|
"item add 14404 1",
|
||||||
|
"item add 15401 1"
|
||||||
|
],
|
||||||
|
"获取20级套装B套": [
|
||||||
|
"item add 11404 1",
|
||||||
|
"item add 12404 1",
|
||||||
|
"item add 13504 1",
|
||||||
|
"item add 14403 1",
|
||||||
|
"item add 15501 1"
|
||||||
|
],
|
||||||
|
"装备15级圣遗物A套": [
|
||||||
|
"equip add 51110 1",
|
||||||
|
"equip add 51120 1",
|
||||||
|
"equip add 51130 1",
|
||||||
|
"equip add 51140 1",
|
||||||
|
"equip add 51150 1"
|
||||||
|
],
|
||||||
|
"装备15级圣遗物B套": [
|
||||||
|
"equip add 52110 1",
|
||||||
|
"equip add 52120 1",
|
||||||
|
"equip add 52130 1",
|
||||||
|
"equip add 52140 1",
|
||||||
|
"equip add 52150 1"
|
||||||
|
],
|
||||||
|
"装备20级圣遗物A套": [
|
||||||
|
"equip add 56210 1",
|
||||||
|
"equip add 56220 1",
|
||||||
|
"equip add 56230 1",
|
||||||
|
"equip add 56240 1",
|
||||||
|
"equip add 56250 1"
|
||||||
|
],
|
||||||
|
"装备20级圣遗物B套": [
|
||||||
|
"equip add 60210 1",
|
||||||
|
"equip add 60220 1",
|
||||||
|
"equip add 60230 1",
|
||||||
|
"equip add 60240 1",
|
||||||
|
"equip add 60250 1"
|
||||||
|
],
|
||||||
|
"装备冰守护圣遗物": [
|
||||||
|
"equip add 71410 1",
|
||||||
|
"equip add 71420 1",
|
||||||
|
"equip add 71430 1",
|
||||||
|
"equip add 71440 1",
|
||||||
|
"equip add 71450 1"
|
||||||
|
],
|
||||||
|
"装备雷守护圣遗物": [
|
||||||
|
"equip add 72410 1",
|
||||||
|
"equip add 72420 1",
|
||||||
|
"equip add 72430 1",
|
||||||
|
"equip add 72440 1",
|
||||||
|
"equip add 72450 1"
|
||||||
|
],
|
||||||
|
"装备火守护圣遗物": [
|
||||||
|
"equip add 73410 1",
|
||||||
|
"equip add 73420 1",
|
||||||
|
"equip add 73430 1",
|
||||||
|
"equip add 73440 1",
|
||||||
|
"equip add 73450 1"
|
||||||
|
],
|
||||||
|
"装备自然守护圣遗物": [
|
||||||
|
"equip add 74510 1",
|
||||||
|
"equip add 74520 1",
|
||||||
|
"equip add 74530 1",
|
||||||
|
"equip add 74540 1",
|
||||||
|
"equip add 74550 1"
|
||||||
|
],
|
||||||
|
"装备近战升攻圣遗物": [
|
||||||
|
"equip add 75410 1",
|
||||||
|
"equip add 75420 1",
|
||||||
|
"equip add 75430 1",
|
||||||
|
"equip add 75440 1",
|
||||||
|
"equip add 75450 1"
|
||||||
|
],
|
||||||
|
"装备远程升攻圣遗物": [
|
||||||
|
"equip add 76510 1",
|
||||||
|
"equip add 76520 1",
|
||||||
|
"equip add 76530 1",
|
||||||
|
"equip add 76540 1",
|
||||||
|
"equip add 76550 1"
|
||||||
|
],
|
||||||
|
"装备换人升攻圣遗物": [
|
||||||
|
"equip add 77510 1",
|
||||||
|
"equip add 77520 1",
|
||||||
|
"equip add 77530 1",
|
||||||
|
"equip add 77540 1",
|
||||||
|
"equip add 77550 1"
|
||||||
|
],
|
||||||
|
"装备冰伤害圣遗物": [
|
||||||
|
"equip add 78510 1",
|
||||||
|
"equip add 78520 1",
|
||||||
|
"equip add 78530 1",
|
||||||
|
"equip add 78540 1",
|
||||||
|
"equip add 78550 1"
|
||||||
|
],
|
||||||
|
"装备雷伤害圣遗物": [
|
||||||
|
"equip add 79510 1",
|
||||||
|
"equip add 79520 1",
|
||||||
|
"equip add 79530 1",
|
||||||
|
"equip add 79540 1",
|
||||||
|
"equip add 79550 1"
|
||||||
|
],
|
||||||
|
"装备火伤害圣遗物": [
|
||||||
|
"equip add 80510 1",
|
||||||
|
"equip add 80520 1",
|
||||||
|
"equip add 80530 1",
|
||||||
|
"equip add 80540 1",
|
||||||
|
"equip add 80550 1"
|
||||||
|
],
|
||||||
|
"装备拾球圣遗物": [
|
||||||
|
"equip add 81510 1",
|
||||||
|
"equip add 81520 1",
|
||||||
|
"equip add 81530 1",
|
||||||
|
"equip add 81540 1",
|
||||||
|
"equip add 81550 1"
|
||||||
|
],
|
||||||
|
"装备爆伤圣遗物": [
|
||||||
|
"equip add 82510 1",
|
||||||
|
"equip add 82520 1",
|
||||||
|
"equip add 82530 1",
|
||||||
|
"equip add 82540 1",
|
||||||
|
"equip add 82550 1"
|
||||||
|
],
|
||||||
|
"装备天空套圣遗物": [
|
||||||
|
"equip add 83510 1",
|
||||||
|
"equip add 83520 1",
|
||||||
|
"equip add 83530 1",
|
||||||
|
"equip add 83540 1",
|
||||||
|
"equip add 83550 1"
|
||||||
|
],
|
||||||
|
"获取所有4、5星圣遗物": [
|
||||||
|
"item add 76510",
|
||||||
|
"item add 76520",
|
||||||
|
"item add 76530",
|
||||||
|
"item add 76540",
|
||||||
|
"item add 76550",
|
||||||
|
"item add 77510",
|
||||||
|
"item add 77520",
|
||||||
|
"item add 77530",
|
||||||
|
"item add 77540",
|
||||||
|
"item add 77550",
|
||||||
|
"item add 78510",
|
||||||
|
"item add 78520",
|
||||||
|
"item add 78530",
|
||||||
|
"item add 78540",
|
||||||
|
"item add 78550",
|
||||||
|
"item add 79510",
|
||||||
|
"item add 79520",
|
||||||
|
"item add 79530",
|
||||||
|
"item add 79540",
|
||||||
|
"item add 79550",
|
||||||
|
"item add 80510",
|
||||||
|
"item add 80520",
|
||||||
|
"item add 80530",
|
||||||
|
"item add 80540",
|
||||||
|
"item add 80550",
|
||||||
|
"item add 81510",
|
||||||
|
"item add 81520",
|
||||||
|
"item add 81530",
|
||||||
|
"item add 81540",
|
||||||
|
"item add 81550",
|
||||||
|
"item add 82510",
|
||||||
|
"item add 82520",
|
||||||
|
"item add 82530",
|
||||||
|
"item add 82540",
|
||||||
|
"item add 82550",
|
||||||
|
"item add 83510",
|
||||||
|
"item add 83520",
|
||||||
|
"item add 83530",
|
||||||
|
"item add 83540",
|
||||||
|
"item add 83550",
|
||||||
|
"item add 71410",
|
||||||
|
"item add 72410",
|
||||||
|
"item add 73410",
|
||||||
|
"item add 74410",
|
||||||
|
"item add 71420",
|
||||||
|
"item add 72420",
|
||||||
|
"item add 73420",
|
||||||
|
"item add 74420",
|
||||||
|
"item add 71430",
|
||||||
|
"item add 72430",
|
||||||
|
"item add 73430",
|
||||||
|
"item add 74430",
|
||||||
|
"item add 71440",
|
||||||
|
"item add 72440",
|
||||||
|
"item add 73440",
|
||||||
|
"item add 74440",
|
||||||
|
"item add 71450",
|
||||||
|
"item add 72450",
|
||||||
|
"item add 73450",
|
||||||
|
"item add 74450"
|
||||||
|
],
|
||||||
|
"425版本验收武器": [
|
||||||
|
"item add 11303 1",
|
||||||
|
"item add 15303 1",
|
||||||
|
"item add 15402 1",
|
||||||
|
"item add 15403 1",
|
||||||
|
"item add 15404 1",
|
||||||
|
"item add 13303 1",
|
||||||
|
"item add 13402 1",
|
||||||
|
"item add 13403 1"
|
||||||
|
],
|
||||||
|
"523版本验收武器": [
|
||||||
|
"item add 11305 1",
|
||||||
|
"item add 11306 1",
|
||||||
|
"item add 11406 1",
|
||||||
|
"item add 11407 1",
|
||||||
|
"item add 11408 1",
|
||||||
|
"item add 12306 1",
|
||||||
|
"item add 14303 1",
|
||||||
|
"item add 14305 1",
|
||||||
|
"item add 14306 1"
|
||||||
|
],
|
||||||
|
"627版本验收武器": [
|
||||||
|
"item add 14406 1",
|
||||||
|
"item add 14407 1",
|
||||||
|
"item add 14408 1",
|
||||||
|
"item add 15304 1",
|
||||||
|
"item add 15305 1",
|
||||||
|
"item add 15306 1",
|
||||||
|
"item add 15405 1",
|
||||||
|
"item add 15406 1",
|
||||||
|
"item add 15408 1"
|
||||||
|
],
|
||||||
|
"725版本验收武器": [
|
||||||
|
"item add 12502 1",
|
||||||
|
"item add 13505 1",
|
||||||
|
"item add 14502 1",
|
||||||
|
"item add 15502 1",
|
||||||
|
"item add 12406 1",
|
||||||
|
"item add 12407 1",
|
||||||
|
"item add 12408 1",
|
||||||
|
"item add 13404 1",
|
||||||
|
"item add 15407 1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"角色飞升": {
|
||||||
|
"遇事不决选宗室": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23351 21 1",
|
||||||
|
"Equip Add 23352 21 1",
|
||||||
|
"Equip Add 23353 21 1",
|
||||||
|
"Equip Add 23354 21 1",
|
||||||
|
"Equip Add 23355 21 1"
|
||||||
|
],
|
||||||
|
"血骑士的物伤": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23341 21 1",
|
||||||
|
"Equip Add 23342 21 1",
|
||||||
|
"Equip Add 23343 21 1",
|
||||||
|
"Equip Add 23344 21 1",
|
||||||
|
"Equip Add 23345 21 1"
|
||||||
|
],
|
||||||
|
"炽烈的炎之魔女": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23361 21 1",
|
||||||
|
"Equip Add 23362 21 1",
|
||||||
|
"Equip Add 23363 21 1",
|
||||||
|
"Equip Add 23364 21 1",
|
||||||
|
"Equip Add 23365 21 1"
|
||||||
|
],
|
||||||
|
"如雷的盛怒": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23371 21 1",
|
||||||
|
"Equip Add 23372 21 1",
|
||||||
|
"Equip Add 23373 21 1",
|
||||||
|
"Equip Add 23374 21 1",
|
||||||
|
"Equip Add 23375 21 1"
|
||||||
|
],
|
||||||
|
"冰之川与雪之砂": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23381 21 1",
|
||||||
|
"Equip Add 23382 21 1",
|
||||||
|
"Equip Add 23383 21 1",
|
||||||
|
"Equip Add 23384 21 1",
|
||||||
|
"Equip Add 23385 21 1"
|
||||||
|
],
|
||||||
|
"流浪大地的乐团(弓和法器重击套)": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23391 21 1",
|
||||||
|
"Equip Add 23392 21 1",
|
||||||
|
"Equip Add 23393 21 1",
|
||||||
|
"Equip Add 23394 21 1",
|
||||||
|
"Equip Add 23395 21 1"
|
||||||
|
],
|
||||||
|
"翠绿猎人(风和扩散套)": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23401 21 1",
|
||||||
|
"Equip Add 23402 21 1",
|
||||||
|
"Equip Add 23403 21 1",
|
||||||
|
"Equip Add 23404 21 1",
|
||||||
|
"Equip Add 23405 21 1"
|
||||||
|
],
|
||||||
|
"角斗士套(近战武器的普攻)": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23411 21 1",
|
||||||
|
"Equip Add 23412 21 1",
|
||||||
|
"Equip Add 23413 21 1",
|
||||||
|
"Equip Add 23414 21 1",
|
||||||
|
"Equip Add 23415 21 1"
|
||||||
|
],
|
||||||
|
"火抗和火附着增伤": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23431 21 1",
|
||||||
|
"Equip Add 23432 21 1",
|
||||||
|
"Equip Add 23433 21 1",
|
||||||
|
"Equip Add 23434 21 1",
|
||||||
|
"Equip Add 23435 21 1"
|
||||||
|
],
|
||||||
|
"雷抗和雷附着增伤": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23441 21 1",
|
||||||
|
"Equip Add 23442 21 1",
|
||||||
|
"Equip Add 23443 21 1",
|
||||||
|
"Equip Add 23444 21 1",
|
||||||
|
"Equip Add 23445 21 1"
|
||||||
|
],
|
||||||
|
"冰抗和冰附着增伤": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23451 21 1",
|
||||||
|
"Equip Add 23452 21 1",
|
||||||
|
"Equip Add 23453 21 1",
|
||||||
|
"Equip Add 23454 21 1",
|
||||||
|
"Equip Add 23455 21 1"
|
||||||
|
],
|
||||||
|
"攻击向奶": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23421 21 1",
|
||||||
|
"Equip Add 23422 21 1",
|
||||||
|
"Equip Add 23423 21 1",
|
||||||
|
"Equip Add 23424 21 1",
|
||||||
|
"Equip Add 23425 21 1"
|
||||||
|
],
|
||||||
|
"生命向治疗": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23461 21 1",
|
||||||
|
"Equip Add 23462 21 1",
|
||||||
|
"Equip Add 23463 21 1",
|
||||||
|
"Equip Add 23464 21 1",
|
||||||
|
"Equip Add 23465 21 1"
|
||||||
|
],
|
||||||
|
"防御向治疗": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23471 21 1",
|
||||||
|
"Equip Add 23472 21 1",
|
||||||
|
"Equip Add 23473 21 1",
|
||||||
|
"Equip Add 23474 21 1",
|
||||||
|
"Equip Add 23475 21 1"
|
||||||
|
],
|
||||||
|
"守护套": [
|
||||||
|
"break 6",
|
||||||
|
"level 90",
|
||||||
|
"skill 3 10",
|
||||||
|
"skill 1 10",
|
||||||
|
"talent unlock all",
|
||||||
|
"Equip Add 11501 90 6",
|
||||||
|
"Equip Add 12501 90 6",
|
||||||
|
"Equip Add 13504 90 6",
|
||||||
|
"Equip Add 14502 90 6",
|
||||||
|
"Equip Add 15501 90 6",
|
||||||
|
"Equip Add 23481 21 1",
|
||||||
|
"Equip Add 23482 21 1",
|
||||||
|
"Equip Add 23483 21 1",
|
||||||
|
"Equip Add 23484 21 1",
|
||||||
|
"Equip Add 23485 21 1"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Yuanshen.Gameserver/assets/drops/monster_drops.json
Normal file
1
Yuanshen.Gameserver/assets/drops/monster_drops.json
Normal file
File diff suppressed because one or more lines are too long
429
Yuanshen.Gameserver/assets/excel/AvatarAbilities.json
Normal file
429
Yuanshen.Gameserver/assets/excel/AvatarAbilities.json
Normal file
|
|
@ -0,0 +1,429 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 10000003,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Qin_ExtraAttack",
|
||||||
|
"Avatar_Qin_VortexSmash",
|
||||||
|
"Avatar_Qin_DandelionWind",
|
||||||
|
"Avatar_Qin_DandelionWind_Push",
|
||||||
|
"Avatar_Qin_DandelionWind_Camera",
|
||||||
|
"Avatar_Qin_NormalAttackDamage",
|
||||||
|
"Avatar_Qin_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000005,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_PlayerBoy_NormalAttack_DamageHandler",
|
||||||
|
"Avatar_Player_FlyingBomber",
|
||||||
|
"Avatar_Player_CamCtrl",
|
||||||
|
"Avatar_PlayerBoy_FallingAnthem",
|
||||||
|
"Avatar_PlayerBoy_ExtraAttack_Common",
|
||||||
|
"Avatar_PlayerBoy_ExtraAttack_Wind",
|
||||||
|
"Avatar_Player_UziExplode_Mix",
|
||||||
|
"Avatar_Player_UziExplode",
|
||||||
|
"Avatar_Player_UziExplode_Strike_01",
|
||||||
|
"Avatar_Player_UziExplode_Strike_02",
|
||||||
|
"Avatar_Player_WindBreathe",
|
||||||
|
"Avatar_Player_WindBreathe_CameraController",
|
||||||
|
"Avatar_Player_Wind_Arashi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000006,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Lisa_Attack01",
|
||||||
|
"Avatar_Lisa_Attack02",
|
||||||
|
"Avatar_Lisa_Attack03",
|
||||||
|
"Avatar_Lisa_Attack04",
|
||||||
|
"Avatar_Lisa_NormalAttack04_Blink",
|
||||||
|
"Avatar_Lisa_ExtraAttack",
|
||||||
|
"Avatar_Lisa_ExtraAttack_Damage",
|
||||||
|
"Lisa_ExtraAttack_WhiteBox_FX",
|
||||||
|
"Avatar_Lisa_TriggerAbility2",
|
||||||
|
"Avatar_Lisa_StaticElecField",
|
||||||
|
"Avatar_Lisa_ThunderFall",
|
||||||
|
"Lisa_ThunderFall_FX_WithOutMonster",
|
||||||
|
"Avatar_Lisa_FlickerWave",
|
||||||
|
"Avatar_Lisa_PierceLightning",
|
||||||
|
"Avatar_Lisa_Weather_Trigger",
|
||||||
|
"Avatar_Lisa_Weather_Remove",
|
||||||
|
"Avatar_Lisa_PierceLightning_Ball",
|
||||||
|
"Avatar_Lisa_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000007,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_PlayerGirl_NormalAttack_DamageHandler",
|
||||||
|
"Avatar_Player_FlyingBomber",
|
||||||
|
"Avatar_Player_CamCtrl",
|
||||||
|
"Avatar_PlayerGirl_FallingAnthem",
|
||||||
|
"Avatar_PlayerGirl_ExtraAttack_Common",
|
||||||
|
"Avatar_PlayerGirl_ExtraAttack_Wind",
|
||||||
|
"Avatar_Player_UziExplode_Mix",
|
||||||
|
"Avatar_Player_UziExplode",
|
||||||
|
"Avatar_Player_UziExplode_Strike_01",
|
||||||
|
"Avatar_Player_UziExplode_Strike_02",
|
||||||
|
"Avatar_Player_WindBreathe",
|
||||||
|
"Avatar_Player_WindBreathe_CameraController",
|
||||||
|
"Avatar_Player_Wind_Arashi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000014,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Barbara_Attack01",
|
||||||
|
"Avatar_Barbara_Attack02",
|
||||||
|
"Avatar_Barbara_Attack03",
|
||||||
|
"Avatar_Barbara_Attack04",
|
||||||
|
"Avatar_Barbara_ExtraAttack",
|
||||||
|
"Avatar_Barbara_ExtraAttack_FX",
|
||||||
|
"Avatar_Barbara_ExtraAttack_Damage",
|
||||||
|
"Avatar_Barbara_WetShiled",
|
||||||
|
"Avatar_Barbara_WetShiled_Clear",
|
||||||
|
"Avatar_Barbara_IdolHeal",
|
||||||
|
"Avatar_Barbara_Trigger_Weather",
|
||||||
|
"Avatar_Girl_Catalyst_HitFallToGround_Hide",
|
||||||
|
"Avatar_Barbara_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000015,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Kaeya_DamageHandler",
|
||||||
|
"Avatar_Keaya_FrozenTrap_BS",
|
||||||
|
"Avatar_Keaya_IceShield_Impact",
|
||||||
|
"Avatar_Keaya_IceShield",
|
||||||
|
"Avatar_Kaeya_ExtraAttack",
|
||||||
|
"Avatar_KeayaAttack_Blink",
|
||||||
|
"Avatar_Keaya_FrozenTrap_Clear",
|
||||||
|
"Avatar_Kaeya_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000016,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Diluc_FireCrossCamera",
|
||||||
|
"Avatar_Diluc_ExtraAttack",
|
||||||
|
"Avatar_Diluc_FireCross_Explode",
|
||||||
|
"Avatar_Diluc_Fire_Blade",
|
||||||
|
"Avatar_Diluc_Fire_BladeSet1",
|
||||||
|
"Avatar_Diluc_Fire_BladeSet2",
|
||||||
|
"Avatar_Diluc_Fire_BladeSet3",
|
||||||
|
"Avatar_Diluc_AfterFireBladeCountDown",
|
||||||
|
"Avatar_Diluc_FireCross",
|
||||||
|
"Avatar_Diluc_FireCross_Push1",
|
||||||
|
"Avatar_Diluc_FireCross_Strike",
|
||||||
|
"Avatar_Diluc_Fire_Enhancement",
|
||||||
|
"Avatar_Diluc_NormalAttackDamage",
|
||||||
|
"Avatar_Diluc_ElementArt",
|
||||||
|
"Avatar_Diluc_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000020,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Razor_NormalAttack_DamageHandler",
|
||||||
|
"Avatar_Razor_ExtraAttack",
|
||||||
|
"Avatar_Razor_General_Handler",
|
||||||
|
"Avatar_Razor_ClawMark_Main",
|
||||||
|
"Avatar_Razor_ClawMark_Click",
|
||||||
|
"Avatar_Razor_ClawMark_SetMark",
|
||||||
|
"Avatar_Razor_ClawMark_Charging",
|
||||||
|
"Avatar_Razor_ClawMark_Weather_Remove",
|
||||||
|
"Avatar_Razor_ClawMark_Burst",
|
||||||
|
"Avatar_Razor_ClawMark_EnergyRegain",
|
||||||
|
"Avatar_Razor_LycanMode",
|
||||||
|
"Avatar_Razor_LycanMode_LycanSimulator",
|
||||||
|
"Avatar_Razor_LycanMode_FirstAttackGuaranteer",
|
||||||
|
"Avatar_Razor_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000021,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Ambor_ArrowRain_Alert",
|
||||||
|
"Avatar_Ambor_Arrow_FX",
|
||||||
|
"Avatar_Ambor_ShootAimingArrow_Charge",
|
||||||
|
"Avatar_Ambor_PressShoot",
|
||||||
|
"Avatar_Ambor_AimPressShoot",
|
||||||
|
"Avatar_Ambor_ShootArrow_01",
|
||||||
|
"Avatar_Ambor_ShootArrow_02",
|
||||||
|
"Avatar_Ambor_ShootArrow_03",
|
||||||
|
"Avatar_Ambor_ShootArrow_04",
|
||||||
|
"Avatar_Ambor_ShootArrow_05",
|
||||||
|
"Avatar_Ambor_ShootArrow_Aiming",
|
||||||
|
"Avatar_Ambor_DummyTrigger",
|
||||||
|
"Avatar_Ambor_ThrowDummy_UseSkill",
|
||||||
|
"Avatar_Ambor_ThrowDummy_Short",
|
||||||
|
"Avatar_Ambor_ThrowDummy_4M",
|
||||||
|
"Avatar_Ambor_ThrowDummy_7M",
|
||||||
|
"Avatar_Ambor_ThrowDummy",
|
||||||
|
"Avatar_Ambor_ArrowRain",
|
||||||
|
"Avatar_Ambor_AirShoot",
|
||||||
|
"Avatar_Ambor_ChangeMainSkill",
|
||||||
|
"Avatar_Ambor_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000022,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Venti_PressShoot",
|
||||||
|
"Avatar_Venti_AimPressShoot",
|
||||||
|
"Avatar_Venti_ShootArrow_01",
|
||||||
|
"Avatar_Venti_ShootArrow_02",
|
||||||
|
"Avatar_Venti_ShootArrow_03",
|
||||||
|
"Avatar_Venti_ShootArrow_04",
|
||||||
|
"Avatar_Venti_ShootArrow_05",
|
||||||
|
"Avatar_Venti_ShootArrow_06",
|
||||||
|
"Avatar_Venti_ShootArrow_Aiming",
|
||||||
|
"Avatar_Venti_ShootAimingArrow_Charge",
|
||||||
|
"Avatar_Ambor_AirShoot",
|
||||||
|
"Venti_WindBlade_GravityAdjust",
|
||||||
|
"Avatar_Venti_WindBlade",
|
||||||
|
"Avatar_Venti_Hurricane",
|
||||||
|
"Avatar_Venti_ChangeMainSkill",
|
||||||
|
"Venti_KageBunshinShoot",
|
||||||
|
"SetHairEmissionScaler",
|
||||||
|
"Avatar_Venti_Arrow_FX",
|
||||||
|
"Avatar_Venti_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000023,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Xiangling_Eff_Attack_04",
|
||||||
|
"Avatar_Xiangling_ExtraAttack",
|
||||||
|
"Avatar_Xiangling_ExtraAttack_CreateBullet",
|
||||||
|
"Avatar_Xiangling_PandaFire",
|
||||||
|
"Avatar_Xiangling_PandaFire_Run",
|
||||||
|
"Avatar_Xiangling_PandaBornEffect",
|
||||||
|
"Avatar_Xiangling_PandaBornEffect_Run",
|
||||||
|
"Avatar_Xiangling_FireCircleSkillStart",
|
||||||
|
"Avatar_Xiangling_FireCircle01",
|
||||||
|
"Avatar_Xiangling_FireCircle02",
|
||||||
|
"Avatar_Xiangling_FireCircle03",
|
||||||
|
"Avatar_Xiangling_FireCircle",
|
||||||
|
"Avatar_Xiangling_NormalAttackDamage",
|
||||||
|
"Avatar_Xiangling_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000024,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Beidou_ThunderCounter_Handler",
|
||||||
|
"Avatar_Beidou_ThunderCounter_Strike",
|
||||||
|
"Avatar_Beidou_ExtraAttack",
|
||||||
|
"Avatar_Beidou_ThunderShield",
|
||||||
|
"Avatar_Beidou_ThunderShield_CameraHandler",
|
||||||
|
"Avatar_Beidou_NormalAttackDamage",
|
||||||
|
"Avatar_Beidou_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000025,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Xingqiu_ExtraAttack",
|
||||||
|
"Avatar_Xingqiu_Spoondrift",
|
||||||
|
"Avatar_Xingqiu_PhantomShield",
|
||||||
|
"Avatar_Xingqiu_Minazuki",
|
||||||
|
"Avatar_Xingqiu_Minazuki_Attack01",
|
||||||
|
"Avatar_Xingqiu_Minazuki_Attack02",
|
||||||
|
"Avatar_Xingqiu_Minazuki_Attack03",
|
||||||
|
"Avatar_Xingqiu_PhantomBurst_GainEnergy",
|
||||||
|
"Avatar_Xingqiu_DamageHandler",
|
||||||
|
"Avatar_Xingqiu_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000027,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Ningguang_ExtraAttack_GemBullet_Handler",
|
||||||
|
"Avatar_Ningguang_Attack01",
|
||||||
|
"Avatar_Ningguang_Attack02",
|
||||||
|
"Avatar_Ningguang_Attack03",
|
||||||
|
"Avatar_Ningguang_Attack04",
|
||||||
|
"Avatar_Ningguang_ExtraAttack",
|
||||||
|
"Avatar_Ningguang_Roulette",
|
||||||
|
"Avatar_Ningguang_ExtraAttack_Fx",
|
||||||
|
"Avatar_Ningguang_HideWeapon",
|
||||||
|
"Avatar_Ningguang_Change_Gadget_BornState",
|
||||||
|
"Avatar_Ningguang_Charge_ServantStone",
|
||||||
|
"Avatar_Ningguang_ServantStone",
|
||||||
|
"Avatar_Ningguang_ServantStone_Switch",
|
||||||
|
"Avatar_Ningguang_Trigger_ElementalArt_CD",
|
||||||
|
"Avatar_Ningguang_GemStone_Impact",
|
||||||
|
"Avatar_Ningguang_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000029,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Klee_Attack01",
|
||||||
|
"Avatar_Klee_Attack02",
|
||||||
|
"Avatar_Klee_Attack03",
|
||||||
|
"Avatar_Klee_ExtraAttack",
|
||||||
|
"Avatar_Klee_ExtraAttack_CreateClover",
|
||||||
|
"Avatar_Klee_NormalAttack",
|
||||||
|
"Avatar_Klee_JumpingBomb",
|
||||||
|
"Avatar_Klee_ElementalArt_ClearTrigger",
|
||||||
|
"Avatar_Klee_Clover_Clear",
|
||||||
|
"Avatar_Klee_Elemental_Burst",
|
||||||
|
"Avatar_Klee_CameraRadialBlur",
|
||||||
|
"Avatar_Klee_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000031,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Fischl_ShootAimingArrow_Charge",
|
||||||
|
"Avatar_Fischl_PressShoot",
|
||||||
|
"Avatar_Fischl_AimPressShoot",
|
||||||
|
"Avatar_Fischl_ShootArrow_01",
|
||||||
|
"Avatar_Fischl_ShootArrow_02",
|
||||||
|
"Avatar_Fischl_ShootArrow_03",
|
||||||
|
"Avatar_Fischl_ShootArrow_04",
|
||||||
|
"Avatar_Fischl_ShootArrow_05",
|
||||||
|
"Avatar_Fischl_ShootArrow_Aiming",
|
||||||
|
"Avatar_Fischl_ShootAimingArrow_FX",
|
||||||
|
"Ability_Fischl_Skill_S_CrowSummon_NoAim",
|
||||||
|
"Ability_Fischl_Skill_S_CrowSummon_Aim",
|
||||||
|
"Ability_Fischl_Skill_S_CrowSummon",
|
||||||
|
"Ability_Fischl_Skill_S_ControlCrow_NoAim",
|
||||||
|
"Ability_Fischl_Skill_S_ControlCrow_Hide",
|
||||||
|
"Ability_Fischl_Skill_S_ControlCrow_Show",
|
||||||
|
"Ability_Fischl_Skill_E_CrowQueen",
|
||||||
|
"Avatar_Fischl_ElectricalCrow_Crow_Attack",
|
||||||
|
"Avatar_Fischl_NormalAttack_Smoke",
|
||||||
|
"Avatar_Fischl_CrowSummon_Init",
|
||||||
|
"Avatar_Fischl_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000032,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Bennett_ExtraAttack",
|
||||||
|
"Avatar_Bennett_Shake",
|
||||||
|
"Avatar_Bennett_ElementalArt_Handler",
|
||||||
|
"Avatar_Bennett_ElementalArt_Strike01_01",
|
||||||
|
"Avatar_Bennett_ElementalArt_Strike02_01",
|
||||||
|
"Avatar_Bennett_ElementalArt_Strike02_02",
|
||||||
|
"Avatar_Bennett_ElementalArt_Strike02_03",
|
||||||
|
"Avatar_Bennett_ElementalArt_Strike03_01",
|
||||||
|
"Avatar_Bennett_ElementalArt_Strike03_02",
|
||||||
|
"Avatar_Bennett_ElementalArt_Explode",
|
||||||
|
"Avatar_Bennett_ElementalBurst",
|
||||||
|
"Avatar_Bennett_ElementalBurst_Strike",
|
||||||
|
"Avatar_Bennett_NormalAttackDamage",
|
||||||
|
"Avatar_Bennett_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000034,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Noel_ExtraAttack",
|
||||||
|
"Avatar_Noel_Skill_S",
|
||||||
|
"Avatar_Noel_Skill_E",
|
||||||
|
"Avatar_Noel_Skill_E_ExitCamera",
|
||||||
|
"Avatar_Noel_ProudSkill_NormalAttack_DamageHandler",
|
||||||
|
"Avatar_Noel_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000035,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Qiqi_NormalAttackDamage",
|
||||||
|
"Avatar_Qiqi_ElementalArt_Damage",
|
||||||
|
"Avatar_Qiqi_ExtraAttack",
|
||||||
|
"Avatar_Qiqi_ElementalArt_Clear",
|
||||||
|
"Avatar_Qiqi_ElementalArt",
|
||||||
|
"Avatar_Qiqi_ElementalBurst",
|
||||||
|
"Avatar_Qiqi_ElementalBurst_PostEffect",
|
||||||
|
"Avatar_Qiqi_ElementalBurst_ApplyModifier",
|
||||||
|
"Avatar_Qiqi_ElementalBurst_TriggerAttack",
|
||||||
|
"Avatar_Qiqi_ElementalBurst_CameraRadialBlur",
|
||||||
|
"Avatar_Qiqi_Revive",
|
||||||
|
"Avatar_Qiqi_FallingAnthem",
|
||||||
|
"Avatar_Qiqi_Show_WindZone"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000036,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Chongyun_NormalAttack_DamageHandler",
|
||||||
|
"Avatar_Beidou_ExtraAttack_Chongyun",
|
||||||
|
"Avatar_Chongyun_ElementalArt_Initiate",
|
||||||
|
"Avatar_Chongyun_ElementalArt_Damage",
|
||||||
|
"Avatar_Chongyun_SoulUnleash_Initiate",
|
||||||
|
"Avatar_Chongyun_SoulUnleash_SoulBlades",
|
||||||
|
"Avatar_Chongyun_SoulUnleash_SoulBlades_Quadra",
|
||||||
|
"Avatar_Chongyun_SoulUnleash_Damage",
|
||||||
|
"Avatar_Chongyun_SoulUnleash_Quadra_Damage",
|
||||||
|
"Avatar_Chongyun_IceBlade",
|
||||||
|
"Avatar_Chongyun_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000041,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Mona_NormalAttack_01",
|
||||||
|
"Avatar_Mona_NormalAttack_02",
|
||||||
|
"Avatar_Mona_NormalAttack_03_BS",
|
||||||
|
"Avatar_Mona_NormalAttack_03_Blink",
|
||||||
|
"Avatar_Mona_NormalAttack_03",
|
||||||
|
"Avatar_Mona_NormalAttack_04",
|
||||||
|
"Avatar_Mona_ExtraAttack",
|
||||||
|
"Avatar_Mona_ExtraAttack_FX",
|
||||||
|
"Avatar_Mona_ExtraAttack_Damage",
|
||||||
|
"Avatar_Mona_Phantom_BS",
|
||||||
|
"Avatar_Mona_Phantom_Blink_BS",
|
||||||
|
"Avatar_Mona_Phantom",
|
||||||
|
"Avatar_Mona_Phantom_Blink",
|
||||||
|
"Avatar_Mona_Phantom_Blink_SetVisible",
|
||||||
|
"Avatar_Mona_Phantom_AS_HideWeapon",
|
||||||
|
"Avatar_Mona_StarChart_Camera",
|
||||||
|
"Avatar_Mona_StarChart",
|
||||||
|
"Avatar_Mona_Liquid_TriggerAbility",
|
||||||
|
"Avatar_Mona_LiquidStrike",
|
||||||
|
"Avatar_Mona_EndLiquidStrike",
|
||||||
|
"Avatar_Girl_Catalyst_HitFallToGround_Hide",
|
||||||
|
"Avatar_Mona_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000042,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Keqing_NormalAttack05_CreateBullet",
|
||||||
|
"Avatar_Keqing_NormalAttack_Handler",
|
||||||
|
"Avatar_Keqing_NormalAttack_DamageHandler",
|
||||||
|
"Avatar_Keqing_ExtraAttack",
|
||||||
|
"Avatar_Keqing_ExtraAttack_ExplodeGadget_Eff",
|
||||||
|
"Avatar_Keqing_ExtraAttack_ExplodeGadget",
|
||||||
|
"Avatar_Keqing_Electrans_Charge",
|
||||||
|
"Avatar_Keqing_Electrans_Click",
|
||||||
|
"Avatar_Keqing_Electrans_SetGadget",
|
||||||
|
"Avatar_Keqing_Electrans_Part2",
|
||||||
|
"Avatar_Keqing_Electrans_Strike",
|
||||||
|
"Avatar_Keqing_ElementalBurst_Camera",
|
||||||
|
"Avatar_Keqing_ElementalBurst",
|
||||||
|
"Avatar_Keqing_FallingAnthem"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10000043,
|
||||||
|
"abilities": [
|
||||||
|
"Avatar_Sucrose_Attack01",
|
||||||
|
"Avatar_Sucrose_Attack02",
|
||||||
|
"Avatar_Sucrose_Attack03",
|
||||||
|
"Avatar_Sucrose_Attack04_Pre",
|
||||||
|
"Avatar_Sucrose_Attack04",
|
||||||
|
"Avatar_Sucrose_ExtraAttack",
|
||||||
|
"Avatar_Sucrose_WindBlackHole",
|
||||||
|
"Avatar_Sucrose_DemiHurricane",
|
||||||
|
"Avatar_Girl_Catalyst_HitFallToGround_Hide",
|
||||||
|
"Avatar_Sucrose_FallingAnthem",
|
||||||
|
"Avatar_Sucrose_NormalAttack_DamageHandler"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
3103
Yuanshen.Gameserver/assets/excel/AvatarExcelConfigData.json
Normal file
3103
Yuanshen.Gameserver/assets/excel/AvatarExcelConfigData.json
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
43932
Yuanshen.Gameserver/assets/excel/MaterialExcelConfigData.json
Normal file
43932
Yuanshen.Gameserver/assets/excel/MaterialExcelConfigData.json
Normal file
File diff suppressed because it is too large
Load diff
11106
Yuanshen.Gameserver/assets/excel/MonsterExcelConfigData.json
Normal file
11106
Yuanshen.Gameserver/assets/excel/MonsterExcelConfigData.json
Normal file
File diff suppressed because it is too large
Load diff
754
Yuanshen.Gameserver/assets/excel/OpenStateConfigData.json
Normal file
754
Yuanshen.Gameserver/assets/excel/OpenStateConfigData.json
Normal file
|
|
@ -0,0 +1,754 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"defaultState": true,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"defaultState": true,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 31,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 60
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"system_open_ui_id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 30,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 2
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 37,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 2
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 39,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 14
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 45,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 54,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 35723
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"system_open_ui_id": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 25,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 26,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 27,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 28,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 29,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 32,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 33,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 34,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 35,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 36,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 38,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 40,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 41,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 50,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 53,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 8
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"system_open_ui_id": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 56,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 58,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"system_open_ui_id": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 26
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 43,
|
||||||
|
"defaultState": true,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1001,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1002,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 31101
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1003,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1004,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 5
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1005,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 5
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1006,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 13
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1007,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1008,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 5
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1009,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 31101
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1010,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 5
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 903,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 1
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 901,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 1
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 902,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 1
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 900,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 2010001
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"system_open_ui_id": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1100,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 2003003
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1103,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 8
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1101,
|
||||||
|
"defaultState": true,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1102,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 2211102
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1104,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 39303
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 44,
|
||||||
|
"defaultState": true,
|
||||||
|
"allowClientOpen": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 47,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 35001
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 48,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 99902
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 49,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 35001
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"system_open_ui_id": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1403,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 35311
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1200,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 4000100
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"system_open_ui_id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1201,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 4000110
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1202,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 4000104
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1300,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 1
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1301,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_PLAYER_LEVEL",
|
||||||
|
"param": 20
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
],
|
||||||
|
"system_open_ui_id": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 55,
|
||||||
|
"defaultState": true,
|
||||||
|
"cond": [
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1401,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 35802
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1402,
|
||||||
|
"cond": [
|
||||||
|
{
|
||||||
|
"cond_type": "OPEN_STATE_COND_QUEST",
|
||||||
|
"param": 39403
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
2452
Yuanshen.Gameserver/assets/excel/ReliquaryAffixExcelConfigData.json
Normal file
2452
Yuanshen.Gameserver/assets/excel/ReliquaryAffixExcelConfigData.json
Normal file
File diff suppressed because it is too large
Load diff
242
Yuanshen.Gameserver/assets/excel/ReliquaryExcelConfigData.json
Normal file
242
Yuanshen.Gameserver/assets/excel/ReliquaryExcelConfigData.json
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 23481,
|
||||||
|
"equipType": "EQUIP_RING",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 5099,
|
||||||
|
"appendPropDepotId": 964,
|
||||||
|
"setId": 10003
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23482,
|
||||||
|
"equipType": "EQUIP_NECKLACE",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 2000,
|
||||||
|
"appendPropDepotId": 961,
|
||||||
|
"setId": 10003
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23483,
|
||||||
|
"equipType": "EQUIP_DRESS",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 3095,
|
||||||
|
"appendPropDepotId": 965,
|
||||||
|
"setId": 10003
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23484,
|
||||||
|
"equipType": "EQUIP_BRACER",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 4000,
|
||||||
|
"appendPropDepotId": 962,
|
||||||
|
"setId": 10003
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23485,
|
||||||
|
"equipType": "EQUIP_SHOES",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 1099,
|
||||||
|
"appendPropDepotId": 963,
|
||||||
|
"setId": 10003
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23521,
|
||||||
|
"equipType": "EQUIP_RING",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 5099,
|
||||||
|
"appendPropDepotId": 964,
|
||||||
|
"setId": 10005
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23522,
|
||||||
|
"equipType": "EQUIP_NECKLACE",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 2000,
|
||||||
|
"appendPropDepotId": 961,
|
||||||
|
"setId": 10005
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23523,
|
||||||
|
"equipType": "EQUIP_DRESS",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 3095,
|
||||||
|
"appendPropDepotId": 965,
|
||||||
|
"setId": 10005
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23524,
|
||||||
|
"equipType": "EQUIP_BRACER",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 4000,
|
||||||
|
"appendPropDepotId": 962,
|
||||||
|
"setId": 10005
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23525,
|
||||||
|
"equipType": "EQUIP_SHOES",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 1099,
|
||||||
|
"appendPropDepotId": 963,
|
||||||
|
"setId": 10005
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23511,
|
||||||
|
"equipType": "EQUIP_RING",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 5088,
|
||||||
|
"appendPropDepotId": 964,
|
||||||
|
"setId": 10007
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23512,
|
||||||
|
"equipType": "EQUIP_NECKLACE",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 2000,
|
||||||
|
"appendPropDepotId": 961,
|
||||||
|
"setId": 10007
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23513,
|
||||||
|
"equipType": "EQUIP_DRESS",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 3093,
|
||||||
|
"appendPropDepotId": 965,
|
||||||
|
"setId": 10007
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23514,
|
||||||
|
"equipType": "EQUIP_BRACER",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 4000,
|
||||||
|
"appendPropDepotId": 962,
|
||||||
|
"setId": 10007
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23515,
|
||||||
|
"equipType": "EQUIP_SHOES",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 1095,
|
||||||
|
"appendPropDepotId": 963,
|
||||||
|
"setId": 10007
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23451,
|
||||||
|
"equipType": "EQUIP_RING",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 5094,
|
||||||
|
"appendPropDepotId": 964,
|
||||||
|
"setId": 14001
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23452,
|
||||||
|
"equipType": "EQUIP_NECKLACE",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 2000,
|
||||||
|
"appendPropDepotId": 961,
|
||||||
|
"setId": 14001
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23458,
|
||||||
|
"equipType": "EQUIP_DRESS",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 3096,
|
||||||
|
"appendPropDepotId": 965,
|
||||||
|
"setId": 14001
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23454,
|
||||||
|
"equipType": "EQUIP_BRACER",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 4000,
|
||||||
|
"appendPropDepotId": 962,
|
||||||
|
"setId": 14001
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23455,
|
||||||
|
"equipType": "EQUIP_SHOES",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 1099,
|
||||||
|
"appendPropDepotId": 963,
|
||||||
|
"setId": 14001
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23441,
|
||||||
|
"equipType": "EQUIP_RING",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 5095,
|
||||||
|
"appendPropDepotId": 964,
|
||||||
|
"setId": 14002
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23442,
|
||||||
|
"equipType": "EQUIP_NECKLACE",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 2000,
|
||||||
|
"appendPropDepotId": 961,
|
||||||
|
"setId": 14002
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23443,
|
||||||
|
"equipType": "EQUIP_DRESS",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 3096,
|
||||||
|
"appendPropDepotId": 965,
|
||||||
|
"setId": 14002
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23444,
|
||||||
|
"equipType": "EQUIP_BRACER",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 4000,
|
||||||
|
"appendPropDepotId": 962,
|
||||||
|
"setId": 14002
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23445,
|
||||||
|
"equipType": "EQUIP_SHOES",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 1099,
|
||||||
|
"appendPropDepotId": 963,
|
||||||
|
"setId": 14002
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23431,
|
||||||
|
"equipType": "EQUIP_RING",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 5096,
|
||||||
|
"appendPropDepotId": 964,
|
||||||
|
"setId": 14003
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23432,
|
||||||
|
"equipType": "EQUIP_NECKLACE",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 2000,
|
||||||
|
"appendPropDepotId": 961,
|
||||||
|
"setId": 14003
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23433,
|
||||||
|
"equipType": "EQUIP_DRESS",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 3096,
|
||||||
|
"appendPropDepotId": 965,
|
||||||
|
"setId": 14003
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23434,
|
||||||
|
"equipType": "EQUIP_BRACER",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 4000,
|
||||||
|
"appendPropDepotId": 962,
|
||||||
|
"setId": 14003
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23435,
|
||||||
|
"equipType": "EQUIP_SHOES",
|
||||||
|
"rankLevel": 5,
|
||||||
|
"mainPropDepotId": 1099,
|
||||||
|
"appendPropDepotId": 963,
|
||||||
|
"setId": 14003
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,398 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_TOUGH",
|
||||||
|
"id": 10001,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_HP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_STRONG",
|
||||||
|
"id": 10002,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_HP_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_RUTHLESS",
|
||||||
|
"id": 10003,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_ATTACK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_FATAL",
|
||||||
|
"id": 10004,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_ATTACK_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_GLORY",
|
||||||
|
"id": 10005,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_DEFENSE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_EMINENCE",
|
||||||
|
"id": 10006,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_DEFENSE_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_EXULTATION",
|
||||||
|
"id": 10007,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_CHARGE_EFFICIENCY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_WISDOM",
|
||||||
|
"id": 10008,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_ELEMENT_MASTERY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_GLORY",
|
||||||
|
"id": 10009,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_FIRE_SUB_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_GLORY",
|
||||||
|
"id": 10010,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_ELEC_SUB_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_GLORY",
|
||||||
|
"id": 10011,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_ICE_SUB_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_GLORY",
|
||||||
|
"id": 10012,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_WATER_SUB_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_GLORY",
|
||||||
|
"id": 10013,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_WIND_SUB_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_GLORY",
|
||||||
|
"id": 10014,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_ROCK_SUB_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_GLORY",
|
||||||
|
"id": 10015,
|
||||||
|
"propDepotId": 1000,
|
||||||
|
"propType": "FIGHT_PROP_GRASS_SUB_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_RUTHLESS",
|
||||||
|
"id": 12001,
|
||||||
|
"propDepotId": 2000,
|
||||||
|
"propType": "FIGHT_PROP_ATTACK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_TOUGH",
|
||||||
|
"id": 13001,
|
||||||
|
"propDepotId": 3000,
|
||||||
|
"propType": "FIGHT_PROP_HP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_STRONG",
|
||||||
|
"id": 13002,
|
||||||
|
"propDepotId": 3000,
|
||||||
|
"propType": "FIGHT_PROP_HP_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_RUTHLESS",
|
||||||
|
"id": 13003,
|
||||||
|
"propDepotId": 3000,
|
||||||
|
"propType": "FIGHT_PROP_ATTACK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_FATAL",
|
||||||
|
"id": 13004,
|
||||||
|
"propDepotId": 3000,
|
||||||
|
"propType": "FIGHT_PROP_ATTACK_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_GLORY",
|
||||||
|
"id": 13005,
|
||||||
|
"propDepotId": 3000,
|
||||||
|
"propType": "FIGHT_PROP_DEFENSE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_EMINENCE",
|
||||||
|
"id": 13006,
|
||||||
|
"propDepotId": 3000,
|
||||||
|
"propType": "FIGHT_PROP_DEFENSE_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CRUEL",
|
||||||
|
"id": 13007,
|
||||||
|
"propDepotId": 3000,
|
||||||
|
"propType": "FIGHT_PROP_CRITICAL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_FIERCE",
|
||||||
|
"id": 13008,
|
||||||
|
"propDepotId": 3000,
|
||||||
|
"propType": "FIGHT_PROP_CRITICAL_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_HOLINESS",
|
||||||
|
"id": 13009,
|
||||||
|
"propDepotId": 3000,
|
||||||
|
"propType": "FIGHT_PROP_HEAL_ADD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_WISDOM",
|
||||||
|
"id": 13010,
|
||||||
|
"propDepotId": 3000,
|
||||||
|
"propType": "FIGHT_PROP_ELEMENT_MASTERY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_TOUGH",
|
||||||
|
"id": 14001,
|
||||||
|
"propDepotId": 4000,
|
||||||
|
"propType": "FIGHT_PROP_HP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_TOUGH",
|
||||||
|
"id": 15001,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_HP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_STRONG",
|
||||||
|
"id": 15002,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_HP_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_RUTHLESS",
|
||||||
|
"id": 15003,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_ATTACK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_FATAL",
|
||||||
|
"id": 15004,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_ATTACK_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_GLORY",
|
||||||
|
"id": 15005,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_DEFENSE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_EMINENCE",
|
||||||
|
"id": 15006,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_DEFENSE_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_WISDOM",
|
||||||
|
"id": 15007,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_ELEMENT_MASTERY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 15008,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_FIRE_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 15009,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_ELEC_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 15010,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_ICE_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 15011,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_WATER_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 15012,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_WIND_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 15013,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_ROCK_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 15014,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_GRASS_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 15015,
|
||||||
|
"propDepotId": 5000,
|
||||||
|
"propType": "FIGHT_PROP_PHYSICAL_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_FATAL",
|
||||||
|
"id": 10990,
|
||||||
|
"propDepotId": 1099,
|
||||||
|
"propType": "FIGHT_PROP_ATTACK_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_STRONG",
|
||||||
|
"id": 10980,
|
||||||
|
"propDepotId": 1098,
|
||||||
|
"propType": "FIGHT_PROP_HP_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_EMINENCE",
|
||||||
|
"id": 10970,
|
||||||
|
"propDepotId": 1097,
|
||||||
|
"propType": "FIGHT_PROP_DEFENSE_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_EXULTATION",
|
||||||
|
"id": 10960,
|
||||||
|
"propDepotId": 1096,
|
||||||
|
"propType": "FIGHT_PROP_CHARGE_EFFICIENCY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_WISDOM",
|
||||||
|
"id": 10950,
|
||||||
|
"propDepotId": 1095,
|
||||||
|
"propType": "FIGHT_PROP_ELEMENT_MASTERY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_FATAL",
|
||||||
|
"id": 30990,
|
||||||
|
"propDepotId": 3099,
|
||||||
|
"propType": "FIGHT_PROP_ATTACK_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_STRONG",
|
||||||
|
"id": 30980,
|
||||||
|
"propDepotId": 3098,
|
||||||
|
"propType": "FIGHT_PROP_HP_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_EMINENCE",
|
||||||
|
"id": 30970,
|
||||||
|
"propDepotId": 3097,
|
||||||
|
"propType": "FIGHT_PROP_DEFENSE_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CRUEL",
|
||||||
|
"id": 30960,
|
||||||
|
"propDepotId": 3096,
|
||||||
|
"propType": "FIGHT_PROP_CRITICAL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_FIERCE",
|
||||||
|
"id": 30950,
|
||||||
|
"propDepotId": 3095,
|
||||||
|
"propType": "FIGHT_PROP_CRITICAL_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_HOLINESS",
|
||||||
|
"id": 30940,
|
||||||
|
"propDepotId": 3094,
|
||||||
|
"propType": "FIGHT_PROP_HEAL_ADD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_WISDOM",
|
||||||
|
"id": 30930,
|
||||||
|
"propDepotId": 3093,
|
||||||
|
"propType": "FIGHT_PROP_ELEMENT_MASTERY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_FATAL",
|
||||||
|
"id": 50990,
|
||||||
|
"propDepotId": 5099,
|
||||||
|
"propType": "FIGHT_PROP_ATTACK_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_STRONG",
|
||||||
|
"id": 50980,
|
||||||
|
"propDepotId": 5098,
|
||||||
|
"propType": "FIGHT_PROP_HP_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_EMINENCE",
|
||||||
|
"id": 50970,
|
||||||
|
"propDepotId": 5097,
|
||||||
|
"propType": "FIGHT_PROP_DEFENSE_PERCENT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 50960,
|
||||||
|
"propDepotId": 5096,
|
||||||
|
"propType": "FIGHT_PROP_FIRE_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 50950,
|
||||||
|
"propDepotId": 5095,
|
||||||
|
"propType": "FIGHT_PROP_ELEC_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 50940,
|
||||||
|
"propDepotId": 5094,
|
||||||
|
"propType": "FIGHT_PROP_ICE_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 50930,
|
||||||
|
"propDepotId": 5093,
|
||||||
|
"propType": "FIGHT_PROP_WATER_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 50920,
|
||||||
|
"propDepotId": 5092,
|
||||||
|
"propType": "FIGHT_PROP_WIND_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 50910,
|
||||||
|
"propDepotId": 5091,
|
||||||
|
"propType": "FIGHT_PROP_ROCK_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 50900,
|
||||||
|
"propDepotId": 5090,
|
||||||
|
"propType": "FIGHT_PROP_GRASS_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_CONCENTRATION",
|
||||||
|
"id": 50890,
|
||||||
|
"propDepotId": 5089,
|
||||||
|
"propType": "FIGHT_PROP_PHYSICAL_ADD_HURT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"affixName": "Reliquary_Main_Affix_WISDOM",
|
||||||
|
"id": 50880,
|
||||||
|
"propDepotId": 5088,
|
||||||
|
"propType": "FIGHT_PROP_ELEMENT_MASTERY"
|
||||||
|
}
|
||||||
|
]
|
||||||
4910
Yuanshen.Gameserver/assets/excel/ShopGoodsExcelConfigData.json
Normal file
4910
Yuanshen.Gameserver/assets/excel/ShopGoodsExcelConfigData.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,26 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"shopType": 903,
|
||||||
|
"order": 1,
|
||||||
|
"name": "SHOP_TYPE_MCOIN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"shopType": 1001,
|
||||||
|
"order": 2,
|
||||||
|
"name": "SHOP_TYPE_PAIMON"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"shopType": 902,
|
||||||
|
"order": 3,
|
||||||
|
"name": "SHOP_TYPE_PACKAGE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"shopType": 900,
|
||||||
|
"order": 4,
|
||||||
|
"name": "SHOP_TYPE_RECOMMEND"
|
||||||
|
}
|
||||||
|
]
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue