refactor: Refactor code and show different scripting example

- Refactored code so implementation can be shared between Server and
  DdonGameServer.
- Moved settings into settings module and implemented an example using a
  less structured scripting module interface.
- Created new ScriptableSettings class to assist with interacting with
  non-structured settings module.
- Created README.md files for suggestions and guidelines for module
  implementation.
This commit is contained in:
Paul Campbell 2024-12-23 14:40:57 -05:00
parent 62d1d74e0e
commit e6e5073737
24 changed files with 1107 additions and 716 deletions

View file

@ -20,7 +20,7 @@ namespace Arrowgene.Ddon.Cli.Command
private readonly Setting _setting;
private DdonLoginServer _loginServer;
private DdonGameServer _gameServer;
private ScriptedServerSettings _scriptServerSettings;
private ServerScriptManager _serverScriptManager;
private DdonWebServer _webServer;
private RpcWebServer _rpcWebServer;
private IDatabase _database;
@ -111,15 +111,15 @@ namespace Arrowgene.Ddon.Cli.Command
_assetRepository.Initialize();
}
if (_scriptServerSettings == null)
if (_serverScriptManager == null)
{
_scriptServerSettings = new ScriptedServerSettings(_setting.AssetPath);
_scriptServerSettings.LoadSettings();
_serverScriptManager = new ServerScriptManager(_setting.AssetPath);
_serverScriptManager.Initialize();
}
if (_loginServer == null)
{
_loginServer = new DdonLoginServer(_setting.LoginServerSetting, _scriptServerSettings.GameLogicSetting, _database, _assetRepository);
_loginServer = new DdonLoginServer(_setting.LoginServerSetting, _serverScriptManager.GameServerSettings.GameLogicSetting, _database, _assetRepository);
}
if (_webServer == null)
@ -129,7 +129,7 @@ namespace Arrowgene.Ddon.Cli.Command
if (_gameServer == null)
{
_gameServer = new DdonGameServer(_setting.GameServerSetting, _scriptServerSettings.GameLogicSetting, _database, _assetRepository);
_gameServer = new DdonGameServer(_setting.GameServerSetting, _serverScriptManager.GameServerSettings.GameLogicSetting, _database, _assetRepository);
}
if (_rpcWebServer == null)

View file

@ -2,6 +2,7 @@
using Arrowgene.Ddon.GameServer.Party;
using Arrowgene.Ddon.Server;
using Arrowgene.Ddon.Server.Network;
using Arrowgene.Ddon.Server.Scripting.interfaces;
using Arrowgene.Ddon.Shared.Entity.PacketStructure;
using Arrowgene.Ddon.Shared.Entity.Structure;
using Arrowgene.Ddon.Shared.Model;

View file

