Skip to content

Commit 03c28e4

Browse files
committed
Add JsonItemDataSource, small cleanup (#24)
Client: Fix onClick -> onPress
1 parent d20bc03 commit 03c28e4

24 files changed

+270
-140
lines changed

README.md

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,35 +10,49 @@
1010

1111
## Usage
1212

13+
### Console / WEB UI
14+
1315
1. Get the GameInv server
14-
1. by downloading the [latest build from releases](https://github.com/MP3Martin/GameInv/releases/latest/)
16+
1. by downloading the [latest build from releases](https://github.com/MP3Martin/GameInv/releases/latest/) (the one
17+
with `.exe` and no `WPF` in its name)
1518
2. <details><summary>or ...</summary>or download the <a href="https://download-directory.github.io/?url=https%3A%2F%2Fgithub.com%2FMP3Martin%2FGameInv%2Ftree%2Fmain%2Fserver">server folder</a>, extract it and open it in your favourite IDE, install the required dependencies, modify the code however you want and run the program. Or just use any other way to build the project.</details>
1619
2. Run the program (the server)
1720
3. Press Y to enter the WS server mode.
18-
4. Press Y if you want to use [MySQL DB](#mysql). (has to be configured first, otherwise select N)
19-
5. Open https://mp3martin.github.io/GameInv/ in your browser and everything should connect automatically. Use the
21+
4. Open https://mp3martin.github.io/GameInv/ in your browser and everything should connect automatically. Use the
2022
cogwheel on the top right to change settings.
2123

2224
> [!WARNING]
23-
> Do not run the server publicly, it is just a demo/showcase and not advanced enough to be secure and handle attacks.
25+
> Do not run the server publicly, it is just a demo/showcase and not advanced enough to be perfectly secure and handle
26+
> attacks.
27+
28+
### Windows desktop GUI (WPF)
29+
30+
1. Get GameInv WPF by downloading
31+
the [latest build from releases](https://github.com/MP3Martin/GameInv/releases/latest/) (the one with `.exe` and
32+
`WPF` in its name)
33+
2. Run the program
2434

2535
> [!IMPORTANT]
2636
> Microsoft Defender sometimes reports the automatically built exes as a virus. The current fix is to allow the `.exe`
2737
> in Defender.
2838
39+
> [!TIP]
40+
> Read [configuration](#configuration)
41+
2942
## Configuration
3043

3144
### `.env`
3245

3346
This program allows you to configure various options through the `.env` file or by setting environment variables. The
3447
`.env` file has to be placed in the same directory as the program executable. The [`.env.example`](server/.env.example)
35-
file provides a description for these settings, so read it. To use it, rename `.env.example` to `.env` and modify the
48+
file provides a description for these settings, **so read it**. To use it, rename `.env.example` to `.env` and modify
49+
the
3650
values as needed. Note that environment variables set in the system or command line must be prefixed with `GAMEINV_`,
3751
but the `.env` file does not require this prefix.
3852

3953
### MySQL
4054

41-
If you want the items to be stored in a database (MySQL is currently the only supported one), then you have to connect
55+
If you want the items to be stored in a database (specifically MySQL), then you have to connect
4256
to / start a MySQL server and run the commands inside [gameinv.sql](server/gameinv.sql) to add the schema and the
4357
tables (find a tutorial elsewhere). Then [configure](#configuration) your program to use the MySQL DB.
4458

@@ -47,7 +61,7 @@ tables (find a tutorial elsewhere). Then [configure](#configuration) your progra
4761
There is also a WPF GUI version of the project in [server/GameInv-WPF](server/GameInv-WPF). It only supports Windows 7+
4862
and no
4963
other OS. Tested on Windows 11. That version does not have a WebSocket client, does not have TUI and only has an
50-
integrated WPF GUI with optional DB connection. The usage is the same as in [usage](#usage), but skip steps 3 and 5. If
64+
integrated WPF GUI with optional DB connection. If
5165
you want to download a prebuilt `.exe`, then find the one that has `WPF` in its
5266
name [in the latest release](https://github.com/MP3Martin/GameInv/releases/latest/).
5367

client/src/components/button/AddItemButton.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default function AddItemButton () {
1515

1616
return (
1717
<>
18-
<Button color={'primary'} startContent={<IconPlus />} onClick={() => {
18+
<Button color={'primary'} startContent={<IconPlus />} onPress={() => {
1919
onOpen();
2020
}}>Add an item</Button>
2121
<ModifyItemModal isOpen={isOpen} onClose={onClose} onOpenChange={onOpenChange} />

client/src/components/button/DeleteItemButton.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default function DeleteItemButton ({ item }) {
1818
return (
1919
<>
2020
<Tooltip showArrow content="Delete item" placement="bottom">
21-
<Button isIconOnly color="danger" onClick={onOpen}><IconTrash /></Button>
21+
<Button isIconOnly color="danger" onPress={onOpen}><IconTrash /></Button>
2222
</Tooltip>
2323
<DeleteItemModal isOpen={isOpen} item={item} onClose={onClose} onOpenChange={onOpenChange} />
2424
</>

client/src/components/button/EditItemButton.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default function EditItemButton ({ item }) {
1717
return (
1818
<>
1919
<MyTooltip content="Edit item">
20-
<Button isIconOnly variant={'light'} onClick={onOpen}>
20+
<Button isIconOnly variant={'light'} onPress={onOpen}>
2121
<IconEdit className="text-default-500" size={28} />
2222
</Button>
2323
</MyTooltip>

client/src/components/button/SimulateTimeButton.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default function SimulateTimeButton () {
1515

1616
return (
1717
<>
18-
<Button color={'secondary'} startContent={<IconClock />} onClick={() => {
18+
<Button color={'secondary'} startContent={<IconClock />} onPress={() => {
1919
onOpen();
2020
}}>Simulate time</Button>
2121
<SimulateTimeModal isOpen={isOpen} onClose={onClose} onOpenChange={onOpenChange} />

client/src/components/button/UseItemButton.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export default function UseItemButton ({ item }) {
2323
<>
2424
<MyTooltip content="Use item">
2525
<Button isIconOnly isDisabled={locked || (item.damagePerUse == null)} isLoading={locked} variant={'light'}
26-
onClick={() => {
26+
onPress={() => {
2727
_useItem(item);
2828
}}><IconPick className="text-default-500" size={28} /></Button>
2929
</MyTooltip>

client/src/components/navbar/InfoButton.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default function InfoButton ({ tooltipPlacement = 'bottom' }) {
5050
</div>
5151
</div>
5252
} isOpen={isOpen} popoverPlacement={'bottom-end'} title="Info" tooltipPlacement={tooltipPlacement} trigger={
53-
<Button isIconOnly size={'md'} variant={'light'} onClick={() => isOpen ? onClose : onOpen}>
53+
<Button isIconOnly size={'md'} variant={'light'} onPress={() => isOpen ? onClose : onOpen}>
5454
<IconInfoCircle className="text-default-500" size={28} />
5555
</Button>
5656
} onOpenChange={onOpenChange} />

client/src/components/navbar/SettingsButton.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default function SettingsButton ({ tooltipPlacement = 'bottom' }) {
1717
return (
1818
<>
1919
<MyTooltip content="Settings" placement={tooltipPlacement}>
20-
<Button isIconOnly size={'md'} variant={'light'} onClick={onOpen}>
20+
<Button isIconOnly size={'md'} variant={'light'} onPress={onOpen}>
2121
<IconSettings className="text-default-500" size={28} />
2222
</Button>
2323
</MyTooltip>

server/.env.example

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,38 @@
66
#
77
# Below are options available in the format of "OPTION_NAME (type, default value) - description"
88
# If no default specified below and the option is not set (or has an empty value),
9-
# then the program may ask for it at runtime.
9+
# then the program may ask for it at runtime when in console mode. If the option has a default
10+
# value and no value is set (empty also means no value), then the program will automatically
11+
# use the default value.
1012
#
11-
# - USE_DB (bool) - whether to use MySQL DB
12-
# - DB_CONNECTION_STRING (string) - the connection string for MySQL
13-
# - USE_WS_SERVER (bool) - whether to use the WebSocket server (true) or console TUI (false)
13+
# - STORAGE_TYPE (string, "None") - which data storage to use
14+
# - Possible values:
15+
# - "None" - do not save data (will lose data on exit)
16+
# - "MySQL" - use MySQL DB; don't forget to set DB_CONNECTION_STRING
17+
# - "JSON" - use a .json file; default path is right next to the executable;
18+
# you can set STORAGE_FILE_PATH to change the path
19+
# - USE_WS_SERVER (bool) - whether to use the WebSocket server (true) or console TUI (false); ignored in
20+
# WPF mode. If value is not set and console/terminal mode is active,
21+
# a question prompt shows on launch.
22+
# - DB_CONNECTION_STRING (string, null) - the connection string for MySQL DB
23+
# - STORAGE_FILE_PATH (string, "GameInv.json") - path to the storage file; applicable only if STORAGE_TYPE is
24+
# set to something that uses a file. On Windows, use slashes (/)
25+
# instead of backslashes (\). You can sepcify an absolute or
26+
# relative path. If only a directory path is specified, then the
27+
# default file name ("GameInv.json") is automatically added.
1428
# - WS_URI (string, "ws://0.0.0.0:9081") - URI that the WebSocket server should start with
1529
# - WS_PASS (string, "changeme") - password for the WebSocket connection (set the same password for your client)
1630

17-
USE_DB=
18-
DB_CONNECTION_STRING=
31+
# General options
32+
STORAGE_TYPE=
1933
USE_WS_SERVER=
34+
35+
# Database
36+
DB_CONNECTION_STRING=
37+
38+
# File
39+
STORAGE_FILE_PATH=
40+
41+
# WebSocket
2042
WS_URI=
2143
WS_PASS=

server/GameInv-WPF/Init.cs

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
global using static GameInv_WPF.UtilsNS.Utils;
2-
global using static GameInv.UtilsNS.Consts;
32
global using static GameInv.UtilsNS.Consts.Colors;
43
global using static GameInv.UtilsNS.Utils;
54
using System.Runtime.InteropServices;
6-
using System.Windows;
75
using GameInv_WPF.UtilsNS;
86
using Pastel;
97
using Sherlog;
@@ -23,46 +21,14 @@ public static GameInv.GameInv Initialize() {
2321
InitLogger();
2422
Log.Info("Starting...");
2523

26-
var useDb = MyEnv.GetBool("USE_DB");
27-
if (useDb is null) {
28-
switch (MessageBox.Show("Use database?",
29-
"Question", MessageBoxButton.YesNoCancel)) {
30-
case MessageBoxResult.Cancel or MessageBoxResult.None:
31-
{
32-
Environment.Exit(0);
33-
break;
34-
}
35-
case MessageBoxResult.Yes or MessageBoxResult.OK:
36-
{
37-
useDb = true;
38-
break;
39-
}
40-
case MessageBoxResult.No:
41-
{
42-
useDb = false;
43-
break;
44-
}
45-
}
46-
47-
useDb ??= null!;
48-
}
49-
50-
var dbConnectionString = MyEnv.GetString("DB_CONNECTION_STRING");
51-
if ((bool)useDb && dbConnectionString is null) {
52-
ErrorPresenter.Present(string.Format(Errors.NoDbConnectionString, EnvPrefix)); // TODO: try setting typeof clasnane
53-
Environment.Exit(1);
54-
}
55-
56-
var itemDataSource = (bool)useDb ? CreateItemDataSource(dbConnectionString!) : null;
24+
CheckDbConnectionString(ErrorPresenter);
5725

5826
Log.Info($"Creating a new instance of {nameof(GameInv).Pastel(Highlight)}...");
5927

6028
try {
61-
return new(ErrorPresenter, itemDataSource: itemDataSource);
29+
return new(ErrorPresenter);
6230
} catch (Exception e) {
63-
var msg = e.ToString();
64-
Log.Error(msg);
65-
ShowErrorMessageBox(msg);
31+
ErrorPresenter.Present(e.ToString());
6632
Environment.Exit(1);
6733
}
6834

server/GameInv-WPF/UtilsNS/MessageBoxErrorPresenter.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
using GameInv.UtilsNS.ErrorPresenterNS;
1+
using GameInv.UtilsNS.ErrorPresenter;
22

33
namespace GameInv_WPF.UtilsNS {
44
public class MessageBoxErrorPresenter : IErrorPresenter {
5-
public void Present(string message, Type? classType = null, bool pause = false) {
5+
public void Present(string message, bool pause = false) {
66
var log = GetLogger(GetCallerClassType());
77

8-
log.Error(message);
8+
log.Error("\n" + message);
99
ShowErrorMessageBox(message);
1010
}
1111
}

server/GameInv-WPF/Windows/MainWindow/MainWindow.xaml.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ public MainWindow() {
2323
Items = (ObservableCollection<Item>)_gameInv.Inventory.Items;
2424

2525
DataContext = this;
26+
27+
Closing += (_, _) => {
28+
_gameInv.OnClosing();
29+
};
2630
}
2731

2832
public ObservableCollection<Item> Items { get; } = null!;

server/GameInv/Db/IItemDataSource.cs renamed to server/GameInv/DataSource/IItemDataSource.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using GameInv.ItemNS;
22

3-
namespace GameInv.Db {
3+
namespace GameInv.DataSource {
44
public interface IItemDataSource {
55
public string SourceName { get; }
66

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using System.Text;
2+
using GameInv.ItemNS;
3+
using Newtonsoft.Json;
4+
5+
namespace GameInv.DataSource {
6+
public class JsonItemDataSource : IItemDataSource {
7+
private static readonly Logger Log = GetLogger();
8+
private readonly GameInv _gameInv;
9+
private readonly ResettableCountdownTimer _saveTimer;
10+
private bool _saving;
11+
private string _storagePath = null!;
12+
13+
public JsonItemDataSource(GameInv gameInv) {
14+
_gameInv = gameInv;
15+
_saveTimer = new(SaveData, 1000);
16+
17+
_gameInv.Closing += MoreGuaranteedSaveData;
18+
}
19+
20+
public string SourceName => "JSON file";
21+
22+
public IEnumerable<Item>? GetItems(out string? errorMessage) {
23+
errorMessage = null;
24+
void LogFileLocationInfo() {
25+
Log.Info($"{SourceName} location: {_storagePath}");
26+
}
27+
28+
try {
29+
const string defaultFileName = "GameInv.json";
30+
_storagePath = MyEnv.GetString("STORAGE_FILE_PATH") ?? defaultFileName;
31+
_storagePath = Path.GetFullPath(_storagePath, AppDomain.CurrentDomain.BaseDirectory);
32+
if (Directory.Exists(_storagePath)) {
33+
_storagePath = Path.Combine(_storagePath, defaultFileName);
34+
}
35+
36+
if (!File.Exists(_storagePath)) {
37+
Log.Info($"{SourceName} doesn't exist, creating.");
38+
File.WriteAllText(_storagePath, "[]");
39+
LogFileLocationInfo();
40+
return [];
41+
}
42+
43+
LogFileLocationInfo();
44+
45+
var saveData = File.ReadAllText(_storagePath, Encoding.UTF8);
46+
var itemsAsItemData = JsonConvert.DeserializeObject<ItemData[]>(saveData);
47+
var items = itemsAsItemData?.Select(i => (Item)i).ToArray();
48+
return items;
49+
} catch (Exception ex) {
50+
errorMessage = FormatException(ex);
51+
return null;
52+
}
53+
}
54+
55+
public bool UpdateItem(Item item) {
56+
return ResetSaveTimer();
57+
}
58+
59+
public bool UpdateItems(IEnumerable<Item> items) {
60+
return ResetSaveTimer();
61+
}
62+
63+
public bool RemoveItem(Item item) {
64+
return ResetSaveTimer();
65+
}
66+
67+
public bool RemoveItems(IEnumerable<Item> items) {
68+
return ResetSaveTimer();
69+
}
70+
71+
private void SaveData() {
72+
if (_saving) return;
73+
_saving = true;
74+
Log.Info("Started saving...");
75+
try {
76+
var itemsAsItemData = _gameInv.Inventory.Select(i => (ItemData)i).ToArray();
77+
var saveData = JsonConvert.SerializeObject(itemsAsItemData);
78+
File.WriteAllText(_storagePath, saveData);
79+
Log.Info($"Saved data to {SourceName}");
80+
} catch (Exception ex) {
81+
_gameInv.ErrorPresenter.Present("Error saving data.\n\n" + FormatException(ex));
82+
} finally {
83+
_saving = false;
84+
}
85+
}
86+
87+
/// <summary>
88+
/// Tries to wait for previous saving to finish and then calls <see cref="SaveData" />.
89+
/// Gives up after 5 seconds.
90+
/// </summary>
91+
private void MoreGuaranteedSaveData() {
92+
const int timeoutSeconds = 5;
93+
if (SpinWait.SpinUntil(() => _saving == false, timeoutSeconds * 1000)) {
94+
SaveData();
95+
} else {
96+
_gameInv.ErrorPresenter.Present($"Previous save operation didn't finish " +
97+
$"in {timeoutSeconds}+ seconds. Some data may be lost.");
98+
}
99+
}
100+
101+
private bool ResetSaveTimer() {
102+
_saveTimer.Reset();
103+
return true;
104+
}
105+
}
106+
}

0 commit comments

Comments
 (0)