diff --git a/.github/workflows/ci_suite.yml b/.github/workflows/ci_suite.yml
index d29a4c2d96ea..46492e19abe6 100644
--- a/.github/workflows/ci_suite.yml
+++ b/.github/workflows/ci_suite.yml
@@ -30,7 +30,7 @@ jobs:
run_linters:
name: Run Linters
needs: start_gate
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
@@ -75,6 +75,7 @@ jobs:
bash tools/ci/install_node.sh
bash tools/ci/install/install_spaceman_dmm.sh dreamchecker
bash tools/ci/install_ripgrep.sh
+ sudo apt install -y python3-pip
tools/bootstrap/python -c ''
- name: Give Linters A Go
id: linter-setup
@@ -140,19 +141,25 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v4
+ - name: Restore Flyway
+ uses: actions/cache@v4
+ with:
+ path: ~/flyway
+ key: ${{ runner.os }}-flyway-${{ hashFiles('dependencies.sh') }}
- name: Restore BYOND cache
uses: actions/cache@v4
with:
path: ~/BYOND
key: ${{ runner.os }}-byond-${{ hashFiles('dependencies.sh') }}
+ - name: Install flyway
+ run: |
+ bash tools/ci/install/install_flyway.sh
- name: Setup database
run: |
sudo systemctl start mysql
- mysql -u root -proot -e 'CREATE DATABASE ss13_ci;'
- mysql -u root -proot ss13_ci < SQL/database_schema_prefixed.sql
- mysql -u root -proot ss13_ci < SQL/unified_schema.sql
- # mysql -u root -proot -e 'CREATE DATABASE tg_ci_prefixed;'
- # mysql -u root -proot tg_ci_prefixed < SQL/tgstation_schema_prefixed.sql
+ mysql -u root -proot -e 'CREATE DATABASE ss13;'
+ source dependencies.sh
+ ~/flyway/flyway-$FLYWAY_VERSION/flyway -user=root -password=root -url=jdbc:mariadb://localhost:3306/ss13 -locations="filesystem:sql/migrations" migrate
- name: Install rust-g
run: |
bash tools/ci/install/install_rust_g.sh
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 87869168fcf6..44df201905e9 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -8,6 +8,7 @@
"donkie.vscode-tgstation-test-adapter",
"anturk.dmi-editor",
"aaron-bond.better-comments",
- "ss13.opendream"
+ "ss13.opendream",
+ "tamasfe.even-better-toml"
]
}
diff --git a/README.md b/README.md
index b0228569fead..a957b846c105 100644
--- a/README.md
+++ b/README.md
@@ -42,8 +42,12 @@ On **May 9, 2022** we have changed the way to compile the codebase.
## SQL Setup
The SQL backend for the library and stats tracking requires a MariaDB server.
-Your server details go in /config/legacy/dbconfig.txt, and the SQL schema is in /SQL/tgstation_schema.sql.
-More detailed setup instructions arecoming soon, for now ask in our Discord.
+Your server details go in /config/legacy/dbconfig.txt.
+
+Flyway is used for setup and migration. Run the migrations in `sql/migrations` against your database, and everything should just work.
+We do not use table prefixes.
+
+More detailed setup instructions are coming soon, for now ask in our Discord.
todo: update this section
@@ -62,6 +66,7 @@ These are also the folders you are likely going to encounter while managing the
- /players: player data, like saves and characters get dumped in here
- /tmp: server scratch space
- /assets - for asset generation
+ - /config - used as scratch space for config
You only need to make the top level folders (e.g. config, data) static folders in TGS4.
diff --git a/SQL/database_schema.sql b/SQL/database_schema.sql
deleted file mode 100644
index f1b50df6fd40..000000000000
--- a/SQL/database_schema.sql
+++ /dev/null
@@ -1,439 +0,0 @@
-/**
- * make sure to bump schema version and mark changes in database_changelog.md!
- *
- * default prefix is rp_
- * find replace case sensitive %_PREFIX_%
- * PRESERVE ANY vr_'s! We need to replace those tables and features at some point, that's how we konw.
- **/
-
--- core --
-
---
--- Table structure for table `schema_revision`
---
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%schema_revision` (
- `major` TINYINT(3) unsigned NOT NULL,
- `minor` TINYINT(3) unsigned NOT NULL,
- `date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
- PRIMARY KEY (`major`, `minor`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- persistence --
-
--- SSpersistence modules/bulk_entity
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%persistence_bulk_entity` (
- `id` INT(24) NOT NULL AUTO_INCREMENT,
- `generation` INT(11) NOT NULL,
- `persistence_key` VARCHAR(64) NOT NULL,
- `level_id` VARCHAR(64) NOT NULL,
- `data` MEDIUMTEXT,
- `round_id` INT(11) NOT NULL,
- PRIMARY KEY (`id`),
- INDEX(`level_id`, `generation`, `persistence_key`),
- INDEX(`level_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- SSpersistence modules/level_objects
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%persistence_static_level_objects` (
- `generation` INT(11) NOT NULL,
- `object_id` VARCHAR(64) NOT NULL,
- `level_id` VARCHAR(64) NOT NULL,
- `data` MEDIUMTEXT NOT NULL,
- PRIMARY KEY(`generation`, `object_id`, `level_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- SSpersistence modules/level_objects
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%persistence_static_map_objects` (
- `generation` INT(11) NOT NULL,
- `object_id` VARCHAR(64) NOT NULL,
- `map_id` VARCHAR(64) NOT NULL,
- `data` MEDIUMTEXT NOT NULL,
- PRIMARY KEY(`generation`, `object_id`, `map_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- SSpersistence modules/level_objects
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%persistence_static_global_objects` (
- `generation` INT(11) NOT NULL,
- `object_id` VARCHAR(64) NOT NULL,
- `data` MEDIUMTEXT NOT NULL,
- PRIMARY KEY(`generation`, `object_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- SSpersistence modules/level_objects
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%persistence_dynamic_objects` (
- `generation` INT(11) NOT NULL,
- `object_id` INT(24) NOT NULL AUTO_INCREMENT,
- `level_id` VARCHAR(64) NOT NULL,
- `prototype_id` VARCHAR(256) NOT NULL,
- `status` INT(24) NOT NULL DEFAULT 0,
- `data` MEDIUMTEXT NOT NULL,
- `x` INT(8) NOT NULL,
- `y` INT(8) NoT NULL,
- PRIMARY KEY(`object_id`, `generation`),
- INDEX(`object_id`),
- INDEX(`level_id`, `generation`),
- INDEX(`prototype_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- SSpersistence modules/spatial_metadata
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%persistence_level_metadata` (
- `created` DATETIME NOT NULL DEFAULT Now(),
- `saved` DATETIME NOT NULL,
- `saved_round_id` INT(11) NOT NULL,
- `level_id` VARCHAR(64) NOT NULL,
- `data` MEDIUMTEXT NOT NULL,
- `generation` INT(11) NOT NULL,
- PRIMARY KEY(`level_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- SSpersistence modules/string_kv
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%persistence_string_kv` (
- `created` DATETIME NOT NULL DEFAULT Now(),
- `modified` DATETIME NOT NULL,
- `key` VARCHAR(64) NOT NULL,
- `value` MEDIUMTEXT NULL,
- `group` VARCHAR(64) NOT NULL,
- `revision` INT(11) NOT NULL,
- PRIMARY KEY(`key`, `group`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- photography --
-
--- picture table --
--- used to store data about pictures --
--- hash is in sha1 format. --
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%pictures` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `hash` char(40) NOT NULL,
- `created` datetime NOT NULL DEFAULT Now(),
- `width` int NOT NULL,
- `height` int NOT NULL,
- PRIMARY KEY (`id`),
- UNIQUE KEY `hash` (`hash`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- photograph table --
--- used to store data about photographs --
--- picture is picture hash in picture table --
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%photographs` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `picture` char(40) NOT NULL,
- `created` datetime NOT NULL DEFAULT Now(),
- `scene` MEDIUMTEXT null,
- `desc` MEDIUMTEXT null,
- CONSTRAINT `linked_picture` FOREIGN KEY (`picture`)
- REFERENCES `%_PREFIX_%pictures` (`hash`)
- ON DELETE CASCADE
- ON UPDATE CASCADE,
- PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- Players --
-
--- Player lookup table --
--- Used to look up player ID from ckey, as well as --
--- store last computerid/ip for a ckey. --
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%player_lookup` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `ckey` varchar(32) NOT NULL,
- `firstseen` datetime NOT NULL,
- `lastseen` datetime NOT NULL,
- `ip` varchar(18) NOT NULL,
- `computerid` varchar(32) NOT NULL,
- `lastadminrank` varchar(32) NOT NULL DEFAULT 'Player',
- `playerid` int(11),
- PRIMARY KEY (`id`),
- UNIQUE KEY `ckey` (`ckey`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- Primary player table --
--- Allows for one-to-many player-ckey association. --
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%player` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `flags` int(24) NOT NULL DEFAULT 0,
- `firstseen` datetime NOT NULL DEFAULT Now(),
- `lastseen` datetime NOT NULL,
- `misc` MEDIUMTEXT NOT NULL,
- PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- Playtime / JEXP --
-
--- Role Time Table - Master --
--- Stores total role time. --
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%playtime` (
- `player` INT(11) NOT NULL,
- `roleid` VARCHAR(64) NOT NULL,
- `minutes` INT UNSIGNED NOT NULL,
- PRIMARY KEY(`player`, `roleid`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- Role Time - Logging --
--- Stores changes in role time --
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%playtime_log` (
- `player` INT(11),
- `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
- `roleid` VARCHAR(64) NOT NULL,
- `delta` INT(11) NOT NULL,
- `datetime` TIMESTAMP NOT NULL DEFAULT NOW() ON UPDATE NOW(),
- PRIMARY KEY (`id`),
- KEY `player` (`player`),
- KEY `roleid` (`roleid`),
- KEY `datetime` (`datetime`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-DELIMITER $$
-CREATE TRIGGER `playtimeTlogupdate` AFTER UPDATE ON `%_PREFIX_%playtime` FOR EACH ROW BEGIN INSERT into `%_PREFIX_%playtime_log` (player, roleid, delta) VALUES (NEW.player, NEW.roleid, NEW.minutes-OLD.minutes);
-END
-$$
-CREATE TRIGGER `playtimeTloginsert` AFTER INSERT ON `%_PREFIX_%playtime` FOR EACH ROW BEGIN INSERT into `%_PREFIX_%playtime_log` (player, roleid, delta) VALUES (NEW.player, NEW.roleid, NEW.minutes);
-END
-$$
-CREATE TRIGGER `playtimeTlogdelete` AFTER DELETE ON `%_PREFIX_%playtime` FOR EACH ROW BEGIN INSERT into `%_PREFIX_%playtime_log` (player, roleid, delta) VALUES (OLD.player, OLD.roleid, 0-OLD.minutes);
-END
-$$
-DELIMITER ;
-
-
--- Preferences --
-
--- Stores game preferences --
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%game_preferences` (
- `player` INT(11) NOT NULL,
- `entries` MEDIUMTEXT NOT NULL,
- `misc` MEDIUMTEXT NOT NULL,
- `keybinds` MEDIUMTEXT NOT NULL,
- `toggles` MEDIUMTEXT NOT NULL,
- `modified` DATETIME NOT NULL,
- `version` INT(11) NOT NULL,
- PRIMARY KEY (`player`),
- CONSTRAINT `linked_player` FOREIGN KEY (`player`)
- REFERENCES `%_PREFIX_%player` (`id`)
- ON DELETE CASCADE
- ON UPDATE CASCADE
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- Security - Ipintel --
-
--- Ipintel Cache Table --
--- Stores cache entries for IPIntel --
--- IP is in INET_ATON. --
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%ipintel` (
- `ip` INT(10) unsigned NOT NULL,
- `date` TIMESTAMP NOT NULL DEFAULT NOW() ON UPDATE NOW(),
- `intel` double NOT NULL DEFAULT '0',
- PRIMARY KEY (`ip`),
- KEY `idx_ipintel` (`ip`, `intel`, `date`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
---
--- Table structure for table `round`
---
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%round` (
- `id` INT(11) NOT NULL AUTO_INCREMENT,
- `initialize_datetime` DATETIME NOT NULL,
- `start_datetime` DATETIME NULL,
- `shutdown_datetime` DATETIME NULL,
- `end_datetime` DATETIME NULL,
- `server_ip` INT(10) UNSIGNED NOT NULL,
- `server_port` SMALLINT(5) UNSIGNED NOT NULL,
- `commit_hash` CHAR(40) NULL,
- PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- Connection log --
--- Logs all connections to the server. --
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%connection_log` (
- `id` INT(11) NOT NULL AUTO_INCREMENT,
- `datetime` datetime NOT NULL,
- `serverip` varchar(45) NOT NULL,
- `ckey` varchar(32) NOT NULL,
- `ip` varchar(45) NOT NULL,
- `computerid` varchar(32) NOT NULL,
- PRIMARY KEY(`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
--- /datum/character - Character Table --
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%character` (
- `id` INT(11) NOT NULL AUTO_INCREMENT,
- `created` DATETIME NOT NULL DEFAULT Now(),
- `last_played` DATETIME NULL,
- `last_persisted` DATETIME NULL,
- `playerid` INT(11) NOT NULL,
- `canonical_name` VARCHAR(128) NOT NULL,
- `persist_data` MEDIUMTEXT NULL,
- `character_type` VARCHAR(64) NOT NULL,
- PRIMARY KEY(`id`),
- CONSTRAINT `character_has_player` FOREIGN KEY (`playerid`)
- REFERENCES `%_PREFIX_%player` (`id`)
- ON DELETE CASCADE
- ON UPDATE CASCADE,
- UNIQUE (`playerid`, `canonical_name`, `character_type`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%admin` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `ckey` varchar(32) NOT NULL,
- `rank` varchar(32) NOT NULL DEFAULT 'Administrator',
- `level` int(2) NOT NULL DEFAULT '0',
- `flags` int(16) NOT NULL DEFAULT '0',
- PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%admin_log` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `datetime` datetime NOT NULL,
- `adminckey` varchar(32) NOT NULL,
- `adminip` varchar(18) NOT NULL,
- `log` text NOT NULL,
- PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%ban` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `bantime` datetime NOT NULL,
- `serverip` varchar(32) NOT NULL,
- `bantype` varchar(32) NOT NULL,
- `reason` text NOT NULL,
- `job` varchar(32) DEFAULT NULL,
- `duration` int(11) NOT NULL,
- `rounds` int(11) DEFAULT NULL,
- `expiration_time` datetime NOT NULL,
- `ckey` varchar(32) NOT NULL,
- `computerid` varchar(32) NOT NULL,
- `ip` varchar(32) NOT NULL,
- `a_ckey` varchar(32) NOT NULL,
- `a_computerid` varchar(32) NOT NULL,
- `a_ip` varchar(32) NOT NULL,
- `who` text NOT NULL,
- `adminwho` text NOT NULL,
- `edits` text,
- `unbanned` tinyint(1) DEFAULT NULL,
- `unbanned_datetime` datetime DEFAULT NULL,
- `unbanned_ckey` varchar(32) DEFAULT NULL,
- `unbanned_computerid` varchar(32) DEFAULT NULL,
- `unbanned_ip` varchar(32) DEFAULT NULL,
- PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%feedback` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `time` datetime NOT NULL,
- `round_id` int(8) NOT NULL,
- `var_name` varchar(32) NOT NULL,
- `var_value` int(16) DEFAULT NULL,
- `details` text,
- PRIMARY KEY (`id`)
-) ENGINE=MyISAM DEFAULT CHARSET=latin1 ;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%poll_option` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `pollid` int(11) NOT NULL,
- `text` varchar(255) NOT NULL,
- `percentagecalc` tinyint(1) NOT NULL DEFAULT '1',
- `minval` int(3) DEFAULT NULL,
- `maxval` int(3) DEFAULT NULL,
- `descmin` varchar(32) DEFAULT NULL,
- `descmid` varchar(32) DEFAULT NULL,
- `descmax` varchar(32) DEFAULT NULL,
- PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%poll_question` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `polltype` varchar(16) NOT NULL DEFAULT 'OPTION',
- `starttime` datetime NOT NULL,
- `endtime` datetime NOT NULL,
- `question` varchar(255) NOT NULL,
- `adminonly` tinyint(1) DEFAULT '0',
- PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%poll_textreply` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `datetime` datetime NOT NULL,
- `pollid` int(11) NOT NULL,
- `ckey` varchar(32) NOT NULL,
- `ip` varchar(18) NOT NULL,
- `replytext` text NOT NULL,
- `adminrank` varchar(32) NOT NULL DEFAULT 'Player',
- PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%poll_vote` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `datetime` datetime NOT NULL,
- `pollid` int(11) NOT NULL,
- `optionid` int(11) NOT NULL,
- `ckey` varchar(255) NOT NULL,
- `ip` varchar(16) NOT NULL,
- `adminrank` varchar(32) NOT NULL,
- `rating` int(2) DEFAULT NULL,
- PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%privacy` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `datetime` datetime NOT NULL,
- `ckey` varchar(32) NOT NULL,
- `option` varchar(128) NOT NULL,
- PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%death` (
- `id` INT(11) NOT NULL AUTO_INCREMENT ,
- `pod` TEXT NOT NULL COMMENT 'Place of death' ,
- `coord` TEXT NOT NULL COMMENT 'X, Y, Z POD' ,
- `tod` DATETIME NOT NULL COMMENT 'Time of death' ,
- `job` TEXT NOT NULL ,
- `special` TEXT NOT NULL ,
- `name` TEXT NOT NULL ,
- `byondkey` TEXT NOT NULL ,
- `laname` TEXT NOT NULL COMMENT 'Last attacker name' ,
- `lakey` TEXT NOT NULL COMMENT 'Last attacker key' ,
- `gender` TEXT NOT NULL ,
- `bruteloss` INT(11) NOT NULL ,
- `brainloss` INT(11) NOT NULL ,
- `fireloss` INT(11) NOT NULL ,
- `oxyloss` INT(11) NOT NULL ,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%karma` (
- `id` INT(11) NOT NULL AUTO_INCREMENT ,
- `spendername` TEXT NOT NULL ,
- `spenderkey` TEXT NOT NULL ,
- `receivername` TEXT NOT NULL ,
- `receiverkey` TEXT NOT NULL ,
- `receiverrole` TEXT NOT NULL ,
- `receiverspecial` TEXT NOT NULL ,
- `isnegative` TINYINT(1) NOT NULL ,
- `spenderip` TEXT NOT NULL ,
- `time` DATETIME NOT NULL ,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%karmatotals` (
- `id` INT(11) NOT NULL AUTO_INCREMENT ,
- `byondkey` TEXT NOT NULL ,
- `karma` INT(11) NOT NULL ,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%library` (
- `id` INT(11) NOT NULL AUTO_INCREMENT ,
- `author` TEXT NOT NULL ,
- `title` TEXT NOT NULL ,
- `content` TEXT NOT NULL ,
- `category` TEXT NOT NULL ,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-
-CREATE TABLE IF NOT EXISTS `%_PREFIX_%population` (
- `id` INT(11) NOT NULL AUTO_INCREMENT ,
- `playercount` INT(11) NULL DEFAULT NULL ,
- `admincount` INT(11) NULL DEFAULT NULL ,
- `time` DATETIME NOT NULL ,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
diff --git a/SQL/unified_schema.sql b/SQL/unified_schema.sql
deleted file mode 100644
index 32441c5e22e6..000000000000
--- a/SQL/unified_schema.sql
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * make sure to bump schema version and mark changes in database_changelog.md!
- *
- * you MUST use unified_ as a prefix.
- *
- * unified schema for citadel, **sync changes to both servers.**
- **/
-
---
--- Table structure for table `schema_revision`
---
-CREATE TABLE IF NOT EXISTS `unified_schema_revision` (
- `major` TINYINT(3) unsigned NOT NULL,
- `minor` TINYINT(3) unsigned NOT NULL,
- `date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- PRIMARY KEY (`major`, `minor`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
diff --git a/citadel.dme b/citadel.dme
index abf6e2030471..7c7f9707d0ac 100644
--- a/citadel.dme
+++ b/citadel.dme
@@ -51,6 +51,7 @@
#include "code\__DEFINES\fonts.dm"
#include "code\__DEFINES\frames.dm"
#include "code\__DEFINES\gamemode.dm"
+#include "code\__DEFINES\gradient.dm"
#include "code\__DEFINES\holidays.dm"
#include "code\__DEFINES\holomap.dm"
#include "code\__DEFINES\icon_smoothing.dm"
@@ -428,7 +429,6 @@
#include "code\__HELPERS\game\turfs\offsets.dm"
#include "code\__HELPERS\graphs\astar.dm"
#include "code\__HELPERS\icons\alpha.dm"
-#include "code\__HELPERS\icons\color.dm"
#include "code\__HELPERS\icons\download.dm"
#include "code\__HELPERS\icons\flatten.dm"
#include "code\__HELPERS\icons\hologram.dm"
@@ -686,6 +686,10 @@
#include "code\controllers\subsystem\sound\_sound.dm"
#include "code\controllers\subsystem\sound\channel_manager.dm"
#include "code\controllers\subsystem\sound\soundbyte_manager.dm"
+#include "code\controllers\toml_config\toml_config_entry.dm"
+#include "code\controllers\toml_config\toml_configuration.dm"
+#include "code\controllers\toml_config\entries\backend.dm"
+#include "code\controllers\toml_config\entries\backend.repository.dm"
#include "code\datums\ability.dm"
#include "code\datums\ability_handler.dm"
#include "code\datums\access.dm"
@@ -2249,6 +2253,7 @@
#include "code\modules\admin\verbs\debug\fucky_wucky.dm"
#include "code\modules\admin\verbs\debug\profiling.dm"
#include "code\modules\admin\verbs\debug\reestablish_db_connection.dm"
+#include "code\modules\admin\verbs\debug\reload_configuration.dm"
#include "code\modules\admin\verbs\debug\spawn.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2_parser.dm"
@@ -4355,6 +4360,7 @@
#include "code\modules\paperwork\faxmachine_vr.dm"
#include "code\modules\paperwork\filingcabinet.dm"
#include "code\modules\paperwork\folders.dm"
+#include "code\modules\paperwork\folders_premade.dm"
#include "code\modules\paperwork\handlabeler.dm"
#include "code\modules\paperwork\paper_bundle.dm"
#include "code\modules\paperwork\paperbin.dm"
diff --git a/code/__DEFINES/controllers/_repository.dm b/code/__DEFINES/controllers/_repository.dm
index b2f5702f7524..9f08048dc8d8 100644
--- a/code/__DEFINES/controllers/_repository.dm
+++ b/code/__DEFINES/controllers/_repository.dm
@@ -7,6 +7,9 @@
//* This is here in [code/__DEFINES/controllers/_repositories.dm] for compile order reasons. *//
/datum/controller/subsystem/repository/proc/__init_repositories()
+//* This is here in [code/__DEFINES/controllers/_repositories.dm] for compile order reasons. *//
+/datum/controller/subsystem/repository/proc/__get_all_repositories()
+
// todo: redo recover logic; maybe /datum/controller as a whole should be brushed up
#define REPOSITORY_DEF(what) \
GLOBAL_REAL(RS##what, /datum/controller/repository/##what); \
@@ -27,4 +30,12 @@ GLOBAL_REAL(RS##what, /datum/controller/repository/##what); \
..(); \
RS##what.Initialize(); \
} \
+/datum/controller/subsystem/repository/__get_all_repositories() { \
+ . = ..(); \
+ . += RS##what; \
+} \
/datum/controller/repository/##what
+
+/// Returned from /datum/controller/repository's fetch_or_defer() if we don't have something
+/// on hand, but also don't know that it doesn't exist.
+#define REPOSITORY_FETCH_DEFER "defer"
diff --git a/code/__DEFINES/controllers/dbcore.dm b/code/__DEFINES/controllers/dbcore.dm
index afc6709805cc..19df14d5588f 100644
--- a/code/__DEFINES/controllers/dbcore.dm
+++ b/code/__DEFINES/controllers/dbcore.dm
@@ -17,6 +17,11 @@
*/
#define DB_MINOR_VERSION 3
+//* Tables *//
+
+/// Prefixes are currently disabled.
+#define DB_PREFIX_TABLE_NAME(TABLE) TABLE
+
//* Misc *//
/// pass this into duplicate_key on mass_insert() to overwrite old values
diff --git a/code/__DEFINES/gradient.dm b/code/__DEFINES/gradient.dm
new file mode 100644
index 000000000000..573a339880b9
--- /dev/null
+++ b/code/__DEFINES/gradient.dm
@@ -0,0 +1,4 @@
+// spacemandmm doesn't really implement gradient() right, so let's just handle that here yeah?
+#define rgb_gradient(index, args...) UNLINT(gradient(args, index))
+#define hsl_gradient(index, args...) UNLINT(gradient(args, space = COLORSPACE_HSL, index))
+#define hsv_gradient(index, args...) UNLINT(gradient(args, space = COLORSPACE_HSV, index))
diff --git a/code/__DEFINES/rust_g.dm b/code/__DEFINES/rust_g.dm
index 36b814968752..cfb41fa39325 100644
--- a/code/__DEFINES/rust_g.dm
+++ b/code/__DEFINES/rust_g.dm
@@ -15,25 +15,36 @@
// On Windows, looks in the standard places for `rust_g.dll`.
// On Linux, looks in `.`, `$LD_LIBRARY_PATH`, and `~/.byond/bin` for either of
// `librust_g.so` (preferred) or `rust_g` (old).
+// On OpenDream, `rust_g64.dll` / `librust_g64.so` are used instead.
/* This comment bypasses grep checks */ /var/__rust_g
+#ifndef OPENDREAM
+#define RUST_G_BASE "rust_g"
+#else
+#define RUST_G_BASE "rust_g64"
+#endif
+
/proc/__detect_rust_g()
if (world.system_type == UNIX)
- if (fexists("./librust_g.so"))
+ if (fexists("./lib[RUST_G_BASE].so"))
// No need for LD_LIBRARY_PATH badness.
- return __rust_g = "./librust_g.so"
- else if (fexists("./rust_g"))
+ return __rust_g = "./lib[RUST_G_BASE].so"
+#ifndef OPENDREAM
+ else if (fexists("./[RUST_G_BASE]"))
// Old dumb filename.
- return __rust_g = "./rust_g"
- else if (fexists("[world.GetConfig("env", "HOME")]/.byond/bin/rust_g"))
+ return __rust_g = "./[RUST_G_BASE]"
+ else if (fexists("[world.GetConfig("env", "HOME")]/.byond/bin/[RUST_G_BASE]"))
// Old dumb filename in `~/.byond/bin`.
- return __rust_g = "rust_g"
+ return __rust_g = RUST_G_BASE
+#endif
else
// It's not in the current directory, so try others
- return __rust_g = "librust_g.so"
+ return __rust_g = "lib[RUST_G_BASE].so"
else
- return __rust_g = "rust_g"
+ return __rust_g = RUST_G_BASE
+
+#undef RUST_G_BASE
#define RUST_G (__rust_g || __detect_rust_g())
#endif
@@ -135,7 +146,7 @@
#define rustg_dmi_icon_states(fname) RUSTG_CALL(RUST_G, "dmi_icon_states")(fname)
#define rustg_file_read(fname) RUSTG_CALL(RUST_G, "file_read")(fname)
-#define rustg_file_exists(fname) RUSTG_CALL(RUST_G, "file_exists")(fname)
+#define rustg_file_exists(fname) (RUSTG_CALL(RUST_G, "file_exists")(fname) == "true")
#define rustg_file_write(text, fname) RUSTG_CALL(RUST_G, "file_write")(text, fname)
#define rustg_file_append(text, fname) RUSTG_CALL(RUST_G, "file_append")(text, fname)
#define rustg_file_get_line_count(fname) text2num(RUSTG_CALL(RUST_G, "file_get_line_count")(fname))
@@ -146,35 +157,15 @@
#define text2file(text, fname) rustg_file_append(text, "[fname]")
#endif
-/**
- * Please see code/datums/math/vec2.dm.
- */
-#define rustg_geometry_delaunay_triangulate_to_graph(point_json) RUSTG_CALL(RUST_G, "geometry_delaunay_triangulate_to_graph")(point_json)
+/// Returns the git hash of the given revision, ex. "HEAD".
+#define rustg_git_revparse(rev) RUSTG_CALL(RUST_G, "rg_git_revparse")(rev)
/**
- * Please see code/datums/math/vec2.dm.
+ * Returns the date of the given revision in the format YYYY-MM-DD.
+ * Returns null if the revision is invalid.
*/
-#define rustg_geometry_delaunay_voronoi_graph(packed) RUSTG_CALL(RUST_G, "geometry_delaunay_voronoi_graph")(packed)
-
-#define rustg_git_revparse(rev) RUSTG_CALL(RUST_G, "rg_git_revparse")(rev)
#define rustg_git_commit_date(rev) RUSTG_CALL(RUST_G, "rg_git_commit_date")(rev)
-#define rustg_hash_string(algorithm, text) RUSTG_CALL(RUST_G, "hash_string")(algorithm, text)
-#define rustg_hash_file(algorithm, fname) RUSTG_CALL(RUST_G, "hash_file")(algorithm, fname)
-#define rustg_hash_generate_totp(seed) RUSTG_CALL(RUST_G, "generate_totp")(seed)
-#define rustg_hash_generate_totp_tolerance(seed, tolerance) RUSTG_CALL(RUST_G, "generate_totp_tolerance")(seed, tolerance)
-
-#define RUSTG_HASH_MD5 "md5"
-#define RUSTG_HASH_SHA1 "sha1"
-#define RUSTG_HASH_SHA256 "sha256"
-#define RUSTG_HASH_SHA512 "sha512"
-#define RUSTG_HASH_XXH64 "xxh64"
-#define RUSTG_HASH_BASE64 "base64"
-
-#ifdef RUSTG_OVERRIDE_BUILTINS
- #define md5(thing) (isfile(thing) ? rustg_hash_file(RUSTG_HASH_MD5, "[thing]") : rustg_hash_string(RUSTG_HASH_MD5, thing))
-#endif
-
#define RUSTG_HTTP_METHOD_GET "get"
#define RUSTG_HTTP_METHOD_PUT "put"
#define RUSTG_HTTP_METHOD_DELETE "delete"
@@ -196,6 +187,74 @@
#define rustg_noise_get_at_coordinates(seed, x, y) RUSTG_CALL(RUST_G, "noise_get_at_coordinates")(seed, x, y)
+#define rustg_sql_connect_pool(options) RUSTG_CALL(RUST_G, "sql_connect_pool")(options)
+#define rustg_sql_query_async(handle, query, params) RUSTG_CALL(RUST_G, "sql_query_async")(handle, query, params)
+#define rustg_sql_query_blocking(handle, query, params) RUSTG_CALL(RUST_G, "sql_query_blocking")(handle, query, params)
+#define rustg_sql_connected(handle) RUSTG_CALL(RUST_G, "sql_connected")(handle)
+#define rustg_sql_disconnect_pool(handle) RUSTG_CALL(RUST_G, "sql_disconnect_pool")(handle)
+#define rustg_sql_check_query(job_id) RUSTG_CALL(RUST_G, "sql_check_query")("[job_id]")
+
+#define rustg_time_microseconds(id) text2num(RUSTG_CALL(RUST_G, "time_microseconds")(id))
+#define rustg_time_milliseconds(id) text2num(RUSTG_CALL(RUST_G, "time_milliseconds")(id))
+#define rustg_time_reset(id) RUSTG_CALL(RUST_G, "time_reset")(id)
+
+/// Returns the timestamp as a string
+/proc/rustg_unix_timestamp()
+ return RUSTG_CALL(RUST_G, "unix_timestamp")()
+
+#define rustg_raw_read_toml_file(path) json_decode(RUSTG_CALL(RUST_G, "toml_file_to_json")(path) || "null")
+
+/proc/rustg_read_toml_file(path)
+ var/list/output = rustg_raw_read_toml_file(path)
+ if (output["success"])
+ return json_decode(output["content"])
+ else
+ CRASH(output["content"])
+
+#define rustg_raw_toml_encode(value) json_decode(RUSTG_CALL(RUST_G, "toml_encode")(json_encode(value)))
+
+/proc/rustg_toml_encode(value)
+ var/list/output = rustg_raw_toml_encode(value)
+ if (output["success"])
+ return output["content"]
+ else
+ CRASH(output["content"])
+
+#define rustg_url_encode(text) RUSTG_CALL(RUST_G, "url_encode")("[text]")
+#define rustg_url_decode(text) RUSTG_CALL(RUST_G, "url_decode")(text)
+
+#ifdef RUSTG_OVERRIDE_BUILTINS
+ #define url_encode(text) rustg_url_encode(text)
+ #define url_decode(text) rustg_url_decode(text)
+#endif
+
+//! Citadel rust-g addons
+/**
+ * Please see code/datums/math/vec2.dm.
+ */
+#define rustg_geometry_delaunay_triangulate_to_graph(point_json) RUSTG_CALL(RUST_G, "geometry_delaunay_triangulate_to_graph")(point_json)
+
+/**
+ * Please see code/datums/math/vec2.dm.
+ */
+#define rustg_geometry_delaunay_voronoi_graph(packed) RUSTG_CALL(RUST_G, "geometry_delaunay_voronoi_graph")(packed)
+
+#define rustg_hash_string(algorithm, text) RUSTG_CALL(RUST_G, "hash_string")(algorithm, text)
+#define rustg_hash_file(algorithm, fname) RUSTG_CALL(RUST_G, "hash_file")(algorithm, fname)
+#define rustg_hash_generate_totp(seed) RUSTG_CALL(RUST_G, "generate_totp")(seed)
+#define rustg_hash_generate_totp_tolerance(seed, tolerance) RUSTG_CALL(RUST_G, "generate_totp_tolerance")(seed, tolerance)
+
+#define RUSTG_HASH_MD5 "md5"
+#define RUSTG_HASH_SHA1 "sha1"
+#define RUSTG_HASH_SHA256 "sha256"
+#define RUSTG_HASH_SHA512 "sha512"
+#define RUSTG_HASH_XXH64 "xxh64"
+#define RUSTG_HASH_BASE64 "base64"
+
+#ifdef RUSTG_OVERRIDE_BUILTINS
+ #define md5(thing) (isfile(thing) ? rustg_hash_file(RUSTG_HASH_MD5, "[thing]") : rustg_hash_string(RUSTG_HASH_MD5, thing))
+#endif
+
/**
* Register a list of nodes into a rust library. This list of nodes must have been serialized in a json.
* Node {// Index of this node in the list of nodes
@@ -275,65 +334,3 @@
* Note: `count` was added in Redis version 6.2.0
*/
#define rustg_redis_lpop(key, count) RUSTG_CALL(RUST_G, "redis_lpop")(key, count)
-
-#define rustg_sql_connect_pool(options) RUSTG_CALL(RUST_G, "sql_connect_pool")(options)
-#define rustg_sql_query_async(handle, query, params) RUSTG_CALL(RUST_G, "sql_query_async")(handle, query, params)
-#define rustg_sql_query_blocking(handle, query, params) RUSTG_CALL(RUST_G, "sql_query_blocking")(handle, query, params)
-#define rustg_sql_connected(handle) RUSTG_CALL(RUST_G, "sql_connected")(handle)
-#define rustg_sql_disconnect_pool(handle) RUSTG_CALL(RUST_G, "sql_disconnect_pool")(handle)
-#define rustg_sql_check_query(job_id) RUSTG_CALL(RUST_G, "sql_check_query")("[job_id]")
-
-#define rustg_time_microseconds(id) text2num(RUSTG_CALL(RUST_G, "time_microseconds")(id))
-#define rustg_time_milliseconds(id) text2num(RUSTG_CALL(RUST_G, "time_milliseconds")(id))
-#define rustg_time_reset(id) RUSTG_CALL(RUST_G, "time_reset")(id)
-
-/// Returns the timestamp as a string
-/proc/rustg_unix_timestamp()
- return RUSTG_CALL(RUST_G, "unix_timestamp")()
-
-#define rustg_raw_read_toml_file(path) json_decode(RUSTG_CALL(RUST_G, "toml_file_to_json")(path) || "null")
-
-/proc/rustg_read_toml_file(path)
- var/list/output = rustg_raw_read_toml_file(path)
- if (output["success"])
- return json_decode(output["content"])
- else
- CRASH(output["content"])
-
-#define rustg_raw_toml_encode(value) json_decode(RUSTG_CALL(RUST_G, "toml_encode")(json_encode(value)))
-
-/proc/rustg_toml_encode(value)
- var/list/output = rustg_raw_toml_encode(value)
- if (output["success"])
- return output["content"]
- else
- CRASH(output["content"])
-
-#define rustg_unzip_download_async(url, unzip_directory) RUSTG_CALL(RUST_G, "unzip_download_async")(url, unzip_directory)
-#define rustg_unzip_check(job_id) RUSTG_CALL(RUST_G, "unzip_check")("[job_id]")
-
-#define rustg_url_encode(text) RUSTG_CALL(RUST_G, "url_encode")("[text]")
-#define rustg_url_decode(text) RUSTG_CALL(RUST_G, "url_decode")(text)
-
-#ifdef RUSTG_OVERRIDE_BUILTINS
- #define url_encode(text) rustg_url_encode(text)
- #define url_decode(text) rustg_url_decode(text)
-#endif
-
-/**
- * This proc generates a noise grid using worley noise algorithm
- *
- * Returns a single string that goes row by row, with values of 1 representing an alive cell, and a value of 0 representing a dead cell.
- *
- * Arguments:
- * * region_size: The size of regions
- * * threshold: the value that determines wether a cell is dead or alive
- * * node_per_region_chance: chance of a node existiing in a region
- * * size: size of the returned grid
- * * node_min: minimum amount of nodes in a region (after the node_per_region_chance is applied)
- * * node_max: maximum amount of nodes in a region
- */
-#define rustg_worley_generate(region_size, threshold, node_per_region_chance, size, node_min, node_max) \
- RUSTG_CALL(RUST_G, "worley_generate")(region_size, threshold, node_per_region_chance, size, node_min, node_max)
-
-
diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm
index 7b1d6e8a71af..44c21c2b069d 100644
--- a/code/__HELPERS/icons.dm
+++ b/code/__HELPERS/icons.dm
@@ -18,98 +18,88 @@ remember you first need to setup an /icon var like so:
GLOBAL_DATUM_INIT(my_icon, /icon, new('iconfile.dmi'))
icon/ChangeOpacity(amount = 1)
- A very common operation in DM is to try to make an icon more or less transparent. Making an icon more
- transparent is usually much easier than making it less so, however. This proc basically is a frontend
- for MapColors() which can change opacity any way you like, in much the same way that SetIntensity()
- can make an icon lighter or darker. If amount is 0.5, the opacity of the icon will be cut in half.
- If amount is 2, opacity is doubled and anything more than half-opaque will become fully opaque.
+ A very common operation in DM is to try to make an icon more or less transparent. Making an icon more
+ transparent is usually much easier than making it less so, however. This proc basically is a frontend
+ for MapColors() which can change opacity any way you like, in much the same way that SetIntensity()
+ can make an icon lighter or darker. If amount is 0.5, the opacity of the icon will be cut in half.
+ If amount is 2, opacity is doubled and anything more than half-opaque will become fully opaque.
+icon/GrayScale()
+ Converts the icon to grayscale instead of a fully colored icon. Alpha values are left intact.
icon/ColorTone(tone)
- Similar to GrayScale(), this proc converts the icon to a range of black -> tone -> white, where tone is an
- RGB color (its alpha is ignored). This can be used to create a sepia tone or similar effect.
- See also the global ColorTone() proc.
+ Similar to GrayScale(), this proc converts the icon to a range of black -> tone -> white, where tone is an
+ RGB color (its alpha is ignored). This can be used to create a sepia tone or similar effect.
+ See also the global ColorTone() proc.
icon/MinColors(icon)
- The icon is blended with a second icon where the minimum of each RGB pixel is the result.
- Transparency may increase, as if the icons were blended with ICON_ADD. You may supply a color in place of an icon.
+ The icon is blended with a second icon where the minimum of each RGB pixel is the result.
+ Transparency may increase, as if the icons were blended with ICON_ADD. You may supply a color in place of an icon.
icon/MaxColors(icon)
- The icon is blended with a second icon where the maximum of each RGB pixel is the result.
- Opacity may increase, as if the icons were blended with ICON_OR. You may supply a color in place of an icon.
-icon/Opaque(background = "#000000")
- All alpha values are set to 255 throughout the icon. Transparent pixels become black, or whatever background color you specify.
+ The icon is blended with a second icon where the maximum of each RGB pixel is the result.
+ Opacity may increase, as if the icons were blended with ICON_OR. You may supply a color in place of an icon.
+icon/Opaque(background = COLOR_BLACK)
+ All alpha values are set to 255 throughout the icon. Transparent pixels become black, or whatever background color you specify.
icon/BecomeAlphaMask()
- You can convert a simple grayscale icon into an alpha mask to use with other icons very easily with this proc.
- The black parts become transparent, the white parts stay white, and anything in between becomes a translucent shade of white.
+ You can convert a simple grayscale icon into an alpha mask to use with other icons very easily with this proc.
+ The black parts become transparent, the white parts stay white, and anything in between becomes a translucent shade of white.
icon/AddAlphaMask(mask)
- The alpha values of the mask icon will be blended with the current icon. Anywhere the mask is opaque,
- the current icon is untouched. Anywhere the mask is transparent, the current icon becomes transparent.
- Where the mask is translucent, the current icon becomes more transparent.
+ The alpha values of the mask icon will be blended with the current icon. Anywhere the mask is opaque,
+ the current icon is untouched. Anywhere the mask is transparent, the current icon becomes transparent.
+ Where the mask is translucent, the current icon becomes more transparent.
icon/UseAlphaMask(mask, mode)
- Sometimes you may want to take the alpha values from one icon and use them on a different icon.
- This proc will do that. Just supply the icon whose alpha mask you want to use, and src will change
- so it has the same colors as before but uses the mask for opacity.
+ Sometimes you may want to take the alpha values from one icon and use them on a different icon.
+ This proc will do that. Just supply the icon whose alpha mask you want to use, and src will change
+ so it has the same colors as before but uses the mask for opacity.
COLOR MANAGEMENT AND HSV
RGB isn't the only way to represent color. Sometimes it's more useful to work with a model called HSV, which stands for hue, saturation, and value.
- * The hue of a color describes where it is along the color wheel. It goes from red to yellow to green to
- cyan to blue to magenta and back to red.
- * The saturation of a color is how much color is in it. A color with low saturation will be more gray,
- and with no saturation at all it is a shade of gray.
- * The value of a color determines how bright it is. A high-value color is vivid, moderate value is dark,
- and no value at all is black.
+ * The hue of a color describes where it is along the color wheel. It goes from red to yellow to green to
+ cyan to blue to magenta and back to red.
+ * The saturation of a color is how much color is in it. A color with low saturation will be more gray,
+ and with no saturation at all it is a shade of gray.
+ * The value of a color determines how bright it is. A high-value color is vivid, moderate value is dark,
+ and no value at all is black.
-Just as BYOND uses "#rrggbb" to represent RGB values, a similar format is used for HSV: "#hhhssvv". The hue is three
-hex digits because it ranges from 0 to 0x5FF.
+While rgb is typically stored in the #rrggbb" format (with optional "aa" on the end), HSV never needs to be displayed.
+Most procs that work in HSV "space" will simply accept RGB inputs and convert them in place using rgb2num(color, space = COLORSPACE_HSV).
- * 0 to 0xFF - red to yellow
- * 0x100 to 0x1FF - yellow to green
- * 0x200 to 0x2FF - green to cyan
- * 0x300 to 0x3FF - cyan to blue
- * 0x400 to 0x4FF - blue to magenta
- * 0x500 to 0x5FF - magenta to red
+That said, if you want to manually modify these values rgb2hsv() will hand you back a list in the format list(hue, saturation, value, alpha).
+Converting back is simple, just a hsv2rgb(hsv) call
-Knowing this, you can figure out that red is "#000ffff" in HSV format, which is hue 0 (red), saturation 255 (as colorful as possible),
-value 255 (as bright as possible). Green is "#200ffff" and blue is "#400ffff".
+Hue ranges from 0 to 360 (it's in degrees of a color wheel)
+Saturation ranges from 0 to 100
+Value ranges from 0 to 100
-More than one HSV color can match the same RGB color.
+Knowing this, you can figure out that red is list(0, 100, 100) in HSV format, which is hue 0 (red), saturation 100 (as colorful as possible),
+value 255 (as bright as possible). Green is list(120, 100, 100) and blue is list(240, 100, 100).
+
+It is worth noting that while we do not have helpers for them currently, these same ideas apply to all of byond's color spaces
+HSV (hue saturation value), HSL (hue satriation luminosity) and HCY (hue chroma luminosity)
Here are some procs you can use for color management:
-ReadRGB(rgb)
- Takes an RGB string like "#ffaa55" and converts it to a list such as list(255,170,85). If an RGBA format is used
- that includes alpha, the list will have a fourth item for the alpha value.
-hsv(hue, sat, val, apha)
- Counterpart to rgb(), this takes the values you input and converts them to a string in "#hhhssvv" or "#hhhssvvaa"
- format. Alpha is not included in the result if null.
-ReadHSV(rgb)
- Takes an HSV string like "#100FF80" and converts it to a list such as list(256,255,128). If an HSVA format is used that
- includes alpha, the list will have a fourth item for the alpha value.
-RGBtoHSV(rgb)
- Takes an RGB or RGBA string like "#ffaa55" and converts it into an HSV or HSVA color such as "#080aaff".
-HSVtoRGB(hsv)
- Takes an HSV or HSVA string like "#080aaff" and converts it into an RGB or RGBA color such as "#ff55aa".
BlendRGB(rgb1, rgb2, amount)
- Blends between two RGB or RGBA colors using regular RGB blending. If amount is 0, the first color is the result;
- if 1, the second color is the result. 0.5 produces an average of the two. Values outside the 0 to 1 range are allowed as well.
- The returned value is an RGB or RGBA color.
-BlendHSV(hsv1, hsv2, amount)
- Blends between two HSV or HSVA colors using HSV blending, which tends to produce nicer results than regular RGB
- blending because the brightness of the color is left intact. If amount is 0, the first color is the result; if 1,
- the second color is the result. 0.5 produces an average of the two. Values outside the 0 to 1 range are allowed as well.
- The returned value is an HSV or HSVA color.
-BlendRGBasHSV(rgb1, rgb2, amount)
- Like BlendHSV(), but the colors used and the return value are RGB or RGBA colors. The blending is done in HSV form.
+ Blends between two RGB or RGBA colors using regular RGB blending. If amount is 0, the first color is the result;
+ if 1, the second color is the result. 0.5 produces an average of the two. Values outside the 0 to 1 range are allowed as well.
+ Returns an RGB or RGBA string
+BlendHSV(rgb1, rgb2, amount)
+ Blends between two RGB or RGBA colors using HSV blending, which tends to produce nicer results than regular RGB
+ blending because the brightness of the color is left intact. If amount is 0, the first color is the result; if 1,
+ the second color is the result. 0.5 produces an average of the two. Values outside the 0 to 1 range are allowed as well.
+ Returns an RGB or RGBA string
HueToAngle(hue)
- Converts a hue to an angle range of 0 to 360. Angle 0 is red, 120 is green, and 240 is blue.
+ Converts a hue to an angle range of 0 to 360. Angle 0 is red, 120 is green, and 240 is blue.
AngleToHue(hue)
- Converts an angle to a hue in the valid range.
-RotateHue(hsv, angle)
- Takes an HSV or HSVA value and rotates the hue forward through red, green, and blue by an angle from 0 to 360.
- (Rotating red by 60° produces yellow.) The result is another HSV or HSVA color with the same saturation and value
- as the original, but a different hue.
+ Converts an angle to a hue in the valid range.
+RotateHue(rgb, angle)
+ Takes an RGB or RGBA value and rotates the hue forward through red, green, and blue by an angle from 0 to 360.
+ (Rotating red by 60° produces yellow.)
+ Returns an RGB or RGBA string
+GrayScale(rgb)
+ Takes an RGB or RGBA color and converts it to grayscale. Returns an RGB or RGBA string.
ColorTone(rgb, tone)
- Similar to GrayScale(), this proc converts an RGB or RGBA color to a range of black -> tone -> white instead of
- using strict shades of gray. The tone value is an RGB color; any alpha value is ignored.
+ Similar to GrayScale(), this proc converts an RGB or RGBA color to a range of black -> tone -> white instead of
+ using strict shades of gray. The tone value is an RGB color; any alpha value is ignored.
*/
#define TO_HEX_DIGIT(n) ascii2text((n&15) + ((n&15)<10 ? 48 : 87))
@@ -124,14 +114,18 @@ ColorTone(rgb, tone)
Shift(SOUTH,6)
Shift(EAST,1)
- // Multiply all alpha values by this float
+// Multiply all alpha values by this float
/icon/proc/ChangeOpacity(opacity = 1)
MapColors(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,opacity, 0,0,0,0)
+// Convert to grayscale
+/icon/proc/GrayScale()
+ MapColors(0.3,0.3,0.3, 0.59,0.59,0.59, 0.11,0.11,0.11, 0,0,0)
+
/icon/proc/ColorTone(tone)
- greyscale()
+ GrayScale()
- var/list/TONE = ReadRGB(tone)
+ var/list/TONE = rgb2num(tone)
var/gray = round(TONE[1]*0.3 + TONE[2]*0.59 + TONE[3]*0.11, 1)
var/icon/upper = (255-gray) ? new(src) : null
@@ -147,36 +141,36 @@ ColorTone(rgb, tone)
// Take the minimum color of two icons; combine transparency as if blending with ICON_ADD
/icon/proc/MinColors(icon)
- var/icon/I = new(src)
- I.Opaque()
- I.Blend(icon, ICON_SUBTRACT)
- Blend(I, ICON_SUBTRACT)
+ var/icon/new_icon = new(src)
+ new_icon.Opaque()
+ new_icon.Blend(icon, ICON_SUBTRACT)
+ Blend(new_icon, ICON_SUBTRACT)
// Take the maximum color of two icons; combine opacity as if blending with ICON_OR
/icon/proc/MaxColors(icon)
- var/icon/I
+ var/icon/new_icon
if(isicon(icon))
- I = new(icon)
+ new_icon = new(icon)
else
// solid color
- I = new(src)
- I.Blend("#000000", ICON_OVERLAY)
- I.SwapColor("#000000", null)
- I.Blend(icon, ICON_OVERLAY)
- var/icon/J = new(src)
- J.Opaque()
- I.Blend(J, ICON_SUBTRACT)
- Blend(I, ICON_OR)
+ new_icon = new(src)
+ new_icon.Blend(COLOR_BLACK, ICON_OVERLAY)
+ new_icon.SwapColor(COLOR_BLACK, null)
+ new_icon.Blend(icon, ICON_OVERLAY)
+ var/icon/blend_icon = new(src)
+ blend_icon.Opaque()
+ new_icon.Blend(blend_icon, ICON_SUBTRACT)
+ Blend(new_icon, ICON_OR)
// make this icon fully opaque--transparent pixels become black
-/icon/proc/Opaque(background = "#000000")
+/icon/proc/Opaque(background = COLOR_BLACK)
SwapColor(null, background)
MapColors(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,0, 0,0,0,1)
// Change a grayscale icon into a white icon where the original color becomes the alpha
// I.e., black -> transparent, gray -> translucent white, white -> solid white
/icon/proc/BecomeAlphaMask()
- SwapColor(null, "#000000ff") // don't let transparent become gray
+ SwapColor(null, "#000000ff") // don't let transparent become gray
MapColors(0,0,0,0.3, 0,0,0,0.59, 0,0,0,0.11, 0,0,0,0, 1,1,1,0)
/icon/proc/UseAlphaMask(mask)
@@ -184,254 +178,29 @@ ColorTone(rgb, tone)
AddAlphaMask(mask)
/icon/proc/AddAlphaMask(mask)
- var/icon/M = new(mask)
- M.Blend("#ffffff", ICON_SUBTRACT)
+ var/icon/mask_icon = new(mask)
+ mask_icon.Blend("#ffffff", ICON_SUBTRACT)
// apply mask
- Blend(M, ICON_ADD)
+ Blend(mask_icon, ICON_ADD)
+
+/// Converts an rgb color into a list storing hsva
+/// Exists because it's useful to have a guaranteed alpha value
+/proc/rgb2hsv(rgb)
+ var/list/hsv = rgb2num(rgb, COLORSPACE_HSV)
+ if(length(hsv) < 4)
+ hsv += 255 // Max alpha, just to make life easy
+ return hsv
+
+/// Converts a list storing hsva into an rgb color
+/proc/hsv2rgb(hsv)
+ if(length(hsv) < 3)
+ return COLOR_BLACK
+ if(length(hsv) == 3)
+ return rgb(hsv[1], hsv[2], hsv[3], space = COLORSPACE_HSV)
+ return rgb(hsv[1], hsv[2], hsv[3], hsv[4], space = COLORSPACE_HSV)
/*
- HSV format is represented as "#hhhssvv" or "#hhhssvvaa"
-
- Hue ranges from 0 to 0x5ff (1535)
-
- 0x000 = red
- 0x100 = yellow
- 0x200 = green
- 0x300 = cyan
- 0x400 = blue
- 0x500 = magenta
-
- Saturation is from 0 to 0xff (255)
-
- More saturation = more color
- Less saturation = more gray
-
- Value ranges from 0 to 0xff (255)
-
- Higher value means brighter color
- */
-
-/**
- * reads RGB or RGBA values to list
- * @return list(r, g, b) or list(r, g, b, a), values 0 to 255.
- */
-/proc/ReadRGB(rgb)
- if(!rgb)
- return
-
- // interpret the HSV or HSVA value
- var/i=1,start=1
- if(text2ascii(rgb) == 35) ++start // skip opening #
- var/ch,which=0,r=0,g=0,b=0,alpha=0,usealpha
- var/digits=0
- for(i=start, i<=length(rgb), ++i)
- ch = text2ascii(rgb, i)
- if(ch < 48 || (ch > 57 && ch < 65) || (ch > 70 && ch < 97) || ch > 102)
- break
- ++digits
- if(digits == 8)
- break
-
- var/single = digits < 6
- if(digits != 3 && digits != 4 && digits != 6 && digits != 8)
- return
- if(digits == 4 || digits == 8)
- usealpha = 1
- for(i=start, digits>0, ++i)
- ch = text2ascii(rgb, i)
- if(ch >= 48 && ch <= 57)
- ch -= 48
- else if(ch >= 65 && ch <= 70)
- ch -= 55
- else if(ch >= 97 && ch <= 102)
- ch -= 87
- else
- break
- --digits
- switch(which)
- if(0)
- r = (r << 4) | ch
- if(single)
- r |= r << 4
- ++which
- else if(!(digits & 1))
- ++which
- if(1)
- g = (g << 4) | ch
- if(single)
- g |= g << 4
- ++which
- else if(!(digits & 1))
- ++which
- if(2)
- b = (b << 4) | ch
- if(single)
- b |= b << 4
- ++which
- else if(!(digits & 1))
- ++which
- if(3)
- alpha = (alpha << 4) | ch
- if(single)
- alpha |= alpha << 4
-
- . = list(r, g, b)
- if(usealpha)
- . += alpha
-
-/proc/ReadHSV(hsv)
- if(!hsv)
- return
-
- // interpret the HSV or HSVA value
- var/i=1,start=1
- if(text2ascii(hsv) == 35)
- ++start // skip opening #
- var/ch,which=0,hue=0,sat=0,val=0,alpha=0,usealpha
- var/digits=0
- for(i=start, i<=length(hsv), ++i)
- ch = text2ascii(hsv, i)
- if(ch < 48 || (ch > 57 && ch < 65) || (ch > 70 && ch < 97) || ch > 102)
- break
- ++digits
- if(digits == 9)
- break
- if(digits > 7)
- usealpha = 1
- if(digits <= 4)
- ++which
- if(digits <= 2)
- ++which
- for(i=start, digits>0, ++i)
- ch = text2ascii(hsv, i)
- if(ch >= 48 && ch <= 57)
- ch -= 48
- else if(ch >= 65 && ch <= 70)
- ch -= 55
- else if(ch >= 97 && ch <= 102)
- ch -= 87
- else
- break
- --digits
- switch(which)
- if(0)
- hue = (hue << 4) | ch
- if(digits == (usealpha ? 6 : 4))
- ++which
- if(1)
- sat = (sat << 4) | ch
- if(digits == (usealpha ? 4 : 2))
- ++which
- if(2)
- val = (val << 4) | ch
- if(digits == (usealpha ? 2 : 0))
- ++which
- if(3)
- alpha = (alpha << 4) | ch
-
- . = list(hue, sat, val)
- if(usealpha)
- . += alpha
-
-/proc/HSVtoRGB(hsv)
- if(!hsv)
- return "#000000"
- var/list/HSV = ReadHSV(hsv)
- if(!HSV)
- return "#000000"
-
- var/hue = HSV[1]
- var/sat = HSV[2]
- var/val = HSV[3]
-
- // Compress hue into easier-to-manage range
- hue -= hue >> 8
- if(hue >= 0x5fa)
- hue -= 0x5fa
-
- var/hi,mid,lo,r,g,b
- hi = val
- lo = round((255 - sat) * val / 255, 1)
- mid = lo + round(abs(round(hue, 510) - hue) * (hi - lo) / 255, 1)
- if(hue >= 765)
- if(hue >= 1275) {r=hi; g=lo; b=mid}
- else if(hue >= 1020) {r=mid; g=lo; b=hi }
- else {r=lo; g=mid; b=hi }
- else
- if(hue >= 510) {r=lo; g=hi; b=mid}
- else if(hue >= 255) {r=mid; g=hi; b=lo }
- else {r=hi; g=mid; b=lo }
-
- return (HSV.len > 3) ? rgb(r,g,b,HSV[4]) : rgb(r,g,b)
-
-/proc/RGBtoHSV(rgb)
- if(!rgb)
- return "#0000000"
- var/list/RGB = ReadRGB(rgb)
- if(!RGB)
- return "#0000000"
-
- var/r = RGB[1]
- var/g = RGB[2]
- var/b = RGB[3]
- var/hi = max(r,g,b)
- var/lo = min(r,g,b)
-
- var/val = hi
- var/sat = hi ? round((hi-lo) * 255 / hi, 1) : 0
- var/hue = 0
-
- if(sat)
- var/dir
- var/mid
- if(hi == r)
- if(lo == b) {hue=0; dir=1; mid=g}
- else {hue=1535; dir=-1; mid=b}
- else if(hi == g)
- if(lo == r) {hue=512; dir=1; mid=b}
- else {hue=511; dir=-1; mid=r}
- else if(hi == b)
- if(lo == g) {hue=1024; dir=1; mid=r}
- else {hue=1023; dir=-1; mid=g}
- hue += dir * round((mid-lo) * 255 / (hi-lo), 1)
-
- return hsv(hue, sat, val, (RGB.len>3 ? RGB[4] : null))
-
-/proc/hsv(hue, sat, val, alpha)
- if(hue < 0 || hue >= 1536)
- hue %= 1536
- if(hue < 0)
- hue += 1536
- if((hue & 0xFF) == 0xFF)
- ++hue
- if(hue >= 1536)
- hue = 0
- if(sat < 0)
- sat = 0
- if(sat > 255)
- sat = 255
- if(val < 0)
- val = 0
- if(val > 255)
- val = 255
- . = "#"
- . += TO_HEX_DIGIT(hue >> 8)
- . += TO_HEX_DIGIT(hue >> 4)
- . += TO_HEX_DIGIT(hue)
- . += TO_HEX_DIGIT(sat >> 4)
- . += TO_HEX_DIGIT(sat)
- . += TO_HEX_DIGIT(val >> 4)
- . += TO_HEX_DIGIT(val)
- if(!isnull(alpha))
- if(alpha < 0)
- alpha = 0
- if(alpha > 255)
- alpha = 255
- . += TO_HEX_DIGIT(alpha >> 4)
- . += TO_HEX_DIGIT(alpha)
-
-/*
- Smooth blend between HSV colors
+ Smooth blend between RGB colors interpreted as HSV
amount=0 is the first color
amount=1 is the second color
@@ -440,63 +209,7 @@ ColorTone(rgb, tone)
amount<0 or amount>1 are allowed
*/
/proc/BlendHSV(hsv1, hsv2, amount)
- var/list/HSV1 = ReadHSV(hsv1)
- var/list/HSV2 = ReadHSV(hsv2)
-
- // add missing alpha if needed
- if(HSV1.len < HSV2.len)
- HSV1 += 255
- else if(HSV2.len < HSV1.len)
- HSV2 += 255
- var/usealpha = HSV1.len > 3
-
- // normalize hsv values in case anything is screwy
- if(HSV1[1] > 1536)
- HSV1[1] %= 1536
- if(HSV2[1] > 1536)
- HSV2[1] %= 1536
- if(HSV1[1] < 0)
- HSV1[1] += 1536
- if(HSV2[1] < 0)
- HSV2[1] += 1536
- if(!HSV1[3]) {HSV1[1] = 0; HSV1[2] = 0}
- if(!HSV2[3]) {HSV2[1] = 0; HSV2[2] = 0}
-
- // no value for one color means don't change saturation
- if(!HSV1[3])
- HSV1[2] = HSV2[2]
- if(!HSV2[3])
- HSV2[2] = HSV1[2]
- // no saturation for one color means don't change hues
- if(!HSV1[2])
- HSV1[1] = HSV2[1]
- if(!HSV2[2])
- HSV2[1] = HSV1[1]
-
- // Compress hues into easier-to-manage range
- HSV1[1] -= HSV1[1] >> 8
- HSV2[1] -= HSV2[1] >> 8
-
- var/hue_diff = HSV2[1] - HSV1[1]
- if(hue_diff > 765)
- hue_diff -= 1530
- else if(hue_diff <= -765)
- hue_diff += 1530
-
- var/hue = round(HSV1[1] + hue_diff * amount, 1)
- var/sat = round(HSV1[2] + (HSV2[2] - HSV1[2]) * amount, 1)
- var/val = round(HSV1[3] + (HSV2[3] - HSV1[3]) * amount, 1)
- var/alpha = usealpha ? round(HSV1[4] + (HSV2[4] - HSV1[4]) * amount, 1) : null
-
- // normalize hue
- if(hue < 0 || hue >= 1530)
- hue %= 1530
- if(hue < 0)
- hue += 1530
- // decompress hue
- hue += round(hue / 255)
-
- return hsv(hue, sat, val, alpha)
+ return hsv_gradient(amount, 0, hsv1, 1, hsv2, "loop")
/*
Smooth blend between RGB colors
@@ -508,25 +221,7 @@ ColorTone(rgb, tone)
amount<0 or amount>1 are allowed
*/
/proc/BlendRGB(rgb1, rgb2, amount)
- var/list/RGB1 = ReadRGB(rgb1)
- var/list/RGB2 = ReadRGB(rgb2)
-
- // add missing alpha if needed
- if(RGB1.len < RGB2.len)
- RGB1 += 255
- else if(RGB2.len < RGB1.len)
- RGB2 += 255
- var/usealpha = RGB1.len > 3
-
- var/r = round(RGB1[1] + (RGB2[1] - RGB1[1]) * amount, 1)
- var/g = round(RGB1[2] + (RGB2[2] - RGB1[2]) * amount, 1)
- var/b = round(RGB1[3] + (RGB2[3] - RGB1[3]) * amount, 1)
- var/alpha = usealpha ? round(RGB1[4] + (RGB2[4] - RGB1[4]) * amount, 1) : null
-
- return isnull(alpha) ? rgb(r, g, b) : rgb(r, g, b, alpha)
-
-/proc/BlendRGBasHSV(rgb1, rgb2, amount)
- return HSVtoRGB(RGBtoHSV(rgb1), RGBtoHSV(rgb2), amount)
+ return rgb_gradient(amount, 0, rgb1, 1, rgb2, "loop")
/proc/HueToAngle(hue)
// normalize hsv in case anything is screwy
@@ -547,10 +242,9 @@ ColorTone(rgb, tone)
hue += round(hue / 255)
return hue
-
// positive angle rotates forward through red->green->blue
-/proc/RotateHue(hsv, angle)
- var/list/HSV = ReadHSV(hsv)
+/proc/RotateHue(rgb, angle)
+ var/list/HSV = rgb2hsv(rgb)
// normalize hsv in case anything is screwy
if(HSV[1] >= 1536)
@@ -573,18 +267,24 @@ ColorTone(rgb, tone)
// decompress hue
HSV[1] += round(HSV[1] / 255)
- return hsv(HSV[1], HSV[2], HSV[3], (HSV.len > 3 ? HSV[4] : null))
+ return hsv2rgb(HSV)
+
+// Convert an rgb color to grayscale
+/proc/GrayScale(rgb)
+ var/list/RGB = rgb2num(rgb)
+ var/gray = RGB[1]*0.3 + RGB[2]*0.59 + RGB[3]*0.11
+ return (RGB.len > 3) ? rgb(gray, gray, gray, RGB[4]) : rgb(gray, gray, gray)
// Change grayscale color to black->tone->white range
/proc/ColorTone(rgb, tone)
- var/list/RGB = ReadRGB(rgb)
- var/list/TONE = ReadRGB(tone)
+ var/list/RGB = rgb2num(rgb)
+ var/list/TONE = rgb2num(tone)
var/gray = RGB[1]*0.3 + RGB[2]*0.59 + RGB[3]*0.11
var/tone_gray = TONE[1]*0.3 + TONE[2]*0.59 + TONE[3]*0.11
if(gray <= tone_gray)
- return BlendRGB("#000000", tone, gray/(tone_gray || 1))
+ return BlendRGB(COLOR_BLACK, tone, gray/(tone_gray || 1))
else
return BlendRGB(tone, "#ffffff", (gray-tone_gray)/((255-tone_gray) || 1))
@@ -603,45 +303,45 @@ ColorTone(rgb, tone)
var/lo3 = text2ascii(hex, 7) // B
var/hi4 = text2ascii(hex, 8) // A
var/lo4 = text2ascii(hex, 9) // A
- return list(((hi1>= 65 ? hi1-55 : hi1-48)<<4) | (lo1 >= 65 ? lo1-55 : lo1-48),
+ return list(((hi1 >= 65 ? hi1-55 : hi1-48)<<4) | (lo1 >= 65 ? lo1-55 : lo1-48),
((hi2 >= 65 ? hi2-55 : hi2-48)<<4) | (lo2 >= 65 ? lo2-55 : lo2-48),
((hi3 >= 65 ? hi3-55 : hi3-48)<<4) | (lo3 >= 65 ? lo3-55 : lo3-48),
((hi4 >= 65 ? hi4-55 : hi4-48)<<4) | (lo4 >= 65 ? lo4-55 : lo4-48))
-/proc/getIconMask(atom/A)//By yours truly. Creates a dynamic mask for a mob/whatever. /N
- var/icon/alpha_mask = new(A.icon,A.icon_state)//So we want the default icon and icon state of A.
- for(var/V in A.overlays)//For every image in overlays. var/image/I will not work, don't try it.
- var/image/I = V
- if(I.layer>A.layer)
+/proc/getIconMask(atom/atom_to_mask)//By yours truly. Creates a dynamic mask for a mob/whatever. /N
+ var/icon/alpha_mask = new(atom_to_mask.icon, atom_to_mask.icon_state)//So we want the default icon and icon state of atom_to_mask.
+ for(var/iterated_image in atom_to_mask.overlays)//For every image in overlays. var/image/image will not work, don't try it.
+ var/image/image = iterated_image
+ if(image.layer > atom_to_mask.layer)
continue//If layer is greater than what we need, skip it.
- var/icon/image_overlay = new(I.icon,I.icon_state)//Blend only works with icon objects.
+ var/icon/image_overlay = new(image.icon, image.icon_state)//Blend only works with icon objects.
//Also, icons cannot directly set icon_state. Slower than changing variables but whatever.
- alpha_mask.Blend(image_overlay,ICON_OR)//OR so they are lumped together in a nice overlay.
+ alpha_mask.Blend(image_overlay, ICON_OR)//OR so they are lumped together in a nice overlay.
return alpha_mask//And now return the mask.
/mob/proc/AddCamoOverlay(atom/A)//A is the atom which we are using as the overlay.
var/icon/opacity_icon = new(A.icon, A.icon_state)//Don't really care for overlays/underlays.
//Now we need to culculate overlays+underlays and add them together to form an image for a mask.
- var/icon/alpha_mask = getIconMask(src)//get_flat_icon(src) is accurate but SLOW. Not designed for running each tick. This is also a little slow since it's blending a bunch of icons together but good enough.
+ var/icon/alpha_mask = getIconMask(src)//getFlatIcon(src) is accurate but SLOW. Not designed for running each tick. This is also a little slow since it's blending a bunch of icons together but good enough.
opacity_icon.AddAlphaMask(alpha_mask)//Likely the main source of lag for this proc. Probably not designed to run each tick.
opacity_icon.ChangeOpacity(0.4)//Front end for MapColors so it's fast. 0.5 means half opacity and looks the best in my opinion.
- for(var/i=0,i<5,i++)//And now we add it as overlays. It's faster than creating an icon and then merging it.
- var/image/I = image("icon" = opacity_icon, "icon_state" = A.icon_state, "layer" = layer+0.8)//So it's above other stuff but below weapons and the like.
+ for(var/i in 1 to 5)//And now we add it as overlays. It's faster than creating an icon and then merging it.
+ var/image/camo_image = image("icon" = opacity_icon, "icon_state" = A.icon_state, "layer" = layer+0.8)//So it's above other stuff but below weapons and the like.
switch(i)//Now to determine offset so the result is somewhat blurred.
- if(1)
- I.pixel_x--
if(2)
- I.pixel_x++
+ camo_image.pixel_x--
if(3)
- I.pixel_y--
+ camo_image.pixel_x++
if(4)
- I.pixel_y++
- add_overlay(I)//And finally add the overlay.
+ camo_image.pixel_y--
+ if(5)
+ camo_image.pixel_y++
+ add_overlay(camo_image)//And finally add the overlay.
-/proc/getHologramIcon(icon/A, safety = TRUE)//If safety is on, a new icon is not created.
+/proc/getHologramIcon(icon/A, safety = TRUE, opacity = 0.5)//If safety is on, a new icon is not created.
var/icon/flat_icon = safety ? A : new(A)//Has to be a new icon to not constantly change the same icon.
// flat_icon.ColorTone(rgb(125,180,225))//Let's make it bluish.
- // flat_icon.ChangeOpacity(0.5)//Make it half transparent.
+ // flat_icon.ChangeOpacity(opacity)
var/icon/alpha_mask = new('icons/effects/effects.dmi', "scanline")//Scanline effect.
flat_icon.AddAlphaMask(alpha_mask)//Finally, let's mix in a distortion effect.
return flat_icon
@@ -691,7 +391,7 @@ ColorTone(rgb, tone)
letter = lowertext(letter)
var/image/text_image = new(loc = A)
- text_image.maptext = "[letter]"
+ text_image.maptext = MAPTEXT("[letter]")
text_image.pixel_x = 7
text_image.pixel_y = 5
qdel(atom_icon)
@@ -724,64 +424,53 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
*/
//Interface for using DrawBox() to draw 1 pixel on a coordinate.
//Returns the same icon specifed in the argument, but with the pixel drawn
-/proc/DrawPixel(icon/I,colour,drawX,drawY)
- if(!I)
+/proc/DrawPixel(icon/icon_to_use, colour, drawX, drawY)
+ if(!icon_to_use)
return 0
- var/Iwidth = I.Width()
- var/Iheight = I.Height()
+ var/icon_width = icon_to_use.Width()
+ var/icon_height = icon_to_use.Height()
- if(drawX > Iwidth || drawX <= 0)
+ if(drawX > icon_width || drawX <= 0)
return 0
- if(drawY > Iheight || drawY <= 0)
+ if(drawY > icon_height || drawY <= 0)
return 0
- I.DrawBox(colour,drawX, drawY)
- return I
-
+ icon_to_use.DrawBox(colour, drawX, drawY)
+ return icon_to_use
//Interface for easy drawing of one pixel on an atom.
/atom/proc/DrawPixelOn(colour, drawX, drawY)
- var/icon/I = new(icon)
- var/icon/J = DrawPixel(I, colour, drawX, drawY)
- if(J) //Only set the icon if it succeeded, the icon without the pixel is 1000x better than a black square.
- icon = J
- return J
- return 0
-/*
-//For creating consistent icons for human looking simple animals
-/proc/get_flat_human_icon(icon_id, datum/role/job/J, datum/preferences/prefs, dummy_key, showDirs = GLOB.cardinals, outfit_override = null)
- var/static/list/humanoid_icon_cache = list()
- if(!icon_id || !humanoid_icon_cache[icon_id])
- var/mob/living/carbon/human/dummy/body = generate_or_wait_for_human_dummy(dummy_key)
-
- if(prefs)
- prefs.copy_to(body,TRUE,FALSE)
- if(J)
- J.equip(body, TRUE, FALSE, outfit_override = outfit_override)
- else if (outfit_override)
- body.equipOutfit(outfit_override,visualsOnly = TRUE)
-
-
- var/icon/out_icon = icon('icons/effects/effects.dmi', "nothing")
- for(var/D in showDirs)
- body.setDir(D)
- body.compile_overlays()
- var/icon/partial = get_flat_icon(body)
- out_icon.Insert(partial,dir=D)
-
- humanoid_icon_cache[icon_id] = out_icon
- dummy_key? unset_busy_human_dummy(dummy_key) : qdel(body)
- return out_icon
- else
- return humanoid_icon_cache[icon_id]
-*/
+ var/icon/icon_one = new(icon)
+ var/icon/result = DrawPixel(icon_one, colour, drawX, drawY)
+ if(result) //Only set the icon if it succeeded, the icon without the pixel is 1000x better than a black square.
+ icon = result
+ return result
+ return FALSE
+
//Hook, override to run code on- wait this is images
//Images have dir without being an atom, so they get their own definition.
//Lame.
/image/proc/setDir(newdir)
dir = newdir
+/// Gets a dummy savefile for usage in icon generation.
+/// Savefiles generated from this proc will be empty.
+/proc/get_dummy_savefile(from_failure = FALSE)
+ var/static/next_id = 0
+ if(next_id++ > 9)
+ next_id = 0
+ var/savefile_path = "tmp/dummy-save-[next_id].sav"
+ try
+ if(fexists(savefile_path))
+ fdel(savefile_path)
+ return new /savefile(savefile_path)
+ catch(var/exception/error)
+ // if we failed to create a dummy once, try again; maybe someone slept somewhere they shouldn't have
+ if(from_failure) // this *is* the retry, something fucked up
+ CRASH("get_dummy_savefile failed to create a dummy savefile: '[error]'")
+ return get_dummy_savefile(from_failure = TRUE)
+
/**
* Converts an icon to base64. Operates by putting the icon in the iconCache savefile,
* exporting it as text, and then parsing the base64 from that.
@@ -790,21 +479,31 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
/proc/icon2base64(icon/icon)
if (!isicon(icon))
return FALSE
- var/savefile/dummySave = new("tmp/dummySave.sav")
+ var/savefile/dummySave = get_dummy_savefile()
WRITE_FILE(dummySave["dummy"], icon)
var/iconData = dummySave.ExportText("dummy")
var/list/partial = splittext(iconData, "{")
- . = replacetext(copytext_char(partial[2], 3, -5), "\n", "") //if cleanup fails we want to still return the correct base64
- dummySave.Unlock()
- dummySave = null
- fdel("tmp/dummySave.sav") //if you get the idea to try and make this more optimized, make sure to still call unlock on the savefile after every write to unlock it.
+ return replacetext(copytext_char(partial[2], 3, -5), "\n", "") //if cleanup fails we want to still return the correct base64
-/proc/icon2html(thing, target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null)
+/**
+ * generate an asset for the given icon or the icon of the given appearance for [thing], and send it to any clients in target.
+ * Arguments:
+ * * thing - either a /icon object, or an object that has an appearance (atom, image, mutable_appearance).
+ * * target - either a reference to or a list of references to /client's or mobs with clients
+ * * icon_state - string to force a particular icon_state for the icon to be used
+ * * dir - dir number to force a particular direction for the icon to be used
+ * * frame - what frame of the icon_state's animation for the icon being used
+ * * moving - whether or not to use a moving state for the given icon
+ * * sourceonly - if TRUE, only generate the asset and send back the asset url, instead of tags that display the icon to players
+ * * extra_clases - string of extra css classes to use when returning the icon string
+ */
+/proc/icon2html(atom/thing, client/target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null)
if (!thing)
return
// if(SSlag_switch.measures[DISABLE_USR_ICON2HTML] && usr && !HAS_TRAIT(usr, TRAIT_BYPASS_MEASURES))
// return
- var/icon/I = thing
+
+ var/icon/icon2collapse = thing
if (!target)
return
@@ -816,33 +515,34 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
targets = list(target)
else
targets = target
- if (!targets.len)
- return
+ if(!length(targets))
+ return
- if (!isicon(I))
+ if (!isicon(icon2collapse))
if (isfile(thing)) //special snowflake
- var/url = SSassets.send_anonymous_file(targets, I, "png")
+ var/url = SSassets.send_anonymous_file(targets, icon2collapse, "png")
if(sourceonly)
return url
return ""
- var/atom/A = thing
- I = A.icon
+ //its either an atom, image, or mutable_appearance, we want its icon var
+ icon2collapse = thing.icon
if (isnull(icon_state))
- icon_state = A.icon_state
- if (!(icon_state in icon_states(I, 1)))
- icon_state = initial(A.icon_state)
+ icon_state = thing.icon_state
+ //Despite casting to atom, this code path supports mutable appearances, so let's be nice to them
+ if (isnull(icon_state) || !(icon_state in icon_states(icon2collapse, 1)))
+ icon_state = initial(thing.icon_state)
if (isnull(dir))
- dir = initial(A.dir)
+ dir = initial(thing.dir)
if (isnull(dir))
- dir = A.dir
+ dir = thing.dir
if (ishuman(thing)) // Shitty workaround for a BYOND issue.
- var/icon/temp = I
- I = icon()
- I.Insert(temp, dir = SOUTH)
+ var/icon/temp = icon2collapse
+ icon2collapse = icon()
+ icon2collapse.Insert(temp, dir = SOUTH)
dir = SOUTH
else
if (isnull(dir))
@@ -850,45 +550,43 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
if (isnull(icon_state))
icon_state = ""
- I = icon(I, icon_state, dir, frame, moving)
+ icon2collapse = icon(icon2collapse, icon_state, dir, frame, moving)
- var/url = SSassets.send_anonymous_file(targets, I, "png")
+ var/url = SSassets.send_anonymous_file(targets, icon2collapse, "png")
if(sourceonly)
return url
return ""
-/proc/icon2base64html(thing)
- if (!thing)
+/proc/icon2base64html(target)
+ if (!target)
return
var/static/list/bicon_cache = list()
- if (isicon(thing))
- var/icon/I = thing
- var/icon_base64 = icon2base64(I)
+ if (isicon(target))
+ var/icon/target_icon = target
+ var/icon_base64 = icon2base64(target_icon)
- if (I.Height() > world.icon_size || I.Width() > world.icon_size)
+ if (target_icon.Height() > world.icon_size || target_icon.Width() > world.icon_size)
var/icon_md5 = md5(icon_base64)
icon_base64 = bicon_cache[icon_md5]
if (!icon_base64) // Doesn't exist yet, make it.
- bicon_cache[icon_md5] = icon_base64 = icon2base64(I)
-
+ bicon_cache[icon_md5] = icon_base64 = icon2base64(target_icon)
return ""
// Either an atom or somebody fucked up and is gonna get a runtime, which I'm fine with.
- var/atom/A = thing
- var/key = "[istype(A.icon, /icon) ? "[REF(A.icon)]" : A.icon]:[A.icon_state]"
-
+ var/atom/target_atom = target
+ var/key = "[istype(target_atom.icon, /icon) ? "[REF(target_atom.icon)]" : target_atom.icon]:[target_atom.icon_state]"
if (!bicon_cache[key]) // Doesn't exist, make it.
- var/icon/I = icon(A.icon, A.icon_state, SOUTH, 1)
- if (ishuman(thing)) // Shitty workaround for a BYOND issue.
- var/icon/temp = I
- I = icon()
- I.Insert(temp, dir = SOUTH)
+ var/icon/target_icon = icon(target_atom.icon, target_atom.icon_state, SOUTH, 1)
+ if (ishuman(target)) // Shitty workaround for a BYOND issue.
+ var/icon/temp = target_icon
+ target_icon = icon()
+ target_icon.Insert(temp, dir = SOUTH)
- bicon_cache[key] = icon2base64(I)
+ bicon_cache[key] = icon2base64(target_icon)
- return ""
+ return ""
/// Costlier version of icon2html() that uses get_flat_icon() to account for overlays, underlays, etc. Use with extreme moderation, ESPECIALLY on mobs.
/proc/costly_icon2html(thing, target, sourceonly = FALSE)
@@ -903,16 +601,41 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
var/icon/I = get_flat_icon(thing)
return icon2html(I, target, sourceonly = sourceonly)
+/// Perform a shake on an atom, resets its position afterwards
+/atom/proc/Shake(pixelshiftx = 2, pixelshifty = 2, duration = 2.5 SECONDS, shake_interval = 0.02 SECONDS)
+ var/initialpixelx = pixel_x
+ var/initialpixely = pixel_y
+ animate(src, pixel_x = initialpixelx + rand(-pixelshiftx,pixelshiftx), pixel_y = initialpixelx + rand(-pixelshifty,pixelshifty), time = shake_interval, flags = ANIMATION_PARALLEL)
+ for (var/i in 3 to ((duration / shake_interval))) // Start at 3 because we already applied one, and need another to reset
+ animate(pixel_x = initialpixelx + rand(-pixelshiftx,pixelshiftx), pixel_y = initialpixely + rand(-pixelshifty,pixelshifty), time = shake_interval)
+ animate(pixel_x = initialpixelx, pixel_y = initialpixely, time = shake_interval)
-/// VSTATION SPECIFIC ///
+// citadel edit: use this so we can VV it at a negligible performance hit.
+GLOBAL_LIST_EMPTY(icon_exists_cache)
+
+///Checks if the given iconstate exists in the given file, caching the result. Setting scream to TRUE will print a stack trace ONCE.
+/proc/icon_exists(file, state, scream)
+ if(GLOB.icon_exists_cache[file]?[state])
+ return TRUE
+
+ if(GLOB.icon_exists_cache[file]?[state] == FALSE)
+ return FALSE
+
+ var/list/states = icon_states(file)
+
+ if(!GLOB.icon_exists_cache[file])
+ GLOB.icon_exists_cache[file] = list()
+
+ if(state in states)
+ GLOB.icon_exists_cache[file][state] = TRUE
+ return TRUE
+ else
+ GLOB.icon_exists_cache[file][state] = FALSE
+ if(scream)
+ stack_trace("Icon Lookup for state: [state] in file [file] failed.")
+ return FALSE
-//For photo camera.
-/proc/build_composite_icon(atom/A)
- var/icon/composite = icon(A.icon, A.icon_state, A.dir, 1)
- for(var/O in A.overlays)
- var/image/I = O
- composite.Blend(icon(I.icon, I.icon_state, I.dir, 1), ICON_OVERLAY)
- return composite
+/// VSTATION SPECIFIC ///
/proc/adjust_brightness(color, value)
if (!color)
@@ -920,7 +643,7 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
if (!value)
return color
- var/list/RGB = ReadRGB(color)
+ var/list/RGB = rgb2num(color)
RGB[1] = clamp(RGB[1]+value,0,255)
RGB[2] = clamp(RGB[2]+value,0,255)
RGB[3] = clamp(RGB[3]+value,0,255)
@@ -945,87 +668,6 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
swapped = 1
return result
-/proc/gen_hud_image(file, person, state, plane)
- var/image/img = image(file, person, state)
- img.plane = plane //Thanks Byond.
- img.layer = MOB_LAYER-0.2
- img.appearance_flags = APPEARANCE_UI
- return img
-
-/**
- * Animate a 'halo' around an object.
- *
- * This proc is not exactly cheap. You'd be well advised to set up many-loops rather than call this super-often. get_compound_icon is
- * mostly to blame for this. If Byond ever implements a way to get something's icon more 'gently' than this, do that instead.
- *
- * * A - This is the atom to put the halo on
- * * simple_icons - If set to TRUE, will just perform a very basic icon and icon_state steal. DO USE when possible.
- * * color - This is the color for the halo
- * * anim_duration - This decides how fast (or slow) the animation plays
- * * offset - Mysterious variable that determines size of the halo's gap from icon
- * * loops - How many times the animation loops
- * * grow_to - Relative to the size of the icon, how big the halo grows while fading (don't use negatives for inward halos, use < 1)
- * * pixel_scale - If you'd like the halo to use pixel scale or the default 'fuzzy' scale
- */
-/proc/animate_aura(atom/A, simple_icons, color = "#00FF22", anim_duration = 5, offset = 1, loops = 1, grow_to = 2, pixel_scale = FALSE)
- ASSERT(A)
-
- //Take a guess at this, if they didn't set it
- if(isnull(simple_icons))
- if(ismob(A))
- simple_icons = FALSE
- else
- simple_icons = TRUE
-
- //Get their icon
- var/icon/hole
-
- if(simple_icons)
- hole = icon(A.icon, A.icon_state)
- else
- hole = get_compound_icon(A)
-
- hole.MapColors(0,0,0, 0,0,0, 0,0,0, 1,1,1) //White.
-
- //Make a bigger version
- var/icon/grower = new(hole)
- var/orig_width = grower.Width()
- var/orig_height = grower.Height()
- var/end_width = orig_width+(offset*2)
- var/end_height = orig_height+(offset*2)
- var/half_diff_width = (end_width-orig_width)*0.5
- var/half_diff_height = (end_height-orig_height)*0.5
-
- //Make icon black
- grower.SwapColor("#FFFFFF","#000000") //Black.
-
- //Scale both icons big so we don't have to deal with low-pixel garbage issues
- grower.Scale(orig_width*10,orig_height*10)
- hole.Scale(orig_width*9,orig_height*9)
-
- //Blend the hole in
- grower.Blend(hole,ICON_OVERLAY, x = ((orig_width*10-orig_width*9)*0.5)+1, y = ((orig_height*10-orig_height*9)*0.5)+1)
-
- //Swap white to zero alpha
- grower.SwapColor("#FFFFFF","#00000000")
-
- //Color it
- grower.SwapColor("#000000",color)
-
- //Scale it to final height
- grower.Scale(end_width,end_height)
-
- //Flick it onto them
- var/image/img = image(grower,A)
- if(pixel_scale)
- img.appearance_flags |= PIXEL_SCALE
- img.pixel_x = half_diff_width*-1
- img.pixel_y = half_diff_height*-1
- A.flick_overlay_view(img, anim_duration*loops, TRUE)
-
- //Animate it growing
- animate(img, alpha = 0, transform = matrix()*grow_to, time = anim_duration, loop = loops)
-
/*
* * Accurate - Use more accurate color averaging, usually has better results and prevents muddied or overly dark colors. Mad thanks to wwjnc.
* * ignoreGreyscale - Excempts greyscale colors from the color list, useful for filtering outlines or plate overlays.
@@ -1042,14 +684,14 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
var/final_average
if (accurate) //keeping it legible
for(var/i = 1 to total)
- RGB = ReadRGB(colors[i])
+ RGB = rgb2num(colors[i])
colorsum[1] += RGB[1]*RGB[1]
colorsum[2] += RGB[2]*RGB[2]
colorsum[3] += RGB[3]*RGB[3]
final_average = rgb(sqrt(colorsum[1]/total), sqrt(colorsum[2]/total), sqrt(colorsum[3]/total))
else
for(var/i = 1 to total)
- RGB = ReadRGB(colors[i])
+ RGB = rgb2num(colors[i])
colorsum[1] += RGB[1]
colorsum[2] += RGB[2]
colorsum[3] += RGB[3]
@@ -1062,7 +704,7 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
for(var/y_pixel = 1 to I.Height())
var/this_color = I.GetPixel(x_pixel, y_pixel)
if(this_color)
- if (ignoreGreyscale && ReadHSV(RGBtoHSV(this_color))[2] == 0) //If saturation is 0, must be greyscale
+ if (ignoreGreyscale && rgb2hsv(this_color)[2] == 0) //If saturation is 0, must be greyscale
continue
colors.Add(this_color)
return colors
@@ -1084,36 +726,8 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
for(var/i in 1 to (12 - cm.len))
cm += 0
if(!islist(color))
- color = ReadRGB(color)
+ color = rgb2num(color)
color[1] = color[1] * cm[1] + color[2] * cm[2] + color[3] * cm[3] + cm[10] * 255
color[2] = color[1] * cm[4] + color[2] * cm[5] + color[3] * cm[6] + cm[11] * 255
color[3] = color[1] * cm[7] + color[2] * cm[8] + color[3] * cm[9] + cm[12] * 255
return rgb(color[1], color[2], color[3])
-
-// citadel edit: use this so we can VV it at a negligible performance hit.
-GLOBAL_LIST_EMPTY(icon_exists_cache)
-/**
- * Checks if the given iconstate exists in the given file, caching the result.
- * Setting scream to TRUE will print a stack trace ONCE.
- * Written by Kapu1178
- */
-/proc/icon_exists(file, state, scream)
- if(GLOB.icon_exists_cache[file]?[state])
- return TRUE
-
- if(GLOB.icon_exists_cache[file]?[state] == FALSE)
- return FALSE
-
- var/list/states = icon_states(file)
-
- if(!GLOB.icon_exists_cache[file])
- GLOB.icon_exists_cache[file] = list()
-
- if(state in states)
- GLOB.icon_exists_cache[file][state] = TRUE
- return TRUE
- else
- GLOB.icon_exists_cache[file][state] = FALSE
- if(scream)
- stack_trace("Icon Lookup for state: [state] in file [file] failed.")
- return FALSE
diff --git a/code/__HELPERS/icons/color.dm b/code/__HELPERS/icons/color.dm
deleted file mode 100644
index 63ecf63c08a6..000000000000
--- a/code/__HELPERS/icons/color.dm
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * Turns the icon into a greyscaled icon. Alpha masks are untouched.
- */
-/icon/proc/greyscale()
- MapColors(
- GREYSCALE_MULT_R, GREYSCALE_MULT_R, GREYSCALE_MULT_R,
- GREYSCALE_MULT_G, GREYSCALE_MULT_G, GREYSCALE_MULT_G,
- GREYSCALE_MULT_B, GREYSCALE_MULT_B, GREYSCALE_MULT_B
- )
-
-/**
- * rgb value to greyscale
- */
-/proc/rgb_greyscale(rgb)
- var/list/unpacked = ReadRGB(rgb)
- var/gray = unpacked[1] * GREYSCALE_MULT_R + unpacked[2] *GREYSCALE_MULT_G + unpacked[3] * GREYSCALE_MULT_B
- return (unpacked.len > 3) ? rgb(gray, gray, gray, unpacked[4]) : rgb(gray, gray, gray)
diff --git a/code/__HELPERS/nameof.dm b/code/__HELPERS/nameof.dm
index 7cd5777f4652..6e625578ae60 100644
--- a/code/__HELPERS/nameof.dm
+++ b/code/__HELPERS/nameof.dm
@@ -4,6 +4,12 @@
* datum may be null, but it does need to be a typed var.
**/
#define NAMEOF(datum, X) (#X || ##datum.##X)
+/**
+ * NAMEOF: Compile time checked variable name to string conversion
+ * evaluates to a string equal to "X", but compile errors if X isn't a var on datum.
+ * datum may be null, but it does need to be a typed var.
+ **/
+#define NAMEOF_PROC(datum, X) (#X || ##datum.##X())
/**
* NAMEOF that actually works in static definitions because src::type requires src to be defined
diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm
index 7fcdc575e3a9..5c4a63dc189a 100644
--- a/code/__HELPERS/text.dm
+++ b/code/__HELPERS/text.dm
@@ -1,23 +1,12 @@
/**
* Holds procs designed to help with filtering text
* Contains groups:
- * ! SQL sanitization
* ! Text sanitization
* ! Text searches
* ! Text modification
* ! Misc
*/
-/**
- *! SQL sanitization
- */
-
-/proc/format_table_name(table)
- return CONFIG_GET(string/sql_server_prefix) + table
-
-/proc/format_unified_table_name(table)
- return CONFIG_GET(string/sql_unified_prefix) + table
-
/**
*! Text sanitization
*/
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index 96cb0f30f932..0b4a527fc430 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -1372,15 +1372,6 @@ var/list/WALLITEMS = list(
if(337.5)
return "North-Northwest"
-/atom/proc/Shake(pixelshiftx = 15, pixelshifty = 15, duration = 250)
- var/initialpixelx = pixel_x
- var/initialpixely = pixel_y
- var/shiftx = rand(-pixelshiftx,pixelshiftx)
- var/shifty = rand(-pixelshifty,pixelshifty)
- animate(src, pixel_x = pixel_x + shiftx, pixel_y = pixel_y + shifty, time = 0.2, loop = duration)
- pixel_x = initialpixelx
- pixel_y = initialpixely
-
/**
* get_holder_at_turf_level(): Similar to get_turf(), will return the "highest up" holder of this atom, excluding the turf.
* Example: A fork inside a box inside a locker will return the locker. Essentially, get_just_before_turf().
diff --git a/code/___compile_options.dm b/code/___compile_options.dm
index 3e1a9d6c87f3..f0e40b346275 100644
--- a/code/___compile_options.dm
+++ b/code/___compile_options.dm
@@ -113,26 +113,43 @@
#endif
// ## CBT BUILD DEFINES
-
-#ifdef CIBUILDING
- #define UNIT_TESTS
+#if defined(CIBUILDING) && !defined(OPENDREAM)
+#define UNIT_TESTS
#endif
#ifdef CITESTING
- #define TESTING
+#define TESTING
#endif
+#if defined(UNIT_TESTS)
+ //Hard del testing defines
+ // TODO: enable once reftrack is back
+ // #define REFERENCE_TRACKING
+ // #define REFERENCE_TRACKING_DEBUG
+ // #define FIND_REF_NO_CHECK_TICK
+ // #define GC_FAILURE_HARD_LOOKUP
+ //Ensures all early assets can actually load early
+ #define DO_NOT_DEFER_ASSETS
+ //Test at full capacity, the extra cost doesn't matter
+ #define TIMER_DEBUG
+#endif
#ifdef TGS
// TGS performs its own build of dm.exe, but includes a prepended TGS define.
#define CBT
#endif
-// ## LEGACY WARNING
-#if !(defined(CBT) || defined(SPACEMAN_DMM) || defined(OPENDREAM))
- #warn Building with Dream Maker is no longer supported and will result in errors.
- #warn In order to build, run BUILD.bat in the root directory.
- #warn Consider switching to VSCode editor instead, where you can press Ctrl+Shift+B to build.
+#if defined(OPENDREAM)
+ #if !defined(CIBUILDING)
+ #warn You are building with OpenDream. Remember to build TGUI manually.
+ #warn You can do this by running tgui-build.cmd from the bin directory.
+ #endif
+#else
+ #if !defined(CBT) && !defined(SPACEMAN_DMM)
+ #warn Building with Dream Maker is no longer supported and will result in errors.
+ #warn In order to build, run BUILD.cmd in the root directory.
+ #warn Consider switching to VSCode editor instead, where you can press Ctrl+Shift+B to build.
+ #endif
#endif
/**
diff --git a/code/controllers/README.md b/code/controllers/README.md
new file mode 100644
index 000000000000..1364ca595eea
--- /dev/null
+++ b/code/controllers/README.md
@@ -0,0 +1,13 @@
+# Controllers
+
+Backend controllers orchestrating the game.
+
+## Globals
+
+Many, but not all, controllers are accessible from anywhere in the code with standardized names.
+
+- Master: As the name implies, the Master Controller performs init, shutdown, and acts as a process scheduler during a round.
+- Failsafe: A controller that ensures the Master Controller is running properly.
+- Configuration: A global datum that holds server configuration.
+- RSname: Repository controllers storing /datum/prototype's that can be queried.
+- SSname: Subsystems that handle init behavior, ticking, and other game functions..
diff --git a/code/controllers/configuration/configuration.dm b/code/controllers/configuration/configuration.dm
index b933dea3bd4f..5474503030a5 100644
--- a/code/controllers/configuration/configuration.dm
+++ b/code/controllers/configuration/configuration.dm
@@ -8,19 +8,6 @@
var/list/entries
var/list/entries_by_type
- // var/list/maplist
- // var/datum/map_config/defaultmap
-
- /*
- var/list/modes // allowed modes
- var/list/gamemode_cache
- var/list/votable_modes // votable modes
- var/list/storyteller_cache
- var/list/mode_names
- var/list/mode_reports
- var/list/mode_false_report_weight
- */
-
var/motd
/// If the configuration is loaded
@@ -59,7 +46,7 @@
loaded = TRUE
if (Master)
- Master.OnConfigLoad()
+ Master.on_config_loaded()
/datum/controller/configuration/proc/full_wipe()
if(IsAdminAdvancedProcCall())
diff --git a/code/controllers/configuration/entries/database.dm b/code/controllers/configuration/entries/database.dm
index 3fbc32d38fe7..f930d31acbfb 100644
--- a/code/controllers/configuration/entries/database.dm
+++ b/code/controllers/configuration/entries/database.dm
@@ -4,9 +4,6 @@
/datum/config_entry/string/sql_server_prefix
protection = CONFIG_ENTRY_LOCKED
-/datum/config_entry/string/sql_unified_prefix
- protection = CONFIG_ENTRY_LOCKED
-
/datum/config_entry/string/sql_address
protection = CONFIG_ENTRY_HIDDEN | CONFIG_ENTRY_LOCKED
config_entry_value = "localhost"
diff --git a/code/controllers/controller.dm b/code/controllers/controller.dm
index 93d5897ff453..252845318f52 100644
--- a/code/controllers/controller.dm
+++ b/code/controllers/controller.dm
@@ -15,6 +15,9 @@
// todo: kil
var/verbose_logging = FALSE
+/datum/controller/vv_delete()
+ return FALSE
+
/**
* Called to initialize a controller.
*
diff --git a/code/controllers/master.dm b/code/controllers/master.dm
index 7d0c002dad77..60a8334a375b 100644
--- a/code/controllers/master.dm
+++ b/code/controllers/master.dm
@@ -117,6 +117,9 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
load_configuration()
if(!config)
config = new
+ if(!Configuration)
+ Configuration = new
+ Configuration.Initialize()
//# 2. set up random seed
if(!random_seed)
@@ -883,10 +886,12 @@ GLOBAL_REAL(Master, /datum/controller/master) = new
var/datum/controller/subsystem/SS = S
SS.StopLoadingMap()
-/datum/controller/master/proc/OnConfigLoad()
+/datum/controller/master/proc/on_config_loaded()
for (var/thing in subsystems)
var/datum/controller/subsystem/SS = thing
- SS.OnConfigLoad()
+ SS.on_config_loaded()
+ for(var/datum/controller/repository/repository in SSrepository.get_all_repositories())
+ repository.on_config_loaded()
/**
* CitRP snowflake special: Check if any subsystems are sleeping.
diff --git a/code/controllers/repository.dm b/code/controllers/repository.dm
index 402a48e30dd4..a53aa8e7ffc2 100644
--- a/code/controllers/repository.dm
+++ b/code/controllers/repository.dm
@@ -26,6 +26,17 @@
/// expected type of prototype
var/expected_type
+ /// database key; this is immutable.
+ /// * persistence is disabled if this is not set
+ var/database_key
+ /// store version
+ /// * persistence is disabled if this is not set
+ /// * migration is triggered if this doesn't match a loaded entry
+ /// * this should only ever be incremented.
+ var/store_version
+ /// store enabled? Updated by config reloads.
+ var/store_enabled = FALSE
+
/// by-id lookup
var/list/id_lookup
/// by-type lookup
@@ -34,6 +45,11 @@
/// fetched subtype lists
var/tmp/list/subtype_lists
+ /// 'doesn't exist' cache for DB loads
+ var/tmp/list/doesnt_exist_cache
+ var/const/doesnt_exist_cache_trim_at = 1000
+ var/const/doesnt_exist_cache_trim_to = 500
+
/// temporary id to path lookup used during init
// todo: figure out a way to not do this, this is bad
var/tmp/list/init_reverse_lookup_shim
@@ -43,6 +59,7 @@
type_lookup = list()
subtype_lists = list()
init_reverse_lookup_shim = list()
+ doesnt_exist_cache = list()
for(var/datum/prototype/casted as anything in subtypesof(expected_type))
if(initial(casted.abstract_type) == casted)
continue
@@ -57,6 +74,14 @@
init_reverse_lookup_shim = null
return ..()
+/datum/controller/repository/vv_edit_var(var_name, var_value, mass_edit, raw_edit)
+ switch(var_name)
+ if(NAMEOF(src, store_version), NAMEOF(src, database_key), NAMEOF(src, store_enabled))
+ return FALSE
+ if(NAMEOF(src, expected_type))
+ return FALSE
+ return ..()
+
/**
* Repository Recover()
*
@@ -82,6 +107,13 @@
. = FALSE
src.subtype_lists = list()
+/**
+ * Called when config is reloaded.
+ */
+/datum/controller/repository/proc/on_config_loaded()
+ SHOULD_CALL_PARENT(TRUE)
+ store_enabled = Configuration.get_entry(/datum/toml_config_entry/backend/repository/persistence) && database_key && store_version
+
/**
* regenerates entries, kicking out anything that's in the way
*/
@@ -105,6 +137,7 @@
*
* * Allows passing in a prototype instance which will be returned as itself.
* Useful for procs that should accept types, IDs, *and* instances.
+ * * Unlike fetch local / fetch or defer, this **can** sleep!
*
* prototypes returned should never, ever be modified
*
@@ -119,7 +152,102 @@
if(init_reverse_lookup_shim)
var/potential_path = init_reverse_lookup_shim[type_or_id]
return fetch(potential_path)
- return id_lookup[type_or_id]
+ . = id_lookup[type_or_id]
+ if(.)
+ return
+ if(!store_enabled)
+ return
+ if(doesnt_exist_cache[type_or_id])
+ return
+ return handle_db_load(type_or_id)
+ else if(ispath(type_or_id))
+ . = type_lookup[type_or_id]
+ if(.)
+ return
+ if(initial(type_or_id.abstract_type) == type_or_id)
+ CRASH("tried to fetch an abstract prototype")
+ var/datum/prototype/loading = new type_or_id
+ loading.hardcoded = TRUE
+ load(loading)
+ return loading
+ else if(istype(type_or_id))
+ return type_or_id
+ else
+ CRASH("what?")
+
+/**
+ * Fetches a prototype by type or ID.
+ *
+ * * Allows passing in a prototype instance which will be returned as itself.
+ * Useful for procs that should accept types, IDs, *and* instances.
+ * * If something doesn't exist and we don't know if it exists in the database, we throw a runtime error.
+ * * If fetching a hardcoded path, this should generally be used as it never sleeps.
+ *
+ * prototypes returned should never, ever be modified
+ *
+ * @return prototype instance or null
+ */
+/datum/controller/repository/proc/fetch_local_or_throw(datum/prototype/type_or_id) as /datum/prototype
+ RETURN_TYPE(/datum/prototype)
+ // todo: optimize
+ if(isnull(type_or_id))
+ return
+ else if(istext(type_or_id))
+ if(init_reverse_lookup_shim)
+ var/potential_path = init_reverse_lookup_shim[type_or_id]
+ return fetch_local_or_throw(potential_path)
+ . = id_lookup[type_or_id]
+ if(.)
+ return
+ if(!store_enabled)
+ return
+ if(doesnt_exist_cache[type_or_id])
+ return
+ CRASH("fetch_local_or_throw of [type_or_id] couldn't determine if id existed without a fetch.")
+ else if(ispath(type_or_id))
+ . = type_lookup[type_or_id]
+ if(.)
+ return
+ if(initial(type_or_id.abstract_type) == type_or_id)
+ CRASH("tried to fetch an abstract prototype")
+ var/datum/prototype/loading = new type_or_id
+ loading.hardcoded = TRUE
+ load(loading)
+ return loading
+ else if(istype(type_or_id))
+ return type_or_id
+ else
+ CRASH("what?")
+
+/**
+ * Fetches a prototype by type or ID.
+ *
+ * * Allows passing in a prototype instance which will be returned as itself.
+ * Useful for procs that should accept types, IDs, *and* instances.
+ * * If something doesn't exist and we don't know if it exists in the database, we return
+ * REPOSITORY_FETCH_DEFER. The caller should invoke normal fetch() at a time when sleeping is allowed.
+ *
+ * prototypes returned should never, ever be modified
+ *
+ * @return prototype instance or null
+ */
+/datum/controller/repository/proc/fetch_or_defer(datum/prototype/type_or_id) as /datum/prototype
+ RETURN_TYPE(/datum/prototype)
+ // todo: optimize
+ if(isnull(type_or_id))
+ return
+ else if(istext(type_or_id))
+ if(init_reverse_lookup_shim)
+ var/potential_path = init_reverse_lookup_shim[type_or_id]
+ return fetch_or_defer(potential_path)
+ . = id_lookup[type_or_id]
+ if(.)
+ return
+ if(!store_enabled)
+ return
+ if(doesnt_exist_cache[type_or_id])
+ return
+ return REPOSITORY_FETCH_DEFER
else if(ispath(type_or_id))
. = type_lookup[type_or_id]
if(.)
@@ -165,7 +293,7 @@
for(var/datum/prototype/casted as anything in subtypesof(path))
if(initial(casted.abstract_type) == casted)
continue
- var/datum/prototype/instance = fetch(casted)
+ var/datum/prototype/instance = fetch_local_or_throw(casted)
generating += instance
return generating
@@ -184,7 +312,11 @@
* After this call, the repository now owns the instance, not whichever system created it.
*/
/datum/controller/repository/proc/register(datum/prototype/instance)
- return load(instance)
+ . = load(instance)
+ if(!.)
+ return
+ if(store_enabled)
+ handle_db_store(instance)
//* Private API *//
@@ -203,7 +335,7 @@
CRASH("attempted to load an instance that collides with a currently loaded instance on type.")
if(!instance.register())
. = FALSE
- CRASH("instance refused to unregister. this is undefined behavior.")
+ CRASH("instance failed to register. this is undefined behavior.")
id_lookup[instance.id] = instance
if(instance.hardcoded)
// invalidate cache
@@ -222,7 +354,7 @@
PROTECTED_PROC(TRUE)
if(!instance.unregister())
. = FALSE
- CRASH("instance refused to unregister. this is undefined behavior.")
+ CRASH("instance failed to unregister. this is undefined behavior.")
id_lookup -= instance.id
if(instance.hardcoded)
// invalidate cache
@@ -230,3 +362,90 @@
subtype_lists = list()
type_lookup -= instance.type
return TRUE
+
+/**
+ * Perform migration on a data-list from the database.
+ *
+ * * Edit the passed in list directly.
+ * * This should update to latest.
+ *
+ * todo: proc to auto-migrate everything.
+ */
+/datum/controller/repository/proc/migrate(list/modifying, from_version)
+ PROTECTED_PROC(TRUE)
+
+/datum/controller/repository/proc/handle_db_store(datum/prototype/instance)
+ doesnt_exist_cache -= instance.id
+
+ // intentionally allow admin proccalls to bypass checks in NewQuery()
+ var/old_usr = usr
+ usr = null
+
+ var/datum/db_query/store_query = SSdbcore.NewQuery(
+ "INSERT INTO " + DB_PREFIX_TABLE_NAME("backend_repository") + "(repository, id, version, data) VALUES \
+ (:repo, :id, :version, :data) ON DUPLICATE KEY UPDATE data = :data, modifiedTime = Now(), version = :version",
+ list(
+ "repo" = database_key,
+ "id" = instance.id,
+ "version" = store_version,
+ "data" = json_encode(instance.serialize()),
+ ),
+ )
+
+ usr = old_usr
+
+ store_query.Execute(TRUE)
+ qdel(store_query)
+
+/datum/controller/repository/proc/handle_db_load(instance_id)
+ if(doesnt_exist_cache[instance_id])
+ return
+
+ // intentionally allow admin proccalls to bypass checks in NewQuery()
+ var/old_usr = usr
+ usr = null
+
+ var/datum/db_query/load_query = SSdbcore.NewQuery(
+ "SELECT version, data FROM " + DB_PREFIX_TABLE_NAME("backend_repository") + " WHERE repository = :repo, id = :id",
+ list(
+ "repo" = database_key,
+ "id" = instance_id,
+ ),
+ )
+
+ usr = old_usr
+
+ load_query.Execute(TRUE)
+
+ if(!length(load_query.item))
+ mark_doesnt_exist(instance_id)
+ else
+ var/list/fetched = load_query.item[1]
+ var/version = fetched[1]
+ var/encoded_data = fetched[2]
+ var/list/decoded_data = json_decode(encoded_data)
+ var/migrated = FALSE
+
+ if(version < store_version)
+ migrate(decoded_data, version)
+ migrated = TRUE
+ else if(version == store_version)
+ mark_doesnt_exist(instance_id)
+ CRASH("[version] was not less or eq to [store_version]. something's very wrong!")
+
+ var/datum/prototype/loaded_instance = new expected_type
+ loaded_instance.deserialize(decoded_data)
+ if(!load(loaded_instance))
+ mark_doesnt_exist(instance_id)
+ CRASH("[instance_id] failed to load into the repository during database load!")
+ . = loaded_instance
+
+ if(migrated)
+ handle_db_store(loaded_instance)
+
+ qdel(load_query)
+
+/datum/controller/repository/proc/mark_doesnt_exist(instance_id)
+ doesnt_exist_cache[instance_id] = TRUE
+ if(length(doesnt_exist_cache) > doesnt_exist_cache_trim_at)
+ doesnt_exist_cache.len = doesnt_exist_cache_trim_to
diff --git a/code/controllers/repository/designs.dm b/code/controllers/repository/designs.dm
index 23ed3186a240..2667472b765f 100644
--- a/code/controllers/repository/designs.dm
+++ b/code/controllers/repository/designs.dm
@@ -4,6 +4,7 @@
REPOSITORY_DEF(designs)
name = "Repository - Designs"
expected_type = /datum/prototype/design
+ database_key = "design"
//* caches *//
diff --git a/code/controllers/repository/guidebook.dm b/code/controllers/repository/guidebook.dm
index f5ea86eb1870..33c878a9fb77 100644
--- a/code/controllers/repository/guidebook.dm
+++ b/code/controllers/repository/guidebook.dm
@@ -1,6 +1,7 @@
//* This file is explicitly licensed under the MIT license. *//
//* Copyright (c) 2023 Citadel Station developers. *//
+// todo: this shouldn't be a repository, it can just be a controller
REPOSITORY_DEF(guidebook)
name = "Repository - Guidebook"
expected_type = /datum/prototype/guidebook_section
diff --git a/code/controllers/subsystem.dm b/code/controllers/subsystem.dm
index bf1333d28b4e..3287b34004fc 100644
--- a/code/controllers/subsystem.dm
+++ b/code/controllers/subsystem.dm
@@ -421,7 +421,7 @@
state = SS_PAUSING
/// Called after the config has been loaded or reloaded.
-/datum/controller/subsystem/proc/OnConfigLoad()
+/datum/controller/subsystem/proc/on_config_loaded()
return
/**
diff --git a/code/controllers/subsystem/assets.dm b/code/controllers/subsystem/assets.dm
index eff83427a200..ff29b03f16ef 100644
--- a/code/controllers/subsystem/assets.dm
+++ b/code/controllers/subsystem/assets.dm
@@ -191,7 +191,7 @@ SUBSYSTEM_DEF(assets)
/datum/controller/subsystem/assets/proc/get_dynamic_item_url_by_name(name)
return dynamic_asset_items_by_name[name]?.get_url()
-/datum/controller/subsystem/assets/OnConfigLoad()
+/datum/controller/subsystem/assets/on_config_loaded()
var/newtransporttype = /datum/asset_transport/browse_rsc
switch (CONFIG_GET(string/asset_transport))
if ("webroot")
diff --git a/code/controllers/subsystem/characters/storage.dm b/code/controllers/subsystem/characters/storage.dm
index 5a1b6045010c..bb84401ee120 100644
--- a/code/controllers/subsystem/characters/storage.dm
+++ b/code/controllers/subsystem/characters/storage.dm
@@ -33,7 +33,7 @@
// last played is not updated by this proc
// everything else can though!
var/datum/db_query/update_query = SSdbcore.NewQuery(
- "UPDATE [format_table_name("character")] \
+ "UPDATE [DB_PREFIX_TABLE_NAME("character")] \
SET[persisting? " last_persisted = NOW()," : ""] canonical_name = :name, persist_data = :data, \
playerid = :pid \
WHERE id = :id",
@@ -48,7 +48,7 @@
qdel(update_query)
else
var/datum/db_query/insert_query = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("character")] \
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("character")] \
(`created`, `last_played`, `last_persisted`, `playerid`, `canonical_name`, \
`persist_data`, `character_type`) \
VALUES (NOW(), NULL, [persisting? "NOW" : "NULL"], :pid, :name, :data, :type)",
@@ -85,7 +85,7 @@
var/datum/db_query/load_query = SSdbcore.NewQuery(
"SELECT `id` FROM \
- [format_table_name("character")] WHERE playerid = :id AND canonical_name = :name AND character_type = :type",
+ [DB_PREFIX_TABLE_NAME("character")] WHERE playerid = :id AND canonical_name = :name AND character_type = :type",
list(
"id" = playerid,
"canonical_name" = name,
@@ -134,7 +134,7 @@
var/datum/db_query/load_query = SSdbcore.NewQuery(
"SELECT `created`, `last_played`, `last_persisted`, `playerid`, `canonical_name`, `persist_data`, `character_type` FROM \
- [format_table_name("character")] WHERE id = :id",
+ [DB_PREFIX_TABLE_NAME("character")] WHERE id = :id",
list(
"id" = id,
)
@@ -190,7 +190,7 @@
. = list()
var/datum/db_query/iteration_query = SSdbcore.ExecuteQuery(
- "SELECT id FROM [format_table_name("character")] WHERE playerid = :id",
+ "SELECT id FROM [DB_PREFIX_TABLE_NAME("character")] WHERE playerid = :id",
list(
"id" = playerid
)
@@ -223,7 +223,7 @@
// section below can never be allowed to runtime
var/datum/db_query/mark_query = SSdbcore.ExecuteQuery(
- "UPDATE [format_table_name("character")] SET last_played = NOW() WHERE id = :id",
+ "UPDATE [DB_PREFIX_TABLE_NAME("character")] SET last_played = NOW() WHERE id = :id",
list(
"id" = id
)
diff --git a/code/controllers/subsystem/dbcore/_dbcore.dm b/code/controllers/subsystem/dbcore/_dbcore.dm
index 106a7fe09119..7e2b57d43fef 100644
--- a/code/controllers/subsystem/dbcore/_dbcore.dm
+++ b/code/controllers/subsystem/dbcore/_dbcore.dm
@@ -46,7 +46,7 @@ SUBSYSTEM_DEF(dbcore)
//This is as close as we can get to the true round end before Disconnect() without changing where it's called, defeating the reason this is a subsystem
if(SSdbcore.Connect())
var/datum/db_query/query_round_shutdown = SSdbcore.NewQuery(
- "UPDATE [format_table_name("round")] SET shutdown_datetime = Now() WHERE id = :round_id",
+ "UPDATE [DB_PREFIX_TABLE_NAME("round")] SET shutdown_datetime = Now() WHERE id = :round_id",
list("round_id" = GLOB.round_id)
)
query_round_shutdown.Execute()
@@ -114,7 +114,7 @@ SUBSYSTEM_DEF(dbcore)
if(CONFIG_GET(flag/sql_enabled))
if(Connect())
log_world("Database connection established.")
- var/datum/db_query/query_db_version = NewQuery("SELECT major, minor FROM [format_table_name("schema_revision")] ORDER BY date DESC LIMIT 1")
+ var/datum/db_query/query_db_version = NewQuery("SELECT major, minor FROM [DB_PREFIX_TABLE_NAME("schema_revision")] ORDER BY date DESC LIMIT 1")
query_db_version.Execute()
if(query_db_version.NextRow())
db_major = text2num(query_db_version.item[1])
@@ -135,7 +135,7 @@ SUBSYSTEM_DEF(dbcore)
if(!Connect())
return
var/datum/db_query/query_round_initialize = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("round")] (initialize_datetime, server_ip, server_port) VALUES (Now(), INET_ATON(:internet_address), :port)",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("round")] (initialize_datetime, server_ip, server_port) VALUES (Now(), INET_ATON(:internet_address), :port)",
list("internet_address" = world.internet_address || "0", "port" = "[world.port]")
)
query_round_initialize.Execute(async = FALSE)
@@ -147,7 +147,7 @@ SUBSYSTEM_DEF(dbcore)
if(!Connect())
return
var/datum/db_query/query_round_start = SSdbcore.NewQuery(
- "UPDATE [format_table_name("round")] SET start_datetime = Now() WHERE id = :round_id",
+ "UPDATE [DB_PREFIX_TABLE_NAME("round")] SET start_datetime = Now() WHERE id = :round_id",
list("round_id" = GLOB.round_id)
)
query_round_start.Execute()
@@ -157,7 +157,7 @@ SUBSYSTEM_DEF(dbcore)
if(!Connect())
return
var/datum/db_query/query_round_end = SSdbcore.NewQuery(
- "UPDATE [format_table_name("round")] SET end_datetime = Now() WHERE id = :round_id",
+ "UPDATE [DB_PREFIX_TABLE_NAME("round")] SET end_datetime = Now() WHERE id = :round_id",
list("round_id" = GLOB.round_id)
)
query_round_end.Execute()
diff --git a/code/controllers/subsystem/ipintel.dm b/code/controllers/subsystem/ipintel.dm
index 170e520e4cfe..e0f1dfd5cbb7 100644
--- a/code/controllers/subsystem/ipintel.dm
+++ b/code/controllers/subsystem/ipintel.dm
@@ -20,7 +20,7 @@ SUBSYSTEM_DEF(ipintel)
/// max retries
var/max_retries = 1
-/datum/controller/subsystem/ipintel/OnConfigLoad()
+/datum/controller/subsystem/ipintel/on_config_loaded()
. = ..()
enabled = !!CONFIG_GET(flag/ipintel_enabled)
consequetive_errors = 0
@@ -139,7 +139,7 @@ SUBSYSTEM_DEF(ipintel)
/datum/controller/subsystem/ipintel/proc/ipintel_cache_fetch_impl(address)
PRIVATE_PROC(TRUE)
var/datum/db_query/fetch = SSdbcore.NewQuery(
- "SELECT date, intel, TIMESTAMPDIFF(MINUTE,date,NOW()) FROM [format_table_name("ipintel")] WHERE ip = INET_ATON(:ip)",
+ "SELECT date, intel, TIMESTAMPDIFF(MINUTE,date,NOW()) FROM [DB_PREFIX_TABLE_NAME("ipintel")] WHERE ip = INET_ATON(:ip)",
list(
"ip" = address,
)
@@ -167,7 +167,7 @@ SUBSYSTEM_DEF(ipintel)
/datum/controller/subsystem/ipintel/proc/ipintel_cache_store_impl(datum/ipintel/entry)
PRIVATE_PROC(TRUE)
var/datum/db_query/update = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("ipintel")] (ip, intel) VALUES (INET_ATON(:ip), :intel) \
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("ipintel")] (ip, intel) VALUES (INET_ATON(:ip), :intel) \
ON DUPLICATE KEY UPDATE intel = VALUES(intel), date = NOW()",
list(
"ip" = entry.address,
diff --git a/code/controllers/subsystem/materials.dm b/code/controllers/subsystem/materials.dm
index 4cd8fae04146..edd53d804671 100644
--- a/code/controllers/subsystem/materials.dm
+++ b/code/controllers/subsystem/materials.dm
@@ -108,12 +108,16 @@ SUBSYSTEM_DEF(materials)
// todo: optimize
. = list()
for(var/i in 1 to length(L))
- var/key = L[i]
- var/datum/prototype/material/resolved = RSmaterials.fetch(key)
- if(isnull(resolved))
- continue
+ var/datum/prototype/material/key = L[i]
var/value = L[key]
- .[resolved.id] = value
+ if(istype(key))
+ key = key.id
+ else if(ispath(key))
+ key = initial(key.id)
+ else if(istext(key))
+ else
+ CRASH("what? '[key]'")
+ .[key] = value
/**
* ensures a list is full of material references for keys
@@ -126,10 +130,14 @@ SUBSYSTEM_DEF(materials)
. = list()
for(var/i in 1 to length(L))
var/key = L[i]
- var/datum/prototype/material/resolved = RSmaterials.fetch(key)
- if(isnull(resolved))
- continue
var/value = L[key]
+ var/datum/prototype/material/resolved = RSmaterials.fetch_or_defer(key)
+ switch(resolved)
+ if(null)
+ continue
+ if(REPOSITORY_FETCH_DEFER)
+ // todo: handle this
+ continue
.[resolved] = value
/**
@@ -142,9 +150,15 @@ SUBSYSTEM_DEF(materials)
. = list()
for(var/i in 1 to length(L))
var/key = L[i]
- var/value = L[key]
- var/datum/prototype/material/resolved = RSmaterials.fetch(value)
- .[key] = resolved?.id
+ var/datum/prototype/material/value = L[key]
+ if(istype(value))
+ value = value.id
+ else if(ispath(value))
+ value = initial(value.id)
+ else if(istext(value))
+ else
+ CRASH("what? '[value]'")
+ .[key] = value
/**
* ensures a list is full of material references for values
@@ -157,7 +171,12 @@ SUBSYSTEM_DEF(materials)
for(var/i in 1 to length(L))
var/key = L[i]
var/value = L[key]
- var/datum/prototype/material/resolved = RSmaterials.fetch(value)
+ var/datum/prototype/material/resolved = RSmaterials.fetch_or_defer(value)
+ switch(resolved)
+ if(REPOSITORY_FETCH_DEFER)
+ // todo: handle this
+ else
+ value = resolved
.[key] = resolved
/**
diff --git a/code/controllers/subsystem/persistence/modules/bulk_entity.dm b/code/controllers/subsystem/persistence/modules/bulk_entity.dm
index 7d04d37568ee..a17d389e00be 100644
--- a/code/controllers/subsystem/persistence/modules/bulk_entity.dm
+++ b/code/controllers/subsystem/persistence/modules/bulk_entity.dm
@@ -19,7 +19,7 @@
for(var/datum/bulk_entity_chunk/chunk as anything in chunks)
var/datum/db_query/query = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("persistence_bulk_entity")] \
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("persistence_bulk_entity")] \
(generation, persistence_key, level_id, data, round_id) \
VALUES (:generation, :persistence, :level, :data, :round)",
list(
@@ -47,7 +47,7 @@
usr = null
var/datum/db_query/query = SSdbcore.NewQuery(
- "SELECT data FROM [format_table_name("persistence_bulk_entity")] \
+ "SELECT data FROM [DB_PREFIX_TABLE_NAME("persistence_bulk_entity")] \
WHERE generation = :generation AND persistence_key = :persistence AND level_id = :level",
list(
"generation" = generation,
@@ -85,7 +85,7 @@
SSdbcore.dangerously_block_on_multiple_unsanitized_queries(
list(
- "TRUNCATE TABLE [format_table_name("persistence_bulk_entity")]",
+ "TRUNCATE TABLE [DB_PREFIX_TABLE_NAME("persistence_bulk_entity")]",
),
)
@@ -99,7 +99,7 @@
usr = null
SSdbcore.RunQuery(
- "DELETE FROM [format_table_name("persistence_bulk_entity")] WHERE level_id = :level",
+ "DELETE FROM [DB_PREFIX_TABLE_NAME("persistence_bulk_entity")] WHERE level_id = :level",
list(
"level" = level_id,
),
diff --git a/code/controllers/subsystem/persistence/modules/level_objects.dm b/code/controllers/subsystem/persistence/modules/level_objects.dm
index 43072527d789..8016594e6314 100644
--- a/code/controllers/subsystem/persistence/modules/level_objects.dm
+++ b/code/controllers/subsystem/persistence/modules/level_objects.dm
@@ -20,7 +20,7 @@
switch(entity.obj_persist_static_mode)
if(OBJ_PERSIST_STATIC_MODE_LEVEL)
query = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("persistence_static_level_objects")] (generation, object_id, level_id, data) \
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("persistence_static_level_objects")] (generation, object_id, level_id, data) \
VALUES (:generation, :object_id, :level_id, :data) ON DUPLICATE KEY UPDATE \
data = VALUES(data)",
list(
@@ -32,7 +32,7 @@
)
if(OBJ_PERSIST_STATIC_MODE_MAP)
query = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("persistence_static_map_objects")] (generation, object_id, map_id, data) \
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("persistence_static_map_objects")] (generation, object_id, map_id, data) \
VALUES (:generation, :object_id, :map_id, :data) ON DUPLICATE KEY UPDATE \
data = VALUES(data)",
list(
@@ -44,7 +44,7 @@
)
if(OBJ_PERSIST_STATIC_MODE_GLOBAL)
query = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("persistence_static_global_objects")] (generation, object_id, data) \
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("persistence_static_global_objects")] (generation, object_id, data) \
VALUES (:generation, :object_id, :data) ON DUPLICATE KEY UPDATE \
data = VALUES(data)",
list(
@@ -79,7 +79,7 @@
var/datum/db_query/query
if(entity.obj_persist_dynamic_id != PERSISTENCE_DYNAMIC_ID_AUTOSET)
query = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("persistence_dynamic_objects")] (id, generation, status, data, prototype_id, level_id, x, y) \
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("persistence_dynamic_objects")] (id, generation, status, data, prototype_id, level_id, x, y) \
VALUES (:status, :data, :prototype, :level, :x, :y) ON DUPLICATE KEY UPDATE \
x = VALUES(x), y = VALUES(y), data = VALUES(data), prototype = VALUES(prototype), level = VALUES(level), \
status = VALUES(status)",
@@ -97,7 +97,7 @@
query.warn_execute()
else
query = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("persistence_dynamic_objects")] (status, data, prototype_id, level_id, x, y) \
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("persistence_dynamic_objects")] (status, data, prototype_id, level_id, x, y) \
VALUES (:status, :data, :prototype, :level, :x, :y)",
list(
"status" = entity.obj_persist_dynamic_status,
@@ -139,7 +139,7 @@
var/datum/db_query/query = SSdbcore.NewQuery(
"SELECT object_id, prototype_id, status, data, x, y \
- FROM [format_table_name("persistence_dynamic_objects")] \
+ FROM [DB_PREFIX_TABLE_NAME("persistence_dynamic_objects")] \
WHERE level_id = :level AND generation = :generation",
list(
"generation" = generation,
@@ -200,7 +200,7 @@
switch(entity.obj_persist_static_mode)
if(OBJ_PERSIST_STATIC_MODE_GLOBAL)
query = SSdbcore.NewQuery(
- "SELECT data FROM [format_table_name("persistence_static_global_objects")] \
+ "SELECT data FROM [DB_PREFIX_TABLE_NAME("persistence_static_global_objects")] \
WHERE object_id = :object AND generation = :generation",
list(
"object" = entity.obj_persist_static_id,
@@ -209,7 +209,7 @@
)
if(OBJ_PERSIST_STATIC_MODE_LEVEL)
query = SSdbcore.NewQuery(
- "SELECT data FROM [format_table_name("persistence_static_level_objects")] \
+ "SELECT data FROM [DB_PREFIX_TABLE_NAME("persistence_static_level_objects")] \
WHERE object_id = :object AND level_id = :level AND generation = :generation",
list(
"object" = entity.obj_persist_static_id,
@@ -220,7 +220,7 @@
bind_id = level_id
if(OBJ_PERSIST_STATIC_MODE_MAP)
query = SSdbcore.NewQuery(
- "SELECT data FROM [format_table_name("persistence_static_map_objects")] \
+ "SELECT data FROM [DB_PREFIX_TABLE_NAME("persistence_static_map_objects")] \
WHERE object_id = :object AND map_id = :map AND generation = :generation",
list(
"object" = entity.obj_persist_static_id,
@@ -258,9 +258,9 @@
SSdbcore.dangerously_block_on_multiple_unsanitized_queries(
list(
- "TRUNCATE TABLE [format_table_name("persistence_static_map_objects")]",
- "TRUNCATE TABLE [format_table_name("persistence_static_level_objects")]",
- "TRUNCATE TABLE [format_table_name("persistence_static_global_objects")]",
+ "TRUNCATE TABLE [DB_PREFIX_TABLE_NAME("persistence_static_map_objects")]",
+ "TRUNCATE TABLE [DB_PREFIX_TABLE_NAME("persistence_static_level_objects")]",
+ "TRUNCATE TABLE [DB_PREFIX_TABLE_NAME("persistence_static_global_objects")]",
),
)
@@ -276,7 +276,7 @@
usr = null
SSdbcore.RunQuery(
- "DELETE FROM [format_table_name("persistence_static_level_objects")] WHERE level_id = :level",
+ "DELETE FROM [DB_PREFIX_TABLE_NAME("persistence_static_level_objects")] WHERE level_id = :level",
list(
"level" = level_id,
),
@@ -294,7 +294,7 @@
usr = null
SSdbcore.RunQuery(
- "DELETE FROM [format_table_name("persistence_static_map_objects")] WHERE map_id = :map",
+ "DELETE FROM [DB_PREFIX_TABLE_NAME("persistence_static_map_objects")] WHERE map_id = :map",
list(
"map" = map_id,
),
@@ -312,7 +312,7 @@
usr = null
SSdbcore.RunQuery(
- "DELETE FROM [format_table_name("persistence_static_global_objects")]",
+ "DELETE FROM [DB_PREFIX_TABLE_NAME("persistence_static_global_objects")]",
)
usr = intentionally_allow_admin_proccall
@@ -327,7 +327,7 @@
usr = null
SSdbcore.RunQuery(
- "TRUNCATE TABLE [format_table_name("persistence_dynamic_objects")]",
+ "TRUNCATE TABLE [DB_PREFIX_TABLE_NAME("persistence_dynamic_objects")]",
)
usr = intentionally_allow_admin_proccall
@@ -342,7 +342,7 @@
usr = null
SSdbcore.RunQuery(
- "DELETE FROM [format_table_name("persistence_dynamic_objects")] WHERE level_id = :level",
+ "DELETE FROM [DB_PREFIX_TABLE_NAME("persistence_dynamic_objects")] WHERE level_id = :level",
list(
"level" = level_id,
),
diff --git a/code/controllers/subsystem/persistence/modules/spatial_metadata.dm b/code/controllers/subsystem/persistence/modules/spatial_metadata.dm
index 8318e210040a..22d8c1a1b4b6 100644
--- a/code/controllers/subsystem/persistence/modules/spatial_metadata.dm
+++ b/code/controllers/subsystem/persistence/modules/spatial_metadata.dm
@@ -66,7 +66,7 @@
var/datum/db_query/query = SSdbcore.NewQuery(
"SELECT TIMESTAMPDIFF(HOUR, saved, NOW()), saved_round_id, data, generation \
- FROM [format_table_name("persistence_level_metadata")] \
+ FROM [DB_PREFIX_TABLE_NAME("persistence_level_metadata")] \
WHERE level_id = :level",
list(
"level" = level_id,
@@ -103,7 +103,7 @@
src.round_id_saved = GLOB.round_number
SSdbcore.RunQuery(
- "INSERT INTO [format_table_name("persistence_level_metadata")] (saved, saved_round_id, level_id, data, generation) \
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("persistence_level_metadata")] (saved, saved_round_id, level_id, data, generation) \
VALUES (Now(), :round, :level, :data, :generation) ON DUPLICATE KEY UPDATE \
data = VALUES(data), generation = VALUES(generation), saved_round_id = VALUES(saved_round_id), saved = VALUES(saved)",
list(
diff --git a/code/controllers/subsystem/persistence/modules/string_kkv.dm b/code/controllers/subsystem/persistence/modules/string_kkv.dm
index 6ae51d36a225..66b1ff79468d 100644
--- a/code/controllers/subsystem/persistence/modules/string_kkv.dm
+++ b/code/controllers/subsystem/persistence/modules/string_kkv.dm
@@ -60,7 +60,7 @@
var/oldusr = usr
usr = null
var/datum/db_query/query = SSdbcore.NewQuery(
- "SELECT `value` FROM [format_table_name("persistence_string_kkv")] WHERE `group` = :group AND `key` = :key",
+ "SELECT `value` FROM [DB_PREFIX_TABLE_NAME("persistence_string_kkv")] WHERE `group` = :group AND `key` = :key",
list(
"group" = group,
"key" = key
@@ -79,7 +79,7 @@
var/oldusr = usr
usr = null
var/datum/db_query/query = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("persistence_string_kkv")] (`group`, `key`, `value`) VALUES (:group, :key, :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), `modified` = Now(), `revision` = `revision` + 1",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("persistence_string_kkv")] (`group`, `key`, `value`) VALUES (:group, :key, :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`), `modified` = Now(), `revision` = `revision` + 1",
list(
"group" = group,
"key" = key,
diff --git a/code/controllers/subsystem/persistence/world.dm b/code/controllers/subsystem/persistence/world.dm
index 30ec4b2cfd2a..e1f82acdc8dd 100644
--- a/code/controllers/subsystem/persistence/world.dm
+++ b/code/controllers/subsystem/persistence/world.dm
@@ -234,21 +234,21 @@
for(var/datum/map_level_persistence/level_metadata as anything in ordered_level_metadata)
SSdbcore.RunQuery(
- "DELETE FROM [format_table_name("persistence_bulk_entity")] WHERE level_id = :level, generation != :generation",
+ "DELETE FROM [DB_PREFIX_TABLE_NAME("persistence_bulk_entity")] WHERE level_id = :level, generation != :generation",
list(
"level" = level_metadata.level_id,
"generation" = level_metadata.generation,
),
)
SSdbcore.RunQuery(
- "DELETE FROM [format_table_name("persistence_static_level_objects")] WHERE level_id = :level, generation != :generation",
+ "DELETE FROM [DB_PREFIX_TABLE_NAME("persistence_static_level_objects")] WHERE level_id = :level, generation != :generation",
list(
"level" = level_metadata.level_id,
"generation" = level_metadata.generation,
),
)
SSdbcore.RunQuery(
- "DELETE FROM [format_table_name("persistence_dynamic_objects")] WHERE level_id = :level, generation != :generation",
+ "DELETE FROM [DB_PREFIX_TABLE_NAME("persistence_dynamic_objects")] WHERE level_id = :level, generation != :generation",
list(
"level" = level_metadata.level_id,
"generation" = level_metadata.generation,
diff --git a/code/controllers/subsystem/photography.dm b/code/controllers/subsystem/photography.dm
index 7e4fbbcbccd4..37f23abe4c9a 100644
--- a/code/controllers/subsystem/photography.dm
+++ b/code/controllers/subsystem/photography.dm
@@ -115,7 +115,7 @@ SUBSYSTEM_DEF(photography)
var/datum/db_query/query = SSdbcore.NewQuery(
{"
- INSERT INTO [format_table_name("pictures")]
+ INSERT INTO [DB_PREFIX_TABLE_NAME("pictures")]
(`hash`, `width`, `height`) VALUES
(:hash, :width, :height)
"},
@@ -140,7 +140,7 @@ SUBSYSTEM_DEF(photography)
var/datum/db_query/query = SSdbcore.NewQuery(
{"
SELECT `width`, `height`
- FROM [format_table_name("pictures")]
+ FROM [DB_PREFIX_TABLE_NAME("pictures")]
WHERE `hash` = :hash
"},
list(
@@ -195,7 +195,7 @@ SUBSYSTEM_DEF(photography)
var/datum/db_query/query = SSdbcore.NewQuery(
{"
- INSERT INTO [format_table_name("photographs")]
+ INSERT INTO [DB_PREFIX_TABLE_NAME("photographs")]
(`picture`, `scene`, `desc`) VALUES
(:hash, :scene, :desc)
"},
@@ -220,7 +220,7 @@ SUBSYSTEM_DEF(photography)
var/datum/db_query/query = SSdbcore.NewQuery(
{"
SELECT `picture`, `scene`, `desc`
- FROM [format_table_name("photographs")]
+ FROM [DB_PREFIX_TABLE_NAME("photographs")]
WHERE `id` = :id
"},
list(
diff --git a/code/controllers/subsystem/playtime.dm b/code/controllers/subsystem/playtime.dm
index 617cdbf03350..5f1d1c2ec220 100644
--- a/code/controllers/subsystem/playtime.dm
+++ b/code/controllers/subsystem/playtime.dm
@@ -46,7 +46,7 @@ SUBSYSTEM_DEF(playtime)
"player" = playerid
)
C.persistent.playtime_queued = list()
- SSdbcore.MassInsertLegacy(format_table_name("playtime"), built, duplicate_key = "ON DUPLICATE KEY UPDATE minutes = minutes + VALUES(minutes)")
+ SSdbcore.MassInsertLegacy(DB_PREFIX_TABLE_NAME("playtime"), built, duplicate_key = "ON DUPLICATE KEY UPDATE minutes = minutes + VALUES(minutes)")
/**
* returns a list of playtime roles
diff --git a/code/controllers/subsystem/repository.dm b/code/controllers/subsystem/repository.dm
index 04db037a4973..4464288f229d 100644
--- a/code/controllers/subsystem/repository.dm
+++ b/code/controllers/subsystem/repository.dm
@@ -11,3 +11,6 @@ SUBSYSTEM_DEF(repository)
__create_repositories()
__init_repositories()
return SS_INIT_SUCCESS
+
+/datum/controller/subsystem/repository/proc/get_all_repositories()
+ return __get_all_repositories()
diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm
index 44f7944a2b98..5a204c1320d3 100644
--- a/code/controllers/subsystem/statpanel.dm
+++ b/code/controllers/subsystem/statpanel.dm
@@ -89,6 +89,10 @@ SUBSYSTEM_DEF(statpanels)
STATPANEL_DATA_CLICK(config.stat_key(), config.stat_entry(), "\ref[config]")
else
STATPANEL_DATA_LINE("FATAL - NO CONFIG")
+ if(config)
+ STATPANEL_DATA_CLICK(Configuration.stat_key(), Configuration.stat_entry(), "\ref[Configuration]")
+ else
+ STATPANEL_DATA_LINE("FATAL - NO CONFIG (NEW)")
STATPANEL_DATA_ENTRY("BYOND:", "(FPS:[world.fps]) (TickCount:[world.time/world.tick_lag]) (TickDrift:[round(Master.tickdrift,1)]([round((Master.tickdrift/(world.time/world.tick_lag))*100,0.1)]%)) (Internal Tick Usage: [round(MAPTICK_LAST_INTERNAL_TICK_USAGE,0.1)]%)")
if(Master)
STATPANEL_DATA_CLICK(Master.stat_key(), Master.stat_entry(), "\ref[Master]")
diff --git a/code/controllers/toml_config/README.md b/code/controllers/toml_config/README.md
new file mode 100644
index 000000000000..025e5b593f15
--- /dev/null
+++ b/code/controllers/toml_config/README.md
@@ -0,0 +1,3 @@
+# Configuration Module
+
+Experimental new TOML configuration to replace old configuration with eventually.
diff --git a/code/controllers/toml_config/entries/backend.dm b/code/controllers/toml_config/entries/backend.dm
new file mode 100644
index 000000000000..25e66949ac30
--- /dev/null
+++ b/code/controllers/toml_config/entries/backend.dm
@@ -0,0 +1,6 @@
+//* This file is explicitly licensed under the MIT license. *//
+//* Copyright (c) 2024 Citadel Station Developers *//
+
+/datum/toml_config_entry/backend
+ abstract_type = /datum/toml_config_entry/backend
+ category = "backend"
diff --git a/code/controllers/toml_config/entries/backend.repository.dm b/code/controllers/toml_config/entries/backend.repository.dm
new file mode 100644
index 000000000000..9c812b38a105
--- /dev/null
+++ b/code/controllers/toml_config/entries/backend.repository.dm
@@ -0,0 +1,14 @@
+//* This file is explicitly licensed under the MIT license. *//
+//* Copyright (c) 2024 Citadel Station Developers *//
+
+/datum/toml_config_entry/backend/repository
+ abstract_type = /datum/toml_config_entry/backend/repository
+ category = "backend.repository"
+
+/datum/toml_config_entry/backend/repository/persistence
+ key = "persistence"
+ desc = {"
+ Enable repository persistence. This requires the database to be available. Without this, most persistence
+ features will not function.
+ "}
+ default = TRUE
diff --git a/code/controllers/toml_config/toml_config_entry.dm b/code/controllers/toml_config/toml_config_entry.dm
new file mode 100644
index 000000000000..88eb4e6039a5
--- /dev/null
+++ b/code/controllers/toml_config/toml_config_entry.dm
@@ -0,0 +1,107 @@
+//* This file is explicitly licensed under the MIT license. *//
+//* Copyright (c) 2024 Citadel Station Developers *//
+
+/**
+ * Config entry.
+ *
+ * Supports at time of writing:
+ * * numbers
+ * * strings
+ * * (nested) lists
+ * * (nested) dictionaries
+ *
+ * Supports at time of writing auditing VV edits to:
+ * * numbers
+ * * strings
+ */
+/datum/toml_config_entry
+ abstract_type = /datum/toml_config_entry
+ /// key / name
+ var/key
+ /// category / where this is
+ var/category
+
+ /// description of this entry
+ var/desc
+
+ /// default value
+ var/default
+ /// current value
+ var/value
+
+ /// vv edit disallowed
+ /// * Does not stop get_entry and set_entry from setting our value. Those are not considered vv-protected.
+ var/vv_locked = FALSE
+ /// vv read disallowed
+ /// * Does not automatically imply [vv_locked].
+ /// * Does not stop get_entry and set_entry from pulling our value. Those are not considered vv-protected.
+ var/vv_secret = FALSE
+ /// sensitive
+ /// * Requires get_sensitive_entry() and set_sensitive_entry() to read/write.
+ /// * Does not actually imply [vv_locked] and [vv_secret].
+ var/sensitive = FALSE
+
+/datum/toml_config_entry/vv_edit_var(var_name, var_value, mass_edit, raw_edit)
+ switch(var_name)
+ if(NAMEOF(src, default))
+ return FALSE
+ if(NAMEOF(src, value))
+ if(vv_locked)
+ return FALSE
+ if(NAMEOF(src, key))
+ return FALSE
+ if(NAMEOF(src, category))
+ return FALSE
+ if(NAMEOF(src, desc))
+ return FALSE
+ if(NAMEOF(src, vv_locked))
+ return FALSE
+ if(NAMEOF(src, vv_secret))
+ return FALSE
+ if(NAMEOF(src, sensitive))
+ return FALSE
+ return ..()
+
+/datum/toml_config_entry/vv_get_var(var_name, resolve)
+ switch(var_name)
+ if(NAMEOF(src, value))
+ if(vv_locked)
+ return "-- secret --"
+ return ..()
+
+/datum/toml_config_entry/CanProcCall(procname)
+ switch(procname)
+ if(NAMEOF_PROC(src, New), NAMEOF_PROC(src, Destroy))
+ return FALSE
+ if(NAMEOF_PROC(src, reset))
+ return FALSE
+ if(NAMEOF_PROC(src, apply))
+ return FALSE
+ return ..()
+
+/datum/toml_config_entry/vv_delete()
+ return FALSE
+
+/**
+ * Called once when resetting.
+ */
+/datum/toml_config_entry/proc/reset()
+ if(isnum(default))
+ value = default
+ else if(istext(default))
+ value = default
+ else if(islist(default))
+ value = deep_copy_list(default)
+ else
+ CRASH("unexpected value in default.")
+
+/**
+ * Called once with the value from each load.
+ *
+ * Can be used to overlay values.
+ *
+ * @params
+ * * raw_config_value - Raw parsed data. We own the reference to this once this proc is called.
+ */
+/datum/toml_config_entry/proc/apply(raw_config_value)
+ value = raw_config_value
diff --git a/code/controllers/toml_config/toml_configuration.dm b/code/controllers/toml_config/toml_configuration.dm
new file mode 100644
index 000000000000..ec2984bcdde6
--- /dev/null
+++ b/code/controllers/toml_config/toml_configuration.dm
@@ -0,0 +1,169 @@
+//* This file is explicitly licensed under the MIT license. *//
+//* Copyright (c) 2024 Citadel Station Developers *//
+
+// todo: maybe rename to config? or keep it as Configuration to keep with naming scheme of other 'system / backend' modules like the MC?
+GLOBAL_REAL(Configuration, /datum/controller/toml_configuration)
+
+// todo: /datum/controller/configuration
+/datum/controller/toml_configuration
+ /// Entries by type.
+ VAR_PRIVATE/list/datum/toml_config_entry/typed_entries
+ /// Entries as same structure as the underlying toml/json
+ VAR_PRIVATE/list/datum/toml_config_entry/keyed_entries
+
+/datum/controller/toml_configuration/CanProcCall(procname)
+ switch(procname)
+ if(NAMEOF_PROC(src, New), NAMEOF_PROC(src, Destroy), NAMEOF_PROC(src, Initialize))
+ return FALSE
+ if(NAMEOF_PROC(src, get_entry), NAMEOF_PROC(src, set_entry))
+ return FALSE
+ if(NAMEOF_PROC(src, get_sensitive_entry), NAMEOF_PROC(src, set_sensitive_entry))
+ return FALSE
+ if(NAMEOF_PROC(src, reload), NAMEOF_PROC(src, reset), NAMEOF_PROC(src, load), NAMEOF_PROC(src, recursively_load_from_list))
+ return FALSE
+ return ..()
+
+/datum/controller/toml_configuration/vv_edit_var(var_name, var_value, mass_edit, raw_edit)
+ switch(var_name)
+ if(NAMEOF(src, keyed_entries))
+ return FALSE
+ return ..()
+
+/datum/controller/toml_configuration/New()
+ if(Configuration != src)
+ if(Configuration)
+ qdel(Configuration)
+ Configuration = src
+
+/datum/controller/toml_configuration/Initialize()
+ keyed_entries = list()
+ typed_entries = list()
+ for(var/datum/toml_config_entry/path as anything in typesof(/datum/toml_config_entry))
+ if(initial(path.abstract_type) == path)
+ continue
+ var/datum/toml_config_entry/entry = new path
+ typed_entries[entry.type] = entry
+ var/list/nesting = splittext(entry.category, ".")
+ var/list/current_list = keyed_entries
+ for(var/i in 1 to length(nesting))
+ LAZYINITLIST(current_list[nesting[i]])
+ current_list = current_list[nesting[i]]
+ current_list[entry.key] = entry
+ reload()
+
+/datum/controller/toml_configuration/stat_key()
+ return "Configuration (New):"
+
+/datum/controller/toml_configuration/stat_entry()
+ return "Edit"
+
+/**
+ * HEY! LISTEN! By calling this proc you are affirming that:
+ *
+ * * The entry type you are passing in is static and not a variable that can be tampered with.
+ * * The value you get will be immediately consumed in a non-VV-able manner.
+ */
+/datum/controller/toml_configuration/proc/get_sensitive_entry(datum/toml_config_entry/entry_type)
+ // todo: cache / optimize
+ var/datum/toml_config_entry/entry = typed_entries[entry_type]
+ if(!entry)
+ return
+ if(!entry.sensitive)
+ CRASH("attempted to get sensitive entry with sensitive get entry.")
+ return entry.value
+
+/**
+ * HEY! LISTEN! By calling this proc you are affirming that:
+ *
+ * * The entry type you are passing in is static and not a variable that can be tampered with.
+ * * The value you are passing in is trusted and validated and not a variable that can be tampered with.
+ */
+/datum/controller/toml_configuration/proc/set_sensitive_entry(datum/toml_config_entry/entry_type, value)
+ // todo: cache / optimize
+ var/datum/toml_config_entry/entry = typed_entries[entry_type]
+ if(!entry)
+ return
+ if(entry.sensitive)
+ CRASH("attempted to set non-sensitive entry with sensitive set entry.")
+ entry.value = value
+
+/datum/controller/toml_configuration/proc/get_entry(datum/toml_config_entry/entry_type)
+ // todo: cache / optimize
+ var/datum/toml_config_entry/entry = typed_entries[entry_type]
+ if(!entry)
+ return
+ if(entry.sensitive)
+ CRASH("attempted to get sensitive entry with normal get entry.")
+ return entry.value
+
+/datum/controller/toml_configuration/proc/set_entry(datum/toml_config_entry/entry_type, value)
+ // todo: cache / optimize
+ var/datum/toml_config_entry/entry = typed_entries[entry_type]
+ if(!entry)
+ return
+ if(entry.sensitive)
+ CRASH("attempted to set sensitive entry with normal set entry.")
+ entry.value = value
+
+/datum/controller/toml_configuration/proc/admin_reload()
+ reload()
+
+/**
+ * Automatically loads default config, and the server's config file.
+ *
+ * todo: allow for overriding directories
+ */
+/datum/controller/toml_configuration/proc/reload()
+ reset()
+ load("config.default/config.toml")
+ load("config/config.toml")
+
+/**
+ * Resets the configuration.
+ */
+/datum/controller/toml_configuration/proc/reset()
+ for(var/path in typed_entries)
+ var/datum/toml_config_entry/entry = typed_entries[path]
+ entry.reset()
+
+/**
+ * Loads from a given layer.
+ * * This will not reset the configuration. Repeated calls to load will allow for layered configuration.
+ *
+ * HEY! LISTEN! By calling this proc you are affirming that:
+ * * The file you are passing in is trusted and not a variable that can be tampered with via VV.
+ */
+/datum/controller/toml_configuration/proc/load(filelike)
+ var/list/decoded
+ if(istext(filelike))
+ if(!fexists(filelike))
+ CRASH("failed to load [filelike]: does not exist")
+ decoded = rustg_read_toml_file(filelike)
+ else if(isfile(filelike))
+ // noa path, it might be rsc cache; rust_g can't read that directly.
+ fdel("tmp/config/loading.toml")
+ fcopy(filelike, "tmp/config/loading.toml")
+ decoded = rustg_read_toml_file("tmp/config/loading.toml")
+ fdel("tmp/config/loading.toml")
+ if(!decoded)
+ CRASH("failed to decode config [filelike]!")
+
+ recursively_load_from_list(decoded, keyed_entries)
+
+/datum/controller/toml_configuration/proc/recursively_load_from_list(list/decoded_list, list/entry_list)
+ if(!decoded_list || !entry_list)
+ return
+ for(var/key in decoded_list)
+ var/value = decoded_list[key]
+ if(islist(value))
+ var/list/next_entry_list = entry_list[key]
+ if(!islist(next_entry_list))
+ // todo: warn
+ else
+ recursively_load_from_list(value, next_entry_list[key])
+ else
+ var/datum/toml_config_entry/entry = entry_list[key]
+ if(!istype(entry))
+ // todo: warn
+ else
+ entry.apply(value)
diff --git a/code/datums/datumvars.dm b/code/datums/datumvars.dm
index c93c2373e7b9..745ad7051e49 100644
--- a/code/datums/datumvars.dm
+++ b/code/datums/datumvars.dm
@@ -1,3 +1,10 @@
+/**
+ * Called when an admin attempts to delete us with introspection tools.
+ */
+/datum/proc/vv_delete()
+ . = TRUE
+ qdel(src)
+
/datum/proc/CanProcCall(procname)
return TRUE
diff --git a/code/game/machinery/gear_painter.dm b/code/game/machinery/gear_painter.dm
index 4989687346ae..8b23bdfd0507 100644
--- a/code/game/machinery/gear_painter.dm
+++ b/code/game/machinery/gear_painter.dm
@@ -285,7 +285,7 @@
/obj/machinery/gear_painter/proc/check_valid_color(list/cm, mob/user)
if(!islist(cm)) // normal
- var/list/HSV = ReadHSV(RGBtoHSV(cm))
+ var/list/HSV = rgb2hsv(cm)
if(HSV[3] < minimum_normal_lightness)
temp = "[cm] is too dark (Minimum lightness: [minimum_normal_lightness])"
return FALSE
@@ -294,7 +294,7 @@
// We test using full red, green, blue, and white
// A predefined number of them must pass to be considered valid
var/passed = 0
-#define COLORTEST(thestring, thematrix) passed += (ReadHSV(RGBtoHSV(RGBMatrixTransform(thestring, thematrix)))[3] >= minimum_matrix_lightness)
+#define COLORTEST(thestring, thematrix) passed += (rgb2hsv(RGBMatrixTransform(thestring, thematrix))[3] >= minimum_matrix_lightness)
COLORTEST("FF0000", cm)
COLORTEST("00FF00", cm)
COLORTEST("0000FF", cm)
diff --git a/code/game/machinery/telecomms/blackbox.dm b/code/game/machinery/telecomms/blackbox.dm
index 61e83517e4c9..fb9cef3c7c4f 100644
--- a/code/game/machinery/telecomms/blackbox.dm
+++ b/code/game/machinery/telecomms/blackbox.dm
@@ -182,7 +182,7 @@ var/obj/machinery/blackbox_recorder/blackbox
var/round_id
var/datum/db_query/query = SSdbcore.RunQuery(
- "SELECT MAX(round_id) AS round_id FROM [format_table_name("feedback")]",
+ "SELECT MAX(round_id) AS round_id FROM [DB_PREFIX_TABLE_NAME("feedback")]",
list()
)
@@ -195,7 +195,7 @@ var/obj/machinery/blackbox_recorder/blackbox
for(var/datum/feedback_variable/FV in feedback)
SSdbcore.RunQuery(
- "INSERT INTO [format_table_name("feedback")] VALUES (null, Now(), :round_id, :variable, :value, :details)",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("feedback")] VALUES (null, Now(), :round_id, :variable, :value, :details)",
list(
"round_id" = "[round_id]",
"variable" = "[FV.get_variable()]",
diff --git a/code/game/objects/items/storage/medical/hypokit.dm b/code/game/objects/items/storage/medical/hypokit.dm
index 6409bb530a8e..055f3d68c0a5 100644
--- a/code/game/objects/items/storage/medical/hypokit.dm
+++ b/code/game/objects/items/storage/medical/hypokit.dm
@@ -13,7 +13,7 @@
var/hypospray_path = /obj/item/hypospray
var/vial_path = /obj/item/reagent_containers/glass/hypovial
- var/vial_amount = 6
+ var/vial_amount = 0
/obj/item/storage/hypokit/legacy_spawn_contents()
. = ..()
@@ -54,6 +54,7 @@
inhand_state = "normal"
hypospray_path = /obj/item/hypospray/advanced/loaded
vial_path = /obj/item/reagent_containers/glass/hypovial/large
+ vial_amount = 6
max_combined_volume = STORAGE_VOLUME_BOX * 2
weight_volume = WEIGHT_VOLUME_NORMAL * 1.5
diff --git a/code/game/objects/items/stream_projector/medichine.dm b/code/game/objects/items/stream_projector/medichine.dm
index 4583723dc05b..21c1652bc25b 100644
--- a/code/game/objects/items/stream_projector/medichine.dm
+++ b/code/game/objects/items/stream_projector/medichine.dm
@@ -182,7 +182,7 @@ GLOBAL_LIST_EMPTY(medichine_cell_datums)
entity_beam.segmentation.color = beam_color
/obj/item/stream_projector/medichine/proc/beam_color(color)
- var/list/decoded = ReadRGB(color)
+ var/list/decoded = rgb2num(color)
return list(
decoded[1] / 255, decoded[2] / 255, decoded[3] / 255,
0, 0, 0,
@@ -539,7 +539,7 @@ GLOBAL_LIST_EMPTY(medichine_cell_datums)
var/list/color_rgb_list
/datum/medichine_cell/New()
- color_rgb_list = ReadRGB(color)
+ color_rgb_list = rgb2num(color)
for(var/i in 1 to length(effects))
var/datum/medichine_effect/effect = effects[i]
if(istype(effect))
diff --git a/code/game/objects/materials.dm b/code/game/objects/materials.dm
index 8f6e73ce51cd..7a8c5573657d 100644
--- a/code/game/objects/materials.dm
+++ b/code/game/objects/materials.dm
@@ -91,7 +91,12 @@
if(islist(material_parts))
var/list/parts = list()
for(var/key in material_parts)
- parts[key] = RSmaterials.fetch(key)
+ var/datum/prototype/material/result = RSmaterials.fetch_or_defer(key)
+ switch(result)
+ if(REPOSITORY_FETCH_DEFER)
+ // todo: handle this
+ result = null
+ parts[key] = result
update_material_multi(parts)
else if(material_parts == MATERIAL_DEFAULT_DISABLED)
else if(material_parts == MATERIAL_DEFAULT_ABSTRACTED)
@@ -99,7 +104,12 @@
// skip specifying parts because abstracted
update_material_multi()
else
- update_material_single((material_parts = RSmaterials.fetch(material_parts)))
+ var/datum/prototype/material/result = RSmaterials.fetch_or_defer(material_parts)
+ switch(result)
+ if(REPOSITORY_FETCH_DEFER)
+ // todo: handle this
+ result = null
+ update_material_single((material_parts = result))
/**
* forces a material update
diff --git a/code/game/objects/obj.dm b/code/game/objects/obj.dm
index 33ebad602a1f..1d820365df08 100644
--- a/code/game/objects/obj.dm
+++ b/code/game/objects/obj.dm
@@ -619,8 +619,8 @@
color = colors[1]
if(COLORATION_MODE_RG_MATRIX)
ASSERT(length(colors) == 2)
- var/list/red_decoded = ReadRGB(colors[1])
- var/list/green_decoded = ReadRGB(colors[2])
+ var/list/red_decoded = rgb2num(colors[1])
+ var/list/green_decoded = rgb2num(colors[2])
color = list(
red_decoded[1] / 255, red_decoded[2] / 255, red_decoded[3] / 255, 0,
green_decoded[1] / 255, green_decoded[2] / 255, green_decoded[3] / 255, 0,
@@ -629,8 +629,8 @@
)
if(COLORATION_MODE_GB_MATRIX)
ASSERT(length(colors) == 2)
- var/list/green_decoded = ReadRGB(colors[1])
- var/list/blue_decoded = ReadRGB(colors[2])
+ var/list/green_decoded = rgb2num(colors[1])
+ var/list/blue_decoded = rgb2num(colors[2])
color = list(
0, 0, 0, 0,
green_decoded[1] / 255, green_decoded[2] / 255, green_decoded[3] / 255, 0,
@@ -639,8 +639,8 @@
)
if(COLORATION_MODE_RB_MATRIX)
ASSERT(length(colors) == 2)
- var/list/red_decoded = ReadRGB(colors[1])
- var/list/blue_decoded = ReadRGB(colors[2])
+ var/list/red_decoded = rgb2num(colors[1])
+ var/list/blue_decoded = rgb2num(colors[2])
color = list(
red_decoded[1] / 255, red_decoded[2] / 255, red_decoded[3] / 255, 0,
0, 0, 0, 0,
@@ -649,9 +649,9 @@
)
if(COLORATION_MODE_RGB_MATRIX)
ASSERT(length(colors) == 3)
- var/list/red_decoded = ReadRGB(colors[1])
- var/list/green_decoded = ReadRGB(colors[2])
- var/list/blue_decoded = ReadRGB(colors[3])
+ var/list/red_decoded = rgb2num(colors[1])
+ var/list/green_decoded = rgb2num(colors[2])
+ var/list/blue_decoded = rgb2num(colors[3])
color = list(
red_decoded[1] / 255, red_decoded[2] / 255, red_decoded[3] / 255, 0,
green_decoded[1] / 255, green_decoded[2] / 255, green_decoded[3] / 255, 0,
diff --git a/code/game/objects/structures/barricade.dm b/code/game/objects/structures/barricade.dm
index e490a2965fa4..e34557966bd6 100644
--- a/code/game/objects/structures/barricade.dm
+++ b/code/game/objects/structures/barricade.dm
@@ -13,7 +13,12 @@
/obj/structure/barricade/Initialize(mapload, datum/prototype/material/material_like)
if(!isnull(material_like))
- set_primary_material(RSmaterials.fetch(material_like))
+ var/resolved_material = RSmaterials.fetch_or_defer(material_like)
+ switch(resolved_material)
+ if(REPOSITORY_FETCH_DEFER)
+ // todo: handle
+ else
+ set_primary_material(resolved_material)
return ..()
/obj/structure/barricade/update_material_single(datum/prototype/material/material)
diff --git a/code/game/objects/structures/low_wall.dm b/code/game/objects/structures/low_wall.dm
index e3419164af54..e3d4b324711c 100644
--- a/code/game/objects/structures/low_wall.dm
+++ b/code/game/objects/structures/low_wall.dm
@@ -48,9 +48,14 @@ GLOBAL_LIST_INIT(wallframe_typecache, typecacheof(list(
paint_color = COLOR_WALL_GUNMETAL
stripe_color = COLOR_WALL_GUNMETAL
-/obj/structure/wall_frame/Initialize(mapload, material)
- if(!isnull(material))
- set_primary_material(RSmaterials.fetch(material))
+/obj/structure/wall_frame/Initialize(mapload, datum/prototype/material/material_like)
+ if(!isnull(material_like))
+ var/resolved_material = RSmaterials.fetch_or_defer(material_like)
+ switch(resolved_material)
+ if(REPOSITORY_FETCH_DEFER)
+ // todo: handle
+ else
+ set_primary_material(resolved_material)
. = ..()
update_overlays()
diff --git a/code/game/objects/structures/noticeboard.dm b/code/game/objects/structures/noticeboard.dm
index 9a710c58a5f2..0bc1d2b3538c 100644
--- a/code/game/objects/structures/noticeboard.dm
+++ b/code/game/objects/structures/noticeboard.dm
@@ -1,40 +1,52 @@
+#define MAX_NOTICES 8
+
/obj/structure/noticeboard
name = "notice board"
desc = "A board for pinning important notices upon."
icon = 'icons/obj/stationobjs.dmi'
- icon_state = "nboard00"
- density = 0
- anchored = 1
+ icon_state = "noticeboard"
+ density = FALSE
+ anchored = TRUE
+ integrity_max = 150
+ /// Current number of a pinned notices
var/notices = 0
/obj/structure/noticeboard/Initialize(mapload, dir, building = FALSE)
+ . = ..()
+
if(building)
pixel_x = (dir & 3)? 0 : (dir == 4 ? -32 : 32)
pixel_y = (dir & 3)? (dir ==1 ? -27 : 27) : 0
- update_icon()
- if(mapload)
- for(var/obj/item/I in loc)
- if(notices > 4)
- break
- if(istype(I, /obj/item/paper))
- I.forceMove(src)
- notices++
- icon_state = "nboard0[notices]"
- . = ..()
+ update_appearance(UPDATE_ICON)
+
+ if(!mapload)
+ return
+
+ for(var/obj/item/I in loc)
+ if(notices > MAX_NOTICES)
+ break
+ if(istype(I, /obj/item/paper))
+ I.forceMove(src)
+ notices++
+ update_appearance(UPDATE_ICON)
//attaching papers!!
-/obj/structure/noticeboard/attackby(var/obj/item/O as obj, var/mob/user as mob)
- if(istype(O, /obj/item/paper))
- if(notices < 5)
- if(!user.attempt_insert_item_for_installation(O, src))
+/obj/structure/noticeboard/attackby(obj/item/O, mob/user, params)
+ if(istype(O, /obj/item/paper) || istype(O, /obj/item/photo))
+ if(!allowed(user))
+ to_chat(user, SPAN_WARNING("You are not authorized to add notices!"))
+ return
+ if(notices < MAX_NOTICES)
+ if(!user.transfer_item_to_loc(O, src))
return
- O.add_fingerprint(user)
- add_fingerprint(user)
notices++
- icon_state = "nboard0[notices]" //update sprite
- to_chat(user, "You pin the paper to the noticeboard.")
+ update_appearance(UPDATE_ICON)
+ to_chat(user, SPAN_NOTICE("You pin the [O] to the noticeboard."))
else
- to_chat(user, "You reach to pin your paper to the board but hesitate. You are certain your paper will not be seen among the many others already attached.")
+ to_chat(user, SPAN_WARNING("The notice board is full!"))
+ // else
+ // return ..()
+
if(O.is_wrench())
to_chat(user, "You start to unwrench the noticeboard.")
playsound(src.loc, O.tool_sound, 50, 1)
@@ -43,52 +55,77 @@
new /obj/item/frame/noticeboard( src.loc )
qdel(src)
-/obj/structure/noticeboard/attack_hand(mob/user, datum/event_args/actor/clickchain/e_args)
- user.do_examinate(src)
-
-// Since Topic() never seems to interact with usr on more than a superficial
-// level, it should be fine to let anyone mess with the board other than ghosts.
-/obj/structure/noticeboard/examine(mob/user, dist) //why the fuck is this shit on examine
- if(!user)
- user = usr
- if(user.Adjacent(src))
- var/dat = "Noticeboard "
- for(var/obj/item/paper/P in src)
- dat += "[P.name]WriteRemove "
- user << browse("
Notices[dat]","window=noticeboard")
- onclose(user, "noticeboard")
+/obj/structure/noticeboard/ui_state(mob/user)
+ return GLOB.physical_state
+
+/obj/structure/noticeboard/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "NoticeBoard", name)
+ ui.open()
+
+/obj/structure/noticeboard/ui_data(mob/user)
+ var/list/data = list()
+ data["allowed"] = allowed(user)
+ data["items"] = list()
+ for(var/obj/item/content in contents)
+ var/list/content_data = list(
+ name = content.name,
+ ref = REF(content)
+ )
+ data["items"] += list(content_data)
+ return data
+
+/obj/structure/noticeboard/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
+ . = ..()
+ if(.)
+ return
+
+ var/obj/item/item = locate(params["ref"]) in contents
+ if(!istype(item) || item.loc != src)
+ return
+
+ var/mob/user = usr
+
+ switch(action)
+ if("examine")
+ // if(istype(item, /obj/item/paper))
+ // item.ui_interact(user) // not using tguipaper
+ // else
+ user.examinate(item)
+ return TRUE
+ if("remove")
+ if(!allowed(user))
+ return
+ remove_item(item, user)
+ return TRUE
+
+/obj/structure/noticeboard/update_overlays()
+ . = ..()
+ if(notices)
+ . += "notices_[notices]"
+
+/**
+ * Removes an item from the notice board
+ *
+ * Arguments:
+ * * item - The item that is to be removed
+ * * user - The mob that is trying to get the item removed, if there is one
+ */
+/obj/structure/noticeboard/proc/remove_item(obj/item/item, mob/user)
+ item.forceMove(drop_location())
+ if(user)
+ user.put_in_hands(item)
+ to_chat(user, SPAN_NOTICE("Removed from board."))
+ notices--
+ update_appearance(UPDATE_ICON)
+
+/obj/structure/noticeboard/deconstructed(disassembled = TRUE)
+ if(!disassembled)
+ new /obj/item/stack/material/wood(loc)
else
- ..()
+ new /obj/item/frame/noticeboard(loc)
+ for(var/obj/item/content in contents)
+ remove_item(content)
-/obj/structure/noticeboard/Topic(href, href_list)
- ..()
- usr.set_machine(src)
- if(href_list["remove"])
- if((usr.stat || usr.restrained())) //For when a player is handcuffed while they have the notice window open
- return
- var/obj/item/P = locate(href_list["remove"])
- if(P && P.loc == src)
- P.loc = get_turf(src) //dump paper on the floor because you're a clumsy fuck
- P.add_fingerprint(usr)
- add_fingerprint(usr)
- notices--
- icon_state = "nboard0[notices]"
- if(href_list["write"])
- if((usr.stat || usr.restrained())) //For when a player is handcuffed while they have the notice window open
- return
- var/obj/item/P = locate(href_list["write"])
- if((P && P.loc == src)) //ifthe paper's on the board
- var/mob/living/M = usr
- if(istype(M))
- var/obj/item/pen/E = M.get_held_item_of_type(/obj/item/pen)
- if(E)
- add_fingerprint(M)
- P.attackby(E, usr)
- else
- to_chat(M, "You'll need something to write with!")
- if(href_list["read"])
- var/obj/item/paper/P = locate(href_list["read"])
- if((P && P.loc == src))
- usr << browse("[P.name][P.info]", "window=[P.name]")
- onclose(usr, "[P.name]")
- return
+#undef MAX_NOTICES
diff --git a/code/game/objects/structures/props/puzzledoor.dm b/code/game/objects/structures/props/puzzledoor.dm
index 66c6b1736827..a73e4cd16311 100644
--- a/code/game/objects/structures/props/puzzledoor.dm
+++ b/code/game/objects/structures/props/puzzledoor.dm
@@ -36,7 +36,7 @@
/obj/machinery/door/blast/puzzle/Initialize(mapload)
. = ..()
- implicit_material = RSmaterials.fetch(/datum/prototype/material/alienalloy/dungeonium)
+ implicit_material = RSmaterials.fetch_local_or_throw(/datum/prototype/material/alienalloy/dungeonium)
if(locks.len)
return
var/check_range = world.view * checkrange_mult
diff --git a/code/game/objects/structures/simple_doors.dm b/code/game/objects/structures/simple_doors.dm
index 273b68e575f6..09824f2a6936 100644
--- a/code/game/objects/structures/simple_doors.dm
+++ b/code/game/objects/structures/simple_doors.dm
@@ -15,9 +15,14 @@
var/isSwitchingStates = 0
var/oreAmount = 7
-/obj/structure/simple_door/Initialize(mapload, material)
- if(!isnull(material))
- set_primary_material(RSmaterials.fetch(material))
+/obj/structure/simple_door/Initialize(mapload, datum/prototype/material/material_like)
+ if(!isnull(material_like))
+ var/resolved_material = RSmaterials.fetch_or_defer(material_like)
+ switch(resolved_material)
+ if(REPOSITORY_FETCH_DEFER)
+ // todo: handle
+ else
+ set_primary_material(resolved_material)
return ..()
/obj/structure/simple_door/update_material_single(datum/prototype/material/material)
diff --git a/code/game/statistics.dm b/code/game/statistics.dm
index e8f73892bd62..2a67a26b9058 100644
--- a/code/game/statistics.dm
+++ b/code/game/statistics.dm
@@ -9,7 +9,7 @@
log_game("SQL ERROR during population polling. Failed to connect.")
else
var/datum/db_query/query = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("population")] (playercount, admincount, time) VALUES (:pc, :ac, NOW())",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("population")] (playercount, admincount, time) VALUES (:pc, :ac, NOW())",
list(
"pc" = sanitizeSQL(playercount),
"ac" = sanitizeSQL(admincount),
@@ -48,7 +48,7 @@
log_game("SQL ERROR during death reporting. Failed to connect.")
else
var/datum/db_query/query = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("death")] (name, byondkey, job, special, pod, tod, laname, lakey, gender, bruteloss, fireloss, brainloss, oxyloss, coord) VALUES \
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("death")] (name, byondkey, job, special, pod, tod, laname, lakey, gender, bruteloss, fireloss, brainloss, oxyloss, coord) VALUES \
(:name, :key, :job, :special, :pod, :time, :laname, :lakey, :gender, :bruteloss, :fireloss, :brainloss, :oxyloss, :coord)",
list(
"name" = sqlname,
@@ -98,7 +98,7 @@
log_game("SQL ERROR during death reporting. Failed to connect.")
else
var/datum/db_query/query = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("death")] (name, byondkey, job, special, pod, tod, laname, lakey, gender, bruteloss, fireloss, brainloss, oxyloss, coord) VALUES \
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("death")] (name, byondkey, job, special, pod, tod, laname, lakey, gender, bruteloss, fireloss, brainloss, oxyloss, coord) VALUES \
(:name, :key, :job, :special, :pod, :time, :laname, :lakey, :geender, :bruteloss, :fireloss, :brainloss, :oxyloss, :coord)",
list(
"name" = sqlname,
@@ -146,7 +146,7 @@
else
var/datum/db_query/max_query = SSdbcore.RunQuery(
- "SELECT MAX(roundid) AS max_round_id FROM [format_table_name("feedback")]",
+ "SELECT MAX(roundid) AS max_round_id FROM [DB_PREFIX_TABLE_NAME("feedback")]",
list(),
)
@@ -168,7 +168,7 @@
var/value = item.get_value()
var/datum/db_query/query = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("feedback")] (id, roundid, time, variable, value) VALUES (null, :rid, Now(), :var, :val)",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("feedback")] (id, roundid, time, variable, value) VALUES (null, :rid, Now(), :var, :val)",
list(
"rid" = newroundid,
"var" = sanitizeSQL(variable),
diff --git a/code/game/turfs/simulated/floor/floor.dm b/code/game/turfs/simulated/floor/floor.dm
index 793169f852d6..98b2f44c5577 100644
--- a/code/game/turfs/simulated/floor/floor.dm
+++ b/code/game/turfs/simulated/floor/floor.dm
@@ -73,7 +73,7 @@
CRASH("additional arg detected in /floor Initialize. turfs do not have init arguments as ChangeTurf does not accept them.")
var/datum/prototype/flooring/set_flooring_to
- if(initial_flooring && (set_flooring_to = RSflooring.fetch(initial_flooring)))
+ if(initial_flooring && (set_flooring_to = RSflooring.fetch_local_or_throw(initial_flooring)))
set_flooring(set_flooring_to, TRUE)
else
// todo: these are only here under else because set flooring will trigger it
diff --git a/code/game/turfs/simulated/floor_types/water.dm b/code/game/turfs/simulated/floor_types/water.dm
index 3e1fd52501f9..beaee03cc0e9 100644
--- a/code/game/turfs/simulated/floor_types/water.dm
+++ b/code/game/turfs/simulated/floor_types/water.dm
@@ -24,7 +24,7 @@
/turf/simulated/floor/water/Initialize(mapload)
. = ..()
- var/datum/prototype/flooring/F = RSflooring.fetch(/datum/prototype/flooring/water)
+ var/datum/prototype/flooring/F = RSflooring.fetch_local_or_throw(/datum/prototype/flooring/water)
footstep_sounds = F?.footstep_sounds
update_icon()
diff --git a/code/game/turfs/simulated/wall/materials.dm b/code/game/turfs/simulated/wall/materials.dm
index 507a17b1032d..1867c7a0725b 100644
--- a/code/game/turfs/simulated/wall/materials.dm
+++ b/code/game/turfs/simulated/wall/materials.dm
@@ -1,7 +1,7 @@
/turf/simulated/wall/proc/init_materials(datum/prototype/material/outer = material_outer, datum/prototype/material/reinforcing = material_reinf, datum/prototype/material/girder = material_girder)
- outer = RSmaterials.fetch(outer)
- reinforcing = RSmaterials.fetch(reinforcing)
- girder = RSmaterials.fetch(girder)
+ outer = RSmaterials.fetch_local_or_throw(outer)
+ reinforcing = RSmaterials.fetch_local_or_throw(reinforcing)
+ girder = RSmaterials.fetch_local_or_throw(girder)
if(!isnull(outer))
material_outer = outer
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index 6d439de3b5aa..93ceaf8ba586 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -682,3 +682,8 @@
thing.update_hiding_underfloor(
(thing.hides_underfloor != OBJ_UNDERFLOOR_NEVER) && we_should_cover,
)
+
+//* VV *//
+
+/turf/vv_delete()
+ ScrapeAway()
diff --git a/code/modules/admin/DB ban/functions.dm b/code/modules/admin/DB ban/functions.dm
index 0f195dd1d715..e332f48c88f2 100644
--- a/code/modules/admin/DB ban/functions.dm
+++ b/code/modules/admin/DB ban/functions.dm
@@ -72,7 +72,7 @@
computerid = ""
if(isnull(ip))
ip = ""
- var/sql = "INSERT INTO [format_table_name("ban")] \
+ var/sql = "INSERT INTO [DB_PREFIX_TABLE_NAME("ban")] \
(`id`,`bantime`,`serverip`,`bantype`,`reason`,`job`,`duration`,`rounds`,`expiration_time`,`ckey`,`computerid`,`ip`,`a_ckey`,`a_computerid`,`a_ip`,`who`,`adminwho`,`edits`,`unbanned`,`unbanned_datetime`,`unbanned_ckey`,`unbanned_computerid`,`unbanned_ip`) \
VALUES (null, Now(), :serverip, :type, :reason, :job, :duration, :rounds, Now() + INTERVAL :duration MINUTE, :ckey, :cid, :ip, :a_ckey, :a_cid, :a_ip, :who, :adminwho, '', null, null, null, null, null)"
SSdbcore.RunQuery(
@@ -133,7 +133,7 @@
else
bantype_sql = "bantype = '[bantype_str]'"
- var/sql = "SELECT id FROM [format_table_name("ban")] WHERE ckey = :ckey AND [bantype_sql] AND (unbanned is null OR unbanned = false)"
+ var/sql = "SELECT id FROM [DB_PREFIX_TABLE_NAME("ban")] WHERE ckey = :ckey AND [bantype_sql] AND (unbanned is null OR unbanned = false)"
if(job)
sql += " AND job = :job"
@@ -183,7 +183,7 @@
return
var/datum/db_query/query = SSdbcore.RunQuery(
- "SELECT ckey, duration, reason FROM [format_table_name("ban")] WHERE id = :id",
+ "SELECT ckey, duration, reason FROM [DB_PREFIX_TABLE_NAME("ban")] WHERE id = :id",
list(
"id" = banid
)
@@ -215,7 +215,7 @@
return
SSdbcore.RunQuery(
- "UPDATE [format_table_name("ban")] SET reason = :reason, \
+ "UPDATE [DB_PREFIX_TABLE_NAME("ban")] SET reason = :reason, \
edits = CONCAT(edits, '- :ckey changed ban reason from \\\":oldreason\\\" to \\\":reason\\\" ') \
WHERE id = :id",
list(
@@ -233,7 +233,7 @@
to_chat(usr, "Cancelled")
return
SSdbcore.RunQuery(
- "UPDATE [format_table_name("ban")] SET duration = :duration, \
+ "UPDATE [DB_PREFIX_TABLE_NAME("ban")] SET duration = :duration, \
edits = CONCAT(edits, '- :ckey changed ban duration from :oldduration to :duration '), expiration_time = DATE_ADD(bantime, INTERVAL :duration MINUTE) \
WHERE id = :id",
list(
@@ -264,7 +264,7 @@
var/pckey
var/datum/db_query/query = SSdbcore.RunQuery(
- "SELECT ckey FROM [format_table_name("ban")] WHERE id = :id",
+ "SELECT ckey FROM [DB_PREFIX_TABLE_NAME("ban")] WHERE id = :id",
list(
"id" = id
)
@@ -292,7 +292,7 @@
message_admins("[key_name_admin(usr)] has lifted [pckey]'s ban.",1)
SSdbcore.RunQuery(
- "UPDATE [format_table_name("ban")] SET unbanned = 1, unbanned_datetime = Now(), unbanned_ckey = :ckey, unbanned_computerid = :cid, unbanned_ip = :ip WHERE id = :id",
+ "UPDATE [DB_PREFIX_TABLE_NAME("ban")] SET unbanned = 1, unbanned_datetime = Now(), unbanned_ckey = :ckey, unbanned_computerid = :cid, unbanned_ip = :ip WHERE id = :id",
list(
"ckey" = unban_ckey,
"cid" = unban_computerid,
@@ -458,7 +458,7 @@
var/datum/db_query/select_query = SSdbcore.RunQuery(
"SELECT id, bantime, bantype, reason, job, duration, expiration_time, ckey, a_ckey, unbanned, unbanned_ckey, unbanned_datetime, edits, ip, computerid \
- FROM [format_table_name("ban")] \
+ FROM [DB_PREFIX_TABLE_NAME("ban")] \
WHERE 1 [playersearch] [adminsearch] [ipsearch] [cidsearch] [bantypesearch] ORDER BY bantime DESC LIMIT 100",
search_params
)
diff --git a/code/modules/admin/IsBanned.dm b/code/modules/admin/IsBanned.dm
index 2eb8325ac572..42bfc1dc3650 100644
--- a/code/modules/admin/IsBanned.dm
+++ b/code/modules/admin/IsBanned.dm
@@ -88,7 +88,7 @@
cidquery = " OR computerid = ':cid' "
var/datum/db_query/query = SSdbcore.RunQuery(
- "SELECT ckey, ip, computerid, a_ckey, reason, expiration_time, duration, bantime, bantype FROM [format_table_name("ban")] WHERE (ckey = :ckey [ipquery] [cidquery]) AND (bantype = 'PERMABAN' OR (bantype = 'TEMPBAN' AND expiration_time > Now())) AND isnull(unbanned)",
+ "SELECT ckey, ip, computerid, a_ckey, reason, expiration_time, duration, bantime, bantype FROM [DB_PREFIX_TABLE_NAME("ban")] WHERE (ckey = :ckey [ipquery] [cidquery]) AND (bantype = 'PERMABAN' OR (bantype = 'TEMPBAN' AND expiration_time > Now())) AND isnull(unbanned)",
list(
"ckey" = ckeytext,
"ip" = address,
diff --git a/code/modules/admin/admin_ranks.dm b/code/modules/admin/admin_ranks.dm
index 29500fb71be7..58583b9bad28 100644
--- a/code/modules/admin/admin_ranks.dm
+++ b/code/modules/admin/admin_ranks.dm
@@ -113,7 +113,7 @@ var/list/admin_ranks = list() //list of all ranks with associated rights
return
var/datum/db_query/query = SSdbcore.RunQuery(
- "SELECT ckey, rank, level, flags FROM [format_table_name("admin")]",
+ "SELECT ckey, rank, level, flags FROM [DB_PREFIX_TABLE_NAME("admin")]",
list()
)
diff --git a/code/modules/admin/banjob.dm b/code/modules/admin/banjob.dm
index 147e5c3cd7cc..65148f569a98 100644
--- a/code/modules/admin/banjob.dm
+++ b/code/modules/admin/banjob.dm
@@ -74,7 +74,7 @@ DEBUG
//Job permabans
var/datum/db_query/query = SSdbcore.RunQuery(
- "SELECT ckey, job FROM [format_table_name("ban")] WHERE bantype = 'JOB_PERMABAN' AND isnull(unbanned)",
+ "SELECT ckey, job FROM [DB_PREFIX_TABLE_NAME("ban")] WHERE bantype = 'JOB_PERMABAN' AND isnull(unbanned)",
list()
)
@@ -86,7 +86,7 @@ DEBUG
//Job tempbans
var/datum/db_query/query1 = SSdbcore.RunQuery(
- "SELECT ckey, job FROM [format_table_name("ban")] WHERE bantype = 'JOB_TEMPBAN' AND isnull(unbanned) AND expiration_time > Now()",
+ "SELECT ckey, job FROM [DB_PREFIX_TABLE_NAME("ban")] WHERE bantype = 'JOB_TEMPBAN' AND isnull(unbanned) AND expiration_time > Now()",
list()
)
diff --git a/code/modules/admin/permissionverbs/permissionedit.dm b/code/modules/admin/permissionverbs/permissionedit.dm
index fad215244819..d3a701a40555 100644
--- a/code/modules/admin/permissionverbs/permissionedit.dm
+++ b/code/modules/admin/permissionverbs/permissionedit.dm
@@ -78,7 +78,7 @@
return
var/datum/db_query/select_query = SSdbcore.RunQuery(
- "SELECT id FROM [format_table_name("admin")] WHERE ckey = :ckey",
+ "SELECT id FROM [DB_PREFIX_TABLE_NAME("admin")] WHERE ckey = :ckey",
list(
"ckey" = adm_ckey
)
@@ -92,14 +92,14 @@
if(new_admin)
SSdbcore.RunQuery(
- "INSERT INTO [format_table_name("admin")] (id, ckey, rank, level, flags) VALUES (null, :ckey, :rank, -1, 0)",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("admin")] (id, ckey, rank, level, flags) VALUES (null, :ckey, :rank, -1, 0)",
list(
"ckey" = adm_ckey,
"rank" = new_rank
)
)
SSdbcore.RunQuery(
- "INSERT INTO [format_table_name("admin_log")] (id, datetime, adminckey, adminip, log) VALUES (NULL, NOW(), :ckey, :ip, :logstr)",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("admin_log")] (id, datetime, adminckey, adminip, log) VALUES (NULL, NOW(), :ckey, :ip, :logstr)",
list(
"ckey" = sanitizeSQL(usr.ckey),
"ip" = sanitizeSQL(usr.client.address),
@@ -110,14 +110,14 @@
else
if(!isnull(admin_id) && isnum(admin_id))
SSdbcore.RunQuery(
- "UPDATE [format_table_name("admin")] SET rank = :rank WHERE id = :id",
+ "UPDATE [DB_PREFIX_TABLE_NAME("admin")] SET rank = :rank WHERE id = :id",
list(
"rank" = new_rank,
"id" = admin_id
)
)
SSdbcore.RunQuery(
- "INSERT INTO [format_table_name("admin_log")] (id, datetime, adminckey, adminip, log) VALUES (NULL, Now(), :ckey, :addr, :log)",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("admin_log")] (id, datetime, adminckey, adminip, log) VALUES (NULL, Now(), :ckey, :addr, :log)",
list(
"ckey" = usr.ckey,
"addr" = usr.client.address,
@@ -155,7 +155,7 @@
return
var/datum/db_query/select_query = SSdbcore.RunQuery(
- "SELECT id, flags FROM [format_table_name("admin")] WHERE ckey = :ckey",
+ "SELECT id, flags FROM [DB_PREFIX_TABLE_NAME("admin")] WHERE ckey = :ckey",
list(
"ckey" = adm_ckey
)
@@ -172,14 +172,14 @@
if(admin_rights & new_permission) //This admin already has this permission, so we are removing it.
SSdbcore.RunQuery(
- "UPDATE [format_table_name("admin")] SET flags = :flags WHERE id = :id",
+ "UPDATE [DB_PREFIX_TABLE_NAME("admin")] SET flags = :flags WHERE id = :id",
list(
"flags" = admin_rights & ~new_permission,
"id" = admin_id
)
)
SSdbcore.RunQuery(
- "INSERT INTO [format_table_name("admin_log")] (id, datetime, adminckey, adminip, log) VALUES (NULL, Now(), :ckey, :addr, :log)",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("admin_log")] (id, datetime, adminckey, adminip, log) VALUES (NULL, Now(), :ckey, :addr, :log)",
list(
"ckey" = usr.ckey,
"addr" = usr.client.address,
@@ -189,14 +189,14 @@
to_chat(usr, "Permission removed.")
else //This admin doesn't have this permission, so we are adding it.
SSdbcore.RunQuery(
- "UPDATE [format_table_name("admin")] SET flags = :flags WHERE id = :id",
+ "UPDATE [DB_PREFIX_TABLE_NAME("admin")] SET flags = :flags WHERE id = :id",
list(
"flags" = admin_rights | new_permission,
"id" = admin_id
)
)
SSdbcore.RunQuery(
- "INSERT INTO [format_table_name("admin_log")] (id, datetime, adminckey, adminip, log) VALUES (NULL, Now(), :ckey, :addr, :log)",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("admin_log")] (id, datetime, adminckey, adminip, log) VALUES (NULL, Now(), :ckey, :addr, :log)",
list(
"ckey" = usr.ckey,
"addr" = usr.client.address,
diff --git a/code/modules/admin/verbs/check_customitem_activity.dm b/code/modules/admin/verbs/check_customitem_activity.dm
index 02e577d13e4d..6b329a5fadb0 100644
--- a/code/modules/admin/verbs/check_customitem_activity.dm
+++ b/code/modules/admin/verbs/check_customitem_activity.dm
@@ -55,7 +55,7 @@ var/inactive_keys = "None "
var/list/inactive_ckeys = list()
if(ckeys_with_customitems.len)
var/datum/db_query/query_inactive = SSdbcore.RunQuery(
- "SELECT ckey, lastseen FROM [format_table_name("player_lookup")] WHERE datediff(Now(), lastseen) > 60",
+ "SELECT ckey, lastseen FROM [DB_PREFIX_TABLE_NAME("player_lookup")] WHERE datediff(Now(), lastseen) > 60",
list()
)
while(query_inactive.NextRow())
@@ -69,7 +69,7 @@ var/inactive_keys = "None "
if(ckeys_with_customitems.len)
for(var/cur_ckey in ckeys_with_customitems)
var/datum/db_query/query_inactive = SSdbcore.RunQuery(
- "SELECT ckey FROM [format_table_name("player_lookup")] WHERE ckey = :ckey",
+ "SELECT ckey FROM [DB_PREFIX_TABLE_NAME("player_lookup")] WHERE ckey = :ckey",
list(
"ckey" = cur_ckey
)
diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm
index fa96e39a4c46..5b40ba3bc97f 100644
--- a/code/modules/admin/verbs/debug.dm
+++ b/code/modules/admin/verbs/debug.dm
@@ -687,16 +687,6 @@
message_admins(log)
log_admin(log)
-/client/proc/reload_configuration()
- set category = "Debug"
- set name = "Reload Configuration"
- set desc = "Force config reload to world default"
- if(!check_rights(R_DEBUG))
- return
- if(alert(usr, "Are you absolutely sure you want to reload the configuration from the default path on the disk, wiping any in-round modificatoins?", "Really reset?", "No", "Yes") == "Yes")
- config.admin_reload()
- load_configuration() //for legacy
-
/datum/admins/proc/quick_nif()
set category = "Fun"
set name = "Quick NIF"
diff --git a/code/modules/admin/verbs/debug/reload_configuration.dm b/code/modules/admin/verbs/debug/reload_configuration.dm
new file mode 100644
index 000000000000..fd80f16133f8
--- /dev/null
+++ b/code/modules/admin/verbs/debug/reload_configuration.dm
@@ -0,0 +1,12 @@
+
+/client/proc/reload_configuration()
+ set category = "Debug"
+ set name = "Reload Configuration"
+ set desc = "Force config reload to world default"
+ if(!check_rights(R_DEBUG))
+ return
+ if(alert(usr, "Are you absolutely sure you want to reload the configuration from the default path on the disk, wiping any in-round modificatoins?", "Really reset?", "No", "Yes") == "Yes")
+ log_and_message_admins("[key_name(usr)] reloaded server configuration.")
+ config.admin_reload()
+ Configuration.admin_reload()
+ load_configuration() //for legacy
diff --git a/code/modules/admin/view_variables/admin_delete.dm b/code/modules/admin/view_variables/admin_delete.dm
index 7157a1cce2bd..16c84012c99f 100644
--- a/code/modules/admin/view_variables/admin_delete.dm
+++ b/code/modules/admin/view_variables/admin_delete.dm
@@ -16,9 +16,9 @@
//SSblackbox.record_feedback("tally", "admin_verb", 1, "Delete") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
if(isturf(D))
var/turf/T = D
- T.ScrapeAway()
+ T.vv_delete()
else
vv_update_display(D, "deleted", VV_MSG_DELETED)
- qdel(D)
+ D.vv_delete()
if(!QDELETED(D))
vv_update_display(D, "deleted", "")
diff --git a/code/modules/artwork/items/poster.dm b/code/modules/artwork/items/poster.dm
index be689b6a4f99..d1be0da56d4d 100644
--- a/code/modules/artwork/items/poster.dm
+++ b/code/modules/artwork/items/poster.dm
@@ -26,7 +26,7 @@
poster_design_id = pick(RSposter_designs.fetch_by_tag_mutable(poster_random_tag))
if(poster_design_id != src.poster_design_id)
src.poster_design_id = poster_design_id
- set_poster_design(RSposter_designs.fetch(poster_design_id))
+ set_poster_design(RSposter_designs.fetch_local_or_throw(poster_design_id))
/obj/item/poster/proc/set_poster_design(datum/prototype/poster_design/design)
src.name = "rolled-up-poster - [design.name]"
diff --git a/code/modules/artwork/structures/poster.dm b/code/modules/artwork/structures/poster.dm
index 4b707581a185..0c2f3147a1e8 100644
--- a/code/modules/artwork/structures/poster.dm
+++ b/code/modules/artwork/structures/poster.dm
@@ -41,7 +41,7 @@
poster_design_id = pick(RSposter_designs.fetch_by_tag_mutable(poster_random_tag))
if(poster_design_id != src.poster_design_id)
src.poster_design_id = poster_design_id
- set_poster_design(RSposter_designs.fetch(poster_design_id))
+ set_poster_design(RSposter_designs.fetch_local_or_throw(poster_design_id))
/obj/structure/poster/proc/set_poster_design(datum/prototype/poster_design/design)
src.name = "rolled-up-poster - [design.name]"
diff --git a/code/modules/artwork/structures/sculpting_block.dm b/code/modules/artwork/structures/sculpting_block.dm
index d9e53ef0ced6..b8598d4cbbde 100644
--- a/code/modules/artwork/structures/sculpting_block.dm
+++ b/code/modules/artwork/structures/sculpting_block.dm
@@ -78,9 +78,15 @@
/// sculpting mask for our block
var/icon/sculpting_rolldown_mask
-/obj/structure/sculpting_block/Initialize(mapload, material)
+/obj/structure/sculpting_block/Initialize(mapload, datum/prototype/material/material_like)
// todo: materials system
- src.material = RSmaterials.fetch(material || src.material)
+ if(!isnull(material_like))
+ var/resolved_material = RSmaterials.fetch_or_defer(material_like)
+ switch(resolved_material)
+ if(REPOSITORY_FETCH_DEFER)
+ // todo: handle
+ else
+ src.material = resolved_material || RSmaterials.fetch_local_or_throw(/datum/prototype/material/steel)
// todo: if it autoinit'd, don't do this
reset_sculpting()
return ..()
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index bb1edf8d647a..8d20b0cbcad9 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -467,7 +467,7 @@
var/sql_system_ckey = sanitizeSQL(system_ckey)
var/sql_ckey = sanitizeSQL(ckey)
//check to see if we noted them in the last day.
- var/datum/DBQuery/query_get_notes = SSdbcore.NewQuery("SELECT id FROM [format_table_name("messages")] WHERE type = 'note' AND targetckey = '[sql_ckey]' AND adminckey = '[sql_system_ckey]' AND timestamp + INTERVAL 1 DAY < NOW() AND deleted = 0 AND expire_timestamp > NOW()")
+ var/datum/DBQuery/query_get_notes = SSdbcore.NewQuery("SELECT id FROM [DB_PREFIX_TABLE_NAME("messages")] WHERE type = 'note' AND targetckey = '[sql_ckey]' AND adminckey = '[sql_system_ckey]' AND timestamp + INTERVAL 1 DAY < NOW() AND deleted = 0 AND expire_timestamp > NOW()")
if(!query_get_notes.Execute())
qdel(query_get_notes)
return
@@ -476,7 +476,7 @@
return
qdel(query_get_notes)
//regardless of above, make sure their last note is not from us, as no point in repeating the same note over and over.
- query_get_notes = SSdbcore.NewQuery("SELECT adminckey FROM [format_table_name("messages")] WHERE targetckey = '[sql_ckey]' AND deleted = 0 AND expire_timestamp > NOW() ORDER BY timestamp DESC LIMIT 1")
+ query_get_notes = SSdbcore.NewQuery("SELECT adminckey FROM [DB_PREFIX_TABLE_NAME("messages")] WHERE targetckey = '[sql_ckey]' AND deleted = 0 AND expire_timestamp > NOW() ORDER BY timestamp DESC LIMIT 1")
if(!query_get_notes.Execute())
qdel(query_get_notes)
return
diff --git a/code/modules/client/connection.dm b/code/modules/client/connection.dm
index 5e45f624d366..8f52aebf6ecb 100644
--- a/code/modules/client/connection.dm
+++ b/code/modules/client/connection.dm
@@ -7,7 +7,7 @@
return
var/datum/db_query/lookup = SSdbcore.NewQuery(
- "SELECT id FROM [format_table_name("player_lookup")] WHERE ckey = :ckey",
+ "SELECT id FROM [DB_PREFIX_TABLE_NAME("player_lookup")] WHERE ckey = :ckey",
list(
"ckey" = ckey,
)
@@ -20,7 +20,7 @@
if(sql_id)
var/datum/db_query/update = SSdbcore.NewQuery(
- "UPDATE [format_table_name("player_lookup")] SET lastseen = Now(), ip = :ip, computerid = :computerid, lastadminrank = :lastadminrank WHERE id = :id",
+ "UPDATE [DB_PREFIX_TABLE_NAME("player_lookup")] SET lastseen = Now(), ip = :ip, computerid = :computerid, lastadminrank = :lastadminrank WHERE id = :id",
list(
"ip" = address,
"computerid" = computer_id,
@@ -33,7 +33,7 @@
else
//New player!! Need to insert all the stuff
var/datum/db_query/insert = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("player_lookup")] (id, ckey, firstseen, lastseen, ip, computerid, lastadminrank) VALUES (null, :ckey, Now(), Now(), :ip, :cid, :rank)",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("player_lookup")] (id, ckey, firstseen, lastseen, ip, computerid, lastadminrank) VALUES (null, :ckey, Now(), Now(), :ip, :cid, :rank)",
list(
"ckey" = ckey,
"ip" = address,
@@ -55,7 +55,7 @@
return
SSdbcore.RunQuery(
- "INSERT INTO [format_table_name("connection_log")] (id, datetime, serverip, ckey, ip, computerid) VALUES (null, Now(), :server, :ckey, :ip, :cid)",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("connection_log")] (id, datetime, serverip, ckey, ip, computerid) VALUES (null, Now(), :server, :ckey, :ip, :cid)",
list(
"server" = "[world.internet_address]:[world.port]",
"ckey" = ckey,
diff --git a/code/modules/client/data/client_data.dm b/code/modules/client/data/client_data.dm
index 8b74a28b368e..b1f70aec30eb 100644
--- a/code/modules/client/data/client_data.dm
+++ b/code/modules/client/data/client_data.dm
@@ -136,7 +136,7 @@ GLOBAL_LIST_EMPTY(client_data)
playtime_mutex = FALSE
return
var/datum/db_query/query = SSdbcore.NewQuery(
- "SELECT `roleid`, `minutes` FROM [format_table_name("playtime")] WHERE player = :player",
+ "SELECT `roleid`, `minutes` FROM [DB_PREFIX_TABLE_NAME("playtime")] WHERE player = :player",
list(
"player" = player_id,
)
diff --git a/code/modules/client/data/player_data.dm b/code/modules/client/data/player_data.dm
index dfbc4c6faa97..591ed3bb33b5 100644
--- a/code/modules/client/data/player_data.dm
+++ b/code/modules/client/data/player_data.dm
@@ -94,7 +94,7 @@ GLOBAL_LIST_EMPTY(player_data)
return
var/datum/db_query/lookup
lookup = SSdbcore.ExecuteQuery(
- "SELECT id, playerid, firstseen FROM [format_table_name("player_lookup")] WHERE ckey = :ckey",
+ "SELECT id, playerid, firstseen FROM [DB_PREFIX_TABLE_NAME("player_lookup")] WHERE ckey = :ckey",
list(
"ckey" = ckey
)
@@ -111,7 +111,7 @@ GLOBAL_LIST_EMPTY(player_data)
lookup_pid = text2num(lookup_pid)
qdel(lookup)
lookup = SSdbcore.ExecuteQuery(
- "SELECT id, flags, datediff(Now(), firstseen), firstseen, misc FROM [format_table_name("player")] WHERE id = :id",
+ "SELECT id, flags, datediff(Now(), firstseen), firstseen, misc FROM [DB_PREFIX_TABLE_NAME("player")] WHERE id = :id",
list(
"id" = lookup_pid
)
@@ -147,7 +147,7 @@ GLOBAL_LIST_EMPTY(player_data)
var/datum/db_query/insert
if(migrate_firstseen)
insert = SSdbcore.ExecuteQuery(
- "INSERT INTO [format_table_name("player")] (flags, firstseen, lastseen, misc) VALUES (:flags, :fs, Now(), :misc)",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("player")] (flags, firstseen, lastseen, misc) VALUES (:flags, :fs, Now(), :misc)",
list(
"flags" = player_flags,
"fs" = migrate_firstseen,
@@ -157,7 +157,7 @@ GLOBAL_LIST_EMPTY(player_data)
player_first_seen = migrate_firstseen
else
insert = SSdbcore.ExecuteQuery(
- "INSERT INTO [format_table_name("player")] (flags, firstseen, lastseen, misc) VALUES (:flags, Now(), Now(), :misc)",
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("player")] (flags, firstseen, lastseen, misc) VALUES (:flags, Now(), Now(), :misc)",
list(
"flags" = player_flags,
"misc" = safe_json_encode(player_misc),
@@ -173,7 +173,7 @@ GLOBAL_LIST_EMPTY(player_data)
qdel(insert)
// now update lookup
insert = SSdbcore.ExecuteQuery(
- "UPDATE [format_table_name("player_lookup")] SET playerid = :pid WHERE id = :id",
+ "UPDATE [DB_PREFIX_TABLE_NAME("player_lookup")] SET playerid = :pid WHERE id = :id",
list(
"id" = lookup_id,
"pid" = insert_id
@@ -211,7 +211,7 @@ GLOBAL_LIST_EMPTY(player_data)
/datum/player_data/proc/_save()
qdel(SSdbcore.ExecuteQuery(
- "UPDATE [format_table_name("player")] SET flags = :flags, misc = :misc WHERE id = :id",
+ "UPDATE [DB_PREFIX_TABLE_NAME("player")] SET flags = :flags, misc = :misc WHERE id = :id",
list(
"flags" = player_flags,
"id" = player_id,
@@ -232,7 +232,7 @@ GLOBAL_LIST_EMPTY(player_data)
if(!block_on_available())
return FALSE
qdel(SSdbcore.ExecuteQuery(
- "UPDATE [format_table_name("player")] SET lastseen = Now() WHERE id = :id",
+ "UPDATE [DB_PREFIX_TABLE_NAME("player")] SET lastseen = Now() WHERE id = :id",
list(
"id" = player_id,
)
diff --git a/code/modules/client/game_preferences/game_preferences.dm b/code/modules/client/game_preferences/game_preferences.dm
index c54d71592dc4..667a98c8d4ed 100644
--- a/code/modules/client/game_preferences/game_preferences.dm
+++ b/code/modules/client/game_preferences/game_preferences.dm
@@ -369,7 +369,7 @@
usr = null
var/datum/db_query/query = SSdbcore.NewQuery(
- "SELECT `toggles`, `entries`, `misc`, `keybinds`, `version` FROM [format_table_name("game_preferences")] \
+ "SELECT `toggles`, `entries`, `misc`, `keybinds`, `version` FROM [DB_PREFIX_TABLE_NAME("game_preferences")] \
WHERE `player` = :player",
list(
"player" = authoritative_player_id,
@@ -413,7 +413,7 @@
usr = null
var/datum/db_query/query = SSdbcore.NewQuery(
- "INSERT INTO [format_table_name("game_preferences")] \
+ "INSERT INTO [DB_PREFIX_TABLE_NAME("game_preferences")] \
(`player`, `toggles`, `entries`, `misc`, `keybinds`, `version`, `modified`) VALUES \
(:player, :toggles, :entries, :misc, :keybinds, :version, Now()) ON DUPLICATE KEY UPDATE \
`player` = VALUES(player), `toggles` = VALUES(toggles), `entries` = VALUES(entries), `misc` = VALUES(misc), \
diff --git a/code/modules/clothing/under/miscellaneous.dm b/code/modules/clothing/under/miscellaneous.dm
index af736577488a..88fc5e8f7790 100644
--- a/code/modules/clothing/under/miscellaneous.dm
+++ b/code/modules/clothing/under/miscellaneous.dm
@@ -2419,6 +2419,16 @@
worn_has_rolldown = UNIFORM_HAS_NO_ROLL
worn_has_rollsleeve = UNIFORM_HAS_NO_ROLL
+/obj/item/clothing/under/replika/sakr
+ name = "medical replikant bodysuit"
+ desc = "A skin-tight bodysuit designed for 2nd generation biosynthetics of the medical variety. Comes with default interfacing ports and a conspicuous lack of leg coverage."
+ description_fluff = "These purpose-made interfacing bodysuits are designed and produced by the Singheim Bureau of Biosynthetic Development for their long-running second generation of Biosynthetics, commonly known by the term Replikant. Although anyone could wear these, their overall cut and metallic ports along the spine make it rather uncomfortable to most."
+ icon = 'icons/clothing/uniform/misc/replika.dmi'
+ icon_state = "sakr"
+ worn_render_flags = WORN_RENDER_SLOT_ONE_FOR_ALL
+ worn_has_rolldown = UNIFORM_HAS_NO_ROLL
+ worn_has_rollsleeve = UNIFORM_HAS_NO_ROLL
+
/obj/item/clothing/under/replika/fklr
name = "command replikant bodysuit"
desc = "A skin-tight bodysuit designed for 2nd generation biosynthetics of the command variety. Comes with interfacing ports, an air of formality, and a conspicuous lack of leg coverage."
diff --git a/code/modules/integrated_electronics/core/integrated_circuit.dm b/code/modules/integrated_electronics/core/integrated_circuit.dm
index 4df26d733387..ab0df67f8899 100644
--- a/code/modules/integrated_electronics/core/integrated_circuit.dm
+++ b/code/modules/integrated_electronics/core/integrated_circuit.dm
@@ -339,10 +339,10 @@ a creative player the means to solve many problems. Circuits are held inside an
if(!check_power())
power_fail()
return FALSE
+ do_work(ord) //Moved it behind next use incase the cooldown is modified in do_work
next_use = world.time + cooldown_per_use
if(assembly)
assembly.ext_next_use = world.time + ext_cooldown
- do_work(ord)
return TRUE
/obj/item/integrated_circuit/proc/do_work(ord)
diff --git a/code/modules/integrated_electronics/subtypes/converters.dm b/code/modules/integrated_electronics/subtypes/converters.dm
index 3ed86bb8ddca..1225cb657346 100644
--- a/code/modules/integrated_electronics/subtypes/converters.dm
+++ b/code/modules/integrated_electronics/subtypes/converters.dm
@@ -458,7 +458,7 @@
/obj/item/integrated_circuit/converter/hsv2hex
name = "hsv to hexadecimal converter"
desc = "This circuit can convert a HSV (Hue, Saturation, and Value) color to a Hexadecimal RGB color."
- extended_desc = "The first pin controls tint (0-359), the second pin controls how intense the tint is (0-255), \
+ extended_desc = "The first pin controls tint (0-360), the second pin controls how intense the tint is (0-255), \
and the third controls how bright the tint is (0 for black, 127 for normal, 255 for white)."
icon_state = "hsv-hex"
inputs = list(
@@ -476,7 +476,7 @@
var/saturation = get_pin_data(IC_INPUT, 2)
var/value = get_pin_data(IC_INPUT, 3)
if(isnum(hue) && isnum(saturation) && isnum(value))
- result = HSVtoRGB(hsv(AngleToHue(hue),saturation,value))
+ result = hsv2rgb(list(hue, saturation, value))
set_pin_data(IC_OUTPUT, 1, result)
push_data()
diff --git a/code/modules/integrated_electronics/subtypes/input.dm b/code/modules/integrated_electronics/subtypes/input.dm
index 5679c18b6a82..2dd9e32b12b2 100644
--- a/code/modules/integrated_electronics/subtypes/input.dm
+++ b/code/modules/integrated_electronics/subtypes/input.dm
@@ -213,32 +213,93 @@
name = "integrated advanced medical analyzer"
desc = "A very small version of the medibot's medical analyzer. This allows the machine to know how healthy someone is. \
-
- This type is much more precise, allowing the machine to know much more about the target than a normal analyzer."
+ This type is much more precise, allowing the machine to know much more about the target than a normal analyzer. \
+ A better health scanner can be installed to expand functionality. T1 for brain damage, bleeding and infection. \
+ T2 For fractions and internal bleeding. T3 for non medical reagents."
icon_state = "medscan_adv"
complexity = 12
inputs = list("target" = IC_PINTYPE_REF)
outputs = list(
- "brain activity" = IC_PINTYPE_BOOLEAN,
- "is conscious" = IC_PINTYPE_BOOLEAN,
- "total health %" = IC_PINTYPE_NUMBER,
- "total missing health" = IC_PINTYPE_NUMBER,
- "brute damage" = IC_PINTYPE_NUMBER,
- "burn damage" = IC_PINTYPE_NUMBER,
- "tox damage" = IC_PINTYPE_NUMBER,
- "oxy damage" = IC_PINTYPE_NUMBER,
- "clone damage" = IC_PINTYPE_NUMBER,
- "blood loss" = IC_PINTYPE_NUMBER,
- "pain level" = IC_PINTYPE_NUMBER,
- "radiation" = IC_PINTYPE_NUMBER,
- "nutrition" = IC_PINTYPE_NUMBER,
- "list of reagents" = IC_PINTYPE_LIST,
- "quantity of reagents" = IC_PINTYPE_LIST
+ "brain activity" = IC_PINTYPE_BOOLEAN, //1
+ "is conscious" = IC_PINTYPE_BOOLEAN, //2
+ "total health %" = IC_PINTYPE_NUMBER, //3
+ "total missing health" = IC_PINTYPE_NUMBER, //4
+ "brute damage" = IC_PINTYPE_NUMBER, //5
+ "burn damage" = IC_PINTYPE_NUMBER, //6
+ "tox damage" = IC_PINTYPE_NUMBER, //7
+ "oxy damage" = IC_PINTYPE_NUMBER, //8
+ "clone damage" = IC_PINTYPE_NUMBER, //9
+ "blood loss" = IC_PINTYPE_NUMBER, //10
+ "pain level" = IC_PINTYPE_NUMBER, //11
+ "radiation" = IC_PINTYPE_NUMBER, //12
+ "nutrition" = IC_PINTYPE_NUMBER, //13
+ "list of reagents" = IC_PINTYPE_LIST, //14
+ "quantity of reagents" = IC_PINTYPE_LIST, //15
+ "health scan tier" = IC_PINTYPE_NUMBER, //16
+ "brain damage" = IC_PINTYPE_NUMBER, //17
+ "bleeding limbs" = IC_PINTYPE_LIST, //18
+ "infected limbs" = IC_PINTYPE_LIST, //19
+ "internal bleeding" = IC_PINTYPE_LIST, //20
+ "fractured limbs" = IC_PINTYPE_LIST //21
)
activators = list("scan" = IC_PINTYPE_PULSE_IN, "on scanned" = IC_PINTYPE_PULSE_OUT)
spawn_flags = IC_SPAWN_RESEARCH
origin_tech = list(TECH_ENGINEERING = 3, TECH_DATA = 3, TECH_BIO = 4)
power_draw_per_use = 80
+ can_be_asked_input = TRUE
+ var/obj/item/healthanalyzer/current_healthanalyzer
+ var/advscan = 0
+ //T1: brain damage, bleeding, infection. T2: show location of IB and fracture. T3: show non medical reagents
+
+/obj/item/integrated_circuit/input/adv_med_scanner/ask_for_input(mob/living/user, obj/item/I, a_intent)
+ if(!current_healthanalyzer)
+ if(!isobj(I))
+ return FALSE
+ attackby_react(I, user, a_intent)
+ else
+ attack_self(user)
+
+/obj/item/integrated_circuit/input/adv_med_scanner/attackby_react(var/obj/item/healthanalyzer/I, var/mob/living/user)
+ //Check if it truly is a health scanner
+ if(!(istype(I,/obj/item/healthanalyzer)))
+ to_chat(user,"The [I.name] doesn't seem to fit in here.")
+ return
+
+ //Check if there is no other health scanner already inside
+ if(current_healthanalyzer)
+ to_chat(user,"There is already a health scanner inside.")
+ return
+
+ if(!user.attempt_insert_item_for_installation(I, src))
+ return
+
+ //The health scanner is the one we just attached, its location is inside the circuit
+ current_healthanalyzer = I
+
+ to_chat(user,"You slot in the [I.name] inside the assembly.")
+
+ //Set the pin to a weak reference of the current beaker
+ set_pin_data(IC_OUTPUT, 16, I.advscan)
+ advscan = I.advscan
+ push_data()
+
+/obj/item/integrated_circuit/input/adv_med_scanner/attack_self(mob/user)
+ . = ..()
+ if(.)
+ return
+ //Check if no health scanner is attached
+ if(!current_healthanalyzer)
+ to_chat(user, "There is currently no health scanner attached.")
+ return
+
+ //Remove beaker and put in user's hands/location
+ to_chat(user, "You yank the [current_healthanalyzer] out of the slot.")
+ user.put_in_hands(current_healthanalyzer)
+ current_healthanalyzer = null
+ //Reset to 0
+ set_pin_data(IC_OUTPUT, 16, 0)
+ advscan = 0
+ push_data()
/obj/item/integrated_circuit/input/adv_med_scanner/do_work(ord)
if(ord == 1)
@@ -267,11 +328,38 @@
var/cont[0]
var/amt[0]
for(var/datum/reagent/RE in H.reagents.reagent_list)
- if(RE.scannable)
+ if(RE.scannable || advscan >= 3)
cont += RE.id
amt += round(H.reagents.get_reagent_amount(RE.id), 1)
set_pin_data(IC_OUTPUT, 14, cont)
set_pin_data(IC_OUTPUT, 15, amt)
+ var/frac[0]
+ var/ib[0]
+ var/bleed[0]
+ var/infec[0]
+ if(advscan >= 1)
+ set_pin_data(IC_OUTPUT, 17, H.getBrainLoss())
+ for(var/obj/item/organ/external/e in H.organs)
+ if(!e)
+ continue
+ // Bleeding
+ if(e.status & ORGAN_BLEEDING)
+ bleed += e.name
+ // Infections
+ if(e.has_infected_wound())
+ infec += e.name
+ if(advscan >= 2)
+ // Broken limbs
+ if(e.status & ORGAN_BROKEN)
+ frac += e.name
+ // IB
+ for(var/datum/wound/W as anything in e.wounds)
+ if(W.internal)
+ ib += e.name
+ set_pin_data(IC_OUTPUT, 18, bleed)
+ set_pin_data(IC_OUTPUT, 19, infec)
+ set_pin_data(IC_OUTPUT, 20, ib)
+ set_pin_data(IC_OUTPUT, 21, frac)
push_data()
activate_pin(2)
diff --git a/code/modules/integrated_electronics/subtypes/manipulation.dm b/code/modules/integrated_electronics/subtypes/manipulation.dm
index 1853251f7c19..acee9d66a4f8 100644
--- a/code/modules/integrated_electronics/subtypes/manipulation.dm
+++ b/code/modules/integrated_electronics/subtypes/manipulation.dm
@@ -635,12 +635,12 @@
name = "mining drill"
desc = "A mining drill that can drill through rocks."
extended_desc = "A mining drill to strike the earth. It takes some time to get the job done and \
- must remain stationary until complete."
+ must remain stationary until complete. By default an advanced mining drill is installed but better once can be attached."
category_text = "Manipulation"
- ext_cooldown = 1
+ ext_cooldown = 5 //Required to not make instant death circuits
complexity = 40
- cooldown_per_use = 3 SECONDS
- ext_cooldown = 6 SECONDS
+ cooldown_per_use = 1 //We have 'busy' as our safty net
+ can_be_asked_input = TRUE
inputs = list(
"target" = IC_PINTYPE_REF
)
@@ -653,6 +653,9 @@
spawn_flags = IC_SPAWN_RESEARCH
power_draw_per_use = 1000
+ var/obj/item/pickaxe/current_pickaxe
+ var/digspeed = 3 SECONDS
+
var/busy = FALSE
var/targetlock
var/usedx
@@ -662,15 +665,57 @@
var/drill_force = 15
var/turf/simulated/mineral
+/obj/item/integrated_circuit/mining/mining_drill/proc/ask_for_input(mob/living/user, obj/item/I, a_intent)
+ if(!current_pickaxe)
+ if(!isobj(I))
+ return FALSE
+ attackby_react(I, user, a_intent)
+ else
+ attack_self(user)
+
+/obj/item/integrated_circuit/mining/mining_drill/attackby_react(var/obj/item/pickaxe/I, var/mob/living/user)
+ //Check if it truly is a pickaxe
+ if(!(istype(I,/obj/item/pickaxe)))
+ to_chat(user,"The [I.name] doesn't seem to fit in here.")
+ return
+
+ //Check if there is no other pickaxe already inside
+ if(current_pickaxe)
+ to_chat(user,"There is already a [current_pickaxe.name] inside.")
+ return
+
+ if(!user.attempt_insert_item_for_installation(I, src))
+ return
+
+ current_pickaxe = I
+ to_chat(user,"You attach the [I.name] inside the assembly.")
+ digspeed = I.digspeed
+
+/obj/item/integrated_circuit/mining/mining_drill/attack_self(mob/user)
+ . = ..()
+ if(.)
+ return
+ //Check if no drill is attached
+ if(!current_pickaxe)
+ to_chat(user, "There is currently no mining tool attached.")
+ return
+
+ //Remove beaker and put in user's hands/location
+ to_chat(user, "You yank the [current_pickaxe] out of the slot.")
+ user.put_in_hands(current_pickaxe)
+ current_pickaxe = null
+ //Reset to default
+ digspeed = 3 SECONDS
+
/obj/item/integrated_circuit/mining/mining_drill/do_work(ord)
if(ord == 1)
var/atom/target = get_pin_data(IC_INPUT, 1)
var/drill_delay = null
- if(!target || busy)
+ if(!target || busy || !target.Adjacent(assembly))
activate_pin(3)
return
src.assembly.visible_message(SPAN_DANGER("[assembly] starts to drill [target]!"), null, SPAN_WARNING("You hear a drill."))
- drill_delay = isturf(target)? 6 SECONDS : isliving(target) ? issimple(target) ? 2 SECONDS : 3 SECONDS : 4 SECONDS
+ drill_delay = isturf(target)? digspeed : isliving(target) ? issimple(target) ? 2 SECONDS : 3 SECONDS : 4 SECONDS
busy = TRUE
targetlock = target
usedx = assembly.loc.x
diff --git a/code/modules/integrated_electronics/subtypes/output.dm b/code/modules/integrated_electronics/subtypes/output.dm
index 489e89379262..d3116a6ab077 100644
--- a/code/modules/integrated_electronics/subtypes/output.dm
+++ b/code/modules/integrated_electronics/subtypes/output.dm
@@ -177,16 +177,18 @@
desc = "A miniature speaker is attached to this component."
icon_state = "speaker"
complexity = 8
- cooldown_per_use = 4 SECONDS
+ cooldown_per_use = 1 SECONDS //The cooldown is adjusted by sound length
+ var/cooldown_after_use = 1 SECONDS //Extra cooldown added to the sound length
inputs = list(
"sound ID" = IC_PINTYPE_STRING,
"volume" = IC_PINTYPE_NUMBER,
"frequency" = IC_PINTYPE_BOOLEAN
)
- outputs = list()
- activators = list("play sound" = IC_PINTYPE_PULSE_IN)
+ outputs = list("cooldown duration" = IC_PINTYPE_NUMBER)
+ activators = list("play sound" = IC_PINTYPE_PULSE_IN, "on played" = IC_PINTYPE_PULSE_OUT)
power_draw_per_use = 20
var/list/sounds = list()
+ var/list/durations = list() //Audacity works decently well to finding out deciseconds
/obj/item/integrated_circuit/output/sound/Initialize(mapload)
. = ..()
@@ -205,8 +207,13 @@
var/selected_sound = sounds[ID]
if(!selected_sound)
return
+ var/selected_cooldown = durations[ID]
vol = clamp( vol, 0, 100)
playsound(get_turf(src), selected_sound, vol, freq, -1)
+ cooldown_per_use = selected_cooldown + cooldown_after_use
+ set_pin_data(IC_OUTPUT, 1, cooldown_per_use)
+ push_data()
+ activate_pin(2)
/obj/item/integrated_circuit/output/sound/beeper
name = "beeper circuit"
@@ -223,6 +230,16 @@
"synth no" = 'sound/machines/synth_no.ogg',
"warning buzz" = 'sound/machines/warning-buzzer.ogg'
)
+ durations = list(
+ "beep" = 3,
+ "chime" = 4,
+ "buzz sigh" = 3,
+ "buzz twice" = 6,
+ "ping" = 5,
+ "synth yes" = 4,
+ "synth no" = 4,
+ "warning buzz" = 35
+ )
spawn_flags = IC_SPAWN_DEFAULT|IC_SPAWN_RESEARCH
/obj/item/integrated_circuit/output/sound/beepsky
@@ -238,6 +255,16 @@
"radio" = 'sound/voice/bradio.ogg',
"secure day" = 'sound/voice/bsecureday.ogg',
)
+ durations = list(
+ "creep" = 16,
+ "criminal" = 11,
+ "freeze" = 10,
+ "god" = 30,
+ "i am the law" = 22,
+ "insult" = 141, //Yeah it is that long
+ "radio" = 18,
+ "secure day" = 12,
+ )
spawn_flags = IC_SPAWN_RESEARCH
origin_tech = list(TECH_ENGINEERING = 2, TECH_DATA = 2, TECH_ILLEGAL = 1)
@@ -261,9 +288,86 @@
"apple" = 'sound/voice/medibot/mapple.ogg',
"no" = 'sound/voice/medibot/mno.ogg',
)
+ durations = list(
+ "surgeon" = 39,
+ "radar" = 16,
+ "feel better" = 14,
+ "patched up" = 11,
+ "injured" = 21,
+ "insult" = 155,
+ "coming" = 20,
+ "help" = 13,
+ "live" = 15,
+ "lost" = 45,
+ "flies" = 50,
+ "catch" = 38,
+ "delicious" = 7,
+ "apple" = 23,
+ "no" = 16,
+ )
spawn_flags = IC_SPAWN_RESEARCH
origin_tech = list(TECH_ENGINEERING = 2, TECH_DATA = 2, TECH_BIO = 1)
+/obj/item/integrated_circuit/output/sound/hev
+ name = "HEV sound circuit"
+ desc = "Takes a sound name as an input, and will play said sound when pulsed. This circuit is similar to those used in some old RIG suit"
+ cooldown_after_use = 5
+ sounds = list(
+ "bio_warn" = 'sound/voice/Hevsounds/biohazard_detected.wav',
+ "chem_warn" = 'sound/voice/Hevsounds/chemical_detected.wav',
+ "rad_warn" = 'sound/voice/Hevsounds/radiation_detected.wav',
+ "near_death" = 'sound/voice/Hevsounds/near_death.wav',
+ "seek_medic" = 'sound/voice/Hevsounds/seek_medic.wav',
+ "shock_damage" = 'sound/voice/Hevsounds/shock_damage.wav',
+ "blood_loss" = 'sound/voice/Hevsounds/blood_loss.wav',
+ "blood_plasma" = 'sound/voice/Hevsounds/blood_plasma.wav',
+ "blood_toxins" = 'sound/voice/Hevsounds/blood_toxins.wav',
+ "health_critical" = 'sound/voice/Hevsounds/health_critical.wav',
+ "health_dropping" = 'sound/voice/Hevsounds/health_dropping.wav',
+ "health_dropping2" = 'sound/voice/Hevsounds/health_dropping2.wav',
+ "minor_fracture" = 'sound/voice/Hevsounds/minor_fracture.wav',
+ "minor_lacerations" = 'sound/voice/Hevsounds/minor_lacerations.wav',
+ "major_fracture" = 'sound/voice/Hevsounds/major_fracture.wav',
+ "major_lacerations" = 'sound/voice/Hevsounds/major_lacerations.wav',
+ "wound_sterilized" = 'sound/voice/Hevsounds/wound_sterilized.wav',
+ "administering_medical" = 'sound/voice/Hevsounds/administering_medical.wav',
+ "adrenaline_shot" = 'sound/voice/Hevsounds/adrenaline_shot.wav',
+ "automedic_on" = 'sound/voice/Hevsounds/automedic_on.wav',
+ "antitoxin_shot" = 'sound/voice/Hevsounds/antitoxin_shot.wav',
+ "heat_damage" = 'sound/voice/Hevsounds/heat_damage.wav',
+ "morphine_shot" = 'sound/voice/Hevsounds/morphine_shot.wav',
+ "innsuficient_medical" = 'sound/voice/Hevsounds/innsuficient_medical.wav',
+ "internal_bleeding" = 'sound/voice/Hevsounds/internal_bleeding.wav'
+ )
+ durations = list(
+ "bio_warn" = 31,
+ "chem_warn" = 38,
+ "rad_warn" = 45,
+ "near_death" = 36,
+ "seek_medic" = 25,
+ "shock_damage" = 23,
+ "blood_loss" = 22,
+ "blood_plasma" = 26,
+ "blood_toxins" = 38,
+ "health_critical" = 32,
+ "health_dropping" = 35,
+ "health_dropping2" = 29,
+ "minor_fracture" = 24,
+ "minor_lacerations" = 29,
+ "major_fracture" = 24,
+ "major_lacerations" = 30,
+ "wound_sterilized" = 20,
+ "administering_medical" = 31,
+ "adrenaline_shot" = 20,
+ "automedic_on" = 35,
+ "antitoxin_shot" = 22,
+ "heat_damage" = 30,
+ "morphine_shot" = 20,
+ "innsuficient_medical" = 45,
+ "internal_bleeding" = 24
+ )
+ spawn_flags = IC_SPAWN_RESEARCH
+
/obj/item/integrated_circuit/output/video_camera
name = "video camera circuit"
desc = "This small camera allows a remote viewer to see what it sees."
diff --git a/code/modules/language/language.dm b/code/modules/language/language.dm
index be809af403a0..ecb4d2eada44 100644
--- a/code/modules/language/language.dm
+++ b/code/modules/language/language.dm
@@ -232,7 +232,7 @@
// Language handling.
/mob/proc/add_language(var/language)
- var/datum/prototype/language/new_language = RSlanguages.fetch(language) || RSlanguages.legacy_resolve_language_name(language)
+ var/datum/prototype/language/new_language = RSlanguages.fetch_or_defer(language) || RSlanguages.legacy_resolve_language_name(language)
if(!istype(new_language) || (new_language in languages))
return 0
@@ -241,12 +241,12 @@
return 1
/mob/proc/remove_language(var/rem_language)
- var/datum/prototype/language/L = RSlanguages.fetch(rem_language)
+ var/datum/prototype/language/L = RSlanguages.fetch_or_defer(rem_language)
. = (L in languages)
languages.Remove(L)
/mob/living/remove_language(rem_language)
- var/datum/prototype/language/L = RSlanguages.fetch(rem_language)
+ var/datum/prototype/language/L = RSlanguages.fetch_or_defer(rem_language)
if(default_language == L)
default_language = null
return ..()
diff --git a/code/modules/library/lib_machines.dm b/code/modules/library/lib_machines.dm
index 3980cecb08d0..c9b60e2e4b91 100644
--- a/code/modules/library/lib_machines.dm
+++ b/code/modules/library/lib_machines.dm
@@ -49,7 +49,7 @@
AUTHOR
TITLE
CATEGORY
SS13BN
"}
var/datum/db_query/query = SSdbcore.RunQuery(
- "SELECT author, title, category, id FROM [format_table_name("library")] WHERE author LIKE '%:author%' AND title LIKE '%:title%'[category == "Any"? "" : " AND category = :category"]",
+ "SELECT author, title, category, id FROM [DB_PREFIX_TABLE_NAME("library")] WHERE author LIKE '%:author%' AND title LIKE '%:title%'[category == "Any"? "" : " AND category = :category"]",
category == "Any"? list("author" = author, "title" = title) : list("author" = author, "title" = title, "category" = category)
)
@@ -229,7 +229,7 @@
"
- var/counter = 1
- while(S.fields["com_[counter]"])
- P.info += "[S.fields["com_[counter]"]] "
- counter++
- P.info += ""
- P.name = "Security Record ([G.fields["name"]])"
- virgin = 0 //tabbing here is correct- it's possible for people to try and use it
- //before the records have been generated, so we do this inside the loop.
+ if(!virgin)
+ return
+ for(var/datum/data/record/G in data_core.general)
+ var/datum/data/record/S
+ for(var/datum/data/record/R in data_core.security)
+ if((R.fields["name"] == G.fields["name"] || R.fields["id"] == G.fields["id"]))
+ S = R
+ break
+ var/obj/item/paper/P = new /obj/item/paper(src)
+ P.info = "
"
+ var/counter = 1
+ while(S.fields["com_[counter]"])
+ P.info += "[S.fields["com_[counter]"]] "
+ counter++
+ P.info += ""
+ P.name = "Security Record ([G.fields["name"]])"
+ P.update_appearance()
+ virgin = FALSE //tabbing here is correct- it's possible for people to try and use it
+ //before the records have been generated, so we do this inside the loop.
/obj/structure/filingcabinet/security/attack_hand(mob/user, datum/event_args/actor/clickchain/e_args)
populate()
- ..()
+ return ..()
/obj/structure/filingcabinet/security/attack_tk()
populate()
- ..()
+ return ..()
/*
* Medical Record Cabinets
*/
/obj/structure/filingcabinet/medical
- var/virgin = 1
+ var/virgin = TRUE
/obj/structure/filingcabinet/medical/proc/populate()
- if(virgin)
- for(var/datum/data/record/G in data_core.general)
- var/datum/data/record/M
- for(var/datum/data/record/R in data_core.medical)
- if((R.fields["name"] == G.fields["name"] || R.fields["id"] == G.fields["id"]))
- M = R
- break
- if(M)
- var/obj/item/paper/P = new /obj/item/paper(src)
- P.info = "