@ -34,6 +34,7 @@ using Arrowgene.Ddon.GameServer.Shop;
using Arrowgene.Ddon.Server;
using Arrowgene.Ddon.Server.Handler;
using Arrowgene.Ddon.Server.Network;
using Arrowgene.Ddon.Server.Scripting.interfaces;
using Arrowgene.Ddon.Shared;
using Arrowgene.Ddon.Shared.Entity;
using Arrowgene.Ddon.Shared.Entity.PacketStructure;
@ -55,7 +56,7 @@ namespace Arrowgene.Ddon.GameServer
{
ServerSetting = new GameServerSetting(setting);
GameLogicSettings = gameLogicSettings;
ScriptManager = new ScriptManager(this);
ScriptManager = new GameServerScriptManager(this);
ClientLookup = new GameClientLookup();
ChatLogHandler = new ChatLogHandler();
ChatManager = new ChatManager(this);
@ -95,7 +96,7 @@ namespace Arrowgene.Ddon.GameServer
public event EventHandler<ClientConnectionChangeArgs> ClientConnectionChangeEvent;
public GameServerSetting ServerSetting { get; }
public GameLogicSetting GameLogicSettings { get; }
public ScriptManager ScriptManager { get; }
public GameServerScriptManager ScriptManager { get; }
public ChatManager ChatManager { get; }
public ItemManager ItemManager { get; }
public CraftManager CraftManager { get; }

View file

@ -0,0 +1,39 @@
using Arrowgene.Ddon.Server;
using Arrowgene.Ddon.Shared.Scripting;
using Arrowgene.Logging;
namespace Arrowgene.Ddon.GameServer.Scripting
{
public class GlobalVariables
{
public GlobalVariables(DdonGameServer server)
{
Server = server;
}
public DdonGameServer Server { get; }
};
public class GameServerScriptManager : ScriptManager<GlobalVariables>
{
private static readonly ServerLogger Logger = LogProvider.Logger<ServerLogger>(typeof(GameServerScriptManager));
private DdonGameServer Server { get; }
private GlobalVariables Globals { get; }
public NpcExtendedFacilityModule NpcExtendedFacilityModule { get; private set; } = new NpcExtendedFacilityModule();
public GameServerScriptManager(DdonGameServer server) : base(server.AssetRepository.AssetsPath)
{
Server = server;
Globals = new GlobalVariables(Server);
// Add modules to the list so the generic logic can iterate over all scripting modules
ScriptModules[NpcExtendedFacilityModule.ModuleRoot] = NpcExtendedFacilityModule;
}
public override void Initialize()
{
base.Initialize(Globals);
}
}
}

View file

@ -12,6 +12,7 @@ namespace Arrowgene.Ddon.GameServer.Scripting
public override string ModuleRoot => "extended_facilities";
public override string Filter => "*.csx";
public override bool ScanSubdirectories => true;
public override bool EnableHotLoad => true;
public Dictionary<NpcId, INpcExtendedFacility> NpcExtendedFacilities { get; private set; }
@ -37,7 +38,7 @@ namespace Arrowgene.Ddon.GameServer.Scripting
.AddImports("Arrowgene.Ddon.Shared.Model.Quest");
}
public override bool EvaluateResult(ScriptState<object> result)
public override bool EvaluateResult(string path, ScriptState<object> result)
{
if (result == null)
{

View file

@ -26,6 +26,7 @@ using Arrowgene.Ddon.LoginServer.Handler;
using Arrowgene.Ddon.Server;
using Arrowgene.Ddon.Server.Handler;
using Arrowgene.Ddon.Server.Network;
using Arrowgene.Ddon.Server.Scripting.interfaces;
using Arrowgene.Ddon.Shared;
using Arrowgene.Ddon.Shared.Network;
using Arrowgene.Logging;

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using Arrowgene.Ddon.Server;
using Arrowgene.Ddon.Server.Network;
using Arrowgene.Ddon.Server.Scripting.interfaces;
using Arrowgene.Ddon.Shared.Entity.PacketStructure;
using Arrowgene.Ddon.Shared.Entity.Structure;
using Arrowgene.Ddon.Shared.Network;

View file

@ -25,5 +25,8 @@
<ProjectReference Include="..\Arrowgene.Ddon.Database\Arrowgene.Ddon.Database.csproj" />
<ProjectReference Include="..\Arrowgene.Ddon.Shared\Arrowgene.Ddon.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Settings\" />
</ItemGroup>
</Project>

View file

@ -1,325 +0,0 @@
using Arrowgene.Ddon.Shared.Model;
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace Arrowgene.Ddon.Server
{
public class GameLogicSetting
{
/// <summary>
/// Additional factor to change how long crafting a recipe will take to finish.
/// </summary>
public double AdditionalProductionSpeedFactor { get; set; }
/// <summary>
/// Additional factor to change how much a recipe will cost.
/// </summary>
public double AdditionalCostPerformanceFactor { get; set; }
/// <summary>
/// Sets the maximim level that the exp ring will reward a bonus.
/// </summary>
public uint RookiesRingMaxLevel { get; set; }
/// <summary>
/// The multiplier applied to the bonus amount of exp rewarded.
/// Must be a non-negtive value. If it is less than 0.0, a default of 1.0
/// will be selected.
/// </summary>
public double RookiesRingBonus { get; set; }
/// <summary>
/// Controls whether to pass lobby context packets on demand or only on entry to the server.
/// True = Server entry only. Lower packet load, but also causes invisible people in lobbies.
/// False = On-demand. May cause performance issues due to packet load.
/// </summary>
public bool NaiveLobbyContextHandling { get; set; }
/// <summary>
/// Determines the maximum amount of consumable items that can be crafted in one go with a pawn.
/// The default is a value of 10 which is equivalent to the original game's behavior.
/// </summary>
public byte CraftConsumableProductionTimesMax { get; set; }
/// <summary>
/// Configures if party exp is adjusted based on level differences of members.
/// </summary>
public bool EnableAdjustPartyEnemyExp { get; set; }
/// <summary>
/// List of the inclusive ranges of (MinLv, Maxlv, ExpMultiplier). ExpMultiplier is a value
/// from (0.0 - 1.0) which is multipled into the base exp amount to determine the adjusted exp.
/// The minlv and maxlv determine the relative level range that this multiplier should be applied to.
/// </summary>
public List<(uint MinLv, uint MaxLv, double ExpMultiplier)> AdjustPartyEnemyExpTiers { get; set; }
/// <summary>
/// Configures if exp is adjusted based on level differences of members vs target level.
/// </summary>
public bool EnableAdjustTargetLvEnemyExp { get; set; }
/// <summary>
/// List of the inclusive ranges of (MinLv, Maxlv, ExpMultiplier). ExpMultiplier is a value from
/// (0.0 - 1.0) which is multipled into the base exp amount to determine the adjusted exp.
/// The minlv and maxlv determine the relative level range that this multiplier should be applied to.
/// </summary>
public List<(uint MinLv, uint MaxLv, double ExpMultiplier)> AdjustTargetLvEnemyExpTiers { get; set; }
/// <summary>
/// The number of real world minutes that make up an in-game day.
/// </summary>
public uint GameClockTimescale { get; set; }
/// <summary>
/// Use a poisson process to randomly generate a weather cycle containing this many events, using the statistics in WeatherStatistics.
/// </summary>
public uint WeatherSequenceLength { get; set; }
/// <summary>
/// Statistics that drive semirandom weather generation. List is expected to be in (Fair, Cloudy, Rainy) order.
/// meanLength: Average length of the weather, in seconds, when it gets rolled.
/// weight: Relative weight of rolling that weather. Set to 0 to disable.
/// </summary>
public List<(uint MeanLength, uint Weight)> WeatherStatistics { get; set; }
/// <summary>
/// Configures if the Pawn Exp Catchup mechanic is enabled. This mechanic still rewards the player pawn EXP when the pawn is outside
/// the allowed level range and a lower level than the owner.
/// </summary>
public bool EnablePawnCatchup { get; set; }
/// <summary>
/// If the flag EnablePawnCatchup=true, this is the multiplier value used when calculating exp to catch the pawns level back up to the player.
/// </summary>
public double PawnCatchupMultiplier { get; set; }
/// <summary>
/// If the flag EnablePawnCatchup=true, this is the range of level that the pawn falls behind the player before the catchup mechanic kicks in.
/// </summary>
public uint PawnCatchupLvDiff { get; set; }
/// <summary>
/// Configures the default time in seconds a latern is active after igniting it.
/// </summary>
public uint LaternBurnTimeInSeconds { get; set; }
/// <summary>
/// Maximum amount of play points the client will display in the UI.
/// Play points past this point will also trigger a chat log message saying you've reached the cap.
/// </summary>
public uint PlayPointMax { get; set; }
/// <summary>
/// Maximum level for each job.
/// Shared with the login server.
/// </summary>
public uint JobLevelMax { get; set; }
/// <summary>
/// Maximum number of members in a single clan.
/// Shared with the login server.
/// </summary>
public uint ClanMemberMax { get; set; }
/// <summary>
/// Maximum number of characters per account.
/// Shared with the login server.
/// </summary>
public byte CharacterNumMax { get; set; }
/// <summary>
/// Toggles the visual equip set for all characters.
/// Shared with the login server.
/// </summary>
public bool EnableVisualEquip { get; set; }
/// <summary>
/// Maximum entries in the friends list.
/// Shared with the login server.
/// </summary>
public uint FriendListMax { get; set; }
/// <summary>
/// Limits for each wallet type.
/// </summary>
public Dictionary<WalletType, uint> WalletLimits { get; set; }
/// <summary>
/// Number of bazaar entries that are given to new characters.
/// </summary>
public uint DefaultMaxBazaarExhibits { get; set; }
/// <summary>
/// Number of favorite warps that are given to new characters.
/// </summary>
public uint DefaultWarpFavorites { get; set; }
/// <summary>
/// Disables the exp correction if all party members are owned by the same character.
/// </summary>
public bool DisableExpCorrectionForMyPawn { get; set; }
/// <summary>
/// Global modifier for enemy exp calculations to scale up or down.
/// </summary>
public double EnemyExpModifier { get; set; }
/// <summary>
/// Global modifier for quest exp calculations to scale up or down.
/// </summary>
public double QuestExpModifier { get; set; }
/// <summary>
/// Global modifier for pp calculations to scale up or down.
/// </summary>
public double PpModifier { get; set; }
/// <summary>
/// Global modifier for Gold calculations to scale up or down.
/// </summary>
public double GoldModifier { get; set; }
/// <summary>
/// Global modifier for Rift calculations to scale up or down.
/// </summary>
public double RiftModifier { get; set; }
/// <summary>
/// Global modifier for BO calculations to scale up or down.
/// </summary>
public double BoModifier { get; set; }
/// <summary>
/// Global modifier for HO calculations to scale up or down.
/// </summary>
public double HoModifier { get; set; }
/// <summary>
/// Global modifier for JP calculations to scale up or down.
/// </summary>
public double JpModifier { get; set; }
/// <summary>
/// Configures the maximum amount of reward box slots.
/// </summary>
public byte RewardBoxMax { get; set; }
/// <summary>
/// Configures the maximum amount of quests that can be ordered at one time.
/// </summary>
public byte QuestOrderMax { get; set; }
/// <summary>
/// Configures if epitaph rewards are limited once per weekly reset.
/// </summary>
public bool EnableEpitaphWeeklyRewards { get; set; }
/// <summary>
/// Enables main pawns in party to gain EXP and JP from quests
/// Original game apparantly did not have pawns share quest reward, so will set to false for default,
/// change as needed
/// </summary>
public bool EnableMainPartyPawnsQuestRewards { get; set; }
/// <summary>
/// Specifies the time in seconds that a bazaar exhibit will last.
/// By default, the equivalent of 3 days
/// </summary>
public ulong BazaarExhibitionTimeSeconds { get; set; }
/// <summary>
/// Specifies the time in seconds that a slot in the bazaar won't be able to be used again.
/// By default, the equivalent of 1 day
/// </summary>
public ulong BazaarCooldownTimeSeconds { get; set; }
/// <summary>
/// Various URLs used by the client.
/// Shared with the login server.
/// </summary>
public string UrlManual { get; set; }
public string UrlShopDetail { get; set; }
public string UrlShopCounterA { get; set; }
public string UrlShopAttention { get; set; }
public string UrlShopStoneLimit { get; set; }
public string UrlShopCounterB { get; set; }
public string UrlChargeCallback { get; set; }
public string UrlChargeA { get; set; }
public string UrlSample9 { get; set; }
public string UrlSample10 { get; set; }
public string UrlCampaignBanner { get; set; }
public string UrlSupportIndex { get; set; }
public string UrlPhotoupAuthorize { get; set; }
public string UrlApiA { get; set; }
public string UrlApiB { get; set; }
public string UrlIndex { get; set; }
public string UrlCampaign { get; set; }
public string UrlChargeB { get; set; }
public string UrlCompanionImage { get; set; }
public GameLogicSetting()
{
}
void ValidateSettings()
{
if (RookiesRingBonus < 0)
{
RookiesRingBonus = 1.0;
}
if (AdditionalProductionSpeedFactor < 0)
{
CraftConsumableProductionTimesMax = 1;
}
if (AdditionalCostPerformanceFactor < 0)
{
CraftConsumableProductionTimesMax = 1;
}
if (CraftConsumableProductionTimesMax < 1)
{
CraftConsumableProductionTimesMax = 10;
}
if (GameClockTimescale <= 0)
{
GameClockTimescale = 90;
}
if (PawnCatchupMultiplier < 0)
{
PawnCatchupMultiplier = 1.0;
}
if (EnemyExpModifier < 0)
{
EnemyExpModifier = 1.0;
}
if (QuestExpModifier < 0)
{
QuestExpModifier = 1.0;
}
if (PpModifier < 0)
{
PpModifier = 1.0;
}
if (GoldModifier < 0)
{
GoldModifier = 1.0;
}
if (RiftModifier < 0)
{
RiftModifier = 1.0;
}
if (BoModifier < 0)
{
BoModifier = 1.0;
}
if (HoModifier < 0)
{
HoModifier = 1.0;
}
if (JpModifier < 0)
{
JpModifier = 1.0;
}
}
}
}

View file

@ -1,145 +0,0 @@
using Arrowgene.Ddon.Shared;
using Arrowgene.Ddon.Shared.Model;
using Arrowgene.Logging;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using System;
using System.Collections.Generic;
using System.IO;
namespace Arrowgene.Ddon.Server
{
public class ScriptedServerSettings
{
private static readonly ServerLogger Logger = LogProvider.Logger<ServerLogger>(typeof(ScriptedServerSettings));
public class Globals
{
public GameLogicSetting GameLogicSetting { get; set; }
}
private string ScriptsRoot { get; set; }
private Dictionary<string, Script> CompiledScripts;
public GameLogicSetting GameLogicSetting { get; private set; }
public FileSystemWatcher Watcher { get; private set; }
public Script GameLogicSettings
{
get
{
lock (CompiledScripts)
{
return CompiledScripts["GameLogicSettings"];
}
}
set
{
lock (CompiledScripts)
{
CompiledScripts["GameLogicSettings"] = value;
}
}
}
public ScriptedServerSettings(string AssetsPath)
{
ScriptsRoot = $"{AssetsPath}\\scripts";
GameLogicSetting = new GameLogicSetting();
CompiledScripts = new Dictionary<string, Script>();
var ScriptsDirectory = new DirectoryInfo(ScriptsRoot);
if (!ScriptsDirectory.Exists)
{
return;
}
Watcher = SetupFileWatcher();
}
public void LoadSettings()
{
string settingsPath = $"{ScriptsRoot}\\GameLogicSettings.csx";
var options = ScriptOptions.Default
.AddReferences(MetadataReference.CreateFromFile(typeof(GameLogicSetting).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(WalletType).Assembly.Location))
.AddImports("System", "System.Collections", "System.Collections.Generic")
.AddImports("Arrowgene.Ddon.Shared.Model")
.AddImports("Arrowgene.Ddon.Shared.Model.Quest");
Globals globals = new Globals()
{
GameLogicSetting = GameLogicSetting
};
Logger.Info($"Loading Scriptable game settings from {ScriptsRoot}");
Logger.Info($"{settingsPath}");
var code = Util.ReadAllText(settingsPath);
// Load The Game Settings
GameLogicSettings = CSharpScript.Create(
code: code,
options: options,
globalsType: typeof(Globals)
);
if (GameLogicSettings != null)
{
// Execute the script file to populate the settings
GameLogicSettings.RunAsync(globals);
}
}
private FileSystemWatcher SetupFileWatcher()
{
var watcher = new FileSystemWatcher(ScriptsRoot);
watcher.Filter = "GameLogicSettings.csx";
watcher.NotifyFilter = (NotifyFilters.LastWrite);
watcher.Changed += OnChanged;
watcher.Error += OnError;
watcher.EnableRaisingEvents = true;
return watcher;
}
private void OnChanged(object sender, FileSystemEventArgs e)
{
if (e.ChangeType != WatcherChangeTypes.Changed)
{
return;
}
Logger.Info($"Reloading {e.FullPath}");
try
{
Watcher.EnableRaisingEvents = false;
LoadSettings();
}
finally
{
Watcher.EnableRaisingEvents = true;
}
}
private void OnError(object sender, ErrorEventArgs e) =>
PrintException(e.GetException());
private void PrintException(Exception ex)
{
if (ex != null)
{
Logger.Error($"{ex.Message}");
Logger.Error($"Stacktrace:");
PrintException(ex.InnerException);
}
}
}
}

View file

@ -1,55 +1,40 @@
using Arrowgene.Ddon.GameServer.Scripting;
using Arrowgene.Ddon.Server;
using Arrowgene.Ddon.Shared;
using Arrowgene.Logging;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using static Arrowgene.Ddon.Server.ServerScriptManager;
namespace Arrowgene.Ddon.GameServer.Scripting
namespace Arrowgene.Ddon.Shared.Scripting
{
public class ScriptManager
public abstract class ScriptManager<T>
{
private static readonly ServerLogger Logger = LogProvider.Logger<ServerLogger>(typeof(ScriptManager));
public class GlobalVariables
private static readonly ServerLogger Logger = LogProvider.Logger<ServerLogger>(typeof(ScriptManager<T>));
protected Dictionary<string, ScriptModule> ScriptModules { get; private set; }
public string ScriptsRoot { get; private set; }
public T GlobalVariables { get; protected set; }
public ScriptManager(string assetsPath)
{
public GlobalVariables(DdonGameServer server)
{
Server = server;
}
public DdonGameServer Server { get; }
};
public NpcExtendedFacilityModule NpcExtendedFacilityModule { get; private set; } = new NpcExtendedFacilityModule();
private Dictionary<string, ScriptModule> ScriptModules;
public ScriptManager(DdonGameServer server)
{
Server = server;
ScriptsRoot = $"{server.AssetRepository.AssetsPath}\\scripts";
ScriptModules = new Dictionary<string, ScriptModule>()
{
{NpcExtendedFacilityModule.ModuleRoot, NpcExtendedFacilityModule}
};
Globals = new GlobalVariables(Server);
ScriptModules = new Dictionary<string, ScriptModule>();
ScriptsRoot = $"{assetsPath}\\scripts";
}
private DdonGameServer Server { get; }
private string ScriptsRoot { get; }
private GlobalVariables Globals { get; }
public abstract void Initialize();
public void Initialize()
protected void Initialize(T globalVariables)
{
GlobalVariables = globalVariables;
CompileScripts();
SetupFileWatchers();
}
private void CompileScript(ScriptModule module, string path)
protected void CompileScript(ScriptModule module, string path)
{
try
{
@ -58,11 +43,11 @@ namespace Arrowgene.Ddon.GameServer.Scripting
var script = CSharpScript.Create(
code: Util.ReadAllText(path),
options: module.Options(),
globalsType: typeof(GlobalVariables)
globalsType: typeof(T)
);
var result = script.RunAsync(Globals).Result;
if (!module.EvaluateResult(result))
var result = script.RunAsync(GlobalVariables).Result;
if (!module.EvaluateResult(path, result))
{
Logger.Error($"Failed to evaluate the result of executing '{path}'");
}
@ -74,7 +59,7 @@ namespace Arrowgene.Ddon.GameServer.Scripting
}
}
private void CompileScripts()
protected void CompileScripts()
{
foreach (var module in ScriptModules.Values)
{
@ -83,6 +68,11 @@ namespace Arrowgene.Ddon.GameServer.Scripting
Logger.Info($"Compiling scripts for module '{module.ModuleRoot}'");
foreach (var file in Directory.EnumerateFiles(path))
{
if (Path.GetExtension(file) != ".csx")
{
continue;
}
module.Scripts.Add(file);
CompileScript(module, file);
}
@ -93,6 +83,11 @@ namespace Arrowgene.Ddon.GameServer.Scripting
{
foreach (var module in ScriptModules.Values)
{
if (!module.EnableHotLoad)
{
continue;
}
var watcher = new FileSystemWatcher($"{ScriptsRoot}\\{module.ModuleRoot}");
watcher.Filter = module.Filter;
watcher.NotifyFilter = (NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.CreationTime);
@ -108,11 +103,13 @@ namespace Arrowgene.Ddon.GameServer.Scripting
// Enable all the watchers
foreach (var module in ScriptModules.Values)
{
module.Watcher.EnableRaisingEvents = true;
if (module.EnableHotLoad)
{
module.Watcher.EnableRaisingEvents = true;
}
}
}
private void OnChanged(object sender, FileSystemEventArgs e)
protected void OnChanged(object sender, FileSystemEventArgs e)
{
if (e.ChangeType != WatcherChangeTypes.Changed)
{

View file

@ -11,6 +11,13 @@ namespace Arrowgene.Ddon.GameServer.Scripting
public abstract bool ScanSubdirectories { get; }
public FileSystemWatcher Watcher { get; set; }
/// <summary>
/// Determines if this module is able to be hot-loadable or not.
/// If a module is not hot-loadable, it will only be evaluated once
/// when the server first loads.
/// </summary>
public abstract bool EnableHotLoad { get; }
public HashSet<string> Scripts { get; set; }
public ScriptModule()
@ -27,8 +34,9 @@ namespace Arrowgene.Ddon.GameServer.Scripting
/// <summary>
/// Evaluates the result returned by the script.
/// </summary>
/// <param name="result"></param>
/// <param name="path">Path to the script that was executed</param>
/// <param name="result">The result object of the script that executed</param>
/// <returns></returns>
public abstract bool EvaluateResult(ScriptState<object> result);
public abstract bool EvaluateResult(string path, ScriptState<object> result);
}
}

View file

@ -2,8 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Arrowgene.Ddon.GameServer.Scripting
{

View file

@ -0,0 +1,649 @@
using Arrowgene.Ddon.Server.Scripting.utils;
using Arrowgene.Ddon.Shared.Model;
using System.Collections.Generic;
namespace Arrowgene.Ddon.Server.Scripting.interfaces
{
public class GameLogicSetting
{
private ScriptableSettings SettingsData { get; set; }
public GameLogicSetting(ScriptableSettings settingsData)
{
SettingsData = settingsData;
}
private T GetSetting<T>(string key)
{
return SettingsData.Get<T>("GameLogicSettings", key);
}
/// <summary>
/// Additional factor to change how long crafting a recipe will take to finish.
/// </summary>
public double AdditionalProductionSpeedFactor
{
get
{
return GetSetting<double>("AdditionalProductionSpeedFactor");
}
}
/// <summary>
/// Additional factor to change how much a recipe will cost.
/// </summary>
public double AdditionalCostPerformanceFactor
{
get
{
return GetSetting<double>("AdditionalCostPerformanceFactor");
}
}
/// <summary>
/// Sets the maximim level that the exp ring will reward a bonus.
/// </summary>
public uint RookiesRingMaxLevel
{
get
{
return GetSetting<uint>("RookiesRingMaxLevel");
}
}
/// <summary>
/// The multiplier applied to the bonus amount of exp rewarded.
/// Must be a non-negtive value. If it is less than 0.0, a default of 1.0
/// will be selected.
/// </summary>
public double RookiesRingBonus
{
get
{
return GetSetting<double>("RookiesRingBonus");
}
}
/// <summary>
/// Controls whether to pass lobby context packets on demand or only on entry to the server.
/// True = Server entry only. Lower packet load, but also causes invisible people in lobbies.
/// False = On-demand. May cause performance issues due to packet load.
/// </summary>
public bool NaiveLobbyContextHandling
{
get
{
return GetSetting<bool>("NaiveLobbyContextHandling");
}
}
/// <summary>
/// Determines the maximum amount of consumable items that can be crafted in one go with a pawn.
/// The default is a value of 10 which is equivalent to the original game's behavior.
/// </summary>
public byte CraftConsumableProductionTimesMax
{
get
{
return GetSetting<byte>("CraftConsumableProductionTimesMax");
}
}
/// <summary>
/// Configures if party exp is adjusted based on level differences of members.
/// </summary>
public bool EnableAdjustPartyEnemyExp
{
get
{
return GetSetting<bool>("EnableAdjustPartyEnemyExp");
}
}
/// <summary>
/// List of the inclusive ranges of (MinLv, Maxlv, ExpMultiplier). ExpMultiplier is a value
/// from (0.0 - 1.0) which is multipled into the base exp amount to determine the adjusted exp.
/// The minlv and maxlv determine the relative level range that this multiplier should be applied to.
/// </summary>
public List<(uint MinLv, uint MaxLv, double ExpMultiplier)> AdjustPartyEnemyExpTiers
{
get
{
return GetSetting<List<(uint MinLv, uint MaxLv, double ExpMultiplier)>>("AdjustPartyEnemyExpTiers");
}
}
/// <summary>
/// Configures if exp is adjusted based on level differences of members vs target level.
/// </summary>
public bool EnableAdjustTargetLvEnemyExp
{
get
{
return GetSetting<bool>("EnableAdjustTargetLvEnemyExp");
}
}
/// <summary>
/// List of the inclusive ranges of (MinLv, Maxlv, ExpMultiplier). ExpMultiplier is a value from
/// (0.0 - 1.0) which is multipled into the base exp amount to determine the adjusted exp.
/// The minlv and maxlv determine the relative level range that this multiplier should be applied to.
/// </summary>
public List<(uint MinLv, uint MaxLv, double ExpMultiplier)> AdjustTargetLvEnemyExpTiers
{
get
{
return GetSetting<List<(uint MinLv, uint MaxLv, double ExpMultiplier)>>("AdjustTargetLvEnemyExpTiers");
}
}
/// <summary>
/// The number of real world minutes that make up an in-game day.
/// </summary>
public uint GameClockTimescale
{
get
{
return GetSetting<uint>("GameClockTimescale");
}
}
/// <summary>
/// Use a poisson process to randomly generate a weather cycle containing this many events, using the statistics in WeatherStatistics.
/// </summary>
public uint WeatherSequenceLength
{
get
{
return GetSetting<uint>("WeatherSequenceLength");
}
}
/// <summary>
/// Statistics that drive semirandom weather generation. List is expected to be in (Fair, Cloudy, Rainy) order.
/// meanLength: Average length of the weather, in seconds, when it gets rolled.
/// weight: Relative weight of rolling that weather. Set to 0 to disable.
/// </summary>
public List<(uint MeanLength, uint Weight)> WeatherStatistics
{
get
{
return GetSetting<List<(uint MeanLength, uint Weight)>>("WeatherStatistics");
}
}
/// <summary>
/// Configures if the Pawn Exp Catchup mechanic is enabled. This mechanic still rewards the player pawn EXP when the pawn is outside
/// the allowed level range and a lower level than the owner.
/// </summary>
public bool EnablePawnCatchup
{
get
{
return GetSetting<bool>("EnablePawnCatchup");
}
}
/// <summary>
/// If the flag EnablePawnCatchup=true, this is the multiplier value used when calculating exp to catch the pawns level back up to the player.
/// </summary>
public double PawnCatchupMultiplier
{
get
{
return GetSetting<double>("PawnCatchupMultiplier");
}
}
/// <summary>
/// If the flag EnablePawnCatchup=true, this is the range of level that the pawn falls behind the player before the catchup mechanic kicks in.
/// </summary>
public uint PawnCatchupLvDiff
{
get
{
return GetSetting<uint>("PawnCatchupLvDiff");
}
}
/// <summary>
/// Configures the default time in seconds a latern is active after igniting it.
/// </summary>
public uint LaternBurnTimeInSeconds
{
get
{
return GetSetting<uint>("LaternBurnTimeInSeconds");
}
}
/// <summary>
/// Maximum amount of play points the client will display in the UI.
/// Play points past this point will also trigger a chat log message saying you've reached the cap.
/// </summary>
public uint PlayPointMax
{
get
{
return GetSetting<uint>("PlayPointMax");
}
}
/// <summary>
/// Maximum level for each job.
/// Shared with the login server.
/// </summary>
public uint JobLevelMax
{
get
{
return GetSetting<uint>("JobLevelMax");
}
}
/// <summary>
/// Maximum number of members in a single clan.
/// Shared with the login server.
/// </summary>
public uint ClanMemberMax
{
get
{
return GetSetting<uint>("ClanMemberMax");
}
}
/// <summary>
/// Maximum number of characters per account.
/// Shared with the login server.
/// </summary>
public byte CharacterNumMax
{
get
{
return GetSetting<byte>("CharacterNumMax");
}
}
/// <summary>
/// Toggles the visual equip set for all characters.
/// Shared with the login server.
/// </summary>
public bool EnableVisualEquip
{
get
{
return GetSetting<bool>("EnableVisualEquip");
}
}
/// <summary>
/// Maximum entries in the friends list.
/// Shared with the login server.
/// </summary>
public uint FriendListMax
{
get
{
return GetSetting<uint>("FriendListMax");
}
}
/// <summary>
/// Limits for each wallet type.
/// </summary>
public Dictionary<WalletType, uint> WalletLimits
{
get
{
return GetSetting<Dictionary<WalletType, uint>>("WalletLimits");
}
}
/// <summary>
/// Number of bazaar entries that are given to new characters.
/// </summary>
public uint DefaultMaxBazaarExhibits
{
get
{
return GetSetting<uint>("DefaultMaxBazaarExhibits");
}
}
/// <summary>
/// Number of favorite warps that are given to new characters.
/// </summary>
public uint DefaultWarpFavorites
{
get
{
return GetSetting<uint>("DefaultWarpFavorites");
}
}
/// <summary>
/// Disables the exp correction if all party members are owned by the same character.
/// </summary>
public bool DisableExpCorrectionForMyPawn
{
get
{
return GetSetting<bool>("DisableExpCorrectionForMyPawn");
}
}
/// <summary>
/// Global modifier for enemy exp calculations to scale up or down.
/// </summary>
public double EnemyExpModifier
{
get
{
return GetSetting<double>("EnemyExpModifier");
}
}
/// <summary>
/// Global modifier for quest exp calculations to scale up or down.
/// </summary>
public double QuestExpModifier
{
get
{
return GetSetting<double>("QuestExpModifier");
}
}
/// <summary>
/// Global modifier for pp calculations to scale up or down.
/// </summary>
public double PpModifier
{
get
{
return GetSetting<double>("PpModifier");
}
}
/// <summary>
/// Global modifier for Gold calculations to scale up or down.
/// </summary>
public double GoldModifier
{
get
{
return GetSetting<double>("GoldModifier");
}
}
/// <summary>
/// Global modifier for Rift calculations to scale up or down.
/// </summary>
public double RiftModifier
{
get
{
return GetSetting<double>("RiftModifier");
}
}
/// <summary>
/// Global modifier for BO calculations to scale up or down.
/// </summary>
public double BoModifier
{
get
{
return GetSetting<double>("BoModifier");
}
}
/// <summary>
/// Global modifier for HO calculations to scale up or down.
/// </summary>
public double HoModifier
{
get
{
return GetSetting<double>("HoModifier");
}
}
/// <summary>
/// Global modifier for JP calculations to scale up or down.
/// </summary>
public double JpModifier
{
get
{
return GetSetting<double>("JpModifier");
}
}
/// <summary>
/// Configures the maximum amount of reward box slots.
/// </summary>
public byte RewardBoxMax
{
get
{
return GetSetting<byte>("RewardBoxMax");
}
}
/// <summary>
/// Configures the maximum amount of quests that can be ordered at one time.
/// </summary>
public byte QuestOrderMax
{
get
{
return GetSetting<byte>("QuestOrderMax");
}
}
/// <summary>
/// Configures if epitaph rewards are limited once per weekly reset.
/// </summary>
public bool EnableEpitaphWeeklyRewards
{
get
{
return GetSetting<bool>("EnableEpitaphWeeklyRewards");
}
}
/// <summary>
/// Enables main pawns in party to gain EXP and JP from quests
/// Original game apparantly did not have pawns share quest reward, so will set to false for default,
/// change as needed
/// </summary>
public bool EnableMainPartyPawnsQuestRewards
{
get
{
return GetSetting<bool>("EnableMainPartyPawnsQuestRewards");
}
}
/// <summary>
/// Specifies the time in seconds that a bazaar exhibit will last.
/// By default, the equivalent of 3 days
/// </summary>
public ulong BazaarExhibitionTimeSeconds
{
get
{
return GetSetting<ulong>("BazaarExhibitionTimeSeconds");
}
}
/// <summary>
/// Specifies the time in seconds that a slot in the bazaar won't be able to be used again.
/// By default, the equivalent of 1 day
/// </summary>
public ulong BazaarCooldownTimeSeconds
{
get
{
return GetSetting<ulong>("BazaarCooldownTimeSeconds");
}
}
/// <summary>
/// Various URLs used by the client.
/// Shared with the login server.
/// </summary>
public string UrlManual
{
get
{
return GetSetting<string>("UrlManual");
}
}
public string UrlShopDetail
{
get
{
return GetSetting<string>("UrlShopDetail");
}
}
public string UrlShopCounterA
{
get
{
return GetSetting<string>("UrlShopCounterA");
}
}
public string UrlShopAttention
{
get
{
return GetSetting<string>("UrlShopAttention");
}
}
public string UrlShopStoneLimit
{
get
{
return GetSetting<string>("UrlShopStoneLimit");
}
}
public string UrlShopCounterB
{
get
{
return GetSetting<string>("UrlShopCounterB");
}
}
public string UrlChargeCallback
{
get
{
return GetSetting<string>("UrlChargeCallback");
}
}
public string UrlChargeA
{
get
{
return GetSetting<string>("UrlChargeA");
}
}
public string UrlSample9
{
get
{
return GetSetting<string>("UrlSample9");
}
}
public string UrlSample10
{
get
{
return GetSetting<string>("UrlSample10");
}
}
public string UrlCampaignBanner
{
get
{
return GetSetting<string>("UrlCampaignBanner");
}
}
public string UrlSupportIndex
{
get
{
return GetSetting<string>("UrlSupportIndex");
}
}
public string UrlPhotoupAuthorize
{
get
{
return GetSetting<string>("UrlPhotoupAuthorize");
}
}
public string UrlApiA
{
get
{
return GetSetting<string>("UrlApiA");
}
}
public string UrlApiB
{
get
{
return GetSetting<string>("UrlApiB");
}
}
public string UrlIndex
{
get
{
return GetSetting<string>("UrlIndex");
}
}
public string UrlCampaign
{
get
{
return GetSetting<string>("UrlCampaign");
}
}
public string UrlChargeB
{
get
{
return GetSetting<string>("UrlChargeB");
}
}
public string UrlCompanionImage
{
get
{
return GetSetting<string>("UrlCompanionImage");
}
}
}
}

View file

@ -0,0 +1,52 @@
using Arrowgene.Ddon.GameServer.Scripting;
using Arrowgene.Ddon.Server.Scripting.interfaces;
using Arrowgene.Ddon.Server.Scripting.utils;
using Arrowgene.Ddon.Shared.Model;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Scripting;
using System.IO;
namespace Arrowgene.Ddon.Server.Scripting.modules
{
public class GameServerSettingsModule : ScriptModule
{
public override string ModuleRoot => "settings";
public override string Filter => "*.csx";
public override bool ScanSubdirectories => true;
public override bool EnableHotLoad => true;
/// <summary>
/// Settings data is organized as ScriptName.FieldName
/// </summary>
private ScriptableSettings SettingsData { get; set; }
public GameLogicSetting GameLogicSetting { get; private set; }
public GameServerSettingsModule()
{
SettingsData = new ScriptableSettings();
GameLogicSetting = new GameLogicSetting(SettingsData);
}
public override ScriptOptions Options()
{
return ScriptOptions.Default
.AddReferences(MetadataReference.CreateFromFile(typeof(GameLogicSetting).Assembly.Location))
.AddReferences(MetadataReference.CreateFromFile(typeof(WalletType).Assembly.Location))
.AddImports("System", "System.Collections", "System.Collections.Generic")
.AddImports("Arrowgene.Ddon.Shared.Model")
.AddImports("Arrowgene.Ddon.Shared.Model.Quest");
}
public override bool EvaluateResult(string path, ScriptState<object> result)
{
var scriptName = Path.GetFileNameWithoutExtension(path);
foreach (var variable in result.Variables)
{
SettingsData.Set(scriptName, variable.Name, variable.Value);
}
return true;
}
}
}

View file

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
namespace Arrowgene.Ddon.Server.Scripting.utils
{
public class ScriptableSettings
{
/// <summary>
/// Settings data is organized as ScriptName.FieldName
/// </summary>
private Dictionary<string, Dictionary<string, object>> Data { get; set; }
public ScriptableSettings()
{
Data = new Dictionary<string, Dictionary<string, object>>();
}
public Dictionary<string, object> GetScriptData(string scriptName)
{
if (!Data.ContainsKey(scriptName))
{
throw new Exception($"The script '{scriptName}' doesn't exist");
}
return Data[scriptName];
}
public T Get<T>(string scriptName, string variableName)
{
if (!Data.ContainsKey(scriptName))
{
throw new Exception($"The script '{scriptName}' doesn't exist");
}
if (!Data[scriptName].ContainsKey(variableName))
{
throw new Exception($"The setting '{scriptName}.{variableName}' doesn't exist");
}
return (T)Data[scriptName][variableName];
}
public void Set<T>(string scriptName, string variableName, T value)
{
if (!Data.ContainsKey(scriptName))
{
Data[scriptName] = new Dictionary<string, object>();
}
Data[scriptName][variableName] = value;
}
}
}

View file

@ -0,0 +1,36 @@
using Arrowgene.Ddon.Server.Scripting.interfaces;
using Arrowgene.Ddon.Server.Scripting.modules;
using Arrowgene.Ddon.Shared.Scripting;
using Arrowgene.Logging;
namespace Arrowgene.Ddon.Server
{
public class Globals
{
// Currently no script globals are required
// but leave this class as a way to introduce
// globals if needed.
}
public class ServerScriptManager : ScriptManager<Globals>
{
private static readonly ServerLogger Logger = LogProvider.Logger<ServerLogger>(typeof(ServerScriptManager));
private Globals Globals { get; set; }
public GameServerSettingsModule GameServerSettings { get; private set; } = new GameServerSettingsModule();
public ServerScriptManager(string assetsPath) : base(assetsPath)
{
Globals = new Globals();
// Add modules to the list so the generic logic can iterate over all scripting modules
ScriptModules[GameServerSettings.ModuleRoot] = GameServerSettings;
}
public override void Initialize()
{
base.Initialize(Globals);
}
}
}

View file

@ -38,25 +38,8 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Files\Assets\scripts\events\" />
<Folder Include="Files\Assets\scripts\quests\" />
</ItemGroup>
<ItemGroup>
<None Update="Files\Assets\scripts\extended_facilities\Anita1.csx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Files\Assets\scripts\extended_facilities\Damad1.csx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Files\Assets\scripts\extended_facilities\Isel1.csx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Files\Assets\scripts\extended_facilities\Pehr1.csx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Files\Assets\scripts\GameLogicSettings.csx">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<Content Include="Files\Assets\scripts\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View file

@ -1,163 +0,0 @@
/**
* Settings file for Server customization.
* This file supports hotloading.
*/
// Generic Server Settings
GameLogicSetting.NaiveLobbyContextHandling = true;
// Crafting Settings
GameLogicSetting.AdditionalProductionSpeedFactor = 1.0;
GameLogicSetting.AdditionalCostPerformanceFactor = 1.0;
GameLogicSetting.CraftConsumableProductionTimesMax = 10;
// Exp Ring Settings
GameLogicSetting.RookiesRingMaxLevel = 89;
GameLogicSetting.RookiesRingBonus = 1.0;
// EXP Penalty Settings
/**
* @brief Handles EXP penalties for the party based on the
* difference between the lowest leveled member and highest
* leveled member of the party. If the range is larger than
* the last entry in AdjustPartyEnemyExpTiers, a 0% exp rate
* is automatically applied.
*
* Can be turned on/off by configuring AdjustPartyEnemyExp.
*/
GameLogicSetting.EnableAdjustPartyEnemyExp = true;
GameLogicSetting.AdjustPartyEnemyExpTiers = new List<(uint MinLv, uint MaxLv, double ExpMultiplier)>()
{
// MinLv and MaxLv define the relative level difference between the levels of the lowest and
// highest members in the party.
// The ExpMultiplier value can be a value between [0.0, 1.0] (1.0 = 100%, 0.0 = 0%)
//
// MinLv, MaxLv, ExpMultiplier
( 0, 2, 1.0),
( 3, 4, 0.9),
( 5, 6, 0.8),
( 7, 8, 0.6),
( 9, 10, 0.5),
};
/**
* @brief Handles EXP penalties based on the highest leveled member
* in the party and the level of the target enemy. If the range is
* larger than the last entry in AdjustTargetLvEnemyExpTiers, a 0%
* exp rate is automatically applied.
*
* Can be turned on/off by configuring AdjustTargetLvEnemyExp.
*/
GameLogicSetting.EnableAdjustTargetLvEnemyExp = false;
GameLogicSetting.AdjustTargetLvEnemyExpTiers = new List<(uint MinLv, uint MaxLv, double ExpMultiplier)>()
{
// MinLv and MaxLv define the relative level difference between the target and highest member in the party.
// The ExpMultiplier value can be a value between [0.0, 1.0] (1.0 = 100%, 0.0 = 0%)
//
// MinLv, MaxLv, ExpMultiplier
( 0, 2, 1.0),
( 3, 4, 0.9),
( 5, 6, 0.8),
( 7, 8, 0.6),
( 9, 10, 0.5),
};
// Pawn Catchup Settings
GameLogicSetting.EnablePawnCatchup = true;
GameLogicSetting.PawnCatchupMultiplier = 1.5;
GameLogicSetting.PawnCatchupLvDiff = 5;
// Game Time Settings
GameLogicSetting.GameClockTimescale = 90;
// Weather Settings
GameLogicSetting.WeatherSequenceLength = 20;
GameLogicSetting.WeatherStatistics = new List<(uint MeanLength, uint Weight)>()
{
(60 * 30, 1), // Fair
(60 * 30, 1), // Cloudy
(60 * 30, 1), // Rainy
};
// Account Settings
GameLogicSetting.CharacterNumMax = 4;
GameLogicSetting.FriendListMax = 200;
// Player Settings
GameLogicSetting.JobLevelMax = 120;
GameLogicSetting.EnableVisualEquip = true;
GameLogicSetting.DefaultWarpFavorites = 3;
GameLogicSetting.LaternBurnTimeInSeconds = 1500;
// Pawn Settings
GameLogicSetting.EnableMainPartyPawnsQuestRewards = false;
// Bazaar Settings
GameLogicSetting.DefaultMaxBazaarExhibits = 5;
GameLogicSetting.BazaarExhibitionTimeSeconds = (ulong) TimeSpan.FromDays(3).TotalSeconds;
GameLogicSetting.BazaarCooldownTimeSeconds = (ulong) TimeSpan.FromDays(1).TotalSeconds;
// Clan Settings
GameLogicSetting.ClanMemberMax = 100;
// Epitaph Settings
GameLogicSetting.EnableEpitaphWeeklyRewards = false;
// Point Settings
GameLogicSetting.PlayPointMax = 2000;
// Global Point Modifiers
GameLogicSetting.EnemyExpModifier = 1;
GameLogicSetting.QuestExpModifier = 1;
GameLogicSetting.PpModifier = 1;
GameLogicSetting.GoldModifier = 1;
GameLogicSetting.RiftModifier = 1;
GameLogicSetting.BoModifier = 1;
GameLogicSetting.HoModifier = 1;
GameLogicSetting.JpModifier = 1;
GameLogicSetting.RewardBoxMax = 100;
GameLogicSetting.QuestOrderMax = 20;
// Wallet Settings
GameLogicSetting.WalletLimits = new Dictionary<WalletType, uint>()
{
{WalletType.Gold, 999999999},
{WalletType.RiftPoints, 999999999},
{WalletType.BloodOrbs, 500000},
{WalletType.SilverTickets, 999999999},
{WalletType.GoldenGemstones, 99999},
{WalletType.RentalPoints, 99999},
{WalletType.ResetJobPoints, 99},
{WalletType.ResetCraftSkills, 99},
{WalletType.HighOrbs, 5000},
{WalletType.DominionPoints, 999999999},
{WalletType.AdventurePassPoints, 80},
{WalletType.UnknownTickets, 999999999},
{WalletType.BitterblackMazeResetTicket, 3},
{WalletType.GoldenDragonMark, 30},
{WalletType.SilverDragonMark, 150},
{WalletType.RedDragonMark, 99999},
};
// URL Settings
string urlDomain = $"http://localhost:{52099}";
GameLogicSetting.UrlManual = $"{urlDomain}/manual_nfb/";
GameLogicSetting.UrlShopDetail = $"{urlDomain}/shop/ingame/stone/detail";
GameLogicSetting.UrlShopCounterA = $"{urlDomain}/shop/ingame/counter?";
GameLogicSetting.UrlShopAttention = $"{urlDomain}/shop/ingame/attention?";
GameLogicSetting.UrlShopStoneLimit = $"{urlDomain}/shop/ingame/stone/limit";
GameLogicSetting.UrlShopCounterB = $"{urlDomain}/shop/ingame/counter?";
GameLogicSetting.UrlChargeCallback = $"{urlDomain}/opening/entry/ddo/cog_callback/charge";
GameLogicSetting.UrlChargeA = $"{urlDomain}/sp_ingame/charge/";
GameLogicSetting.UrlSample9 = "http://sample09.html";
GameLogicSetting.UrlSample10 = "http://sample10.html";
GameLogicSetting.UrlCampaignBanner = $"{urlDomain}/sp_ingame/campaign/bnr/bnr01.html?";
GameLogicSetting.UrlSupportIndex = $"{urlDomain}/sp_ingame/support/index.html";
GameLogicSetting.UrlPhotoupAuthorize = $"{urlDomain}/api/photoup/authorize";
GameLogicSetting.UrlApiA = $"{urlDomain}/link/api";
GameLogicSetting.UrlApiB = $"{urlDomain}/link/api";
GameLogicSetting.UrlIndex = $"{urlDomain}/sp_ingame/link/index.html";
GameLogicSetting.UrlCampaign = $"{urlDomain}/sp_ingame/campaign/bnr/slide.html";
GameLogicSetting.UrlChargeB = $"{urlDomain}/sp_ingame/charge/";
GameLogicSetting.UrlCompanionImage = $"{urlDomain}/";

View file

@ -0,0 +1,12 @@
# DDON Scripting
The DDON scripting is intended to expose certain server internal details of the game server that a server admin wish to be configure.
While initially implemented as JSON file, as more complex features come into the picture, a more complex configuration archirecture
is required.
The scripting root is `scripts` directory inside the assets directory. Internally the functions which perform reverse lookups for modules will
terminate once reaching the root.
Each directory inside scripts defines the scripting module. A module name should be all lowercase. Inside each module, there should be a `README.md`
file which describes the purpose and usage of the module. It should also describe any guidelines required. When implementing a module, be aware if
you want the module to be hotloadable. If you do, make sure to program in such a way that the settings can reflect as such after an update.

View file

@ -0,0 +1,10 @@
# NPC Extended Facilities
It is possible to inject new NPC options by defining what the game calls `NpcExtendedFacilities`. The type of menu options what can be added are defined in the class [NpcFunction.cs](https://github.com/sebastian-heinz/Arrowgene.DragonsDogmaOnline/blob/develop/Arrowgene.Ddon.Shared/Model/NpcFunction.cs).
## Guidelines
- Name the file after the named constant in [NpcId.cs](https://github.com/sebastian-heinz/Arrowgene.DragonsDogmaOnline/blob/develop/Arrowgene.Ddon.Shared/Model/NpcId.cs)
- The abstract class `INpcExtendedFacility` is used as the interface module.
- When extending `INpcExtendedFacility` keep it simple and name it `NpcExtendedFacility`. The script engine will mangle the object name this avoiding any sort of class name conflicts.
- At the bottom of the file return a new `NpcExtendedFacility`.

View file

@ -0,0 +1,159 @@
/**
* Settings file for Server customization.
* This file supports hotloading.
*/
// Generic Server Settings
bool NaiveLobbyContextHandling = true;
uint GameClockTimescale = 90;
// Game Settings
byte RewardBoxMax = 100;
byte QuestOrderMax = 20;
byte CharacterNumMax = 4;
uint FriendListMax = 200;
uint JobLevelMax = 120;
bool EnableVisualEquip = true;
uint DefaultWarpFavorites = 3;
uint LaternBurnTimeInSeconds = 1500;
// Crafting Settings
double AdditionalProductionSpeedFactor = 1.0;
double AdditionalCostPerformanceFactor = 1.0;
double CraftConsumableProductionTimesMax = 10;
// Exp Ring Settings
bool EnableRookiesRing = false;
uint RookiesRingMaxLevel = 89;
double RookiesRingBonus = 1.0;
// EXP Penalty Settings
/**
* @brief Handles EXP penalties for the party based on the
* difference between the lowest leveled member and highest
* leveled member of the party. If the range is larger than
* the last entry in AdjustPartyEnemyExpTiers, a 0% exp rate
* is automatically applied.
*
* Can be turned on/off by configuring AdjustPartyEnemyExp.
*/
bool EnableAdjustPartyEnemyExp = true;
var AdjustPartyEnemyExpTiers = new List<(uint MinLv, uint MaxLv, double ExpMultiplier)>()
{
// MinLv and MaxLv define the relative level difference between the levels of the lowest and
// highest members in the party.
// The ExpMultiplier value can be a value between [0.0, 1.0] (1.0 = 100%, 0.0 = 0%)
//
// MinLv, MaxLv, ExpMultiplier
( 0, 2, 1.0),
( 3, 4, 0.9),
( 5, 6, 0.8),
( 7, 8, 0.6),
( 9, 10, 0.5),
};
/**
* @brief Handles EXP penalties based on the highest leveled member
* in the party and the level of the target enemy. If the range is
* larger than the last entry in AdjustTargetLvEnemyExpTiers, a 0%
* exp rate is automatically applied.
*
* Can be turned on/off by configuring AdjustTargetLvEnemyExp.
*/
bool EnableAdjustTargetLvEnemyExp = false;
var AdjustTargetLvEnemyExpTiers = new List<(uint MinLv, uint MaxLv, double ExpMultiplier)>()
{
// MinLv and MaxLv define the relative level difference between the target and highest member in the party.
// The ExpMultiplier value can be a value between [0.0, 1.0] (1.0 = 100%, 0.0 = 0%)
//
// MinLv, MaxLv, ExpMultiplier
( 0, 2, 1.0),
( 3, 4, 0.9),
( 5, 6, 0.8),
( 7, 8, 0.6),
( 9, 10, 0.5),
};
// Weather Settings
uint WeatherSequenceLength = 20;
var WeatherStatistics = new List<(uint MeanLength, uint Weight)>()
{
(60 * 30, 1), // Fair
(60 * 30, 1), // Cloudy
(60 * 30, 1), // Rainy
};
// Pawn Settings
bool EnableMainPartyPawnsQuestRewards = false;
bool DisableExpCorrectionForMyPawn = true;
bool EnablePawnCatchup = true;
double PawnCatchupMultiplier = 1.5;
uint PawnCatchupLvDiff = 5;
// Bazaar Settings
uint DefaultMaxBazaarExhibits = 5;
ulong BazaarExhibitionTimeSeconds = (ulong) TimeSpan.FromDays(3).TotalSeconds;
ulong BazaarCooldownTimeSeconds = (ulong) TimeSpan.FromDays(1).TotalSeconds;
// Clan Settings
uint ClanMemberMax = 100;
// Epitaph Settings
bool EnableEpitaphWeeklyRewards = false;
// Point Settings
uint PlayPointMax = 2000;
// Global Point Modifiers
double EnemyExpModifier = 1;
double QuestExpModifier = 1;
double PpModifier = 1;
double GoldModifier = 1;
double RiftModifier = 1;
double BoModifier = 1;
double HoModifier = 1;
double JpModifier = 1;
// Wallet Settings
var WalletLimits = new Dictionary<WalletType, uint>()
{
{WalletType.Gold, 999999999},
{WalletType.RiftPoints, 999999999},
{WalletType.BloodOrbs, 500000},
{WalletType.SilverTickets, 999999999},
{WalletType.GoldenGemstones, 99999},
{WalletType.RentalPoints, 99999},
{WalletType.ResetJobPoints, 99},
{WalletType.ResetCraftSkills, 99},
{WalletType.HighOrbs, 5000},
{WalletType.DominionPoints, 999999999},
{WalletType.AdventurePassPoints, 80},
{WalletType.UnknownTickets, 999999999},
{WalletType.BitterblackMazeResetTicket, 3},
{WalletType.GoldenDragonMark, 30},
{WalletType.SilverDragonMark, 150},
{WalletType.RedDragonMark, 99999},
};
// URL Settings
string urlDomain = $"http://localhost:{52099}";
string UrlManual = $"{urlDomain}/manual_nfb/";
string UrlShopDetail = $"{urlDomain}/shop/ingame/stone/detail";
string UrlShopCounterA = $"{urlDomain}/shop/ingame/counter?";
string UrlShopAttention = $"{urlDomain}/shop/ingame/attention?";
string UrlShopStoneLimit = $"{urlDomain}/shop/ingame/stone/limit";
string UrlShopCounterB = $"{urlDomain}/shop/ingame/counter?";
string UrlChargeCallback = $"{urlDomain}/opening/entry/ddo/cog_callback/charge";
string UrlChargeA = $"{urlDomain}/sp_ingame/charge/";
string UrlSample9 = "http://sample09.html";
string UrlSample10 = "http://sample10.html";
string UrlCampaignBanner = $"{urlDomain}/sp_ingame/campaign/bnr/bnr01.html?";
string UrlSupportIndex = $"{urlDomain}/sp_ingame/support/index.html";
string UrlPhotoupAuthorize = $"{urlDomain}/api/photoup/authorize";
string UrlApiA = $"{urlDomain}/link/api";
string UrlApiB = $"{urlDomain}/link/api";
string UrlIndex = $"{urlDomain}/sp_ingame/link/index.html";
string UrlCampaign = $"{urlDomain}/sp_ingame/campaign/bnr/slide.html";
string UrlChargeB = $"{urlDomain}/sp_ingame/charge/";
string UrlCompanionImage = $"{urlDomain}/";

View file

@ -0,0 +1,18 @@
# Scriptable Settings
This file attemps to establish some guidelines when defining new settings which are parsed by the scripting engine.
## General Considerations
While implementing new GameLogicSettings, consider the possibility that the file can be hotlaoded after the server starts and code according to that assumption.
If the settings interacts with a feature which uses some caching mechanism, make sure to invalid all those caches when the file is reloaded.
## Naming Guidelines
#### Enable Variables
All settings which can enable a feature when set to `true`, should start with the prefix `Enable`.
### Disable Variables
All settings which can disable a feature when set to `true`, should start with the prefix `Disable`.

View file

@ -1,6 +1,8 @@
using System.Collections.Generic;
using Arrowgene.Ddon.GameServer.Scripting;
using Arrowgene.Ddon.Server;
using Arrowgene.Ddon.Server.Scripting.interfaces;
using Arrowgene.Ddon.Server.Scripting.utils;
using Arrowgene.Ddon.Shared;
using Arrowgene.Ddon.Shared.Entity.Structure;
using Arrowgene.Ddon.Shared.Model;
@ -13,22 +15,23 @@ public class CraftManagerTest
{
private readonly DdonGameServer _mockServer;
private readonly CraftManager _craftManager;
private readonly ScriptableSettings _scriptableSettings;
public CraftManagerTest()
{
var settings = new GameServerSetting();
var gameLogicSetting = new GameLogicSetting();
gameLogicSetting.GameClockTimescale = 90;
gameLogicSetting.WeatherSequenceLength = 20;
gameLogicSetting.WeatherStatistics = new List<(uint MeanLength, uint Weight)>()
_scriptableSettings = new ScriptableSettings();
_scriptableSettings.Set("GameLogicSettings", "GameClockTimescale", 90);
_scriptableSettings.Set("GameLogicSettings", "WeatherSequenceLength", 20);
_scriptableSettings.Set("GameLogicSettings", "WeatherStatistics", new List<(uint MeanLength, uint Weight)>()
{
(60 * 30, 1), // Fair
(60 * 30, 1), // Cloudy
(60 * 30, 1), // Rainy
};
});
var gameLogicSetting = new GameLogicSetting(_scriptableSettings);
_mockServer = new DdonGameServer(settings, gameLogicSetting, new MockDatabase(), new AssetRepository("TestFiles"));
_craftManager = new CraftManager(_mockServer);
}
@ -37,7 +40,7 @@ public class CraftManagerTest
public void GetCraftingTimeReductionRate_ShouldReturnCorrectValue()
{
List<uint> productionSpeedLevels = new List<uint> { 10, 20, 30 };
_mockServer.GameLogicSettings.AdditionalProductionSpeedFactor = 1.0;
_scriptableSettings.Set("GameLogicSettings", "AdditionalProductionSpeedFactor", 1.0);
double result = _craftManager.GetCraftingTimeReductionRate(productionSpeedLevels);
@ -49,7 +52,7 @@ public class CraftManagerTest
{
List<uint> productionSpeedLevels = new List<uint> { 70, 70, 70, 70 };
const uint recipeTime = 100;
_mockServer.GameLogicSettings.AdditionalProductionSpeedFactor = 1.0;
_scriptableSettings.Set("GameLogicSettings", "AdditionalProductionSpeedFactor", 1.0);
uint result = _craftManager.CalculateRecipeProductionSpeed(recipeTime, productionSpeedLevels);
@ -61,7 +64,7 @@ public class CraftManagerTest
{
List<uint> productionSpeedLevels = new List<uint> { 70, 70, 70, 70 };
const uint recipeTime = 100;
_mockServer.GameLogicSettings.AdditionalProductionSpeedFactor = 100;
_scriptableSettings.Set("GameLogicSettings", "AdditionalProductionSpeedFactor", 100.0);
uint result = _craftManager.CalculateRecipeProductionSpeed(recipeTime, productionSpeedLevels);