Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions sql/updates/update_2025-01-14_1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TABLE `storage_team` (
`accountId` bigint(20) NOT NULL,
`itemId` bigint(20) NOT NULL,
`position` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;

ALTER TABLE `storage_team`
ADD PRIMARY KEY (`accountId`,`itemId`),
ADD KEY `itemId` (`itemId`);

ALTER TABLE `storage_team`
ADD CONSTRAINT `storage_team_ibfk_1` FOREIGN KEY (`accountId`) REFERENCES `accounts` (`accountId`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `storage_team_ibfk_2` FOREIGN KEY (`itemId`) REFERENCES `items` (`itemUniqueId`) ON DELETE CASCADE ON UPDATE CASCADE;
6 changes: 6 additions & 0 deletions src/Shared/Configuration/Files/Web.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ public class WebConfFile : ConfFile
/// </summary>
public List<CgiProcessor> CgiProcessors { get; private set; }

/// <summary>
/// Returns the port for the guild web server.
/// </summary>
public int GuildPort { get; private set; }

/// <summary>
/// Loads conf file and its options from the given path.
/// </summary>
Expand All @@ -30,6 +35,7 @@ public void Load(string filePath)

this.EnableApiAccountCreation = this.GetBool("enable_api_account_creation", false);
this.CgiProcessors = this.GetCgiProcessors();
this.GuildPort = this.GetInt("guild_port", 9004);
}

/// <summary>
Expand Down
13 changes: 13 additions & 0 deletions src/Shared/Configuration/Files/World.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ public class WorldConfFile : ConfFile
public int StorageMaxSize { get; protected set; }
public int StorageMaxExtensions { get; protected set; }
public bool StorageMultiStack { get; protected set; }
public int TeamStorageFee { get; protected set; }
public int TeamStorageDefaultSize { get; protected set; }
public int TeamStorageExtCost { get; protected set; }
public int TeamStorageMaxSilverExpands { get; protected set; }
public int TeamStorageMaxSize { get; protected set; }
public int TeamStorageMinimumLevelRequired { get; protected set; }

// summons.conf
public bool BlueOrbFollowWarp { get; protected set; }
Expand Down Expand Up @@ -123,6 +129,13 @@ public void Load(string filePath)
this.StorageMaxSize = this.GetInt("storage_max_size", 110);
this.StorageMultiStack = this.GetBool("storage_multi_stack", true);

this.TeamStorageFee = this.GetInt("team_storage_fee", 0);
this.TeamStorageDefaultSize = this.GetInt("team_storage_default_size", 5);
this.TeamStorageExtCost = this.GetInt("team_storage_ext_cost", 200000);
this.TeamStorageMaxSilverExpands = this.GetInt("team_storage_max_silver_expands", 9);
this.TeamStorageMaxSize = this.GetInt("team_storage_max_size", 70);
this.TeamStorageMinimumLevelRequired = this.GetInt("team_storage_min_level_req", 15);

this.ExpRate = this.GetFloat("exp_rate", 100);
this.JobExpRate = this.GetFloat("job_exp_rate", 100);

Expand Down
3 changes: 2 additions & 1 deletion src/Shared/Game/Const/Items.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,8 @@ public enum InventoryItemRemoveMsg : byte
public enum InventoryType : byte
{
Inventory = 0,
Warehouse = 1,
PersonalStorage = 1,
TeamStorage = 6,
}

public enum InventoryAddType : byte
Expand Down
50 changes: 50 additions & 0 deletions src/Shared/Game/Const/Web/AccountWarehouse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Melia.Shared.Game.Const.Web
{
public class AccountWarehouse
{
public AccountWarehouse()
{
this.List = new List<List>
{
new List()
{
Index = "0",
Title = "",
},
new List()
{
Index = "1",
Title = "",
},
new List()
{
Index = "2",
Title = "",
},
new List()
{
Index = "3",
Title = "",
}
};
}
[JsonProperty("list")]
public List<List> List { get; set; }
}

public class List
{
[JsonProperty("index")]
public string Index { get; set; }

[JsonProperty("title")]
public string Title { get; set; }
}
}
1 change: 1 addition & 0 deletions src/Shared/Network/NormalOp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public static class Zone
public const int SetSessionKey = 0x14F;
public const int ItemDrop = 0x152;
public const int NGSCallback = 0x170;
public const int StorageSilverTransaction = 0x171;
public const int HeadgearVisibilityUpdate = 0x17C;
public const int UpdateSkillUI = 0x189;
public const int AdventureBook = 0x197;
Expand Down
14 changes: 14 additions & 0 deletions src/WebServer/Controllers/BaseController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ protected async Task SendText(string mimeType, HttpStatusCode statusCode, string
await sw.WriteAsync(content);
}

/// <summary>
/// Sends content as binary with the given mime type.
/// </summary>
/// <param name="mimeType"></param>
/// <param name="content"></param>
protected void SendBinary(string mimeType, byte[] content)
{
this.Response.StatusCode = 200;
this.Response.ContentType = mimeType;

using (var stream = this.Response.OutputStream)
stream.Write(content, 0, content.Length);
}

/// <summary>
/// A string writer that uses UTF-8 without BOM (byte-order mark).
/// </summary>
Expand Down
49 changes: 49 additions & 0 deletions src/WebServer/Controllers/TosGuildController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.IO;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using EmbedIO;
using EmbedIO.Routing;
using Melia.Shared.Game.Const.Web;
using Melia.Web.Const;
using Melia.Web.Util;

namespace Melia.Web.Controllers
{
/// <summary>
/// Controller for TOS Guild and Team Storage related endpoints.
/// </summary>
public class TosGuildController : BaseController
{
/// <summary>
/// Returns the list of account warehouse (team storage) tabs.
/// </summary>
/// <remarks>
/// This endpoint is called by the client when accessing team storage
/// to display the storage tabs/categories in the UI.
/// </remarks>
[Route(HttpVerbs.Get, "/accountwarehouse/get")]
public async Task GetAccountWarehouse()
{
// Return the default account warehouse structure with 4 tabs
// The client uses this to display the storage tabs in the UI
var accountWarehouse = new AccountWarehouse();
var json = JsonSerializer.Serialize(accountWarehouse);

await this.SendText(MimeTypes.Text.Plain, json);
}

/// <summary>
/// Stub implementation for guild emblem endpoint.
/// </summary>
/// <param name="guildId">The guild ID</param>
/// <returns></returns>
[Route(HttpVerbs.Get, "/guildemblem/{guildId}")]
public async Task GetGuildEmblem(long guildId)
{
// Stub: Just return empty success response
await this.SendText(MimeTypes.Text.Plain, string.Empty);
}
}
}
50 changes: 50 additions & 0 deletions src/WebServer/Util/FileUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace Melia.Web.Util
{
/// <summary>
/// Utility class for file operations.
/// </summary>
public static class FileUtils
{
/// <summary>
/// Computes MD5 hash of the given byte array.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public static string GetMD5Hash(byte[] input)
{
var md5 = MD5.Create();
var array = md5.ComputeHash(input);
var stringBuilder = new StringBuilder();

for (var i = 0; i < array.Length; i++)
{
stringBuilder.Append(array[i].ToString("x2"));
}

return stringBuilder.ToString();
}

/// <summary>
/// Reads a file and returns its contents as a byte array.
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public static byte[] FileToByteArray(string fileName)
{
byte[] fileData = null;

using (var fs = File.OpenRead(fileName))
{
using (var binaryReader = new BinaryReader(fs))
{
fileData = binaryReader.ReadBytes((int)fs.Length);
}
}
return fileData;
}
}
}
50 changes: 50 additions & 0 deletions src/WebServer/WebServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class WebServer : Server
public readonly static WebServer Instance = new();

private EmbedIO.WebServer _server;
private EmbedIO.WebServer _guildServer;

/// <summary>
/// Returns the server's inter-server communicator.
Expand Down Expand Up @@ -60,6 +61,7 @@ public override void Run(string[] args)
this.CheckDependencies();

this.StartWebServer();
this.StartGuildWebServer();
this.StartCommunicator();

ConsoleUtil.RunningTitle();
Expand Down Expand Up @@ -367,5 +369,53 @@ private void StartWebServer()
ConsoleUtil.Exit(1);
}
}

