AsYetUntitled / Framework

Altis Life RPG mission framework for Arma 3 originally made by @TAWTonic.
Other
247 stars 310 forks source link

[request] making bank vault persistent over restarts? #707

Closed ghost closed 4 years ago

ghost commented 4 years ago

Sorry for another misused bug report, but don't know where to ask it.

So, the current way the bank is filled after a restart is calculated by number of players / 2 per 30 minutes. As to attack the bank already requires at least 5 cops to be active filling the bank this way can be an issue for rather small communities. To have a chance one would have to gather enough players in an event like style so it's at least a 5vs5. As most servers restarted every 6 hours (I also seen some restarting every 4 hours or as long as every 8 hours) this gets about 11 cycles (the 12th one would already be after the 6 hour mark when the server already restarted). Assume that all 10 players are online for the whole 6 hours and the heist is done in the last 30 minutes to get the maximum this would "only" end up on about 55 gold bars which sell for 95k each makes the team, if successful, "just" 5.225.000 bucks on average. So, if the team is lucky and get's the gold to the trader within the last 30 minutes each team member would end up with a bit over 1 million. Only way to really increase it without touching the configs is the have more players on the server.

As the amount stored in the safe is reset on server restart would there be a way possible to make it persistent using the database? Maybe to counter-balance it the formular how the increase of gold over time is done could be redone so it depends more on time rather than on how many players are active. And if someone has implemented something like a tax system it may could interact with it?

In addition to that, at least on most of the servers I played on the past there were rules like "a heist has to be announced to the cops upfront" or even in the servers forum (kind of like set up a player-initiated mini-event) so just "wait for required amount of cops online and go for it without any warning" is rarely found and done on many servers.

I tried to search Google about this topic but couldn't dig up useful help about this.

DomT602 commented 4 years ago

Personally I don't think this belongs in the framework, if servers wanted to do it like this, there are a number of ways they could do it, such as profileNamespace of the database.

ghost commented 4 years ago

I can see it would require quite a lot of rework on the framework and the database. This may not fit here, so feel free to point me somewhere else: Do you have suggestions how to implement it? My idea was something along those lines to add another table to the database (not just for the one bank value but for other persistent stuff I may want to add in the future), add the required SQL statements to AL.ini, add a "save to database" in the fn_cleanup.sqf and on server startup load it from the database. At least that seem to be not that hard to implement. Would you suggest another approach?

DomT602 commented 4 years ago

You could use the profile of the server and have the variable fed_funds (profileNamespace getVariable ["fed_funds",0];), and that saves over restarts. You could then just keep adding this in the federal update part of fn_cleanup. This totally avoids edits to the database.

ghost commented 4 years ago

Thanks, I seem to have got it working. At least the variable is saved when the half-hour cleanup is run and is correctly reloaded when I restart the server. One minor detail I hope you also may have an idea how to overcome: As I'm use the headless client I noticed that the server instance and the HC instance save their profilNamespace variables in different files. So when I run the server with HC the current variable is saved in the HC profile, but when run without the HC it gets saved in the server profile (named "Player1" on my server). Do you know a way other than using the database to unify this so no matter if the HC is used or not the variable is saved in the same spot?

DomT602 commented 4 years ago

Yes the server and HC have different profiles as you'd expect (as HC is a client after all). You'd need to 'sync' the HC and server (so when one updates, you send the new value to the other to update its own).

DomT602 commented 4 years ago

Anyway - as this is something that would be done by a server owner, this issue will be closed. Feel free to join the discord and speak to me if you want any other help with this.

ghost commented 4 years ago

Ok, I see, maybe not (yet) a feature to be part of the framework itself. Anyway, thanks for help so far, I got it at least working on my current setup. Maybe I tinker with it again if and when my server gains some players.

ghost commented 4 years ago

I don't know if anyone will ever read this, but it took me quite some time to get it done using the datase. I guess this could had be done cleaner, but hey, I'm new to all that stuff and happy to have got at least solved some way.

So, first of all I created a new table in my altis life database, called "persistence". It has four columns: id (int, primary key, auto increment), name (text, default not null), datanum (int, default null) and datastr (text, default null) As the database and all other tables are set to utf8mb4_general_ci I set it for this new table and the two text fields. Next, to actually communicate with this new table, I added six new SQL strings to AL.ini:

[insertPersistenceNum]
SQL1_1 = INSERT INTO persistence (name, datanum) values (?, ?);
SQL1_INPUTS = 1, 2

[insertPersistenceStr]
SQL1_1 = INSERT INTO persistence (name, datastr) values (?, ?);
SQL1_INPUTS = 1, 2

[updatePersistenceNum]
SQL1_1 = UPDATE persistence SET datanum = ? WHERE name = ?
SQL1_INPUTS = 2, 1

[updatePersistenceStr]
SQL1_1 = UPDATE persistence SET datastr = ? WHERE name = ?
SQL1_INPUTS = 2, 1

