diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 40768604b565..e79928edd2ee 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -104,6 +104,7 @@ Maintainers can revert your changes if they feel they are not worth maintaining - [Embedding TGUI Components in Chat](../../tgui/docs/chat-embedded-components.md) - [Hard Deletes](./guides/HARDDELETES.md) +- [Quickly setting up a development database with ezdb](./guides/EZDB.md) - [MC Tab Guide](./guides/MC_tab.md) - [Tick system](./guides/TICK_ORDER.md) - [UI Development](../tgui/README.md) diff --git a/.github/guides/EZDB.md b/.github/guides/EZDB.md new file mode 100644 index 000000000000..cee9d7deaf18 --- /dev/null +++ b/.github/guides/EZDB.md @@ -0,0 +1,12 @@ +# Quickly setting up a development database with ezdb +While you do not need a database to code for yogstation, it is a prerequisite to many important features, especially on the admin side. Thus, if you are working in any code that benefits from it, it can be helpful to have one handy. + +**ezdb** is a tool for quickly setting up an isolated development database. It will manage downloading MariaDB, creating the database, setting it up, and updating it when the code evolves. It is not recommended for use in production servers, but is perfect for quick development. + +To run ezdb, go to `tools/ezdb`, and double-click on ezdb.bat. This will set up the database on port 1338, but you can configure this with `--port`. When it is done, you should be able to launch yogstation as normal and have database access. This runs on the same Python bootstrapper as things like the map merge tool, which can sometimes be flaky. + +If you wish to delete the ezdb database, delete the `db` folder as well as `config/ezdb.txt`. + +To update ezdb, run the script again. This will both look for any updates in the database changelog, as well as update your schema revision. + +Contact Mothblocks if you face any issues in this process. diff --git a/.gitignore b/.gitignore index 9cd796be4556..bf6bfc5cf8c7 100644 --- a/.gitignore +++ b/.gitignore @@ -218,3 +218,7 @@ tools/MapAtmosFixer/MapAtmosFixer/bin/* #KDIFF3 files *.orig + +# ezdb +/db/ +/config/ezdb.txt diff --git a/SQL/database_changelog.txt b/SQL/database_changelog.md similarity index 98% rename from SQL/database_changelog.txt rename to SQL/database_changelog.md index ef9e3dc2e40c..5b3c483bd169 100644 --- a/SQL/database_changelog.txt +++ b/SQL/database_changelog.md @@ -11,6 +11,7 @@ In any query remember to add a prefix to the table names if you use one. version 5.13 2023-05-10 Adds allow_vpn to bound credentials flags +```sql ALTER TABLE `bound_credentials` MODIFY COLUMN flags set('bypass_bans','allow_proxies') DEFAULT NULL NULL; CREATE TABLE `proxy_cache` ( `ip` int(11) unsigned NOT NULL, @@ -19,28 +20,36 @@ CREATE TABLE `proxy_cache` ( PRIMARY KEY (`ip`), CONSTRAINT `data` CHECK (json_valid(`data`)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` +```sql CREATE OR REPLACE EVENT proxy_cache_ttl ON SCHEDULE EVERY 1 HOUR DO DELETE FROM proxy_cache WHERE (last_updated + INTERVAL 1 DAY) < current_timestamp(); +``` version 5.12 2023-04-10 Adds playtime to notes +```sql ALTER TABLE `messages` ADD `playtime` int(10) unsigned DEFAULT NULL; CREATE TRIGGER messagesTloghours BEFORE INSERT ON `messages` FOR EACH ROW SET NEW.playtime = (SELECT minutes FROM role_time rt WHERE rt.ckey = NEW.targetckey AND rt.job = 'Living'); +``` version 5.11 2023-01-03 Adds comment to credentials binding +```sql ALTER TABLE `bound_credentials` ADD comment text NULL; +``` version 5.10 2022-05-18, alexkar598 Adds credentials binding +```sql CREATE TABLE `bound_credentials` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `ckey` varchar(32) NOT NULL, @@ -52,27 +61,33 @@ CREATE TABLE `bound_credentials` ( KEY `idx_cid_lookup` (`computerid`), KEY `idx_ip_lookup` (`ip`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` version 5.9 23 November 2021, by adamsogm Adds the datetime column to the primary key for the MFA table +```sql ALTER TABLE ss13_mfa_logins DROP PRIMARY KEY, ADD PRIMARY KEY (`ckey`,`ip`,`cid`, `datetime`); +``` version 5.8 25 October 2021, by adamsogm Modifies the admin_tickets table, and adds the admin_ticket_interactions table Do not forget to apply the prefix to the foreign key on the admin_ticket_interactions table +```sql ALTER TABLE `admin_tickets` DROP COLUMN `content`, DROP COLUMN `rating`, MODIFY `a_ckey` varchar(32), ADD INDEX `idx_round` (`round_id`), ADD INDEX `idx_round_ticket` (`round_id`,`ticket_id`); +``` +```sql DROP TABLE IF EXISTS `admin_ticket_interactions`; CREATE TABLE IF NOT EXISTS `admin_ticket_interactions` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, @@ -83,15 +98,19 @@ CREATE TABLE IF NOT EXISTS `admin_ticket_interactions` ( PRIMARY KEY (`id`), FOREIGN KEY (`ticket_id`) REFERENCES `admin_tickets`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` version 5.7 23 September 2021, by adamsogm Adds table for storing mfa login cache and a column for storing TOTP seeds and one for mfa backup code +```sql ALTER TABLE player ADD COLUMN totp_seed varchar(20), ADD COLUMN mfa_backup varchar(128); +``` +```sql DROP TABLE IF EXISTS `mfa_logins`; CREATE TABLE IF NOT EXISTS `mfa_logins` ( `ckey` varchar(32) NOT NULL, @@ -100,6 +119,7 @@ CREATE TABLE IF NOT EXISTS `mfa_logins` ( `datetime` timestamp NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`ckey`,`ip`,`cid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` ---------------------------------------------------- @@ -107,9 +127,10 @@ version 5.6 14 August 2021, by alexkar598 Adds index on connection_log +```sql create index idx_review on connection_log (ckey, computerid, ip); - +``` ---------------------------------------------------- @@ -117,7 +138,9 @@ version 5.5 14 August 2021, by JamieD1 Adds column in mentor for position +```sql ALTER TABLE `mentor` ADD COLUMN `position` VARSET(32) UNSIGNED NOT NULL AFTER `ckey`; +``` ---------------------------------------------------- @@ -125,6 +148,7 @@ version 5.4 18 May 2020, by TheGamer01 Adds table for antag tokens +```sql CREATE TABLE `antag_tokens` ( `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `ckey` VARCHAR(32) NULL NOT NULL, @@ -137,6 +161,7 @@ CREATE TABLE `antag_tokens` ( `round_id` int(11) unsigned NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; +``` ---------------------------------------------------- @@ -144,23 +169,29 @@ version 5.3 10 Dec 2019, by Nichlas0010 Added two tables for achievements, one for the metadata and one for storing achieved achievements, as well as a misc table which is essentially just a glorified assoc list +```sql CREATE TABLE `achievements` ( `name` VARCHAR(32) NOT NULL, `id` INT UNSIGNED NOT NULL, `descr` VARCHAR(2048) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; +``` +```sql CREATE TABLE `earned_achievements` ( `ckey` VARCHAR(32) NOT NULL, `id` INT UNSIGNED NOT NULL ) ENGINE=InnoDB; +``` +```sql CREATE TABLE `misc` ( `key` VARCHAR(32) NOT NULL, `value` VARCHAR(2048) NOT NULL, PRIMARY KEY (`key`) ) ENGINE=InnoDB; +``` ---------------------------------------------------- @@ -168,13 +199,17 @@ Version 5.2, 28 Aug 2019, by AffectedArc07/TheGamer01 Added a field to the `player` table to track ckey and discord ID relationships +```sql ALTER TABLE `player` ADD COLUMN `discord_id` BIGINT NULL DEFAULT NULL AFTER `flags`; +``` + ---------------------------------------------------- Version 5.1, 25 Feb 2018, by MrStonedOne Added four tables to enable storing of stickybans in the database since byond can lose them, and to enable disabling stickybans for a round without depending on a crash free round. Existing stickybans are automagically imported to the tables. +```sql CREATE TABLE `stickyban` ( `ckey` VARCHAR(32) NOT NULL, `reason` VARCHAR(2048) NOT NULL, @@ -182,7 +217,9 @@ CREATE TABLE `stickyban` ( `datetime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`ckey`) ) ENGINE=InnoDB; +``` +```sql CREATE TABLE `stickyban_matched_ckey` ( `stickyban` VARCHAR(32) NOT NULL, `matched_ckey` VARCHAR(32) NOT NULL, @@ -191,7 +228,9 @@ CREATE TABLE `stickyban_matched_ckey` ( `exempt` TINYINT(1) NOT NULL DEFAULT '0', PRIMARY KEY (`stickyban`, `matched_ckey`) ) ENGINE=InnoDB; +``` +```sql CREATE TABLE `stickyban_matched_ip` ( `stickyban` VARCHAR(32) NOT NULL, `matched_ip` INT UNSIGNED NOT NULL, @@ -199,7 +238,9 @@ CREATE TABLE `stickyban_matched_ip` ( `last_matched` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`stickyban`, `matched_ip`) ) ENGINE=InnoDB; +``` +```sql CREATE TABLE `stickyban_matched_cid` ( `stickyban` VARCHAR(32) NOT NULL, `matched_cid` VARCHAR(32) NOT NULL, @@ -207,6 +248,7 @@ CREATE TABLE `stickyban_matched_cid` ( `last_matched` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`stickyban`, `matched_cid`) ) ENGINE=InnoDB; +``` ---------------------------------------------------- @@ -216,6 +258,7 @@ Modified ban table to remove the need for the `bantype` column, a python script See the file 'ban_conversion_2018-10-28.py' for instructions on how to use the script. A new ban table can be created with the query: +```sql CREATE TABLE `ban` ( `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `bantime` DATETIME NOT NULL, @@ -245,20 +288,25 @@ CREATE TABLE `ban` ( KEY `idx_ban_isbanned_details` (`ckey`,`ip`,`computerid`,`role`,`unbanned_datetime`,`expiration_time`), KEY `idx_ban_count` (`bantime`,`a_ckey`,`applies_to_admins`,`unbanned_datetime`,`expiration_time`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; +``` ---------------------------------------------------- Version 4.7, 18 August 2018, by CitrusGender Modified table `messages`, adding column `severity` to classify notes based on their severity. +```sql ALTER TABLE `messages` ADD `severity` enum('high','medium','minor','none') DEFAULT NULL AFTER `expire_timestamp` +``` ---------------------------------------------------- Version 4.6, 11 August 2018, by Jordie0608 Modified table `messages`, adding column `expire_timestamp` to allow for auto-"deleting" messages. +```sql ALTER TABLE `messages` ADD `expire_timestamp` DATETIME NULL DEFAULT NULL AFTER `secret`; +``` ---------------------------------------------------- @@ -266,26 +314,34 @@ Version 4.5, 9 July 2018, by Jordie0608 Modified table `player`, adding column `byond_key` to store a user's key along with their ckey. To populate this new column run the included script 'populate_key_2018-07', see the file for use instructions. +```sql ALTER TABLE `player` ADD `byond_key` VARCHAR(32) DEFAULT NULL AFTER `ckey`; +``` ---------------------------------------------------- Version 4.4, 9 May 2018, by Jordie0608 Modified table `round`, renaming column `start_datetime` to `initialize_datetime` and `end_datetime` to `shutdown_datetime` and adding columns to replace both under the same name in preparation for changes to TGS server initialization. +```sql ALTER TABLE `round` ALTER `start_datetime` DROP DEFAULT; +``` + +```sql ALTER TABLE `round` CHANGE COLUMN `start_datetime` `initialize_datetime` DATETIME NOT NULL AFTER `id`, ADD COLUMN `start_datetime` DATETIME NULL DEFAULT NULL AFTER `initialize_datetime`, CHANGE COLUMN `end_datetime` `shutdown_datetime` DATETIME NULL DEFAULT NULL AFTER `start_datetime`, ADD COLUMN `end_datetime` DATETIME NULL DEFAULT NULL AFTER `shutdown_datetime`; +``` ---------------------------------------------------- Version 4.3, 9 May 2018, by MrStonedOne Added table `role_time_log` and triggers `role_timeTlogupdate`, `role_timeTloginsert` and `role_timeTlogdelete` to update it from changes to `role_time` +```sql CREATE TABLE `role_time_log` ( `id` BIGINT NOT NULL AUTO_INCREMENT , `ckey` VARCHAR(32) NOT NULL , `job` VARCHAR(128) NOT NULL , `delta` INT NOT NULL , `datetime` TIMESTAMP on update CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP , PRIMARY KEY (`id`), INDEX (`ckey`), INDEX (`job`), INDEX (`datetime`)) ENGINE = InnoDB; DELIMITER $$ @@ -299,6 +355,7 @@ CREATE TRIGGER `role_timeTlogdelete` AFTER DELETE ON `role_time` FOR EACH ROW B END $$ DELIMITER ; +``` ---------------------------------------------------- Version 4.2, 17 April 2018, by Jordie0608 @@ -315,6 +372,7 @@ This change was made to enable use of sql-based admin loading. To import your existing admins and ranks run the included script 'admin_import_2018-02-03.py', see the file for use instructions. Legacy file-based admin loading is still supported, if you want to continue using it the script doesn't need to be run. +```sql ALTER TABLE `admin` CHANGE COLUMN `rank` `rank` VARCHAR(32) NOT NULL AFTER `ckey`, DROP COLUMN `id`, @@ -323,14 +381,18 @@ ALTER TABLE `admin` DROP COLUMN `email`, DROP PRIMARY KEY, ADD PRIMARY KEY (`ckey`); +``` +```sql ALTER TABLE `admin_log` CHANGE COLUMN `datetime` `datetime` DATETIME NOT NULL AFTER `id`, CHANGE COLUMN `adminckey` `adminckey` VARCHAR(32) NOT NULL AFTER `datetime`, CHANGE COLUMN `adminip` `adminip` INT(10) UNSIGNED NOT NULL AFTER `adminckey`, ADD COLUMN `operation` ENUM('add admin','remove admin','change admin rank','add rank','remove rank','change rank flags') NOT NULL AFTER `adminip`, CHANGE COLUMN `log` `log` VARCHAR(1000) NOT NULL AFTER `operation`; +``` +```sql ALTER TABLE `admin_ranks` CHANGE COLUMN `rank` `rank` VARCHAR(32) NOT NULL FIRST, CHANGE COLUMN `flags` `flags` SMALLINT UNSIGNED NOT NULL AFTER `rank`, @@ -339,6 +401,7 @@ ALTER TABLE `admin_ranks` DROP COLUMN `id`, DROP PRIMARY KEY, ADD PRIMARY KEY (`rank`); +``` ---------------------------------------------------- @@ -348,6 +411,8 @@ Modified feedback table to use json, a python script is used to migrate data to See the file 'feedback_conversion_2017-11-12.py' for instructions on how to use the script. A new json feedback table can be created with: + +```sql CREATE TABLE `feedback` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `datetime` datetime NOT NULL, @@ -358,12 +423,14 @@ CREATE TABLE `feedback` ( `json` json NOT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM +``` ---------------------------------------------------- Version 3.4, 28 August 2017, by MrStonedOne Modified table 'messages', adding a deleted column and editing all indexes to include it +```sql ALTER TABLE `messages` ADD COLUMN `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' AFTER `edits`, DROP INDEX `idx_msg_ckey_time`, @@ -372,6 +439,7 @@ DROP INDEX `idx_msg_type_ckey_time_odr`, ADD INDEX `idx_msg_ckey_time` (`targetckey`,`timestamp`, `deleted`), ADD INDEX `idx_msg_type_ckeys_time` (`type`,`targetckey`,`adminckey`,`timestamp`, `deleted`), ADD INDEX `idx_msg_type_ckey_time_odr` (`type`,`targetckey`,`timestamp`, `deleted`); +``` ---------------------------------------------------- @@ -379,11 +447,13 @@ Version 3.3, 25 August 2017, by Jordie0608 Modified tables 'connection_log', 'legacy_population', 'library', 'messages' and 'player' to add additional 'round_id' tracking in various forms and 'server_ip' and 'server_port' to the table 'messages'. +```sql ALTER TABLE `connection_log` ADD COLUMN `round_id` INT(11) UNSIGNED NOT NULL AFTER `server_port`; ALTER TABLE `legacy_population` ADD COLUMN `round_id` INT(11) UNSIGNED NOT NULL AFTER `server_port`; ALTER TABLE `library` ADD COLUMN `round_id_created` INT(11) UNSIGNED NOT NULL AFTER `deleted`; ALTER TABLE `messages` ADD COLUMN `server_ip` INT(10) UNSIGNED NOT NULL AFTER `server`, ADD COLUMN `server_port` SMALLINT(5) UNSIGNED NOT NULL AFTER `server_ip`, ADD COLUMN `round_id` INT(11) UNSIGNED NOT NULL AFTER `server_port`; ALTER TABLE `player` ADD COLUMN `firstseen_round_id` INT(11) UNSIGNED NOT NULL AFTER `firstseen`, ADD COLUMN `lastseen_round_id` INT(11) UNSIGNED NOT NULL AFTER `lastseen`; +``` ---------------------------------------------------- @@ -391,9 +461,11 @@ Version 3.2, 18 August 2017, by Cyberboss and nfreader Modified table 'death', adding the columns `last_words` and 'suicide'. +```sql ALTER TABLE `death` ADD COLUMN `last_words` varchar(255) DEFAULT NULL AFTER `staminaloss`, ADD COLUMN `suicide` tinyint(0) NOT NULL DEFAULT '0' AFTER `last_words`; +``` Remember to add a prefix to the table name if you use them. @@ -403,9 +475,13 @@ Version 3.1, 20th July 2017, by Shadowlight213 Added role_time table to track time spent playing departments. Also, added flags column to the player table. +```sql CREATE TABLE `role_time` ( `ckey` VARCHAR(32) NOT NULL , `job` VARCHAR(128) NOT NULL , `minutes` INT UNSIGNED NOT NULL, PRIMARY KEY (`ckey`, `job`) ) ENGINE = InnoDB; +``` +```sql ALTER TABLE `player` ADD `flags` INT NOT NULL default '0' AFTER `accountjoindate`; +``` Remember to add a prefix to the table name if you use them. @@ -417,14 +493,18 @@ Added schema_revision to store the current db revision, why start at 3.0? because: 15:09 <+MrStonedOne> 1.0 was erro, 2.0 was when i removed erro_, 3.0 was when jordie made all the strings that hold numbers numbers +```sql CREATE TABLE `schema_revision` ( `major` TINYINT(3) UNSIGNED NOT NULL , `minor` TINYINT(3) UNSIGNED NOT NULL , `date` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY ( `major`,`minor` ) ) ENGINE = INNODB; +``` +```sql INSERT INTO `schema_revision` (`major`, `minor`) VALUES (3, 0); +``` Remember to add a prefix to the table name if you use them. @@ -434,7 +514,9 @@ Remember to add a prefix to the table name if you use them. Modified table 'poll_option', adding the column 'default_percentage_calc'. +```sql ALTER TABLE `poll_option` ADD COLUMN `default_percentage_calc` TINYINT(1) UNSIGNED NOT NULL DEFAULT '1' AFTER `descmax` +``` Remember to add a prefix to the table name if you use them. @@ -444,7 +526,9 @@ Remember to add a prefix to the table name if you use them. Modified table 'poll_option', removing the column 'percentagecalc'. +```sql ALTER TABLE `poll_option` DROP COLUMN `percentagecalc` +``` Remember to add a prefix to the table name if you use them. diff --git a/code/controllers/configuration/configuration.dm b/code/controllers/configuration/configuration.dm index c0e0bbdfb943..0202bf63aadf 100644 --- a/code/controllers/configuration/configuration.dm +++ b/code/controllers/configuration/configuration.dm @@ -48,6 +48,8 @@ for(var/J in legacy_configs) LoadEntries(J) break + if (fexists("[directory]/ezdb.txt")) + LoadEntries("ezdb.txt") loadmaplist(CONFIG_MAPS_FILE) LoadMOTD() LoadPolicy() diff --git a/code/controllers/configuration/entries/dbconfig.dm b/code/controllers/configuration/entries/dbconfig.dm index a697b2b988a5..665fea48a828 100644 --- a/code/controllers/configuration/entries/dbconfig.dm +++ b/code/controllers/configuration/entries/dbconfig.dm @@ -1,4 +1,4 @@ -/datum/config_entry/flag/sql_enabled // for sql switching +/datum/config_entry/flag/sql_enabled // for sql switching protection = CONFIG_ENTRY_LOCKED /datum/config_entry/flag/mfa_enabled @@ -50,5 +50,20 @@ /datum/config_entry/number/bsql_thread_limit config_entry_value = 50 min_val = 1 + deprecated_by = /datum/config_entry/number/pooling_max_sql_connections -/datum/config_entry/flag/bsql_debug +/datum/config_entry/number/bsql_thread_limit/DeprecationUpdate(value) + return value + +/datum/config_entry/number/pooling_min_sql_connections + default = 1 + min_val = 1 + +/datum/config_entry/number/pooling_max_sql_connections + default = 25 + min_val = 1 + +/// The exe for mariadbd.exe. +/// Shouldn't really be set on production servers, primarily for EZDB. +/datum/config_entry/string/db_daemon + protection = CONFIG_ENTRY_LOCKED | CONFIG_ENTRY_HIDDEN diff --git a/code/controllers/subsystem/dbcore.dm b/code/controllers/subsystem/dbcore.dm index 937c177dd348..34152e98ee59 100644 --- a/code/controllers/subsystem/dbcore.dm +++ b/code/controllers/subsystem/dbcore.dm @@ -16,6 +16,8 @@ SUBSYSTEM_DEF(dbcore) var/connection // Arbitrary handle returned from rust_g. + var/db_daemon_started = FALSE + /datum/controller/subsystem/dbcore/Initialize() //We send warnings to the admins during subsystem init, as the clients will be New'd and messages //will queue properly with goonchat @@ -51,6 +53,7 @@ SUBSYSTEM_DEF(dbcore) qdel(query_round_shutdown) if(IsConnected()) Disconnect() + stop_db_daemon() //nu /datum/controller/subsystem/dbcore/can_vv_get(var_name) @@ -74,6 +77,8 @@ SUBSYSTEM_DEF(dbcore) if(!CONFIG_GET(flag/sql_enabled)) return FALSE + + stop_db_daemon() var/user = CONFIG_GET(string/feedback_login) var/pass = CONFIG_GET(string/feedback_password) @@ -81,7 +86,8 @@ SUBSYSTEM_DEF(dbcore) var/address = CONFIG_GET(string/address) var/port = CONFIG_GET(number/port) var/timeout = max(CONFIG_GET(number/async_query_timeout), CONFIG_GET(number/blocking_query_timeout)) - var/thread_limit = CONFIG_GET(number/bsql_thread_limit) + var/min_sql_connections = CONFIG_GET(number/pooling_min_sql_connections) + var/max_sql_connections = CONFIG_GET(number/pooling_max_sql_connections) var/result = json_decode(rustg_sql_connect_pool(json_encode(list( "host" = address, @@ -89,10 +95,10 @@ SUBSYSTEM_DEF(dbcore) "user" = user, "pass" = pass, "db_name" = db, - "max_threads" = 5, "read_timeout" = timeout, "write_timeout" = timeout, - "max_threads" = thread_limit, + "min_threads" = min_sql_connections, + "max_threads" = max_sql_connections, )))) . = (result["status"] == "ok") if (.) @@ -279,6 +285,47 @@ Delayed insert mode was removed in mysql 7 and only works with MyISAM type table . = Query.Execute(async) qdel(Query) +/datum/controller/subsystem/dbcore/proc/start_db_daemon() + set waitfor = FALSE + + if (db_daemon_started) + return + + db_daemon_started = TRUE + + var/daemon = CONFIG_GET(string/db_daemon) + if (!daemon) + return + + ASSERT(fexists(daemon), "Configured db_daemon doesn't exist") + + var/list/result = world.shelleo("echo \"Starting ezdb daemon, do not close this window\" && [daemon]") + var/result_code = result[1] + if (!result_code || result_code == 1) + return + + stack_trace("Failed to start DB daemon: [result_code]\n[result[3]]") + +/datum/controller/subsystem/dbcore/proc/stop_db_daemon() + set waitfor = FALSE + + if (!db_daemon_started) + return + + db_daemon_started = FALSE + + var/daemon = CONFIG_GET(string/db_daemon) + if (!daemon) + return + + switch (world.system_type) + if (MS_WINDOWS) + var/list/result = world.shelleo("Get-Process | ? { $_.Path -eq '[daemon]' } | Stop-Process") + ASSERT(result[1], "Failed to stop DB daemon: [result[3]]") + if (UNIX) + var/list/result = world.shelleo("kill $(pgrep -f '[daemon]')") + ASSERT(result[1], "Failed to stop DB daemon: [result[3]]") + /datum/DBQuery // Inputs var/connection diff --git a/dependencies.sh b/dependencies.sh index ceae6b6abeb8..8f6360c72b76 100755 --- a/dependencies.sh +++ b/dependencies.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh #Project dependencies file #Final authority on what's required to fully build the project @@ -17,5 +17,8 @@ export NODE_VERSION_PRECISE=14.16.1 # SpacemanDMM git tag export SPACEMAN_DMM_VERSION=suite-1.8 +# Python version for mapmerge and other tools +export PYTHON_VERSION=3.9.0 + # Auxmos git tag export AUXMOS_VERSION=434ed4ca7a0bf072f9861bd6e54552af8fb9e27f diff --git a/tools/HitboxExpander/.gitignore b/tools/HitboxExpander/.gitignore deleted file mode 100644 index ec7f5fd7c18f..000000000000 --- a/tools/HitboxExpander/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -Imaging-1.1.7/ -zlib/ - diff --git a/tools/HitboxExpander/Hitbox Expander.bat b/tools/HitboxExpander/Hitbox Expander.bat new file mode 100644 index 000000000000..ac69e5505dbd --- /dev/null +++ b/tools/HitboxExpander/Hitbox Expander.bat @@ -0,0 +1,2 @@ +@call "%~dp0\..\bootstrap\python" -m HitboxExpander %* +@pause diff --git a/tools/HitboxExpander/README b/tools/HitboxExpander/README.txt old mode 100755 new mode 100644 similarity index 58% rename from tools/HitboxExpander/README rename to tools/HitboxExpander/README.txt index df9cb5babb03..67a5b8bf42de --- a/tools/HitboxExpander/README +++ b/tools/HitboxExpander/README.txt @@ -1,10 +1,7 @@ -Setup: Unzip the zip files in third_party/ so that you have the directories -third_party/Imaging-1.1.7/ and third_party/zlib/. - -Usage: python hitbox_expander.py - -This tool expands the hitbox of the given image by 1 pixel. -Works by changing some of the fully-transparent pixels to alpha=1 black pixels. -Naked human eye usually cannot notice the difference. - -No space carps or corgis have been used or injured in the production of this tool. +Usage: tools/bootstrap/python -m HitboxExpander + +This tool expands the hitbox of the given image by 1 pixel. +Works by changing some of the fully-transparent pixels to alpha=1 black pixels. +Naked human eye usually cannot notice the difference. + +No space carps or corgis have been used or injured in the production of this tool. diff --git a/tools/HitboxExpander/hitbox_expander.py b/tools/HitboxExpander/__main__.py old mode 100755 new mode 100644 similarity index 74% rename from tools/HitboxExpander/hitbox_expander.py rename to tools/HitboxExpander/__main__.py index 0f1991bd3534..c06352440c7a --- a/tools/HitboxExpander/hitbox_expander.py +++ b/tools/HitboxExpander/__main__.py @@ -2,21 +2,10 @@ import sys import inspect import shutil - -def AddToPath(path): - if path not in sys.path: - sys.path.insert(0, path) - delimeter = ':' if os.name == "posix" else ";" - os.environ['PATH'] = path + delimeter + os.environ['PATH'] +import PIL.Image as Image current_dir = os.path.split(inspect.getfile(inspect.currentframe()))[0] -AddToPath(os.path.abspath(os.path.join(current_dir, "third_party/Imaging-1.1.7/PIL"))) -AddToPath(os.path.abspath(os.path.join(current_dir, "third_party/zlib"))) - -import Image -import _imaging - def PngSave(im, file): # From http://blog.client9.com/2007/08/28/python-pil-and-png-metadata-take-2.html @@ -25,13 +14,13 @@ def PngSave(im, file): reserved = ('interlace', 'gamma', 'dpi', 'transparency', 'aspect') # undocumented class - import PngImagePlugin + import PIL.PngImagePlugin as PngImagePlugin meta = PngImagePlugin.PngInfo() # copy metadata into new object - for k,v in im.info.iteritems(): + for k,v in im.info.items(): if k in reserved: continue - meta.add_text(k, v, 0) + meta.add_text(k, str(v), 0) # and save im.save(file, "PNG", pnginfo=meta) @@ -44,7 +33,7 @@ def ProcessFile(path): try: im = Image.open(path) - print name + ": " + im.format, im.size, im.mode + print(name + ": " + im.format, im.size, im.mode) if im.mode != "RGBA": return width, height = im.size @@ -76,15 +65,19 @@ def add(x, y): pix[coords] = (0, 0, 0, 1) PngSave(im, path) - except: - print "Could not process " + name + except Exception as e: + print("Could not process " + name) + print(e) root_dir = os.path.abspath(os.path.join(current_dir, "../../")) icons_dir = os.path.join(root_dir, "icons") def Main(): if len(sys.argv) != 2: - print "Usage: hitbox_expander.py filename.dmi" + if os.name == 'nt': + print("Usage: drag-and-drop a .dmi onto `Hitbox Expander.bat`\n or") + with open(os.path.join(current_dir, "README.txt")) as f: + print(f.read()) return 0 try: @@ -101,7 +94,7 @@ def Main(): ProcessFile(path) return 0 - print "File not found: " + sys.argv[1] + print("File not found: " + sys.argv[1]) if __name__ == "__main__": Main() diff --git a/tools/HitboxExpander/hitbox_expander.sh b/tools/HitboxExpander/hitbox_expander.sh new file mode 100644 index 000000000000..aca0ebf9e355 --- /dev/null +++ b/tools/HitboxExpander/hitbox_expander.sh @@ -0,0 +1,3 @@ +#!/bin/sh +set -e +exec "$(dirname "$0")/../bootstrap/python" -m HitboxExpander "$@" diff --git a/tools/HitboxExpander/third_party/Imaging-1.1.7.zip b/tools/HitboxExpander/third_party/Imaging-1.1.7.zip deleted file mode 100755 index a9af836b11ab..000000000000 Binary files a/tools/HitboxExpander/third_party/Imaging-1.1.7.zip and /dev/null differ diff --git a/tools/HitboxExpander/third_party/zlib.zip b/tools/HitboxExpander/third_party/zlib.zip deleted file mode 100755 index af4a2fd4c5c5..000000000000 Binary files a/tools/HitboxExpander/third_party/zlib.zip and /dev/null differ diff --git a/tools/bootstrap/node_.ps1 b/tools/bootstrap/node_.ps1 index ac8d3b343f16..1107d9542c8e 100644 --- a/tools/bootstrap/node_.ps1 +++ b/tools/bootstrap/node_.ps1 @@ -20,7 +20,8 @@ function Download-Node { Write-Output "Downloading Node v$NodeVersion (may take a while)" New-Item $NodeTargetDir -ItemType Directory -ErrorAction silentlyContinue | Out-Null $WebClient = New-Object Net.WebClient - $WebClient.DownloadFile($NodeSource, $NodeTarget) + $WebClient.DownloadFile($NodeSource, "$NodeTarget.downloading") + Rename-Item "$NodeTarget.downloading" $NodeTarget } ## Convenience variables diff --git a/tools/bootstrap/python37._pth b/tools/bootstrap/python37._pth deleted file mode 100644 index 4fe54372613d..000000000000 --- a/tools/bootstrap/python37._pth +++ /dev/null @@ -1,6 +0,0 @@ -python37.zip -. -..\..\.. - -# Uncomment to run site.main() automatically -import site diff --git a/tools/bootstrap/python_.ps1 b/tools/bootstrap/python_.ps1 index 93a6f756960c..f37589f10644 100644 --- a/tools/bootstrap/python_.ps1 +++ b/tools/bootstrap/python_.ps1 @@ -49,8 +49,13 @@ if (!(Test-Path $PythonExe -PathType Leaf)) { [System.IO.Compression.ZipFile]::ExtractToDirectory($Archive, $PythonDir) + $PythonVersionArray = $PythonVersion.Split(".") + $PythonVersionString = "python$($PythonVersionArray[0])$($PythonVersionArray[1])" + Write-Output "Generating PATH descriptor." + New-Item "$Cache/$PythonVersionString._pth" | Out-Null + Set-Content "$Cache/$PythonVersionString._pth" "$PythonVersionString.zip`n.`n..\..\..`nimport site`n" # Copy a ._pth file without "import site" commented, so pip will work - Copy-Item "$Bootstrap/python37._pth" $PythonDir ` + Copy-Item "$Cache/$PythonVersionString._pth" $PythonDir ` -ErrorAction Stop Remove-Item $Archive diff --git a/tools/ezdb/__main__.py b/tools/ezdb/__main__.py new file mode 100644 index 000000000000..c817aa4d6541 --- /dev/null +++ b/tools/ezdb/__main__.py @@ -0,0 +1,15 @@ +import argparse +from .steps import STEPS + +parser = argparse.ArgumentParser() +parser.add_argument("--port", type = int, default = 1338) + +args = parser.parse_args() + +for step in STEPS: + if not step.should_run(): + continue + + step.run(args) + +print("Done!") diff --git a/tools/ezdb/ezdb.bat b/tools/ezdb/ezdb.bat new file mode 100644 index 000000000000..b4257fe3c364 --- /dev/null +++ b/tools/ezdb/ezdb.bat @@ -0,0 +1,2 @@ +@call "%~dp0\..\bootstrap\python" -m ezdb %* +@pause diff --git a/tools/ezdb/ezdb/__init__.py b/tools/ezdb/ezdb/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tools/ezdb/ezdb/changes.py b/tools/ezdb/ezdb/changes.py new file mode 100644 index 000000000000..607ea1efea35 --- /dev/null +++ b/tools/ezdb/ezdb/changes.py @@ -0,0 +1,31 @@ +import re +from dataclasses import dataclass +from .paths import get_changelog_path + +REGEX_CHANGE = r"-+\s*Version (?P[0-9]+)\.(?P[0-9]+), .+?\`\`\`sql\s*(?P.+?)\s*\`\`\`.*?-{5}" + +@dataclass +class Change: + major_version: int + minor_version: int + sql: str + +def get_changes() -> list[Change]: + with open(get_changelog_path(), "r") as file: + changelog = file.read() + changes = [] + + for change_match in re.finditer(REGEX_CHANGE, changelog, re.MULTILINE | re.DOTALL): + changes.append(Change( + int(change_match.group("major")), + int(change_match.group("minor")), + change_match.group("sql") + )) + + changes.sort(key = lambda change: (change.major_version, change.minor_version), reverse = True) + + return changes + +def get_current_version(): + changes = get_changes() + return (changes[0].major_version, changes[0].minor_version) diff --git a/tools/ezdb/ezdb/config.py b/tools/ezdb/ezdb/config.py new file mode 100644 index 000000000000..87d853cd42bf --- /dev/null +++ b/tools/ezdb/ezdb/config.py @@ -0,0 +1,22 @@ +from .paths import get_config_path +from typing import Optional + +def read_config() -> Optional[dict[str, str]]: + config_path = get_config_path() + if not config_path.exists(): + return None + + with config_path.open('r') as file: + lines = file.readlines() + entries = {} + + for line in lines: + if line.startswith("#"): + continue + if " " not in line: + continue + + key, value = line.split(" ", 1) + entries[key.strip()] = value.strip() + + return entries diff --git a/tools/ezdb/ezdb/mysql.py b/tools/ezdb/ezdb/mysql.py new file mode 100644 index 000000000000..249704fe04de --- /dev/null +++ b/tools/ezdb/ezdb/mysql.py @@ -0,0 +1,68 @@ +import atexit +import mysql.connector +import subprocess +from contextlib import closing +from .config import read_config +from .paths import get_mariadb_client_path, get_mariadb_daemon_path + +def open_connection(): + config = read_config() + assert config["FEEDBACK_PASSWORD"] is not None, "No password found in config file" + + connection = mysql.connector.connect( + user = config["FEEDBACK_LOGIN"], + password = config["FEEDBACK_PASSWORD"], + port = int(config["PORT"]), + raise_on_warnings = True, + ) + + connection.autocommit = True + + return closing(connection) + +# We use custom things like delimiters, so we can't use the built-in cursor.execute +def execute_sql(sql: str): + config = read_config() + assert config is not None, "No config file found" + assert config["FEEDBACK_PASSWORD"] is not None, "No password found in config file" + + subprocess.run( + [ + str(get_mariadb_client_path()), + "-u", + "root", + "-p" + config["FEEDBACK_PASSWORD"], + "--port", + config["PORT"], + "--database", + config["FEEDBACK_DATABASE"], + ], + input = sql, + encoding = "utf-8", + check = True, + stderr = subprocess.STDOUT, + ) + +def insert_new_schema_query(major_version: int, minor_version: int): + return f"INSERT INTO `schema_revision` (`major`, `minor`) VALUES ({major_version}, {minor_version})" + +process = None +def start_daemon(): + global process + if process is not None: + return + + print("Starting MariaDB daemon...") + config = read_config() + assert config is not None, "No config file found" + + process = subprocess.Popen( + [ + str(get_mariadb_daemon_path()), + "--port", + config["PORT"], + ], + stderr = subprocess.PIPE, + ) + + atexit.register(process.kill) diff --git a/tools/ezdb/ezdb/paths.py b/tools/ezdb/ezdb/paths.py new file mode 100644 index 000000000000..502e6762b35f --- /dev/null +++ b/tools/ezdb/ezdb/paths.py @@ -0,0 +1,34 @@ +import pathlib + +def get_root_path(): + current_path = pathlib.Path(__file__) + while current_path.name != 'tools': + current_path = current_path.parent + return current_path.parent + +def get_config_path(): + return get_root_path() / 'config' / 'ezdb.txt' + +def get_db_path(): + return get_root_path() / 'db' + +def get_data_path(): + return get_db_path() / 'data' + +def get_mariadb_bin_path(): + return get_db_path() / 'bin' + +def get_mariadb_client_path(): + return get_mariadb_bin_path() / 'mariadb.exe' + +def get_mariadb_daemon_path(): + return get_mariadb_bin_path() / 'mariadbd.exe' + +def get_mariadb_install_db_path(): + return get_mariadb_bin_path() / 'mariadb-install-db.exe' + +def get_initial_schema_path(): + return get_root_path() / 'SQL' / 'tgstation_schema.sql' + +def get_changelog_path(): + return get_root_path() / 'SQL' / 'database_changelog.md' diff --git a/tools/ezdb/steps/__init__.py b/tools/ezdb/steps/__init__.py new file mode 100644 index 000000000000..969a1029d79c --- /dev/null +++ b/tools/ezdb/steps/__init__.py @@ -0,0 +1,11 @@ +from .download_mariadb import DownloadMariaDB +from .install_database import InstallDatabase +from .install_initial_schema import InstallInitialSchema +from .update_schema import UpdateSchema + +STEPS = [ + DownloadMariaDB, + InstallDatabase, + InstallInitialSchema, + UpdateSchema, +] diff --git a/tools/ezdb/steps/download_mariadb.py b/tools/ezdb/steps/download_mariadb.py new file mode 100644 index 000000000000..ecaf01cf6e23 --- /dev/null +++ b/tools/ezdb/steps/download_mariadb.py @@ -0,0 +1,50 @@ +import os +import pathlib +import tempfile +import urllib.request +import zipfile +from ..ezdb.paths import get_config_path, get_data_path, get_db_path, get_mariadb_bin_path, get_mariadb_daemon_path, get_mariadb_install_db_path +from .step import Step + +# Theoretically, this could use the REST API that MariaDB has to find the URL given a version: +# https://downloads.mariadb.org/rest-api/mariadb/10.11 +DOWNLOAD_URL = "http://downloads.mariadb.org/rest-api/mariadb/10.11.2/mariadb-10.11.2-winx64.zip" +FOLDER_NAME = "mariadb-10.11.2-winx64" + +temp_extract_path = get_db_path() / "_temp/" + +class DownloadMariaDB(Step): + @staticmethod + def should_run() -> bool: + return not get_mariadb_bin_path().exists() + + @staticmethod + def run(args): + if temp_extract_path.exists(): + print("Deleting old temporary extract folder") + temp_extract_path.rmdir() + + print("Downloading portable MariaDB...") + + # delete = False so we can write to it + temporary_file = tempfile.NamedTemporaryFile(delete = False) + + try: + urllib.request.urlretrieve(DOWNLOAD_URL, temporary_file.name) + + print("Extracting...") + os.makedirs(temp_extract_path, exist_ok = True) + with zipfile.ZipFile(temporary_file) as zip_file: + for file in zip_file.namelist(): + if file.startswith(f"{FOLDER_NAME}/bin/"): + with zip_file.open(file) as source, open(temp_extract_path / pathlib.Path(file).name, "wb") as target: + target.write(source.read()) + + print("Moving...") + + temp_extract_path.rename(get_mariadb_bin_path()) + finally: + temporary_file.close() + + if temp_extract_path.exists(): + temp_extract_path.rmdir() diff --git a/tools/ezdb/steps/install_database.py b/tools/ezdb/steps/install_database.py new file mode 100644 index 000000000000..698faf781ed5 --- /dev/null +++ b/tools/ezdb/steps/install_database.py @@ -0,0 +1,46 @@ +import argparse +import secrets +import subprocess +from ..ezdb.paths import get_config_path, get_data_path, get_mariadb_bin_path, get_mariadb_daemon_path, get_mariadb_install_db_path +from .step import Step + +def create_password() -> str: + return secrets.token_urlsafe(40) + +class InstallDatabase(Step): + @staticmethod + def should_run() -> bool: + # If the db folder exists, but the config doesn't, we cancelled + # halfway through and ought to start over by deleting the data folder. + return get_mariadb_bin_path().exists() and (not get_data_path().exists() or not get_config_path().exists()) + + @staticmethod + def run(args: argparse.Namespace): + data_folder = get_data_path() + if data_folder.exists(): + print("Deleting old data folder") + data_folder.rmdir() + + password = create_password() + + print("Installing database...") + + subprocess.run( + [ + str(get_mariadb_install_db_path()), + f"--port={args.port}", + f"--password={password}", + ], + check = True, + stderr = subprocess.STDOUT, + ) + + print("Creating config...") + with open(get_config_path(), "w") as file: + file.write("SQL_ENABLED\n") + file.write(f"PORT {args.port}\n") + file.write(f"FEEDBACK_LOGIN root\n") + file.write(f"FEEDBACK_PASSWORD {password}\n") + file.write("FEEDBACK_DATABASE tgstation\n") + file.write("FEEDBACK_TABLEPREFIX\n") + file.write(f"DB_DAEMON {str(get_mariadb_daemon_path())}") diff --git a/tools/ezdb/steps/install_initial_schema.py b/tools/ezdb/steps/install_initial_schema.py new file mode 100644 index 000000000000..bb7ee519ae70 --- /dev/null +++ b/tools/ezdb/steps/install_initial_schema.py @@ -0,0 +1,53 @@ +from contextlib import closing +from ..ezdb.changes import get_current_version +from ..ezdb.config import read_config +from ..ezdb.mysql import execute_sql, insert_new_schema_query, open_connection, start_daemon +from ..ezdb.paths import get_initial_schema_path +from .step import Step + +class InstallInitialSchema(Step): + @staticmethod + def should_run() -> bool: + start_daemon() + + config = read_config() + assert config is not None, "No config file found" + + database = config["FEEDBACK_DATABASE"] + assert database is not None, "No database found in config file" + + with open_connection() as connection: + with closing(connection.cursor()) as cursor: + cursor.execute(f"SHOW DATABASES LIKE '{database}'") + if cursor.fetchone() is None: + return True + + cursor.execute(f"USE {database}") + cursor.execute("SHOW TABLES LIKE 'schema_revision'") + if cursor.fetchone() is None: + return True + + cursor.execute("SELECT * FROM `schema_revision` LIMIT 1") + if cursor.fetchone() is None: + return True + + return False + + @staticmethod + def run(args): + print("Installing initial schema...") + + config = read_config() + assert config is not None, "No config file found" + + with open_connection() as connection: + with closing(connection.cursor()) as cursor: + database = config["FEEDBACK_DATABASE"] + cursor.execute(f"CREATE DATABASE {database}") + cursor.execute(f"USE {database}") + + (major_version, minor_version) = get_current_version() + + with open(get_initial_schema_path(), 'r') as file: + schema = file.read() + execute_sql(schema + ";" + insert_new_schema_query(major_version, minor_version)) diff --git a/tools/ezdb/steps/step.py b/tools/ezdb/steps/step.py new file mode 100644 index 000000000000..6ae4b492df41 --- /dev/null +++ b/tools/ezdb/steps/step.py @@ -0,0 +1,10 @@ +import argparse + +class Step: + @staticmethod + def should_run() -> bool: + raise NotImplementedError() + + @staticmethod + def run(args: argparse.Namespace): + raise NotImplementedError() diff --git a/tools/ezdb/steps/update_schema.py b/tools/ezdb/steps/update_schema.py new file mode 100644 index 000000000000..e5a1c8d872ef --- /dev/null +++ b/tools/ezdb/steps/update_schema.py @@ -0,0 +1,38 @@ +from contextlib import closing +from ..ezdb.changes import get_changes +from ..ezdb.config import read_config +from ..ezdb.mysql import execute_sql, insert_new_schema_query, open_connection +from .step import Step + +class UpdateSchema(Step): + @staticmethod + def should_run() -> bool: + # Last step is always run + return True + + @staticmethod + def run(args): + config = read_config() + assert config is not None, "No config file found" + + database = config["FEEDBACK_DATABASE"] + assert database is not None, "No database found in config file" + + with open_connection() as connection: + with closing(connection.cursor()) as cursor: + cursor.execute(f"USE {database}") + cursor.execute("SELECT major, minor FROM `schema_revision` ORDER BY `major` DESC, `minor` DESC LIMIT 1") + (major_version, minor_version) = cursor.fetchone() + + changes = get_changes() + for change in changes: + if change.major_version != major_version: + print("NOT IMPLEMENTED: Major version change, these historically require extra tooling") + continue + + if change.minor_version > minor_version: + print(f"Running change {change.major_version}.{change.minor_version}") + execute_sql(change.sql + ";" + insert_new_schema_query(change.major_version, change.minor_version)) + else: + print("No updates necessary") + return diff --git a/tools/hooks/install.sh b/tools/hooks/install.sh index 997d2851441e..30785a290bef 100755 --- a/tools/hooks/install.sh +++ b/tools/hooks/install.sh @@ -15,5 +15,5 @@ echo "Installing tgui hooks" ../../tgui/bin/tgui --install-git-hooks echo "Installing Python dependencies" -./python.sh -m pip install -r ../mapmerge2/requirements.txt +./python.sh -m pip install -r ../requirements.txt echo "Done" diff --git a/tools/hooks/install_only_icon_merger.sh b/tools/hooks/install_only_icon_merger.sh index 0b1ef9869a93..ac4420c72242 100644 --- a/tools/hooks/install_only_icon_merger.sh +++ b/tools/hooks/install_only_icon_merger.sh @@ -11,5 +11,5 @@ echo "Installing tgui hooks" ../../tgui/bin/tgui --install-git-hooks echo "Installing Python dependencies" -./python.sh -m pip install -r ../mapmerge2/requirements.txt +./python.sh -m pip install -r ../requirements.txt echo "Done" diff --git a/tools/mapmerge2/requirements.txt b/tools/requirements.txt similarity index 53% rename from tools/mapmerge2/requirements.txt rename to tools/requirements.txt index 5efb583a501c..54c86787040b 100644 --- a/tools/mapmerge2/requirements.txt +++ b/tools/requirements.txt @@ -1,3 +1,6 @@ pygit2==1.11.1 bidict==0.22.0 Pillow==9.3.0 + +# ezdb +mysql-connector-python==8.0.33