/// <summary>
/// Starts guild web server.
/// </summary>
private void StartGuildWebServer()
{
try
{
var url = string.Format("http://*:{0}/", this.Conf.Web.GuildPort);

Swan.Logging.Logger.NoLogging();
Swan.Logging.Logger.RegisterLogger(new YggdrasilLogger(this.Conf.Log.Filter));

EndPointManager.UseIpv6 = false;

var options = new WebServerOptions()
.WithMode(HttpListenerMode.EmbedIO)
.WithUrlPrefix(url);
_guildServer = new EmbedIO.WebServer(options);

var webFolder = "system/web/";
if (Directory.Exists("user/web/"))
webFolder = "user/web/";

_guildServer
.WithWebApi("/", m => m.WithController<TosGuildController>())
.WithStaticFolder("/", webFolder, false, fm =>
{
fm.DefaultDocument = "index.htm";
fm.OnMappingFailed = FileRequestHandler.PassThrough;
fm.OnDirectoryNotListable = FileRequestHandler.PassThrough;
});
_guildServer.RunAsync();

if (_guildServer.State == WebServerState.Stopped)
{
Log.Error("Failed to start guild server, make sure there's only one instance running.");
ConsoleUtil.Exit(1);
}

Log.Status("Guild Server now running on '{0}'", url);
}
catch (Exception ex)
{
Log.Error("Failed to start guild web server: {0}", ex);
ConsoleUtil.Exit(1);
}
}
}
}
6 changes: 6 additions & 0 deletions src/ZoneServer/Database/Account.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Melia.Shared.Scripting;
using Melia.Zone.World;
using Melia.Zone.World.Maps;
using Melia.Zone.World.Storage;