[selectPersistenceNum]
SQL1_1 = SELECT datanum FROM persistence WHERE name = ?
SQL1_INPUTS = 1
OUTPUT = 1

[selectPersistenceStr]
SQL1_1 = SELECT datastr FROM persistence WHERE name = ?
SQL1_INPUTS = 1
OUTPUT = 1-STRING

extDB custom sql is rather strange with datatypes and this took me really long to get it done. When you want to insert a string into the database you don't set the input to STRING. But, to get a string back out you have to set it to STRING. I don't know why, but if you either set also the input to STRING or don't set the output to STRING it does not work and either throws a lot of parsing errors caused by double quotes (like: ""string"") or can't parse the string at all but rather only set it to nil, null or "any". But I guess that's just how extDB works and how it interfaces with the database and arma sqf.

As I now got the base (a new table in database and a way to insert data into it, read them back and also update them (I currently don't have any plans for DELETE - but would be easy with just one additional SQL)) done I now have to look where, when and how the bank vault is manipulated. This can be split into three parts: 1) server start up 2) fn_cleanup 3) vault interaction.

For the server startup it's rather easy: I just replaced the default line (which ends up with 0) with read the last saved value from the database:

fed_bank setVariable ["safe", [format["selectPersistenceNum:%1", "fedBankVault"], 2] call DB_fnc_asyncCall select 0, true];

The next one is fn_cleanup. I fell for the trap that the new value is calculated within the setVariable function so I re-saved the old value over and over again. The easiest solution to me was just to split it into two lines:

private _funds = fed_bank getVariable ["safe", 0];
_funds = round(_funds + ((count playableUnits)/2));
fed_bank setVariable ["safe", _funds, true];
[format["updatePersistenceNum:%1:%2", "fedBankVault", _funds], 1] call DB_fnc_asyncCall;

Attention to detail: If you use the headless client you have to update the headless fn_cleanup as well.

The third interaction, stealing and bringing back the gold, took me the most time to get it correctly. In fact there're two files involved in this: fn_safeTake for the robber to steal from the vault and fn_safeStore for the cops to bring the gold back. It took me a bit to find these files, and I had to decide if I even tinker with them or just let them as is and wait for the next 30min fn_cleanup run. Well, as you might already can tell: I took the long route and modified them. It also required a new function on the server (and HC) and therefore updating the config.cpp for them. The server funtion look like this:

/*
    File: fn_updateBankVault.sqf
*/
private ["_funds", "_query"];
_funds = fed_bank getVariable ["safe", 0];
[format["updatePersistenceNum:%1:%2", "fedBankVault", _funds], 1] call DB_fnc_asyncCall;
true;

As I have put it in the life_server/Functions/MySQL directory when using just the server this function is called DB_fnc_updateBankVault. When using the headless client it's placed in life_hc/MySQL/General:

/*
    File: fn_updateBankVault.sqf
*/
private ["_funds", "_query"];
_funds = fed_bank getVariable ["safe", 0];
[format["updatePersistenceNum:%1:%2", "fedBankVault", _funds], 1] call HC_fnc_asyncCall;
true;

The difference is just to call the correct version of fnasyncCall - either DB on lifeserver or HC on headless client.

The code calling it on take or place gold is added to the second-to-last line before fn_safeInventory is called:

if(life_HC_isActive) then { [] remoteExec ["HC_fnc_updateBankVault", HC_Life] }
else { [] remoteExec ["DB_fnc_updateBankVault", RSERV] };

As explained: This is to externalize the bank into the database, but I guess one could get away with the solution Dom posted. It works the same but stores the value in a file instead of the database. I myself can only see two usecases: 1) paranoia to move everything into the database 2) multi-server setup with shared dataset (like for real big communities when one server can't hold all the players - like most MMORPGs do it). I also did it as I may come up with additional stuff in the future I like to save over restarts in the database. Maybe I try to move other settings over.

ghost commented 4 years ago

If someone is still interested in this I noticed a small bug: I set up a new test server, and aside from the missing table in the database, I missed the point I had set the initial value the very first time per debug console. So, this is the table:

CREATE TABLE IF NOT EXISTS `persistence` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` text NOT NULL DEFAULT '',
  `datanum` int(11) DEFAULT NULL,
  `datastr` text DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

and this has to be replaced in the life_server/init.sqf:

/* Set the amount of gold in the federal reserve at mission start */
private _bankFunds = [format ["selectPersistenceNum:%1", "fedBankVault"], 2] call DB_fnc_asyncCall;
if(_bankFunds isEqualTo []) then
{
    _bankFunds = 0;
    [format ["insertPersistenceNum:%1:%2", "fedBankVault", 0], 1] call DB_fnc_asyncCall;
}
else
{
    _bankFunds = _bankFunds select 0;
};
fed_bank setVariable ["safe", _bankFunds, true];