namespace Melia.Zone.Database
{
Expand Down Expand Up @@ -119,6 +120,11 @@ public PermissionLevel PermissionLevel
/// </summary>
public PremiumStatus Premium { get; } = new();

/// <summary>
/// Returns the account's team storage.
/// </summary>
public TeamStorage TeamStorage { get; set; }

/// <summary>
/// Creates new account.
/// </summary>
Expand Down
19 changes: 16 additions & 3 deletions src/ZoneServer/Database/ZoneDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,21 @@ public Character GetCharacter(long accountId, long characterId)
// non-character information, like account properties, have to be
// loaded from a different location.
character.PersonalStorage.InitSize();
character.TeamStorage.InitSize();
this.LoadStorage(character.PersonalStorage, "storage_personal", "characterId", character.DbId);
this.LoadStorage(character.TeamStorage, "storage_team", "accountId", character.AccountId);

return character;
}

/// <summary>
/// Finishes loading data that depends on both Account and Character objects being present.
/// </summary>
public void AfterLoad(Account account, Character character)
{
account.TeamStorage = new TeamStorage(character);
character.TeamStorage.InitSize();
this.LoadStorage(character.TeamStorage, "storage_team", "accountId", character.AccountId);
}

/// <summary>
/// Loads character's skills.
/// </summary>
Expand Down Expand Up @@ -696,7 +704,12 @@ internal void LoadStorage(Storage storage, string tableName, string idFieldName,

var item = new Item(itemId, amount);

storage.AddAtPosition(item, position, out var addedAmount);
if (itemId == ItemId.Silver && storage is TeamStorage teamStorage)
{
teamStorage.SetSilver(amount);
}
else
storage.AddAtPosition(item, position, out var addedAmount);
}
}
}
Expand Down
Loading