diff --git a/.dockerignore b/.dockerignore
index 9c6ac6b4aa..a4b8a25cdc 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -22,7 +22,6 @@ tgstation.int
tgstation.rsc
tgstation.lk
tgstation.dyn.rsc
-libmariadb.dll
-rust_g.dll
-BSQL.dll
+*.dll
Dockerfile
+tools/bootstrap/.cache
diff --git a/.editorconfig b/.editorconfig
index c44572fbf9..d6adf33378 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -4,7 +4,7 @@ indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
-#end_of_line = lf
+# end_of_line = lf
[*.yml]
indent_style = space
@@ -12,3 +12,9 @@ indent_size = 2
[*.py]
indent_style = space
+
+[*.md]
+trim_trailing_whitespace = false
+
+[Dockerfile]
+indent_style = space
diff --git a/.gitattributes b/.gitattributes
index b23dfe6932..f46a38d1d1 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,8 +1,7 @@
* text=auto
## Enforce text mode and LF line breaks
-## porter note: not yet LFing dm.
-# *.bat text eol=lf
+*.bat text eol=lf
*.css text eol=lf
# *.dm text eol=lf
# *.dme text eol=lf
@@ -12,15 +11,15 @@
*.js text eol=lf
*.json text eol=lf
*.jsx text eol=lf
-# *.md text eol=lf
+*.md text eol=lf
*.py text eol=lf
*.scss text eol=lf
-# *.sh text eol=lf
-# *.sql text eol=lf
+*.sh text eol=lf
+*.sql text eol=lf
*.svg text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
-# *.txt text eol=lf
+*.txt text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
diff --git a/.github/workflows/ci_suite.yml b/.github/workflows/ci_suite.yml
index 76a1c26656..a6b91bd559 100644
--- a/.github/workflows/ci_suite.yml
+++ b/.github/workflows/ci_suite.yml
@@ -10,7 +10,7 @@ jobs:
run_linters:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
name: Run Linters
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Setup cache
@@ -21,18 +21,20 @@ jobs:
- name: Install Tools
run: |
pip3 install setuptools
- bash tools/ci/install_build_tools.sh
+ bash tools/ci/install_node.sh
bash tools/ci/install_spaceman_dmm.sh dreamchecker
- pip3 install -r tools/mapmerge2/requirements.txt
+ tools/bootstrap/python -c ''
- name: Run Linters
run: |
bash tools/ci/check_filedirs.sh tgstation.dme
bash tools/ci/check_changelogs.sh
find . -name "*.php" -print0 | xargs -0 -n1 php -l
find . -name "*.json" -not -path "*/node_modules/*" -print0 | xargs -0 python3 ./tools/json_verifier.py
- bash tools/ci/build_tgui.sh
+ tgui/bin/tgui --lint
+ tgui/bin/tgui --test
bash tools/ci/check_grep.sh
- python3 tools/mapmerge2/dmi.py --test
+ tools/bootstrap/python -m dmi.test
+ tools/bootstrap/python -m mapmerge2.dmm_test
~/dreamchecker > ${GITHUB_WORKSPACE}/output-annotations.txt 2>&1
- name: Annotate Lints
uses: yogstation13/DreamAnnotate@v1
@@ -43,7 +45,7 @@ jobs:
compile_all_maps:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
name: Compile Maps
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Setup cache
@@ -56,11 +58,13 @@ jobs:
bash tools/ci/install_byond.sh
source $HOME/BYOND/byond/bin/byondsetup
python3 tools/ci/template_dm_generator.py
+ tgui/bin/tgui --build
bash tools/ci/dm.sh -DCIBUILDING -DCITESTING -DALL_MAPS tgstation.dme
+
run_all_tests:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
name: Integration Tests
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
services:
mysql:
image: mysql:latest
@@ -87,14 +91,16 @@ jobs:
run: |
sudo dpkg --add-architecture i386
sudo apt update || true
- sudo apt install libssl1.1:i386
+ sudo apt install -o APT::Immediate-Configure=false libssl1.1:i386
bash tools/ci/install_rust_g.sh
- # - name: Compile and run tests
- # run: |
- # bash tools/ci/install_byond.sh
- # source $HOME/BYOND/byond/bin/byondsetup
- # bash tools/ci/dm.sh -DCIBUILDING tgstation.dme
- # bash tools/ci/run_server.sh
+ - name: Compile and run tests
+ run: |
+ bash tools/ci/install_byond.sh
+ source $HOME/BYOND/byond/bin/byondsetup
+ tgui/bin/tgui --build
+ bash tools/ci/dm.sh -DCIBUILDING tgstation.dme
+ # bash tools/ci/run_server.sh
+
test_windows:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
name: Windows Build
diff --git a/.github/workflows/compile_changelogs.yml b/.github/workflows/compile_changelogs.yml
index c8756f0d28..b0e1578815 100644
--- a/.github/workflows/compile_changelogs.yml
+++ b/.github/workflows/compile_changelogs.yml
@@ -7,7 +7,7 @@ on:
jobs:
compile:
name: "Compile changelogs"
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
steps:
- name: "Check for CHANGELOG_ENABLER secret and pass true to output if it exists to be checked by later steps"
id: value_holder
diff --git a/.github/workflows/docker_publish.yml b/.github/workflows/docker_publish.yml
index 7417a382c4..32d160b840 100644
--- a/.github/workflows/docker_publish.yml
+++ b/.github/workflows/docker_publish.yml
@@ -7,7 +7,8 @@ on:
jobs:
publish:
- runs-on: ubuntu-latest
+ if: "!contains(github.event.head_commit.message, '[ci skip]')"
+ runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
diff --git a/.github/workflows/generate_documentation.yml b/.github/workflows/generate_documentation.yml
index d0d61be073..c1b7a2d5e1 100644
--- a/.github/workflows/generate_documentation.yml
+++ b/.github/workflows/generate_documentation.yml
@@ -6,7 +6,7 @@ on:
jobs:
generate_documentation:
if: "!contains(github.event.head_commit.message, '[ci skip]')"
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Setup cache
diff --git a/.github/workflows/round_id_linker.yml b/.github/workflows/round_id_linker.yml
index 37998a93eb..2b028b0863 100644
--- a/.github/workflows/round_id_linker.yml
+++ b/.github/workflows/round_id_linker.yml
@@ -5,7 +5,7 @@ on:
jobs:
link_rounds:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
steps:
- uses: Cidatel-Station-13/round_linker@master #notice: fork the round linkies from tg!!
with:
diff --git a/.github/workflows/update_tgs_dmapi.yml b/.github/workflows/update_tgs_dmapi.yml
index c7bb5c970c..19a72d6702 100644
--- a/.github/workflows/update_tgs_dmapi.yml
+++ b/.github/workflows/update_tgs_dmapi.yml
@@ -7,7 +7,7 @@ on:
jobs:
update-dmapi:
- runs-on: ubuntu-latest
+ runs-on: ubuntu-20.04
name: Update the TGS DMAPI
steps:
- name: Clone
diff --git a/.gitignore b/.gitignore
index e885761104..dd20fecd22 100644
--- a/.gitignore
+++ b/.gitignore
@@ -231,3 +231,6 @@ tools/LinuxOneShot/Database
tools/LinuxOneShot/TGS_Config
tools/LinuxOneShot/TGS_Instances
tools/LinuxOneShot/TGS_Logs
+
+# Common build tooling
+!/tools/build
diff --git a/.tgs4.yml b/.tgs4.yml
new file mode 100644
index 0000000000..932a3a6672
--- /dev/null
+++ b/.tgs4.yml
@@ -0,0 +1,8 @@
+static_files:
+ - name: config
+ populate: true
+ - name: data
+linux_scripts:
+ PreCompile.sh: tools/tgs4_scripts/PreCompile.sh
+windows_scripts:
+ PreCompile.bat: tools/tgs4_scripts/PreCompile.bat
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index bf0d9d2fb9..d384c4535b 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -5,6 +5,7 @@
"EditorConfig.EditorConfig",
"arcanis.vscode-zipfs",
"dbaeumer.vscode-eslint",
- "kevinkyang.auto-comment-blocks"
+ "stylemistake.auto-comment-blocks",
+ "Donkie.vscode-tgstation-test-adapter"
]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index f95b8f7cc6..f290e9f369 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,8 +1,10 @@
{
- "eslint.nodePath": "tgui/.yarn/sdks",
+ "eslint.nodePath": "./tgui/.yarn/sdks",
"eslint.workingDirectories": [
"./tgui"
],
+ "typescript.tsdk": "./tgui/.yarn/sdks/typescript/lib",
+ "typescript.enablePromptUseWorkspaceTsdk": true,
"search.exclude": {
"tgui/.yarn": true,
"tgui/.pnp.*": true
@@ -14,5 +16,6 @@
}
],
"files.eol": "\n",
- "gitlens.advanced.blame.customArguments": ["-w"]
+ "gitlens.advanced.blame.customArguments": ["-w"],
+ "tgstationTestExplorer.project.resultsType": "json"
}
diff --git a/BSQL.dll b/BSQL.dll
deleted file mode 100644
index 861492c8b4..0000000000
Binary files a/BSQL.dll and /dev/null differ
diff --git a/Build.bat b/Build.bat
new file mode 100644
index 0000000000..dd3a6fd9d9
--- /dev/null
+++ b/Build.bat
@@ -0,0 +1,2 @@
+@call tools\build\build
+@pause
diff --git a/Dockerfile b/Dockerfile
index cca7e43a54..0ae4f82be8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,62 +1,85 @@
-FROM tgstation/byond:513.1533 as base
+# base = ubuntu + full apt update
+FROM ubuntu:xenial AS base
-FROM base as rust_g
-
-RUN apt-get update \
+RUN dpkg --add-architecture i386 \
+ && apt-get update \
+ && apt-get upgrade -y \
+ && apt-get dist-upgrade -y \
&& apt-get install -y --no-install-recommends \
- git \
- ca-certificates
+ ca-certificates
+# byond = base + byond installed globally
+FROM base AS byond
+WORKDIR /byond
+
+RUN apt-get install -y --no-install-recommends \
+ curl \
+ unzip \
+ make \
+ libstdc++6:i386
+
+COPY dependencies.sh .
+
+RUN . ./dependencies.sh \
+ && curl "http://www.byond.com/download/build/${BYOND_MAJOR}/${BYOND_MAJOR}.${BYOND_MINOR}_byond_linux.zip" -o byond.zip \
+ && unzip byond.zip \
+ && cd byond \
+ && sed -i 's|install:|&\n\tmkdir -p $(MAN_DIR)/man6|' Makefile \
+ && make install \
+ && chmod 644 /usr/local/byond/man/man6/* \
+ && apt-get purge -y --auto-remove curl unzip make \
+ && cd .. \
+ && rm -rf byond byond.zip
+
+# build = byond + tgstation compiled and deployed to /deploy
+FROM byond AS build
+WORKDIR /tgstation
+
+RUN apt-get install -y --no-install-recommends \
+ curl
+
+COPY . .
+
+RUN env TG_BOOTSTRAP_NODE_LINUX=1 tools/build/build \
+ && tools/deploy.sh /deploy
+
+# rust = base + rustc and i686 target
+FROM base AS rust
+RUN apt-get install -y --no-install-recommends \
+ curl && \
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal \
+ && ~/.cargo/bin/rustup target add i686-unknown-linux-gnu
+
+# rust_g = base + rust_g compiled to /rust_g
+FROM rust AS rust_g
WORKDIR /rust_g
RUN apt-get install -y --no-install-recommends \
- libssl-dev \
- pkg-config \
- curl \
- gcc-multilib \
- && curl https://sh.rustup.rs -sSf | sh -s -- -y --default-host i686-unknown-linux-gnu \
+ pkg-config:i386 \
+ libssl-dev:i386 \
+ gcc-multilib \
+ git \
&& git init \
&& git remote add origin https://github.com/tgstation/rust-g
COPY dependencies.sh .
-RUN /bin/bash -c "source dependencies.sh \
- && git fetch --depth 1 origin \$RUST_G_VERSION" \
+RUN . ./dependencies.sh \
+ && git fetch --depth 1 origin "${RUST_G_VERSION}" \
&& git checkout FETCH_HEAD \
- && ~/.cargo/bin/cargo build --release
-
-FROM base as dm_base
+ && env PKG_CONFIG_ALLOW_CROSS=1 ~/.cargo/bin/cargo build --release --target i686-unknown-linux-gnu
+# final = byond + runtime deps + rust_g + build
+FROM byond
WORKDIR /tgstation
-FROM dm_base as build
+RUN apt-get install -y --no-install-recommends \
+ libssl1.0.0:i386 \
+ zlib1g:i386
-COPY . .
-
-RUN DreamMaker -max_errors 0 tgstation.dme \
- && tools/deploy.sh /deploy \
- && rm /deploy/*.dll
-
-FROM dm_base
-
-EXPOSE 1337
-
-RUN apt-get update \
- && apt-get install -y --no-install-recommends software-properties-common \
- && add-apt-repository ppa:ubuntu-toolchain-r/test \
- && apt-get update \
- && apt-get upgrade -y \
- && apt-get dist-upgrade -y \
- && apt-get install -y --no-install-recommends \
- libmariadb2 \
- mariadb-client \
- libssl1.0.0 \
- && rm -rf /var/lib/apt/lists/* \
- && mkdir -p /root/.byond/bin
-
-COPY --from=rust_g /rust_g/target/release/librust_g.so /root/.byond/bin/rust_g
COPY --from=build /deploy ./
+COPY --from=rust_g /rust_g/target/i686-unknown-linux-gnu/release/librust_g.so ./librust_g.so
VOLUME [ "/tgstation/config", "/tgstation/data" ]
-
ENTRYPOINT [ "DreamDaemon", "tgstation.dmb", "-port", "1337", "-trusted", "-close", "-verbose" ]
+EXPOSE 1337
diff --git a/SQL/ban_conversion_2018-10-28.py b/SQL/ban_conversion_2018-10-28.py
new file mode 100644
index 0000000000..26d928bfd1
--- /dev/null
+++ b/SQL/ban_conversion_2018-10-28.py
@@ -0,0 +1,174 @@
+#Python 3+ Script for converting ban table format as of 2018-10-28 made by Jordie0608
+#
+#Before starting ensure you have installed the mysqlclient package https://github.com/PyMySQL/mysqlclient-python
+#It can be downloaded from command line with pip:
+#pip install mysqlclient
+#
+#You will also have to create a new ban table for inserting converted data to per the schema:
+#CREATE TABLE `ban` (
+# `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+# `bantime` DATETIME NOT NULL,
+# `server_ip` INT(10) UNSIGNED NOT NULL,
+# `server_port` SMALLINT(5) UNSIGNED NOT NULL,
+# `round_id` INT(11) UNSIGNED NOT NULL,
+# `role` VARCHAR(32) NULL DEFAULT NULL,
+# `expiration_time` DATETIME NULL DEFAULT NULL,
+# `applies_to_admins` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0',
+# `reason` VARCHAR(2048) NOT NULL,
+# `ckey` VARCHAR(32) NULL DEFAULT NULL,
+# `ip` INT(10) UNSIGNED NULL DEFAULT NULL,
+# `computerid` VARCHAR(32) NULL DEFAULT NULL,
+# `a_ckey` VARCHAR(32) NOT NULL,
+# `a_ip` INT(10) UNSIGNED NOT NULL,
+# `a_computerid` VARCHAR(32) NOT NULL,
+# `who` VARCHAR(2048) NOT NULL,
+# `adminwho` VARCHAR(2048) NOT NULL,
+# `edits` TEXT NULL DEFAULT NULL,
+# `unbanned_datetime` DATETIME NULL DEFAULT NULL,
+# `unbanned_ckey` VARCHAR(32) NULL DEFAULT NULL,
+# `unbanned_ip` INT(10) UNSIGNED NULL DEFAULT NULL,
+# `unbanned_computerid` VARCHAR(32) NULL DEFAULT NULL,
+# `unbanned_round_id` INT(11) UNSIGNED NULL DEFAULT NULL,
+# PRIMARY KEY (`id`),
+# KEY `idx_ban_isbanned` (`ckey`,`role`,`unbanned_datetime`,`expiration_time`),
+# KEY `idx_ban_isbanned_details` (`ckey`,`ip`,`computerid`,`role`,`unbanned_datetime`,`expiration_time`),
+# KEY `idx_ban_count` (`bantime`,`a_ckey`,`applies_to_admins`,`unbanned_datetime`,`expiration_time`)
+#) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+#This is to prevent the destruction of existing data and allow rollbacks to be performed in the event of an error during conversion
+#Once conversion is complete remember to rename the old and new ban tables; it's up to you if you want to keep the old table
+#
+#To view the parameters for this script, execute it with the argument --help
+#All the positional arguments are required, remember to include prefixes in your table names if you use them
+#An example of the command used to execute this script from powershell:
+#python ban_conversion_2018-10-28.py "localhost" "root" "password" "feedback" "SS13_ban" "SS13_ban_new"
+#I found that this script would complete conversion of 35000 rows in approximately 20 seconds, results will depend on the size of your ban table and computer used
+#
+#The script has been tested to complete with tgstation's ban table as of 2018-09-02 02:19:56
+#In the event of an error the new ban table is automatically truncated
+#The source table is never modified so you don't have to worry about losing any data due to errors
+#Some additional error correction is performed to fix problems specific to legacy and invalid data in tgstation's ban table, these operations are tagged with a 'TG:' comment
+#Even if you don't have any of these specific problems in your ban table the operations won't have matter as they have an insignificant effect on runtime
+#
+#While this script is safe to run with your game server(s) active, any bans created after the script has started won't be converted
+#You will also have to ensure that the code and table names are updated between rounds as neither will be compatible
+
+import MySQLdb
+import argparse
+import sys
+from datetime import datetime
+
+def parse_role(bantype, job):
+ if bantype in ("PERMABAN", "TEMPBAN", "ADMIN_PERMABAN", "ADMIN_TEMPBAN"):
+ role = "Server"
+ else:
+ #TG: Some legacy jobbans are missing the last character from their job string.
+ job_name_fixes = {"A":"AI", "Captai":"Captain", "Cargo Technicia":"Cargo Technician", "Chaplai":"Chaplain", "Che":"Chef", "Chemis":"Chemist", "Chief Enginee":"Chief Engineer", "Chief Medical Office":"Chief Medical Officer", "Cybor":"Cyborg", "Detectiv":"Detective", "Head of Personne":"Head of Personnel", "Head of Securit":"Head of Security", "Mim":"Mime", "pA":"pAI", "Quartermaste":"Quartermaster", "Research Directo":"Research Director", "Scientis":"Scientist", "Security Office":"Security Officer", "Station Enginee":"Station Engineer", "Syndicat":"Syndicate", "Warde":"Warden"}
+ keep_job_names = ("AI", "Head of Personnel", "Head of Security", "OOC", "pAI")
+ if job in job_name_fixes:
+ role = job_name_fixes[job]
+ #Some job names we want to keep the same as .title() would return a different string.
+ elif job in keep_job_names:
+ role = job
+ #And then there's this asshole.
+ elif job == "servant of Ratvar":
+ role = "Servant of Ratvar"
+ else:
+ role = job.title()
+ return role
+
+def parse_admin(bantype):
+ if bantype in ("ADMIN_PERMABAN", "ADMIN_TEMPBAN"):
+ return 1
+ else:
+ return 0
+
+def parse_datetime(bantype, expiration_time):
+ if bantype in ("PERMABAN", "JOB_PERMABAN", "ADMIN_PERMABAN"):
+ expiration_time = None
+ #TG: two bans with an invalid expiration_time due to admins setting the duration to approx. 19 billion years, I'm going to count them as permabans.
+ elif expiration_time == "0000-00-00 00:00:00":
+ expiration_time = None
+ elif not expiration_time:
+ expiration_time = None
+ return expiration_time
+
+def parse_not_null(field):
+ if not field:
+ field = 0
+ return field
+
+def parse_for_empty(field):
+ if not field:
+ field = None
+ #TG: Several bans from 2012, probably from clients disconnecting while a ban was being made.
+ elif field == "BLANK CKEY ERROR":
+ field = None
+ return field
+
+if sys.version_info[0] < 3:
+ raise Exception("Python must be at least version 3 for this script.")
+current_round = 0
+parser = argparse.ArgumentParser()
+parser.add_argument("address", help="MySQL server address (use localhost for the current computer)")
+parser.add_argument("username", help="MySQL login username")
+parser.add_argument("password", help="MySQL login username")
+parser.add_argument("database", help="Database name")
+parser.add_argument("curtable", help="Name of the current ban table (remember prefixes if you use them)")
+parser.add_argument("newtable", help="Name of the new table to insert to, can't be same as the source table (remember prefixes)")
+args = parser.parse_args()
+db=MySQLdb.connect(host=args.address, user=args.username, passwd=args.password, db=args.database)
+cursor=db.cursor()
+current_table = args.curtable
+new_table = args.newtable
+#TG: Due to deleted rows and a legacy ban import being inserted from id 3140 id order is not contiguous or in line with date order. While technically valid, it's confusing and I don't like that.
+#TG: So instead of just running through to MAX(id) we're going to reorder the records by bantime as we go.
+cursor.execute("SELECT id FROM " + current_table + " ORDER BY bantime ASC")
+id_list = cursor.fetchall()
+start_time = datetime.now()
+print("Beginning conversion at {0}".format(start_time.strftime("%Y-%m-%d %H:%M:%S")))
+try:
+ for current_id in id_list:
+ if current_id[0] % 5000 == 0:
+ cur_time = datetime.now()
+ print("Reached row ID {0} Duration: {1}".format(current_id[0], cur_time - start_time))
+ cursor.execute("SELECT * FROM " + current_table + " WHERE id = %s", [current_id[0]])
+ query_row = cursor.fetchone()
+ if not query_row:
+ continue
+ else:
+ #TG: bans with an empty reason which were somehow created with almost every field being null or empty, we can't do much but skip this
+ if not query_row[6]:
+ continue
+ bantime = query_row[1]
+ server_ip = query_row[2]
+ server_port = query_row[3]
+ round_id = query_row[4]
+ applies_to_admins = parse_admin(query_row[5])
+ reason = query_row[6]
+ role = parse_role(query_row[5], query_row[7])
+ expiration_time = parse_datetime(query_row[5], query_row[9])
+ ckey = parse_for_empty(query_row[10])
+ computerid = parse_for_empty(query_row[11])
+ ip = parse_for_empty(query_row[12])
+ a_ckey = parse_not_null(query_row[13])
+ a_computerid = parse_not_null(query_row[14])
+ a_ip = parse_not_null(query_row[15])
+ who = query_row[16]
+ adminwho = query_row[17]
+ edits = parse_for_empty(query_row[18])
+ unbanned_datetime = parse_datetime(None, query_row[20])
+ unbanned_ckey = parse_for_empty(query_row[21])
+ unbanned_computerid = parse_for_empty(query_row[22])
+ unbanned_ip = parse_for_empty(query_row[23])
+ cursor.execute("INSERT INTO " + new_table + " (bantime, server_ip, server_port, round_id, role, expiration_time, applies_to_admins, reason, ckey, ip, computerid, a_ckey, a_ip, a_computerid, who, adminwho, edits, unbanned_datetime, unbanned_ckey, unbanned_ip, unbanned_computerid) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)", (bantime, server_ip, server_port, round_id, role, expiration_time, applies_to_admins, reason, ckey, ip, computerid, a_ckey, a_ip, a_computerid, who, adminwho, edits, unbanned_datetime, unbanned_ckey, unbanned_ip, unbanned_computerid))
+ db.commit()
+ end_time = datetime.now()
+ print("Conversion completed at {0}".format(datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
+ print("Script duration: {0}".format(end_time - start_time))
+except Exception as e:
+ end_time = datetime.now()
+ print("Error encountered on row ID {0} at {1}".format(current_id[0], datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
+ print("Script duration: {0}".format(end_time - start_time))
+ cursor.execute("TRUNCATE {0} ".format(new_table))
+ raise e
+cursor.close()
diff --git a/SQL/database_changelog.txt b/SQL/database_changelog.txt
index 56f05e84f1..410223a005 100644
--- a/SQL/database_changelog.txt
+++ b/SQL/database_changelog.txt
@@ -1,13 +1,256 @@
Any time you make a change to the schema files, remember to increment the database schema version. Generally increment the minor number, major should be reserved for significant changes to the schema. Both values go up to 255.
-The latest database version is 4.7; The query to update the schema revision table is:
+The latest database version is 5.12; The query to update the schema revision table is:
-INSERT INTO `schema_revision` (`major`, `minor`) VALUES (4, 7);
+INSERT INTO `schema_revision` (`major`, `minor`) VALUES (5, 12);
or
-INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (4, 7);
+INSERT INTO `SS13_schema_revision` (`major`, `minor`) VALUES (5, 12);
In any query remember to add a prefix to the table names if you use one.
+####################################################################################################
+NOTICE! BANS AND OTHERS ARENT SET TO THEIR LATEST FORMAT YET AS THE BACKEND DOES NOT SUPPORT IT YET!
+####################################################################################################
+
+-----------------------------------------------------
+
+Version 5.12, 29 December 2020, by Missfox
+Modified table `messages`, adding column `playtime` to show the user's playtime when the note was created.
+
+ALTER TABLE `messages` ADD `playtime` INT(11) NULL DEFAULT(NULL) AFTER `severity`
+
+-----------------------------------------------------
+
+Version 5.11, 7 September 2020, by bobbahbrown, MrStonedOne, and Jordie0608
+
+Adds indices to support search operations on the adminhelp ticket tables. This is to support improved performance on Atlanta Ned's Statbus.
+
+CREATE INDEX `idx_ticket_act_recip` (`action`, `recipient`)
+CREATE INDEX `idx_ticket_act_send` (`action`, `sender`)
+CREATE INDEX `idx_ticket_tic_rid` (`ticket`, `round_id`)
+CREATE INDEX `idx_ticket_act_time_rid` (`action`, `timestamp`, `round_id`)
+
+-----------------------------------------------------
+
+Version 5.10, 7 August 2020, by oranges
+
+Changes how the discord verification process works.
+Adds the discord_links table, and migrates discord id entries from player table to the discord links table in a once off operation and then removes the discord id
+on the player table
+
+START TRANSACTION;
+
+DROP TABLE IF EXISTS `discord_links`;
+CREATE TABLE `discord_links` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `ckey` VARCHAR(32) NOT NULL,
+ `discord_id` BIGINT(20) DEFAULT NULL,
+ `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `one_time_token` VARCHAR(100) NOT NULL,
+ `valid` BOOLEAN NOT NULL DEFAULT FALSE,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB;
+
+INSERT INTO `discord_links` (`ckey`, `discord_id`, `one_time_token`, `valid`) SELECT `ckey`, `discord_id`, CONCAT("presync_from_player_table_", `ckey`), TRUE FROM `player` WHERE discord_id IS NOT NULL;
+
+ALTER TABLE `player` DROP COLUMN `discord_id`;
+
+COMMIT;
+
+-----------------------------------------------------
+
+Version 5.9, 19 April 2020, by Jordie0608
+Updates and improvements to poll handling.
+Added the `deleted` column to tables 'poll_option', 'poll_textreply' and 'poll_vote' and the columns `created_datetime`, `subtitle`, `allow_revoting` and `deleted` to 'poll_question'.
+Changes table 'poll_question' column `createdby_ckey` to be NOT NULL and index `idx_pquest_time_admin` to be `idx_pquest_time_deleted_id` and 'poll_textreply' column `adminrank` to have no default.
+Added procedure `set_poll_deleted` that's called when deleting a poll to set deleted to true on each poll table where rows matching a poll_id argument.
+
+ALTER TABLE `poll_option`
+ ADD COLUMN `deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `default_percentage_calc`;
+
+ALTER TABLE `poll_question`
+ CHANGE COLUMN `createdby_ckey` `createdby_ckey` VARCHAR(32) NOT NULL AFTER `multiplechoiceoptions`,
+ ADD COLUMN `created_datetime` datetime NOT NULL AFTER `polltype`,
+ ADD COLUMN `subtitle` VARCHAR(255) NULL DEFAULT NULL AFTER `question`,
+ ADD COLUMN `allow_revoting` TINYINT(1) UNSIGNED NOT NULL AFTER `dontshow`,
+ ADD COLUMN `deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `allow_revoting`,
+ DROP INDEX `idx_pquest_time_admin`,
+ ADD INDEX `idx_pquest_time_deleted_id` (`starttime`, `endtime`, `deleted`, `id`);
+
+ALTER TABLE `poll_textreply`
+ CHANGE COLUMN `adminrank` `adminrank` varchar(32) NOT NULL AFTER `replytext`,
+ ADD COLUMN `deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `adminrank`;
+
+ALTER TABLE `poll_vote`
+ ADD COLUMN `deleted` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `rating`;
+
+DELIMITER $$
+CREATE PROCEDURE `set_poll_deleted`(
+ IN `poll_id` INT
+)
+SQL SECURITY INVOKER
+BEGIN
+UPDATE `poll_question` SET deleted = 1 WHERE id = poll_id;
+UPDATE `poll_option` SET deleted = 1 WHERE pollid = poll_id;
+UPDATE `poll_vote` SET deleted = 1 WHERE pollid = poll_id;
+UPDATE `poll_textreply` SET deleted = 1 WHERE pollid = poll_id;
+END
+$$
+DELIMITER ;
+
+-----------------------------------------------------
+
+Version 5.8, 7 April 2020, by Jordie0608
+Modified table `messages`, adding column `deleted_ckey` to record who deleted a message.
+
+ALTER TABLE `messages` ADD COLUMN `deleted_ckey` VARCHAR(32) NULL DEFAULT NULL AFTER `deleted`;
+
+-----------------------------------------------------
+
+Version 5.7, 10 January 2020 by Atlanta-Ned
+Added ticket table for tracking ahelp tickets in the database.
+
+DROP TABLE IF EXISTS `ticket`;
+CREATE TABLE `ticket` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `server_ip` int(10) unsigned NOT NULL,
+ `server_port` smallint(5) unsigned NOT NULL,
+ `round_id` int(11) unsigned NOT NULL,
+ `ticket` smallint(11) unsigned NOT NULL,
+ `action` varchar(20) NOT NULL DEFAULT 'Message',
+ `message` text NOT NULL,
+ `timestamp` datetime NOT NULL,
+ `recipient` varchar(32) DEFAULT NULL,
+ `sender` varchar(32) DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-----------------------------------------------------
+
+Version 5.6, 6 December 2019 by Anturke
+Added achievement_name and achievement_description columns to achievement_metadata table.
+
+
+ALTER TABLE `achievement_metadata` ADD COLUMN (`achievement_name` VARCHAR(64) NULL DEFAULT NULL, `achievement_description` VARCHAR(512) NULL DEFAULT NULL);
+
+-----------------------------------------------------
+
+Version 5.5, 26 October 2019 by Anturke
+Added achievement_metadata table.
+
+DROP TABLE IF EXISTS `achievement_metadata`;
+CREATE TABLE `achievement_metadata` (
+ `achievement_key` VARCHAR(32) NOT NULL,
+ `achievement_version` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
+ `achievement_type` enum('achievement','score','award') NULL DEFAULT NULL,
+ PRIMARY KEY (`achievement_key`)
+) ENGINE=InnoDB;
+
+
+-----------------------------------------------------
+
+Version 5.4, 5 October 2019 by Anturke
+Added achievements table.
+See hub migration verb in _achievement_data.dm for details on migrating.
+
+CREATE TABLE `achievements` (
+ `ckey` VARCHAR(32) NOT NULL,
+ `achievement_key` VARCHAR(32) NOT NULL,
+ `value` INT NULL,
+ `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`ckey`,`achievement_key`)
+) ENGINE=InnoDB;
+
+----------------------------------------------------
+
+Version 5.3, 6 July 2019, by Atlanta-Ned
+Added a `feedback` column to the admin table, used for linking to individual admin feedback threads. Currently this is only used for statistics tracking tools such as Statbus and isn't used by the game.
+
+ALTER TABLE `admin` ADD `feedback` VARCHAR(255) NULL DEFAULT NULL AFTER `rank`;
+
+----------------------------------------------------
+
+Version 5.2, 30 May 2019, by AffectedArc07
+Added a field to the `player` table to track ckey and discord ID relationships
+
+ALTER TABLE `player`
+ ADD COLUMN `discord_id` BIGINT NULL DEFAULT NULL AFTER `flags`;
+----------------------------------------------------
+
+Version 5.1, 25 Feb 2018, by MrStonedOne
+Added four tables to enable storing of stickybans in the database since byond can lose them, and to enable disabling stickybans for a round without depending on a crash free round. Existing stickybans are automagically imported to the tables.
+
+CREATE TABLE `stickyban` (
+ `ckey` VARCHAR(32) NOT NULL,
+ `reason` VARCHAR(2048) NOT NULL,
+ `banning_admin` VARCHAR(32) NOT NULL,
+ `datetime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`ckey`)
+) ENGINE=InnoDB;
+
+CREATE TABLE `stickyban_matched_ckey` (
+ `stickyban` VARCHAR(32) NOT NULL,
+ `matched_ckey` VARCHAR(32) NOT NULL,
+ `first_matched` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `last_matched` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `exempt` TINYINT(1) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`stickyban`, `matched_ckey`)
+) ENGINE=InnoDB;
+
+CREATE TABLE `stickyban_matched_ip` (
+ `stickyban` VARCHAR(32) NOT NULL,
+ `matched_ip` INT UNSIGNED NOT NULL,
+ `first_matched` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `last_matched` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`stickyban`, `matched_ip`)
+) ENGINE=InnoDB;
+
+CREATE TABLE `stickyban_matched_cid` (
+ `stickyban` VARCHAR(32) NOT NULL,
+ `matched_cid` VARCHAR(32) NOT NULL,
+ `first_matched` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `last_matched` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`stickyban`, `matched_cid`)
+) ENGINE=InnoDB;
+
+----------------------------------------------------
+
+Version 5.0, 28 October 2018, by Jordie0608
+Modified ban table to remove the need for the `bantype` column, a python script is used to migrate data to this new format.
+
+See the file 'ban_conversion_2018-10-28.py' for instructions on how to use the script.
+
+A new ban table can be created with the query:
+CREATE TABLE `ban` (
+ `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `bantime` DATETIME NOT NULL,
+ `server_ip` INT(10) UNSIGNED NOT NULL,
+ `server_port` SMALLINT(5) UNSIGNED NOT NULL,
+ `round_id` INT(11) UNSIGNED NOT NULL,
+ `role` VARCHAR(32) NULL DEFAULT NULL,
+ `expiration_time` DATETIME NULL DEFAULT NULL,
+ `applies_to_admins` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0',
+ `reason` VARCHAR(2048) NOT NULL,
+ `ckey` VARCHAR(32) NULL DEFAULT NULL,
+ `ip` INT(10) UNSIGNED NULL DEFAULT NULL,
+ `computerid` VARCHAR(32) NULL DEFAULT NULL,
+ `a_ckey` VARCHAR(32) NOT NULL,
+ `a_ip` INT(10) UNSIGNED NOT NULL,
+ `a_computerid` VARCHAR(32) NOT NULL,
+ `who` VARCHAR(2048) NOT NULL,
+ `adminwho` VARCHAR(2048) NOT NULL,
+ `edits` TEXT NULL DEFAULT NULL,
+ `unbanned_datetime` DATETIME NULL DEFAULT NULL,
+ `unbanned_ckey` VARCHAR(32) NULL DEFAULT NULL,
+ `unbanned_ip` INT(10) UNSIGNED NULL DEFAULT NULL,
+ `unbanned_computerid` VARCHAR(32) NULL DEFAULT NULL,
+ `unbanned_round_id` INT(11) UNSIGNED NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `idx_ban_isbanned` (`ckey`,`role`,`unbanned_datetime`,`expiration_time`),
+ KEY `idx_ban_isbanned_details` (`ckey`,`ip`,`computerid`,`role`,`unbanned_datetime`,`expiration_time`),
+ KEY `idx_ban_count` (`bantime`,`a_ckey`,`applies_to_admins`,`unbanned_datetime`,`expiration_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+
----------------------------------------------------
Version 4.7, 18 August 2018, by CitrusGender
@@ -50,8 +293,7 @@ Added table `role_time_log` and triggers `role_timeTlogupdate`, `role_timeTlogin
CREATE TABLE `role_time_log` ( `id` BIGINT NOT NULL AUTO_INCREMENT , `ckey` VARCHAR(32) NOT NULL , `job` VARCHAR(128) NOT NULL , `delta` INT NOT NULL , `datetime` TIMESTAMP on update CURRENT_TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP , PRIMARY KEY (`id`), INDEX (`ckey`), INDEX (`job`), INDEX (`datetime`)) ENGINE = InnoDB;
-DELIMITER
-$$
+DELIMITER $$
CREATE TRIGGER `role_timeTlogupdate` AFTER UPDATE ON `role_time` FOR EACH ROW BEGIN INSERT into role_time_log (ckey, job, delta) VALUES (NEW.CKEY, NEW.job, NEW.minutes-OLD.minutes);
END
$$
@@ -61,7 +303,7 @@ $$
CREATE TRIGGER `role_timeTlogdelete` AFTER DELETE ON `role_time` FOR EACH ROW BEGIN INSERT into role_time_log (ckey, job, delta) VALUES (OLD.ckey, OLD.job, 0-OLD.minutes);
END
$$
-
+DELIMITER ;
----------------------------------------------------
Version 4.2, 17 April 2018, by Jordie0608
diff --git a/SQL/tgstation_schema.sql b/SQL/tgstation_schema.sql
index 34baaaa4c6..9a9847a372 100644
--- a/SQL/tgstation_schema.sql
+++ b/SQL/tgstation_schema.sql
@@ -19,8 +19,9 @@ DROP TABLE IF EXISTS `admin`;
CREATE TABLE `admin` (
`ckey` varchar(32) NOT NULL,
`rank` varchar(32) NOT NULL,
+ `feedback` varchar(255) DEFAULT NULL,
PRIMARY KEY (`ckey`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -40,7 +41,7 @@ CREATE TABLE `admin_log` (
`target` varchar(32) NOT NULL,
`log` varchar(1000) NOT NULL,
PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -56,7 +57,7 @@ CREATE TABLE `admin_ranks` (
`exclude_flags` smallint(5) unsigned NOT NULL,
`can_edit_flags` smallint(5) unsigned NOT NULL,
PRIMARY KEY (`rank`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -67,11 +68,11 @@ DROP TABLE IF EXISTS `ban`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `ban` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `bantime` datetime NOT NULL,
- `server_ip` int(10) unsigned NOT NULL,
- `server_port` smallint(5) unsigned NOT NULL,
- `round_id` int(11) NOT NULL,
+ `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `bantime` DATETIME NOT NULL,
+ `server_ip` INT(10) UNSIGNED NOT NULL,
+ `server_port` SMALLINT(5) UNSIGNED NOT NULL,
+ `round_id` INT(11) UNSIGNED NOT NULL,
`bantype` enum('PERMABAN','TEMPBAN','JOB_PERMABAN','JOB_TEMPBAN','ADMIN_PERMABAN','ADMIN_TEMPBAN') NOT NULL,
`reason` varchar(2048) NOT NULL,
`job` varchar(32) DEFAULT NULL,
@@ -95,7 +96,7 @@ CREATE TABLE `ban` (
KEY `idx_ban_checkban` (`ckey`,`bantype`,`expiration_time`,`unbanned`,`job`),
KEY `idx_ban_isbanned` (`ckey`,`ip`,`computerid`,`bantype`,`expiration_time`,`unbanned`),
KEY `idx_ban_count` (`id`,`a_ckey`,`bantype`,`expiration_time`,`unbanned`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -115,7 +116,7 @@ CREATE TABLE `connection_log` (
`ip` int(10) unsigned NOT NULL,
`computerid` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -152,7 +153,7 @@ CREATE TABLE `death` (
`last_words` varchar(255) DEFAULT NULL,
`suicide` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -171,7 +172,7 @@ CREATE TABLE `feedback` (
`version` tinyint(3) unsigned NOT NULL,
`json` json NOT NULL,
PRIMARY KEY (`id`)
-) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -187,7 +188,7 @@ CREATE TABLE `ipintel` (
`intel` double NOT NULL DEFAULT '0',
PRIMARY KEY (`ip`),
KEY `idx_ipintel` (`ip`,`intel`,`date`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -206,7 +207,7 @@ CREATE TABLE `legacy_population` (
`server_port` smallint(5) unsigned NOT NULL,
`round_id` int(11) unsigned NOT NULL,
PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -231,7 +232,7 @@ CREATE TABLE `library` (
KEY `idx_lib_id_del` (`id`,`deleted`),
KEY `idx_lib_del_title` (`deleted`,`title`),
KEY `idx_lib_search` (`deleted`,`author`,`title`,`category`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -255,14 +256,16 @@ CREATE TABLE `messages` (
`secret` tinyint(1) unsigned NOT NULL,
`expire_timestamp` datetime DEFAULT NULL,
`severity` enum('high','medium','minor','none') DEFAULT NULL,
+ `playtime` int(11) unsigned NULL DEFAULT NULL,
`lasteditor` varchar(32) DEFAULT NULL,
`edits` text,
`deleted` tinyint(1) unsigned NOT NULL DEFAULT '0',
+ `deleted_ckey` VARCHAR(32) NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_msg_ckey_time` (`targetckey`,`timestamp`, `deleted`),
KEY `idx_msg_type_ckeys_time` (`type`,`targetckey`,`adminckey`,`timestamp`, `deleted`),
KEY `idx_msg_type_ckey_time_odr` (`type`,`targetckey`,`timestamp`, `deleted`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -299,7 +302,7 @@ CREATE TABLE IF NOT EXISTS `role_time_log` (
KEY `ckey` (`ckey`),
KEY `job` (`job`),
KEY `datetime` (`datetime`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -324,7 +327,7 @@ CREATE TABLE `player` (
PRIMARY KEY (`ckey`),
KEY `idx_player_cid_ckey` (`computerid`,`ckey`),
KEY `idx_player_ip_ckey` (`ip`,`ckey`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -344,9 +347,10 @@ CREATE TABLE `poll_option` (
`descmid` varchar(32) DEFAULT NULL,
`descmax` varchar(32) DEFAULT NULL,
`default_percentage_calc` tinyint(1) unsigned NOT NULL DEFAULT '1',
+ `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_pop_pollid` (`pollid`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -359,19 +363,23 @@ DROP TABLE IF EXISTS `poll_question`;
CREATE TABLE `poll_question` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`polltype` enum('OPTION','TEXT','NUMVAL','MULTICHOICE','IRV') NOT NULL,
+ `created_datetime` datetime NOT NULL,
`starttime` datetime NOT NULL,
`endtime` datetime NOT NULL,
`question` varchar(255) NOT NULL,
+ `subtitle` varchar(255) DEFAULT NULL,
`adminonly` tinyint(1) unsigned NOT NULL,
`multiplechoiceoptions` int(2) DEFAULT NULL,
- `createdby_ckey` varchar(32) DEFAULT NULL,
+ `createdby_ckey` varchar(32) NOT NULL,
`createdby_ip` int(10) unsigned NOT NULL,
`dontshow` tinyint(1) unsigned NOT NULL,
+ `allow_revoting` tinyint(1) unsigned NOT NULL,
+ `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_pquest_question_time_ckey` (`question`,`starttime`,`endtime`,`createdby_ckey`,`createdby_ip`),
- KEY `idx_pquest_time_admin` (`starttime`,`endtime`,`adminonly`),
+ KEY `idx_pquest_time_deleted_id` (`starttime`,`endtime`, `deleted`, `id`),
KEY `idx_pquest_id_time_type_admin` (`id`,`starttime`,`endtime`,`polltype`,`adminonly`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -388,10 +396,11 @@ CREATE TABLE `poll_textreply` (
`ckey` varchar(32) NOT NULL,
`ip` int(10) unsigned NOT NULL,
`replytext` varchar(2048) NOT NULL,
- `adminrank` varchar(32) NOT NULL DEFAULT 'Player',
+ `adminrank` varchar(32) NOT NULL,
+ `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_ptext_pollid_ckey` (`pollid`,`ckey`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -410,10 +419,11 @@ CREATE TABLE `poll_vote` (
`ip` int(10) unsigned NOT NULL,
`adminrank` varchar(32) NOT NULL,
`rating` int(2) DEFAULT NULL,
+ `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_pvote_pollid_ckey` (`pollid`,`ckey`),
KEY `idx_pvote_optionid_ckey` (`optionid`,`ckey`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -438,7 +448,7 @@ CREATE TABLE `round` (
`map_name` VARCHAR(32) NULL,
`station_name` VARCHAR(80) NULL,
PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
@@ -451,9 +461,113 @@ CREATE TABLE `schema_revision` (
`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=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+--
+-- Table structure for table `stickyban`
+--
+DROP TABLE IF EXISTS `stickyban`;
+CREATE TABLE `stickyban` (
+ `ckey` VARCHAR(32) NOT NULL,
+ `reason` VARCHAR(2048) NOT NULL,
+ `banning_admin` VARCHAR(32) NOT NULL,
+ `datetime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`ckey`)
+) ENGINE=InnoDB;
+
+--
+-- Table structure for table `stickyban_matched_ckey`
+--
+DROP TABLE IF EXISTS `stickyban_matched_ckey`;
+CREATE TABLE `stickyban_matched_ckey` (
+ `stickyban` VARCHAR(32) NOT NULL,
+ `matched_ckey` VARCHAR(32) NOT NULL,
+ `first_matched` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `last_matched` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `exempt` TINYINT(1) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`stickyban`, `matched_ckey`)
+) ENGINE=InnoDB;
+
+--
+-- Table structure for table `stickyban_matched_ip`
+--
+DROP TABLE IF EXISTS `stickyban_matched_ip`;
+CREATE TABLE `stickyban_matched_ip` (
+ `stickyban` VARCHAR(32) NOT NULL,
+ `matched_ip` INT UNSIGNED NOT NULL,
+ `first_matched` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `last_matched` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`stickyban`, `matched_ip`)
+) ENGINE=InnoDB;
+
+--
+-- Table structure for table `stickyban_matched_cid`
+--
+DROP TABLE IF EXISTS `stickyban_matched_cid`;
+CREATE TABLE `stickyban_matched_cid` (
+ `stickyban` VARCHAR(32) NOT NULL,
+ `matched_cid` VARCHAR(32) NOT NULL,
+ `first_matched` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `last_matched` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`stickyban`, `matched_cid`)
+) ENGINE=InnoDB;
+
+--
+-- Table structure for table `achievements`
+--
+DROP TABLE IF EXISTS `achievements`;
+CREATE TABLE `achievements` (
+ `ckey` VARCHAR(32) NOT NULL,
+ `achievement_key` VARCHAR(32) NOT NULL,
+ `value` INT NULL,
+ `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`ckey`,`achievement_key`)
+) ENGINE=InnoDB;
+
+DROP TABLE IF EXISTS `achievement_metadata`;
+CREATE TABLE `achievement_metadata` (
+ `achievement_key` VARCHAR(32) NOT NULL,
+ `achievement_version` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
+ `achievement_type` enum('achievement','score','award') NULL DEFAULT NULL,
+ `achievement_name` VARCHAR(64) NULL DEFAULT NULL,
+ `achievement_description` VARCHAR(512) NULL DEFAULT NULL,
+ PRIMARY KEY (`achievement_key`)
+) ENGINE=InnoDB;
+
+--
+-- Table structure for table `ticket`
+--
+DROP TABLE IF EXISTS `ticket`;
+CREATE TABLE `ticket` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `server_ip` int(10) unsigned NOT NULL,
+ `server_port` smallint(5) unsigned NOT NULL,
+ `round_id` int(11) unsigned NOT NULL,
+ `ticket` smallint(11) unsigned NOT NULL,
+ `action` varchar(20) NOT NULL DEFAULT 'Message',
+ `message` text NOT NULL,
+ `timestamp` datetime NOT NULL,
+ `recipient` varchar(32) DEFAULT NULL,
+ `sender` varchar(32) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `idx_ticket_act_recip` (`action`, `recipient`),
+ KEY `idx_ticket_act_send` (`action`, `sender`),
+ KEY `idx_ticket_tic_rid` (`ticket`, `round_id`),
+ KEY `idx_ticket_act_time_rid` (`action`, `timestamp`, `round_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DELIMITER $$
+CREATE PROCEDURE `set_poll_deleted`(
+ IN `poll_id` INT
+)
+SQL SECURITY INVOKER
+BEGIN
+UPDATE `poll_question` SET deleted = 1 WHERE id = poll_id;
+UPDATE `poll_option` SET deleted = 1 WHERE pollid = poll_id;
+UPDATE `poll_vote` SET deleted = 1 WHERE pollid = poll_id;
+UPDATE `poll_textreply` SET deleted = 1 WHERE pollid = poll_id;
+END
+$$
CREATE TRIGGER `role_timeTlogupdate` AFTER UPDATE ON `role_time` FOR EACH ROW BEGIN INSERT into role_time_log (ckey, job, delta) VALUES (NEW.CKEY, NEW.job, NEW.minutes-OLD.minutes);
END
$$
@@ -463,6 +577,21 @@ $$
CREATE TRIGGER `role_timeTlogdelete` AFTER DELETE ON `role_time` FOR EACH ROW BEGIN INSERT into role_time_log (ckey, job, delta) VALUES (OLD.ckey, OLD.job, 0-OLD.minutes);
END
$$
+DELIMITER ;
+
+--
+-- Table structure for table `discord_links`
+--
+DROP TABLE IF EXISTS `discord_links`;
+CREATE TABLE `discord_links` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `ckey` VARCHAR(32) NOT NULL,
+ `discord_id` BIGINT(20) DEFAULT NULL,
+ `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `one_time_token` VARCHAR(100) NOT NULL,
+ `valid` BOOLEAN NOT NULL DEFAULT FALSE,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
diff --git a/SQL/tgstation_schema_prefixed.sql b/SQL/tgstation_schema_prefixed.sql
index 5cb57a9582..654f45f4f2 100644
--- a/SQL/tgstation_schema_prefixed.sql
+++ b/SQL/tgstation_schema_prefixed.sql
@@ -19,8 +19,9 @@ DROP TABLE IF EXISTS `SS13_admin`;
CREATE TABLE `SS13_admin` (
`ckey` varchar(32) NOT NULL,
`rank` varchar(32) NOT NULL,
+ `feedback` varchar(255) DEFAULT NULL,
PRIMARY KEY (`ckey`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -40,7 +41,7 @@ CREATE TABLE `SS13_admin_log` (
`target` varchar(32) NOT NULL,
`log` varchar(1000) NOT NULL,
PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -56,7 +57,7 @@ CREATE TABLE `SS13_admin_ranks` (
`exclude_flags` smallint(5) unsigned NOT NULL,
`can_edit_flags` smallint(5) unsigned NOT NULL,
PRIMARY KEY (`rank`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -67,11 +68,11 @@ DROP TABLE IF EXISTS `SS13_ban`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `SS13_ban` (
- `id` int(11) NOT NULL AUTO_INCREMENT,
- `bantime` datetime NOT NULL,
- `server_ip` int(10) unsigned NOT NULL,
- `server_port` smallint(5) unsigned NOT NULL,
- `round_id` int(11) NOT NULL,
+ `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
+ `bantime` DATETIME NOT NULL,
+ `server_ip` INT(10) UNSIGNED NOT NULL,
+ `server_port` SMALLINT(5) UNSIGNED NOT NULL,
+ `round_id` INT(11) UNSIGNED NOT NULL,
`bantype` enum('PERMABAN','TEMPBAN','JOB_PERMABAN','JOB_TEMPBAN','ADMIN_PERMABAN','ADMIN_TEMPBAN') NOT NULL,
`reason` varchar(2048) NOT NULL,
`job` varchar(32) DEFAULT NULL,
@@ -95,7 +96,7 @@ CREATE TABLE `SS13_ban` (
KEY `idx_ban_checkban` (`ckey`,`bantype`,`expiration_time`,`unbanned`,`job`),
KEY `idx_ban_isbanned` (`ckey`,`ip`,`computerid`,`bantype`,`expiration_time`,`unbanned`),
KEY `idx_ban_count` (`id`,`a_ckey`,`bantype`,`expiration_time`,`unbanned`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -115,7 +116,7 @@ CREATE TABLE `SS13_connection_log` (
`ip` int(10) unsigned NOT NULL,
`computerid` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -152,7 +153,7 @@ CREATE TABLE `SS13_death` (
`last_words` varchar(255) DEFAULT NULL,
`suicide` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -171,7 +172,7 @@ CREATE TABLE `SS13_feedback` (
`key_type` enum('text', 'amount', 'tally', 'nested tally', 'associative') NOT NULL,
`json` json NOT NULL,
PRIMARY KEY (`id`)
-) ENGINE=MyISAM DEFAULT CHARSET=latin1;
+) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -187,7 +188,7 @@ CREATE TABLE `SS13_ipintel` (
`intel` double NOT NULL DEFAULT '0',
PRIMARY KEY (`ip`),
KEY `idx_ipintel` (`ip`,`intel`,`date`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -206,7 +207,7 @@ CREATE TABLE `SS13_legacy_population` (
`server_port` smallint(5) unsigned NOT NULL,
`round_id` int(11) unsigned NOT NULL,
PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -231,7 +232,7 @@ CREATE TABLE `SS13_library` (
KEY `idx_lib_id_del` (`id`,`deleted`),
KEY `idx_lib_del_title` (`deleted`,`title`),
KEY `idx_lib_search` (`deleted`,`author`,`title`,`category`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -255,14 +256,16 @@ CREATE TABLE `SS13_messages` (
`secret` tinyint(1) unsigned NOT NULL,
`expire_timestamp` datetime DEFAULT NULL,
`severity` enum('high','medium','minor','none') DEFAULT NULL,
+ `playtime` int(11) unsigned NULL DEFAULT NULL,
`lasteditor` varchar(32) DEFAULT NULL,
`edits` text,
`deleted` tinyint(1) unsigned NOT NULL DEFAULT '0',
+ `deleted_ckey` VARCHAR(32) NULL DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_msg_ckey_time` (`targetckey`,`timestamp`, `deleted`),
KEY `idx_msg_type_ckeys_time` (`type`,`targetckey`,`adminckey`,`timestamp`, `deleted`),
KEY `idx_msg_type_ckey_time_odr` (`type`,`targetckey`,`timestamp`, `deleted`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -299,7 +302,7 @@ CREATE TABLE IF NOT EXISTS `SS13_role_time_log` (
KEY `ckey` (`ckey`),
KEY `job` (`job`),
KEY `datetime` (`datetime`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -324,7 +327,7 @@ CREATE TABLE `SS13_player` (
PRIMARY KEY (`ckey`),
KEY `idx_player_cid_ckey` (`computerid`,`ckey`),
KEY `idx_player_ip_ckey` (`ip`,`ckey`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -344,9 +347,10 @@ CREATE TABLE `SS13_poll_option` (
`descmid` varchar(32) DEFAULT NULL,
`descmax` varchar(32) DEFAULT NULL,
`default_percentage_calc` tinyint(1) unsigned NOT NULL DEFAULT '1',
+ `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_pop_pollid` (`pollid`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -359,19 +363,23 @@ DROP TABLE IF EXISTS `SS13_poll_question`;
CREATE TABLE `SS13_poll_question` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`polltype` enum('OPTION','TEXT','NUMVAL','MULTICHOICE','IRV') NOT NULL,
+ `created_datetime` datetime NOT NULL,
`starttime` datetime NOT NULL,
`endtime` datetime NOT NULL,
`question` varchar(255) NOT NULL,
+ `subtitle` varchar(255) DEFAULT NULL,
`adminonly` tinyint(1) unsigned NOT NULL,
`multiplechoiceoptions` int(2) DEFAULT NULL,
- `createdby_ckey` varchar(32) DEFAULT NULL,
+ `createdby_ckey` varchar(32) NOT NULL,
`createdby_ip` int(10) unsigned NOT NULL,
`dontshow` tinyint(1) unsigned NOT NULL,
+ `allow_revoting` tinyint(1) unsigned NOT NULL,
+ `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_pquest_question_time_ckey` (`question`,`starttime`,`endtime`,`createdby_ckey`,`createdby_ip`),
- KEY `idx_pquest_time_admin` (`starttime`,`endtime`,`adminonly`),
+ KEY `idx_pquest_time_deleted_id` (`starttime`,`endtime`, `deleted`, `id`),
KEY `idx_pquest_id_time_type_admin` (`id`,`starttime`,`endtime`,`polltype`,`adminonly`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -388,10 +396,11 @@ CREATE TABLE `SS13_poll_textreply` (
`ckey` varchar(32) NOT NULL,
`ip` int(10) unsigned NOT NULL,
`replytext` varchar(2048) NOT NULL,
- `adminrank` varchar(32) NOT NULL DEFAULT 'Player',
+ `adminrank` varchar(32) NOT NULL,
+ `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_ptext_pollid_ckey` (`pollid`,`ckey`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -410,10 +419,11 @@ CREATE TABLE `SS13_poll_vote` (
`ip` int(10) unsigned NOT NULL,
`adminrank` varchar(32) NOT NULL,
`rating` int(2) DEFAULT NULL,
+ `deleted` tinyint(1) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `idx_pvote_pollid_ckey` (`pollid`,`ckey`),
KEY `idx_pvote_optionid_ckey` (`optionid`,`ckey`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -438,7 +448,7 @@ CREATE TABLE `SS13_round` (
`map_name` VARCHAR(32) NULL,
`station_name` VARCHAR(80) NULL,
PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
@@ -451,9 +461,113 @@ CREATE TABLE `SS13_schema_revision` (
`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=latin1;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+--
+-- Table structure for table `SS13_stickyban`
+--
+DROP TABLE IF EXISTS `SS13_stickyban`;
+CREATE TABLE `SS13_stickyban` (
+ `ckey` VARCHAR(32) NOT NULL,
+ `reason` VARCHAR(2048) NOT NULL,
+ `banning_admin` VARCHAR(32) NOT NULL,
+ `datetime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`ckey`)
+) ENGINE=InnoDB;
+
+--
+-- Table structure for table `SS13_stickyban_matched_ckey`
+--
+DROP TABLE IF EXISTS `SS13_stickyban_matched_ckey`;
+CREATE TABLE `SS13_stickyban_matched_ckey` (
+ `stickyban` VARCHAR(32) NOT NULL,
+ `matched_ckey` VARCHAR(32) NOT NULL,
+ `first_matched` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `last_matched` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ `exempt` TINYINT(1) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`stickyban`, `matched_ckey`)
+) ENGINE=InnoDB;
+
+--
+-- Table structure for table `SS13_stickyban_matched_ip`
+--
+DROP TABLE IF EXISTS `SS13_stickyban_matched_ip`;
+CREATE TABLE `SS13_stickyban_matched_ip` (
+ `stickyban` VARCHAR(32) NOT NULL,
+ `matched_ip` INT UNSIGNED NOT NULL,
+ `first_matched` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `last_matched` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`stickyban`, `matched_ip`)
+) ENGINE=InnoDB;
+
+--
+-- Table structure for table `SS13_stickyban_matched_cid`
+--
+DROP TABLE IF EXISTS `SS13_stickyban_matched_cid`;
+CREATE TABLE `SS13_stickyban_matched_cid` (
+ `stickyban` VARCHAR(32) NOT NULL,
+ `matched_cid` VARCHAR(32) NOT NULL,
+ `first_matched` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `last_matched` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`stickyban`, `matched_cid`)
+) ENGINE=InnoDB;
+
+--
+-- Table structure for table `SS13_achievements`
+--
+DROP TABLE IF EXISTS `SS13_achievements`;
+CREATE TABLE `SS13_achievements` (
+ `ckey` VARCHAR(32) NOT NULL,
+ `achievement_key` VARCHAR(32) NOT NULL,
+ `value` INT NULL,
+ `last_updated` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`ckey`,`achievement_key`)
+) ENGINE=InnoDB;
+
+DROP TABLE IF EXISTS `SS13_achievement_metadata`;
+CREATE TABLE `SS13_achievement_metadata` (
+ `achievement_key` VARCHAR(32) NOT NULL,
+ `achievement_version` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
+ `achievement_type` enum('achievement','score','award') NULL DEFAULT NULL,
+ `achievement_name` VARCHAR(64) NULL DEFAULT NULL,
+ `achievement_description` VARCHAR(512) NULL DEFAULT NULL,
+ PRIMARY KEY (`achievement_key`)
+) ENGINE=InnoDB;
+
+--
+-- Table structure for table `SS13_ticket`
+--
+DROP TABLE IF EXISTS `SS13_ticket`;
+CREATE TABLE `SS13_ticket` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `server_ip` int(10) unsigned NOT NULL,
+ `server_port` smallint(5) unsigned NOT NULL,
+ `round_id` int(11) unsigned NOT NULL,
+ `ticket` smallint(11) unsigned NOT NULL,
+ `action` varchar(20) NOT NULL DEFAULT 'Message',
+ `message` text NOT NULL,
+ `timestamp` datetime NOT NULL,
+ `recipient` varchar(32) DEFAULT NULL,
+ `sender` varchar(32) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `idx_ticket_act_recip` (`action`, `recipient`),
+ KEY `idx_ticket_act_send` (`action`, `sender`),
+ KEY `idx_ticket_tic_rid` (`ticket`, `round_id`),
+ KEY `idx_ticket_act_time_rid` (`action`, `timestamp`, `round_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DELIMITER $$
+CREATE PROCEDURE `set_poll_deleted`(
+ IN `poll_id` INT
+)
+SQL SECURITY INVOKER
+BEGIN
+UPDATE `SS13_poll_question` SET deleted = 1 WHERE id = poll_id;
+UPDATE `SS13_poll_option` SET deleted = 1 WHERE pollid = poll_id;
+UPDATE `SS13_poll_vote` SET deleted = 1 WHERE pollid = poll_id;
+UPDATE `SS13_poll_textreply` SET deleted = 1 WHERE pollid = poll_id;
+END
+$$
CREATE TRIGGER `SS13_role_timeTlogupdate` AFTER UPDATE ON `SS13_role_time` FOR EACH ROW BEGIN INSERT into SS13_role_time_log (ckey, job, delta) VALUES (NEW.CKEY, NEW.job, NEW.minutes-OLD.minutes);
END
$$
@@ -463,6 +577,21 @@ $$
CREATE TRIGGER `SS13_role_timeTlogdelete` AFTER DELETE ON `SS13_role_time` FOR EACH ROW BEGIN INSERT into SS13_role_time_log (ckey, job, delta) VALUES (OLD.ckey, OLD.job, 0-OLD.minutes);
END
$$
+DELIMITER ;
+
+--
+-- Table structure for table `discord_links`
+--
+DROP TABLE IF EXISTS `SS13_discord_links`;
+CREATE TABLE `SS13_discord_links` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `ckey` VARCHAR(32) NOT NULL,
+ `discord_id` BIGINT(20) DEFAULT NULL,
+ `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `one_time_token` VARCHAR(100) NOT NULL,
+ `valid` BOOLEAN NOT NULL DEFAULT FALSE,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
diff --git a/_maps/RandomRuins/SpaceRuins/cloning_facility.dmm b/_maps/RandomRuins/SpaceRuins/cloning_facility.dmm
index d440e2d15b..1f258fad77 100644
--- a/_maps/RandomRuins/SpaceRuins/cloning_facility.dmm
+++ b/_maps/RandomRuins/SpaceRuins/cloning_facility.dmm
@@ -294,7 +294,7 @@
/turf/template_noop,
/area/space/nearstation)
"N" = (
-/obj/item/book/random/triple,
+/obj/item/book/random,
/turf/open/floor/plasteel,
/area/ruin/space/has_grav/powered/ancient_shuttle)
"O" = (
diff --git a/_maps/RandomRuins/SpaceRuins/oldstation.dmm b/_maps/RandomRuins/SpaceRuins/oldstation.dmm
index 8528099d30..43aea1761a 100644
--- a/_maps/RandomRuins/SpaceRuins/oldstation.dmm
+++ b/_maps/RandomRuins/SpaceRuins/oldstation.dmm
@@ -1651,7 +1651,7 @@
/turf/open/floor/plasteel/white,
/area/ruin/space/has_grav/ancientstation/rnd)
"eD" = (
-/obj/machinery/mecha_part_fabricator/offstation,
+/obj/machinery/mecha_part_fabricator,
/obj/effect/decal/cleanable/dirt,
/turf/open/floor/plasteel/white,
/area/ruin/space/has_grav/ancientstation/rnd)
@@ -2010,12 +2010,12 @@
/turf/open/floor/plasteel,
/area/ruin/space/has_grav/ancientstation/deltacorridor)
"fu" = (
-/obj/machinery/rnd/production/protolathe/offstation,
+/obj/machinery/rnd/production/protolathe,
/obj/effect/decal/cleanable/dirt,
/turf/open/floor/plasteel/white,
/area/ruin/space/has_grav/ancientstation/rnd)
"fv" = (
-/obj/machinery/rnd/production/circuit_imprinter/offstation,
+/obj/machinery/rnd/production/circuit_imprinter,
/obj/effect/decal/cleanable/dirt,
/obj/item/reagent_containers/dropper,
/turf/open/floor/plasteel/white,
diff --git a/_maps/RandomZLevels/away_mission/jungleresort.dmm b/_maps/RandomZLevels/away_mission/jungleresort.dmm
index 4afe638edb..9ff92b619a 100644
--- a/_maps/RandomZLevels/away_mission/jungleresort.dmm
+++ b/_maps/RandomZLevels/away_mission/jungleresort.dmm
@@ -413,10 +413,6 @@
/obj/item/toy/figure/chef,
/turf/open/floor/wood,
/area/awaymission/jungleresort)
-"gC" = (
-/obj/item/clothing/head/rice_hat/cursed,
-/turf/open/floor/plating/dirt/jungle,
-/area/awaymission/jungleresort)
"gK" = (
/obj/structure/table/wood,
/obj/item/gun/ballistic/automatic/c20r/toy/unrestricted,
@@ -13828,7 +13824,7 @@ io
du
YF
io
-gC
+io
io
io
ia
@@ -26666,4 +26662,3 @@ bG
bG
bG
"}
-
diff --git a/_maps/map_files/BoxStation/BoxStation.dmm b/_maps/map_files/BoxStation/BoxStation.dmm
index f7245ef535..eb194cf5cf 100644
--- a/_maps/map_files/BoxStation/BoxStation.dmm
+++ b/_maps/map_files/BoxStation/BoxStation.dmm
@@ -30845,6 +30845,7 @@
/area/science)
"bvF" = (
/obj/machinery/requests_console{
+ announcementConsole = 1;
department = "Cargo Bay";
departmentType = 2;
pixel_x = -30
diff --git a/_maps/map_files/CogStation/CogStation.dmm b/_maps/map_files/CogStation/CogStation.dmm
index e41f6c7d89..84968dc961 100644
--- a/_maps/map_files/CogStation/CogStation.dmm
+++ b/_maps/map_files/CogStation/CogStation.dmm
@@ -42885,7 +42885,7 @@
/area/science/xenobiology)
"bPd" = (
/obj/structure/table/reinforced,
-/obj/item/book/random/triple,
+/obj/item/book/random,
/turf/open/floor/engine,
/area/science/xenobiology)
"bPe" = (
diff --git a/_maps/map_files/Deltastation/DeltaStation2.dmm b/_maps/map_files/Deltastation/DeltaStation2.dmm
index 4e2b009c79..94699a7a00 100644
--- a/_maps/map_files/Deltastation/DeltaStation2.dmm
+++ b/_maps/map_files/Deltastation/DeltaStation2.dmm
@@ -106349,10 +106349,10 @@
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
+/obj/structure/closet/crate/freezer/blood,
/turf/open/floor/plasteel/dark,
/area/medical/surgery)
"dyK" = (
-/obj/structure/closet/crate/freezer/blood,
/obj/machinery/vending/wallmed{
name = "Emergency NanoMed";
pixel_x = 26;
@@ -106368,6 +106368,7 @@
/obj/effect/turf_decal/tile/neutral{
dir = 8
},
+/obj/machinery/limbgrower,
/turf/open/floor/plasteel/dark,
/area/medical/surgery)
"dyL" = (
diff --git a/_maps/map_files/PubbyStation/PubbyStation.dmm b/_maps/map_files/PubbyStation/PubbyStation.dmm
index 12c507c2bf..8e23df2e5f 100644
--- a/_maps/map_files/PubbyStation/PubbyStation.dmm
+++ b/_maps/map_files/PubbyStation/PubbyStation.dmm
@@ -37478,11 +37478,10 @@
/turf/open/floor/plasteel/white,
/area/medical/medbay/central)
"bHb" = (
-/obj/structure/closet/crate/freezer/surplus_limbs,
-/obj/item/reagent_containers/glass/beaker/synthflesh,
/obj/machinery/newscaster/security_unit{
pixel_y = 32
},
+/obj/machinery/limbgrower,
/turf/open/floor/plasteel/freezer,
/area/medical/surgery)
"bHc" = (
@@ -38110,6 +38109,8 @@
/obj/effect/turf_decal/tile/blue{
dir = 4
},
+/obj/structure/closet/crate/freezer/surplus_limbs,
+/obj/item/reagent_containers/glass/beaker/synthflesh,
/turf/open/floor/plasteel/white,
/area/medical/surgery)
"bIq" = (
diff --git a/code/__DEFINES/achievements.dm b/code/__DEFINES/achievements.dm
new file mode 100644
index 0000000000..c39531971c
--- /dev/null
+++ b/code/__DEFINES/achievements.dm
@@ -0,0 +1,108 @@
+// Keep the identifiers here below 32 characters, you can put the full display name in the actual achievement datum
+
+#define ACHIEVEMENT_DEFAULT "default"
+#define ACHIEVEMENT_SCORE "score"
+
+//Misc Medal hub IDs
+#define MEDAL_METEOR "Your Life Before Your Eyes"
+#define MEDAL_PULSE "Jackpot"
+#define MEDAL_TIMEWASTE "Overextended The Joke"
+#define MEDAL_RODSUPLEX "Feat of Strength"
+#define MEDAL_CLOWNCARKING "Round and Full"
+#define MEDAL_THANKSALOT "The Best Driver"
+#define MEDAL_HELBITALJANKEN "Hel-bent on Winning"
+#define MEDAL_MATERIALCRAFT "Getting an Upgrade"
+#define MEDAL_DISKPLEASE "Disk, Please!"
+#define MEDAL_GAMER "I'm Not Important"
+#define MEDAL_VENDORSQUISH "Teenage Anarchist"
+#define MEDAL_SWIRLIE "Bowl-d"
+#define MEDAL_SELFOUCH "Hands???"
+#define MEDAL_SANDMAN "Mister Sandman"
+#define MEDAL_CLEANBOSS "Cleanboss"
+#define MEDAL_RULE8 "Rule 8"
+#define MEDAL_LONGSHIFT "longshift"
+#define MEDAL_SNAIL "KKKiiilll mmmeee"
+#define MEDAL_LOOKOUTSIR "Look Out, Sir!"
+#define MEDAL_GOTTEM "GOTTEM"
+#define MEDAL_ASCENSION "Ascension"
+#define MEDAL_FRENCHING "FrenchingTheBubble"
+#define MEDAL_ASH_ASCENSION "Ash"
+#define MEDAL_FLESH_ASCENSION "Flesh"
+#define MEDAL_RUST_ASCENSION "Rust"
+#define MEDAL_VOID_ASCENSION "Void"
+#define MEDAL_TOOLBOX_SOUL "Toolsoul"
+#define MEDAL_CHEM_TUT "Beginner Chemist"
+
+//Skill medal hub IDs
+#define MEDAL_LEGENDARY_MINER "Legendary Miner"
+
+//Mafia medal hub IDs (wins)
+#define MAFIA_MEDAL_ASSISTANT "Assistant"
+#define MAFIA_MEDAL_DETECTIVE "Detective"
+#define MAFIA_MEDAL_PSYCHOLOGIST "Psychologist"
+#define MAFIA_MEDAL_CHAPLAIN "Chaplain"
+#define MAFIA_MEDAL_MD "Medical Doctor"
+#define MAFIA_MEDAL_OFFICER "Security Officer"
+#define MAFIA_MEDAL_LAWYER "Lawyer"
+#define MAFIA_MEDAL_HOP "Head of Personnel"
+#define MAFIA_MEDAL_HOS "Head of Security"
+#define MAFIA_MEDAL_WARDEN "Warden"
+#define MAFIA_MEDAL_CHANGELING "CHANGELING"
+#define MAFIA_MEDAL_THOUGHTFEEDER "Thoughtfeeder"
+#define MAFIA_MEDAL_TRAITOR "Traitor"
+#define MAFIA_MEDAL_NIGHTMARE "Nightmare"
+#define MAFIA_MEDAL_FUGITIVE "Fugitive"
+#define MAFIA_MEDAL_OBSESSED "Obsessed"
+#define MAFIA_MEDAL_CLOWN "Clown"
+
+//Mafia medal hub IDs (misc stuff)
+#define MAFIA_MEDAL_HATED "Universally Hated"
+#define MAFIA_MEDAL_CHARISMATIC "Charismatic"
+#define MAFIA_MEDAL_VIP "VIP"
+
+//Boss medals
+
+// Medal hub IDs for boss medals (Pre-fixes)
+#define BOSS_MEDAL_ANY "Boss Killer"
+#define BOSS_MEDAL_MINER "Blood-drunk Miner Killer"
+#define BOSS_MEDAL_FROSTMINER "Demonic-frost Miner Killer"
+#define BOSS_MEDAL_BUBBLEGUM "Bubblegum Killer"
+#define BOSS_MEDAL_COLOSSUS "Colossus Killer"
+#define BOSS_MEDAL_DRAKE "Drake Killer"
+#define BOSS_MEDAL_HIEROPHANT "Hierophant Killer"
+#define BOSS_MEDAL_LEGION "Legion Killer"
+#define BOSS_MEDAL_TENDRIL "Tendril Exterminator"
+#define BOSS_MEDAL_SWARMERS "Swarmer Beacon Killer"
+#define BOSS_MEDAL_WENDIGO "Wendigo Killer"
+#define BOSS_MEDAL_KINGGOAT "King Goat Killer"
+
+#define BOSS_MEDAL_MINER_CRUSHER "Blood-drunk Miner Crusher"
+#define BOSS_MEDAL_FROSTMINER_CRUSHER "Demonic-frost Miner Crusher"
+#define BOSS_MEDAL_BUBBLEGUM_CRUSHER "Bubblegum Crusher"
+#define BOSS_MEDAL_COLOSSUS_CRUSHER "Colossus Crusher"
+#define BOSS_MEDAL_DRAKE_CRUSHER "Drake Crusher"
+#define BOSS_MEDAL_HIEROPHANT_CRUSHER "Hierophant Crusher"
+#define BOSS_MEDAL_LEGION_CRUSHER "Legion Crusher"
+#define BOSS_MEDAL_SWARMERS_CRUSHER "Swarmer Beacon Crusher"
+#define BOSS_MEDAL_WENDIGO_CRUSHER "Wendigo Crusher"
+#define BOSS_MEDAL_KINGGOAT_CRUSHER "King Goat Crusher"
+
+// Medal hub IDs for boss-kill scores
+#define BOSS_SCORE "Bosses Killed"
+#define MINER_SCORE "BDMs Killed"
+#define FROST_MINER_SCORE "DFMs Killed"
+#define BUBBLEGUM_SCORE "Bubblegum Killed"
+#define COLOSSUS_SCORE "Colossus Killed"
+#define DRAKE_SCORE "Drakes Killed"
+#define HIEROPHANT_SCORE "Hierophants Killed"
+#define LEGION_SCORE "Legion Killed"
+#define SWARMER_BEACON_SCORE "Swarmer Beacs Killed"
+#define WENDIGO_SCORE "Wendigos Killed"
+#define KINGGOAT_SCORE "King Goat Killed"
+#define TENDRIL_CLEAR_SCORE "Tendrils Killed"
+
+// DB ID for hardcore random mode
+#define HARDCORE_RANDOM_SCORE "Hardcore Random Score"
+
+// DB ID for amount of consumed maintenance pills
+#define MAINTENANCE_PILL_SCORE "Maintenance Pill Score"
diff --git a/code/__DEFINES/bsql.config.dm b/code/__DEFINES/bsql.config.dm
deleted file mode 100644
index 3f2e8c4d70..0000000000
--- a/code/__DEFINES/bsql.config.dm
+++ /dev/null
@@ -1,6 +0,0 @@
-#define BSQL_EXTERNAL_CONFIGURATION
-#define BSQL_DEL_PROC(path) ##path/Destroy()
-#define BSQL_DEL_CALL(obj) qdel(##obj)
-#define BSQL_IS_DELETED(obj) (QDELETED(obj))
-#define BSQL_PROTECT_DATUM(path) GENERAL_PROTECT_DATUM(##path)
-#define BSQL_ERROR(message) SSdbcore.ReportError(message)
diff --git a/code/__DEFINES/bsql.dm b/code/__DEFINES/bsql.dm
deleted file mode 100644
index 8f2040449a..0000000000
--- a/code/__DEFINES/bsql.dm
+++ /dev/null
@@ -1,135 +0,0 @@
-//BSQL - DMAPI
-#define BSQL_VERSION "v1.3.0.0"
-
-//types of connections
-#define BSQL_CONNECTION_TYPE_MARIADB "MySql"
-#define BSQL_CONNECTION_TYPE_SQLSERVER "SqlServer"
-
-#define BSQL_DEFAULT_TIMEOUT 5
-#define BSQL_DEFAULT_THREAD_LIMIT 50
-
-//Call this before rebooting or shutting down your world to clean up gracefully. This invalidates all active connection and operation datums
-/world/proc/BSQL_Shutdown()
- return
-
-/*
-Called whenever a library call is made with verbose information, override and do with as you please
- message: English debug message
-*/
-/world/proc/BSQL_Debug(msg)
- return
-
-/*
-Create a new database connection, does not perform the actual connect
- connection_type: The BSQL connection_type to use
- asyncTimeout: The timeout to use for normal operations, 0 for infinite, defaults to BSQL_DEFAULT_TIMEOUT
- blockingTimeout: The timeout to use for blocking operations, must be less than or equal to asyncTimeout, 0 for infinite, defaults to asyncTimeout
- threadLimit: The limit of additional threads BSQL will run simultaneously, defaults to BSQL_DEFAULT_THREAD_LIMIT
-*/
-/datum/BSQL_Connection/New(connection_type, asyncTimeout, blockingTimeout, threadLimit)
- return ..()
-
-/*
-Starts an operation to connect to a database. Should only have 1 successful call
- ipaddress: The ip/hostname of the target server
- port: The port of the target server
- username: The username to login to the target server
- password: The password for the target server
- database: Optional database to connect to. Must be used when trying to do database operations, `USE x` is not sufficient
- Returns: A /datum/BSQL_Operation representing the connection or null if an error occurred
-*/
-/datum/BSQL_Connection/proc/BeginConnect(ipaddress, port, username, password, database)
- return
-
-/*
-Properly quotes a string for use by the database. The connection must be open for this proc to succeed
- str: The string to quote
- Returns: The string quoted on success, null on error
-*/
-/datum/BSQL_Connection/proc/Quote(str)
- return
-
-/*
-Starts an operation for a query
- query: The text of the query. Only one query allowed per invocation, no semicolons
- Returns: A /datum/BSQL_Operation/Query representing the running query and subsequent result set or null if an error occurred
-
- Note for MariaDB: The underlying connection is pooled. In order to use connection state based properties (i.e. LAST_INSERT_ID()) you can guarantee multiple queries will use the same connection by running BSQL_DEL_CALL(query) on the finished /datum/BSQL_Operation/Query and then creating the next one with another call to BeginQuery() with no sleeps in between
-*/
-/datum/BSQL_Connection/proc/BeginQuery(query)
- return
-
-/*
-Checks if the operation is complete. This, in some cases must be called multiple times with false return before a result is present regardless of timespan. For best performance check it once per tick
-
- Returns: TRUE if the operation is complete, FALSE if it's not, null on error
-*/
-/datum/BSQL_Operation/proc/IsComplete()
- return
-
-/*
-Blocks the entire game until the given operation completes. IsComplete should not be checked after calling this to avoid potential side effects.
-
-Returns: TRUE on success, FALSE if the operation wait time exceeded the connection's blockingTimeout setting
-*/
-/datum/BSQL_Operation/proc/WaitForCompletion()
- return
-
-/*
-Get the error message associated with an operation. Should not be used while IsComplete() returns FALSE
-
- Returns: The error message, if any. null otherwise
-*/
-/datum/BSQL_Operation/proc/GetError()
- return
-
-/*
-Get the error code associated with an operation. Should not be used while IsComplete() returns FALSE
-
- Returns: The error code, if any. null otherwise
-*/
-/datum/BSQL_Operation/proc/GetErrorCode()
- return
-
-/*
-Gets an associated list of column name -> value representation of the most recent row in the query. Only valid if IsComplete() returns TRUE. If this returns null and no errors are present there are no more results in the query. Important to note that once IsComplete() returns TRUE it must not be called again without checking this or the row values may be lost
-
- Returns: An associated list of column name -> value for the row. Values will always be either strings or null
-*/
-/datum/BSQL_Operation/Query/proc/CurrentRow()
- return
-
-
-/*
-Code configuration options below
-
-Define this to avoid modifying this file but the following defines must be declared somewhere else before BSQL/includes.dm is included
-*/
-#ifndef BSQL_EXTERNAL_CONFIGURATION
-
-//Modify this if you disagree with byond's GC schemes. Ensure this is called for all connections and operations when they are deleted or they will leak native resources until /world/proc/BSQL_Shutdown() is called
-#define BSQL_DEL_PROC(path) ##path/Del()
-
-//The equivalent of calling del() in your codebase
-#define BSQL_DEL_CALL(obj) del(##obj)
-
-//Returns TRUE if an object is delete
-#define BSQL_IS_DELETED(obj) (obj == null)
-
-//Modify this to add protections to the connection and query datums
-#define BSQL_PROTECT_DATUM(path)
-
-//Modify this to change up error handling for the library
-#define BSQL_ERROR(message) CRASH("BSQL: [##message]")
-
-#endif
-
-/*
-Copyright 2018 Jordan Brown
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-*/
diff --git a/code/__DEFINES/cargo.dm b/code/__DEFINES/cargo.dm
index ac5272057a..40a50fe1e6 100644
--- a/code/__DEFINES/cargo.dm
+++ b/code/__DEFINES/cargo.dm
@@ -30,26 +30,36 @@
#define POD_SHAPE_NORML 1
#define POD_SHAPE_OTHER 2
+#define POD_TRANSIT "1"
+#define POD_FALLING "2"
+#define POD_OPENING "3"
+#define POD_LEAVING "4"
+
#define SUPPLYPOD_X_OFFSET -16
+/// The baseline unit for cargo crates. Adjusting this will change the cost of all in-game shuttles, crate export values, bounty rewards, and all supply pack import values, as they use this as their unit of measurement.
+#define CARGO_CRATE_VALUE 200
+
GLOBAL_LIST_EMPTY(supplypod_loading_bays)
GLOBAL_LIST_INIT(podstyles, list(\
- list(POD_SHAPE_NORML, "pod", TRUE, "default", "yellow", RUBBLE_NORMAL, "supply pod", "A Nanotrasen supply drop pod."),\
- list(POD_SHAPE_NORML, "advpod", TRUE, "bluespace", "blue", RUBBLE_NORMAL, "bluespace supply pod" , "A Nanotrasen Bluespace supply pod. Teleports back to CentCom after delivery."),\
- list(POD_SHAPE_NORML, "advpod", TRUE, "centcom", "blue", RUBBLE_NORMAL, "\improper CentCom supply pod", "A Nanotrasen supply pod, this one has been marked with Central Command's designations. Teleports back to CentCom after delivery."),\
- list(POD_SHAPE_NORML, "darkpod", TRUE, "syndicate", "red", RUBBLE_NORMAL, "blood-red supply pod", "An intimidating supply pod, covered in the blood-red markings of the Syndicate. It's probably best to stand back from this."),\
- list(POD_SHAPE_NORML, "darkpod", TRUE, "deathsquad", "blue", RUBBLE_NORMAL, "\improper Deathsquad drop pod", "A Nanotrasen drop pod. This one has been marked the markings of Nanotrasen's elite strike team."),\
- list(POD_SHAPE_NORML, "pod", TRUE, "cultist", "red", RUBBLE_NORMAL, "bloody supply pod", "A Nanotrasen supply pod covered in scratch-marks, blood, and strange runes."),\
- list(POD_SHAPE_OTHER, "missile", FALSE, FALSE, FALSE, RUBBLE_THIN, "cruise missile", "A big ass missile that didn't seem to fully detonate. It was likely launched from some far-off deep space missile silo. There appears to be an auxillery payload hatch on the side, though manually opening it is likely impossible."),\
- list(POD_SHAPE_OTHER, "smissile", FALSE, FALSE, FALSE, RUBBLE_THIN, "\improper Syndicate cruise missile", "A big ass, blood-red missile that didn't seem to fully detonate. It was likely launched from some deep space Syndicate missile silo. There appears to be an auxillery payload hatch on the side, though manually opening it is likely impossible."),\
- list(POD_SHAPE_OTHER, "box", TRUE, FALSE, FALSE, RUBBLE_WIDE, "\improper Aussec supply crate", "An incredibly sturdy supply crate, designed to withstand orbital re-entry. Has 'Aussec Armory - 2532' engraved on the side."),\
- list(POD_SHAPE_NORML, "clownpod", TRUE, "clown", "green", RUBBLE_NORMAL, "\improper HONK pod", "A brightly-colored supply pod. It likely originated from the Clown Federation."),\
- list(POD_SHAPE_OTHER, "orange", TRUE, FALSE, FALSE, RUBBLE_NONE, "\improper Orange", "An angry orange."),\
- list(POD_SHAPE_OTHER, FALSE, FALSE, FALSE, FALSE, RUBBLE_NONE, "\improper S.T.E.A.L.T.H. pod MKVII", "A supply pod that, under normal circumstances, is completely invisible to conventional methods of detection. How are you even seeing this?"),\
- list(POD_SHAPE_OTHER, "gondola", FALSE, FALSE, FALSE, RUBBLE_NONE, "gondola", "The silent walker. This one seems to be part of a delivery agency."),\
- list(POD_SHAPE_OTHER, FALSE, FALSE, FALSE, FALSE, RUBBLE_NONE, FALSE, FALSE, "rl_click", "give_po")\
+ list(POD_SHAPE_NORML, "pod", TRUE, "default", "yellow", RUBBLE_NORMAL, "supply pod", "A Nanotrasen supply drop pod."),\
+ list(POD_SHAPE_NORML, "advpod", TRUE, "bluespace", "blue", RUBBLE_NORMAL, "bluespace supply pod" , "A Nanotrasen Bluespace supply pod. Teleports back to CentCom after delivery."),\
+ list(POD_SHAPE_NORML, "advpod", TRUE, "centcom", "blue", RUBBLE_NORMAL, "\improper CentCom supply pod", "A Nanotrasen supply pod, this one has been marked with Central Command's designations. Teleports back to CentCom after delivery."),\
+ list(POD_SHAPE_NORML, "darkpod", TRUE, "syndicate", "red", RUBBLE_NORMAL, "blood-red supply pod", "An intimidating supply pod, covered in the blood-red markings of the Syndicate. It's probably best to stand back from this."),\
+ list(POD_SHAPE_NORML, "darkpod", TRUE, "deathsquad", "blue", RUBBLE_NORMAL, "\improper Deathsquad drop pod", "A Nanotrasen drop pod. This one has been marked the markings of Nanotrasen's elite strike team."),\
+ list(POD_SHAPE_NORML, "pod", TRUE, "cultist", "red", RUBBLE_NORMAL, "bloody supply pod", "A Nanotrasen supply pod covered in scratch-marks, blood, and strange runes."),\
+ list(POD_SHAPE_OTHER, "missile", FALSE, FALSE, FALSE, RUBBLE_THIN, "cruise missile", "A big ass missile that didn't seem to fully detonate. It was likely launched from some far-off deep space missile silo. There appears to be an auxillery payload hatch on the side, though manually opening it is likely impossible."),\
+ list(POD_SHAPE_OTHER, "smissile", FALSE, FALSE, FALSE, RUBBLE_THIN, "\improper Syndicate cruise missile", "A big ass, blood-red missile that didn't seem to fully detonate. It was likely launched from some deep space Syndicate missile silo. There appears to be an auxillery payload hatch on the side, though manually opening it is likely impossible."),\
+ list(POD_SHAPE_OTHER, "box", TRUE, FALSE, FALSE, RUBBLE_WIDE, "\improper Aussec supply crate", "An incredibly sturdy supply crate, designed to withstand orbital re-entry. Has 'Aussec Armory - 2532' engraved on the side."),\
+ list(POD_SHAPE_NORML, "clownpod", TRUE, "clown", "green", RUBBLE_NORMAL, "\improper HONK pod", "A brightly-colored supply pod. It likely originated from the Clown Federation."),\
+ list(POD_SHAPE_OTHER, "orange", TRUE, FALSE, FALSE, RUBBLE_NONE, "\improper Orange", "An angry orange."),\
+ list(POD_SHAPE_OTHER, FALSE, FALSE, FALSE, FALSE, RUBBLE_NONE, "\improper S.T.E.A.L.T.H. pod MKVII", "A supply pod that, under normal circumstances, is completely invisible to conventional methods of detection. How are you even seeing this?"),\
+ list(POD_SHAPE_OTHER, "gondola", FALSE, FALSE, FALSE, RUBBLE_NONE, "gondola", "The silent walker. This one seems to be part of a delivery agency."),\
+ list(POD_SHAPE_OTHER, FALSE, FALSE, FALSE, FALSE, RUBBLE_NONE, FALSE, FALSE, "rl_click", "give_po")\
))
+
+//cit
#define PACK_GOODY_NONE 0
#define PACK_GOODY_PUBLIC 1 //can be bought by both privates and cargo
#define PACK_GOODY_PRIVATE 2 //can be bought only by privates
diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm
index fb461acfa4..9f45d8da79 100644
--- a/code/__DEFINES/colors.dm
+++ b/code/__DEFINES/colors.dm
@@ -52,3 +52,9 @@
#define COLOR_ASSEMBLY_BLUE "#38559E"
#define COLOR_ASSEMBLY_PURPLE "#6F6192"
#define COLOR_ASSEMBLY_PINK "#ff4adc"
+
+#define COLOR_WHITE "#FFFFFF"
+#define COLOR_VERY_LIGHT_GRAY "#EEEEEE"
+#define COLOR_SILVER "#C0C0C0"
+#define COLOR_GRAY "#808080"
+#define COLOR_HALF_TRANSPARENT_BLACK "#0000007A"
diff --git a/code/__DEFINES/configuration.dm b/code/__DEFINES/configuration.dm
index 0428a16828..9915563cab 100644
--- a/code/__DEFINES/configuration.dm
+++ b/code/__DEFINES/configuration.dm
@@ -22,3 +22,5 @@
#define POLICYCONFIG_ON_DEFIB_LATE "ON_DEFIB_LATE"
/// Displayed to pyroclastic slimes on spawn
#define POLICYCONFIG_ON_PYROCLASTIC_SENTIENT "PYROCLASTIC_SLIME"
+/// Displayed to pAIs on spawn
+#define POLICYCONFIG_PAI "PAI_SPAWN"
diff --git a/code/__DEFINES/dcs/signals.dm b/code/__DEFINES/dcs/signals.dm
index 42199174e7..c12ec20de2 100644
--- a/code/__DEFINES/dcs/signals.dm
+++ b/code/__DEFINES/dcs/signals.dm
@@ -22,7 +22,7 @@
#define COMPONENT_GLOB_BLOCK_CINEMATIC 1
// signals from globally accessible objects
-/// from SSsun when the sun changes position : (azimuth)
+/// from SSsun when the sun changes position : (primary_sun, suns)
#define COMSIG_SUN_MOVED "sun_moved"
//////////////////////////////////////////////////////////////////
diff --git a/code/__DEFINES/economy.dm b/code/__DEFINES/economy.dm
index 746267c15b..715469ff78 100644
--- a/code/__DEFINES/economy.dm
+++ b/code/__DEFINES/economy.dm
@@ -12,6 +12,9 @@
#define MAX_GRANT_SCI 5000
#define MAX_GRANT_SECMEDSRV 3000
+//What should vending machines charge when you buy something in-department.
+#define VENDING_DISCOUNT 0 // price * discount so 0 = 0
+
#define ACCOUNT_CIV "CIV"
#define ACCOUNT_CIV_NAME "Civil Budget"
#define ACCOUNT_ENG "ENG"
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index ff68f18408..faffea0e6e 100644
--- a/code/__DEFINES/is_helpers.dm
+++ b/code/__DEFINES/is_helpers.dm
@@ -67,17 +67,19 @@ GLOBAL_LIST_INIT(turfs_without_ground, typecacheof(list(
#define isslimeperson(A) (is_species(A, /datum/species/jelly/slime))
#define isluminescent(A) (is_species(A, /datum/species/jelly/luminescent))
#define iszombie(A) (is_species(A, /datum/species/zombie))
+#define isskeleton(A) (is_species(A, /datum/species/skeleton))
+#define ismoth(A) (is_species(A, /datum/species/moth))
#define ishumanbasic(A) (is_species(A, /datum/species/human))
#define iscatperson(A) (ishumanbasic(A) && istype(A.dna.species, /datum/species/human/felinid))
-#define isdwarf(A) (is_species(A, /datum/species/dwarf))
+#define isethereal(A) (is_species(A, /datum/species/ethereal))
+#define isvampire(A) (is_species(A,/datum/species/vampire))
#define isdullahan(A) (is_species(A, /datum/species/dullahan))
+
#define isangel(A) (is_species(A, /datum/species/angel))
-#define isvampire(A) (is_species(A, /datum/species/vampire))
#define ismush(A) (is_species(A, /datum/species/mush))
#define isshadow(A) (is_species(A, /datum/species/shadow))
-#define isskeleton(A) (is_species(A, /datum/species/skeleton))
#define isrobotic(A) (is_species(A, /datum/species/ipc) || is_species(A, /datum/species/synthliz))
-#define isethereal(A) (is_species(A, /datum/species/ethereal))
+#define isdwarf(A) (is_species(A, /datum/species/dwarf))
// Citadel specific species
#define isipcperson(A) (is_species(A, /datum/species/ipc))
@@ -143,6 +145,10 @@ GLOBAL_LIST_INIT(turfs_without_ground, typecacheof(list(
#define ishostile(A) (istype(A, /mob/living/simple_animal/hostile))
+// #define israt(A) (istype(A, /mob/living/simple_animal/hostile/rat))
+
+// #define isregalrat(A) (istype(A, /mob/living/simple_animal/hostile/regalrat))
+
#define isswarmer(A) (istype(A, /mob/living/simple_animal/hostile/swarmer))
#define isguardian(A) (istype(A, /mob/living/simple_animal/hostile/guardian))
@@ -155,6 +161,7 @@ GLOBAL_LIST_INIT(turfs_without_ground, typecacheof(list(
#define isclown(A) (istype(A, /mob/living/simple_animal/hostile/retaliate/clown))
+
//Misc mobs
#define isobserver(A) (istype(A, /mob/dead/observer))
@@ -184,6 +191,8 @@ GLOBAL_LIST_INIT(turfs_without_ground, typecacheof(list(
#define isitem(A) (istype(A, /obj/item))
+#define isstack(A) (istype(A, /obj/item/stack))
+
#define isgrenade(A) (istype(A, /obj/item/grenade))
#define islandmine(A) (istype(A, /obj/effect/mine))
@@ -206,6 +215,8 @@ GLOBAL_LIST_INIT(turfs_without_ground, typecacheof(list(
#define isclothing(A) (istype(A, /obj/item/clothing))
+#define iscash(A) (istype(A, /obj/item/coin) || istype(A, /obj/item/stack/spacecash) || istype(A, /obj/item/holochip))
+
#define isbodypart(A) (istype(A, /obj/item/bodypart))
#define isprojectile(A) (istype(A, /obj/item/projectile))
@@ -240,3 +251,9 @@ GLOBAL_LIST_INIT(glass_sheet_types, typecacheof(list(
#define isshuttleturf(T) (length(T.baseturfs) && (/turf/baseturf_skipover/shuttle in T.baseturfs))
#define isProbablyWallMounted(O) (O.pixel_x > 20 || O.pixel_x < -20 || O.pixel_y > 20 || O.pixel_y < -20)
+#define isbook(O) (is_type_in_typecache(O, GLOB.book_types))
+
+GLOBAL_LIST_INIT(book_types, typecacheof(list(
+ /obj/item/book,
+ /obj/item/spellbook,
+ /obj/item/storage/book)))
diff --git a/code/__DEFINES/medal.dm b/code/__DEFINES/medal.dm
deleted file mode 100644
index e723c7504e..0000000000
--- a/code/__DEFINES/medal.dm
+++ /dev/null
@@ -1,29 +0,0 @@
-// Medal names
-#define BOSS_KILL_MEDAL "Killer"
-#define ALL_KILL_MEDAL "Exterminator" //Killing all of x type
-#define BOSS_KILL_MEDAL_CRUSHER "Crusher"
-
-//Defines for boss medals
-#define BOSS_MEDAL_MINER "Blood-drunk Miner"
-#define BOSS_MEDAL_BUBBLEGUM "Bubblegum"
-#define BOSS_MEDAL_COLOSSUS "Colossus"
-#define BOSS_MEDAL_DRAKE "Drake"
-#define BOSS_MEDAL_HIEROPHANT "Hierophant"
-#define BOSS_MEDAL_LEGION "Legion"
-#define BOSS_MEDAL_TENDRIL "Tendril"
-#define BOSS_MEDAL_SWARMERS "Swarmer Beacon"
-
-// Score names
-#define HIEROPHANT_SCORE "Hierophants Killed"
-#define BOSS_SCORE "Bosses Killed"
-#define BUBBLEGUM_SCORE "Bubblegum Killed"
-#define COLOSSUS_SCORE "Colossus Killed"
-#define DRAKE_SCORE "Drakes Killed"
-#define LEGION_SCORE "Legion Killed"
-#define SWARMER_BEACON_SCORE "Swarmer Beacons Killed"
-#define TENDRIL_CLEAR_SCORE "Tendrils Killed"
-
-//Misc medals
-#define MEDAL_METEOR "Your Life Before Your Eyes"
-#define MEDAL_PULSE "Jackpot"
-#define MEDAL_TIMEWASTE "Overextended The Joke"
diff --git a/code/__DEFINES/misc.dm b/code/__DEFINES/misc.dm
index 30162594d6..51d1b618fc 100644
--- a/code/__DEFINES/misc.dm
+++ b/code/__DEFINES/misc.dm
@@ -435,8 +435,13 @@ GLOBAL_LIST_INIT(pda_reskins, list(PDA_SKIN_CLASSIC = 'icons/obj/pda.dmi', PDA_S
//text files
#define BRAIN_DAMAGE_FILE "traumas.json"
#define ION_FILE "ion_laws.json"
-#define REDPILL_FILE "redpill.json"
#define PIRATE_NAMES_FILE "pirates.json"
+#define REDPILL_FILE "redpill.json"
+#define ARCADE_FILE "arcade.json"
+// #define BOOMER_FILE "boomer.json"
+// #define LOCATIONS_FILE "locations.json"
+// #define WANTED_FILE "wanted_message.json"
+// #define VISTA_FILE "steve.json"
#define FLESH_SCAR_FILE "wounds/flesh_scar_desc.json"
#define BONE_SCAR_FILE "wounds/bone_scar_desc.json"
#define SCAR_LOC_FILE "wounds/scar_loc.json"
diff --git a/code/__DEFINES/reactions.dm b/code/__DEFINES/reactions.dm
index 8f01f453b6..b5d322a091 100644
--- a/code/__DEFINES/reactions.dm
+++ b/code/__DEFINES/reactions.dm
@@ -21,10 +21,10 @@
#define STIMULUM_FIRST_DROP 0.065
#define STIMULUM_SECOND_RISE 0.0009
#define STIMULUM_ABSOLUTE_DROP 0.00000335
-#define REACTION_OPPRESSION_THRESHOLD 5
+#define REACTION_OPPRESSION_THRESHOLD 10
#define NOBLIUM_FORMATION_ENERGY 2e9 //1 Mole of Noblium takes the planck energy to condense.
//Research point amounts
-#define NOBLIUM_RESEARCH_AMOUNT 1000
+#define NOBLIUM_RESEARCH_AMOUNT 25
#define BZ_RESEARCH_SCALE 4
#define BZ_RESEARCH_MAX_AMOUNT 400
#define MIASMA_RESEARCH_AMOUNT 6
diff --git a/code/__DEFINES/robots.dm b/code/__DEFINES/robots.dm
index dda9cfd430..b31fa22962 100644
--- a/code/__DEFINES/robots.dm
+++ b/code/__DEFINES/robots.dm
@@ -8,7 +8,7 @@
#define DEFAULT_SCAN_RANGE 7 //default view range for finding targets.
-//Mode defines
+//Mode defines. If you add a new one make sure you update mode_name in /mob/living/simple_animal/bot
#define BOT_IDLE 0 // idle
#define BOT_HUNT 1 // found target, hunting
#define BOT_PREP_ARREST 2 // at target, preparing to arrest
@@ -27,7 +27,8 @@
#define BOT_NAV 15 // computing navigation
#define BOT_WAIT_FOR_NAV 16 // waiting for nav computation
#define BOT_NO_ROUTE 17 // no destination beacon found (or no route)
-#define BOT_TIPPED 18 // someone tipped a medibot over ;_;
+#define BOT_SHOWERSTANCE 18 // cleaning unhygienic humans
+#define BOT_TIPPED 19 // someone tipped a medibot over ;_;
//Bot types
#define SEC_BOT (1<<0) // Secutritrons (Beepsky) and ED-209s
@@ -37,6 +38,7 @@
#define MED_BOT (1<<4) // Medibots
#define HONK_BOT (1<<5) // Honkbots & ED-Honks
#define FIRE_BOT (1<<6) // Firebots
+#define HYGIENE_BOT (1<<7) // Hygienebots
//AI notification defines
#define NEW_BORG 1
@@ -69,8 +71,15 @@
//Checks to determine borg availability depending on the server's config. These are defines in the interest of reducing copypasta
#define BORG_SEC_AVAILABLE (!CONFIG_GET(flag/disable_secborg) && GLOB.security_level >= CONFIG_GET(number/minimum_secborg_alert))
-//silicon_privileges flags
+//silicon_priviledges flags
#define PRIVILEGES_SILICON (1<<0)
#define PRIVILEGES_PAI (1<<1)
#define PRIVILEGES_BOT (1<<2)
#define PRIVILEGES_DRONE (1<<3)
+
+#define BORG_LAMP_CD_RESET -1 //special value to reset cyborg's lamp_cooldown
+
+/// Defines for whether or not module slots are broken.
+#define BORG_MODULE_ALL_DISABLED (1<<0)
+#define BORG_MODULE_TWO_DISABLED (1<<1)
+#define BORG_MODULE_THREE_DISABLED (1<<2)
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index e84f6eb50b..9c67a6b36c 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -108,7 +108,7 @@
#define INIT_ORDER_SOUNDS 83
#define INIT_ORDER_INSTRUMENTS 82
#define INIT_ORDER_VIS 80
-// #define INIT_ORDER_ACHIEVEMENTS 77
+#define INIT_ORDER_ACHIEVEMENTS 77
#define INIT_ORDER_RESEARCH 75
#define INIT_ORDER_EVENTS 70
#define INIT_ORDER_JOBS 65
diff --git a/code/__DEFINES/tgs.dm b/code/__DEFINES/tgs.dm
index 3225f14d8c..2562bfe4d3 100644
--- a/code/__DEFINES/tgs.dm
+++ b/code/__DEFINES/tgs.dm
@@ -1,6 +1,6 @@
// tgstation-server DMAPI
-#define TGS_DMAPI_VERSION "5.2.10"
+#define TGS_DMAPI_VERSION "5.2.9"
// All functions and datums outside this document are subject to change with any version and should not be relied on.
@@ -67,7 +67,7 @@
#define TGS_EVENT_REPO_CHECKOUT 1
/// When the repository performs a fetch operation. No parameters
#define TGS_EVENT_REPO_FETCH 2
-/// When the repository test merges. Parameters: PR Number, PR Sha, (Nullable) Comment made by TGS user
+/// When the repository merges a pull request. Parameters: PR Number, PR Sha, (Nullable) Comment made by TGS user
#define TGS_EVENT_REPO_MERGE_PULL_REQUEST 3
/// Before the repository makes a sychronize operation. Parameters: Absolute repostiory path
#define TGS_EVENT_REPO_PRE_SYNCHRONIZE 4
@@ -190,21 +190,21 @@
/// Represents a merge of a GitHub pull request.
/datum/tgs_revision_information/test_merge
- /// The test merge number.
+ /// The pull request number.
var/number
- /// The test merge source's title when it was merged.
+ /// The pull request title when it was merged.
var/title
- /// The test merge source's body when it was merged.
+ /// The pull request body when it was merged.
var/body
- /// The Username of the test merge source's author.
+ /// The GitHub username of the pull request's author.
var/author
- /// An http URL to the test merge source.
+ /// An http URL to the pull request.
var/url
- /// The SHA of the test merge when that was merged.
+ /// The SHA of the pull request when that was merged.
var/pull_request_commit
- /// ISO 8601 timestamp of when the test merge was created on TGS.
+ /// ISO 8601 timestamp of when the pull request was merged.
var/time_merged
- /// Optional comment left by the TGS user who initiated the merge.
+ /// (Nullable) Comment left by the TGS user who initiated the merge..
var/comment
/// Represents a connected chat channel.
diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm
index fe46fdc710..99a2e9d0ab 100644
--- a/code/__DEFINES/vv.dm
+++ b/code/__DEFINES/vv.dm
@@ -90,6 +90,9 @@
#define VV_HK_TRIGGER_EMP "empulse"
#define VV_HK_TRIGGER_EXPLOSION "explode"
#define VV_HK_AUTO_RENAME "auto_rename"
+// #define VV_HK_RADIATE "radiate"
+#define VV_HK_EDIT_FILTERS "edit_filters"
+// #define VV_HK_ADD_AI "add_ai"
// /obj
#define VV_HK_OSAY "osay"
diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm
index a554397c41..31f34c5d4c 100644
--- a/code/__HELPERS/_lists.dm
+++ b/code/__HELPERS/_lists.dm
@@ -614,7 +614,7 @@
used_key_list[input_key] = 1
return input_key
-#if DM_VERSION > 513
+#if DM_VERSION > 514
#error Remie said that lummox was adding a way to get a lists
#error contents via list.values, if that is true remove this
#error otherwise, update the version and bug lummox
diff --git a/code/__HELPERS/chat.dm b/code/__HELPERS/chat.dm
new file mode 100644
index 0000000000..57824b6286
--- /dev/null
+++ b/code/__HELPERS/chat.dm
@@ -0,0 +1,74 @@
+/*
+
+Here's how to use the chat system with configs
+
+send2adminchat is a simple function that broadcasts to admin channels
+
+send2chat is a bit verbose but can be very specific
+
+The second parameter is a string, this string should be read from a config.
+What this does is dictacte which TGS4 channels can be sent to.
+
+For example if you have the following channels in tgs4 set up
+- Channel 1, Tag: asdf
+- Channel 2, Tag: bombay,asdf
+- Channel 3, Tag: Hello my name is asdf
+- Channel 4, No Tag
+- Channel 5, Tag: butts
+
+and you make the call:
+
+send2chat("I sniff butts", CONFIG_GET(string/where_to_send_sniff_butts))
+
+and the config option is set like:
+
+WHERE_TO_SEND_SNIFF_BUTTS asdf
+
+It will be sent to channels 1 and 2
+
+Alternatively if you set the config option to just:
+
+WHERE_TO_SEND_SNIFF_BUTTS
+
+it will be sent to all connected chats.
+
+In TGS3 it will always be sent to all connected designated game chats.
+*/
+
+/**
+ * Sends a message to TGS chat channels.
+ *
+ * message - The message to send.
+ * channel_tag - Required. If "", the message with be sent to all connected (Game-type for TGS3) channels. Otherwise, it will be sent to TGS4 channels with that tag (Delimited by ','s).
+ */
+/proc/send2chat(message, channel_tag)
+ if(channel_tag == null || !world.TgsAvailable())
+ return
+
+ var/datum/tgs_version/version = world.TgsVersion()
+ if(channel_tag == "" || version.suite == 3)
+ world.TgsTargetedChatBroadcast(message, FALSE)
+ return
+
+ var/list/channels_to_use = list()
+ for(var/I in world.TgsChatChannelInfo())
+ var/datum/tgs_chat_channel/channel = I
+ var/list/applicable_tags = splittext(channel.custom_tag, ",")
+ if(channel_tag in applicable_tags)
+ channels_to_use += channel
+
+ if(channels_to_use.len)
+ world.TgsChatBroadcast(message, channels_to_use)
+
+/**
+ * Sends a message to TGS admin chat channels.
+ *
+ * category - The category of the mssage.
+ * message - The message to send.
+ */
+/proc/send2adminchat(category, message, embed_links = FALSE)
+ category = replacetext(replacetext(category, "\proper", ""), "\improper", "")
+ message = replacetext(replacetext(message, "\proper", ""), "\improper", "")
+ // if(!embed_links)
+ // message = GLOB.has_discord_embeddable_links.Replace(replacetext(message, "`", ""), " ```$1``` ")
+ world.TgsTargetedChatBroadcast("[category] | [message]", TRUE)
diff --git a/code/__HELPERS/filters.dm b/code/__HELPERS/filters.dm
new file mode 100644
index 0000000000..7be7ca5d73
--- /dev/null
+++ b/code/__HELPERS/filters.dm
@@ -0,0 +1,319 @@
+#define ICON_NOT_SET "Not Set"
+
+//This is stored as a nested list instead of datums or whatever because it json encodes nicely for usage in tgui
+GLOBAL_LIST_INIT(master_filter_info, list(
+ "alpha" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "icon" = ICON_NOT_SET,
+ "render_source" = "",
+ "flags" = 0
+ ),
+ "flags" = list(
+ "MASK_INVERSE" = MASK_INVERSE,
+ "MASK_SWAP" = MASK_SWAP
+ )
+ ),
+ "angular_blur" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "size" = 1
+ )
+ ),
+ /* Not supported because making a proper matrix editor on the frontend would be a huge dick pain.
+ Uncomment if you ever implement it
+ "color" = list(
+ "defaults" = list(
+ "color" = matrix(),
+ "space" = FILTER_COLOR_RGB
+ )
+ ),
+ */
+ "displace" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "size" = null,
+ "icon" = ICON_NOT_SET,
+ "render_source" = ""
+ )
+ ),
+ "drop_shadow" = list(
+ "defaults" = list(
+ "x" = 1,
+ "y" = -1,
+ "size" = 1,
+ "offset" = 0,
+ "color" = COLOR_HALF_TRANSPARENT_BLACK
+ )
+ ),
+ "blur" = list(
+ "defaults" = list(
+ "size" = 1
+ )
+ ),
+ "layer" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "icon" = ICON_NOT_SET,
+ "render_source" = "",
+ "flags" = FILTER_OVERLAY,
+ "color" = "",
+ "transform" = null,
+ "blend_mode" = BLEND_DEFAULT
+ )
+ ),
+ "motion_blur" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0
+ )
+ ),
+ "outline" = list(
+ "defaults" = list(
+ "size" = 0,
+ "color" = COLOR_BLACK,
+ "flags" = NONE
+ ),
+ "flags" = list(
+ "OUTLINE_SHARP" = OUTLINE_SHARP,
+ "OUTLINE_SQUARE" = OUTLINE_SQUARE
+ )
+ ),
+ "radial_blur" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "size" = 0.01
+ )
+ ),
+ "rays" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "size" = 16,
+ "color" = COLOR_WHITE,
+ "offset" = 0,
+ "density" = 10,
+ "threshold" = 0.5,
+ "factor" = 0,
+ "flags" = FILTER_OVERLAY | FILTER_UNDERLAY
+ ),
+ "flags" = list(
+ "FILTER_OVERLAY" = FILTER_OVERLAY,
+ "FILTER_UNDERLAY" = FILTER_UNDERLAY
+ )
+ ),
+ "ripple" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "size" = 1,
+ "repeat" = 2,
+ "radius" = 0,
+ "falloff" = 1,
+ "flags" = NONE
+ ),
+ "flags" = list(
+ "WAVE_BOUNDED" = WAVE_BOUNDED
+ )
+ ),
+ "wave" = list(
+ "defaults" = list(
+ "x" = 0,
+ "y" = 0,
+ "size" = 1,
+ "offset" = 0,
+ "flags" = NONE
+ ),
+ "flags" = list(
+ "WAVE_SIDEWAYS" = WAVE_SIDEWAYS,
+ "WAVE_BOUNDED" = WAVE_BOUNDED
+ )
+ )
+))
+
+#undef ICON_NOT_SET
+
+//Helpers to generate lists for filter helpers
+//This is the only practical way of writing these that actually produces sane lists
+/proc/alpha_mask_filter(x, y, icon/icon, render_source, flags)
+ . = list("type" = "alpha")
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(icon))
+ .["icon"] = icon
+ if(!isnull(render_source))
+ .["render_source"] = render_source
+ if(!isnull(flags))
+ .["flags"] = flags
+
+/proc/angular_blur_filter(x, y, size)
+ . = list("type" = "angular_blur")
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(size))
+ .["size"] = size
+
+/proc/color_matrix_filter(matrix/in_matrix, space)
+ . = list("type" = "color")
+ .["color"] = in_matrix
+ if(!isnull(space))
+ .["space"] = space
+
+/proc/displacement_map_filter(icon, render_source, x, y, size = 32)
+ . = list("type" = "displace")
+ if(!isnull(icon))
+ .["icon"] = icon
+ if(!isnull(render_source))
+ .["render_source"] = render_source
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(size))
+ .["size"] = size
+
+/proc/drop_shadow_filter(x, y, size, offset, color)
+ . = list("type" = "drop_shadow")
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(size))
+ .["size"] = size
+ if(!isnull(offset))
+ .["offset"] = offset
+ if(!isnull(color))
+ .["color"] = color
+
+/proc/gauss_blur_filter(size)
+ . = list("type" = "blur")
+ if(!isnull(size))
+ .["size"] = size
+
+/proc/layering_filter(icon, render_source, x, y, flags, color, transform, blend_mode)
+ . = list("type" = "layer")
+ if(!isnull(icon))
+ .["icon"] = icon
+ if(!isnull(render_source))
+ .["render_source"] = render_source
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(color))
+ .["color"] = color
+ if(!isnull(flags))
+ .["flags"] = flags
+ if(!isnull(transform))
+ .["transform"] = transform
+ if(!isnull(blend_mode))
+ .["blend_mode"] = blend_mode
+
+/proc/motion_blur_filter(x, y)
+ . = list("type" = "motion_blur")
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+
+/proc/outline_filter(size, color, flags)
+ . = list("type" = "outline")
+ if(!isnull(size))
+ .["size"] = size
+ if(!isnull(color))
+ .["color"] = color
+ if(!isnull(flags))
+ .["flags"] = flags
+
+/proc/radial_blur_filter(size, x, y)
+ . = list("type" = "radial_blur")
+ if(!isnull(size))
+ .["size"] = size
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+
+/proc/rays_filter(size, color, offset, density, threshold, factor, x, y, flags)
+ . = list("type" = "rays")
+ if(!isnull(size))
+ .["size"] = size
+ if(!isnull(color))
+ .["color"] = color
+ if(!isnull(offset))
+ .["offset"] = offset
+ if(!isnull(density))
+ .["density"] = density
+ if(!isnull(threshold))
+ .["threshold"] = threshold
+ if(!isnull(factor))
+ .["factor"] = factor
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(flags))
+ .["flags"] = flags
+
+/proc/ripple_filter(radius, size, falloff, repeat, x, y, flags)
+ . = list("type" = "ripple")
+ if(!isnull(radius))
+ .["radius"] = radius
+ if(!isnull(size))
+ .["size"] = size
+ if(!isnull(falloff))
+ .["falloff"] = falloff
+ if(!isnull(repeat))
+ .["repeat"] = repeat
+ if(!isnull(flags))
+ .["flags"] = flags
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+
+/proc/wave_filter(x, y, size, offset, flags)
+ . = list("type" = "wave")
+ if(!isnull(size))
+ .["size"] = size
+ if(!isnull(x))
+ .["x"] = x
+ if(!isnull(y))
+ .["y"] = y
+ if(!isnull(offset))
+ .["offset"] = offset
+ if(!isnull(flags))
+ .["flags"] = flags
+
+/proc/apply_wibbly_filters(atom/in_atom, length)
+ for(var/i in 1 to 7)
+ //This is a very baffling and strange way of doing this but I am just preserving old functionality
+ var/X
+ var/Y
+ var/rsq
+ do
+ X = 60*rand() - 30
+ Y = 60*rand() - 30
+ rsq = X*X + Y*Y
+ while(rsq<100 || rsq>900) // Yeah let's just loop infinitely due to bad luck what's the worst that could happen?
+ var/random_roll = rand()
+ in_atom.add_filter("wibbly-[i]", 5, wave_filter(x = X, y = Y, size = rand() * 2.5 + 0.5, offset = random_roll))
+ var/filter = in_atom.get_filter("wibbly-[i]")
+ animate(filter, offset = random_roll, time = 0, loop = -1, flags = ANIMATION_PARALLEL)
+ animate(offset = random_roll - 1, time = rand() * 20 + 10)
+
+/proc/remove_wibbly_filters(atom/in_atom)
+ var/filter
+ for(var/i in 1 to 7)
+ filter = in_atom.get_filter("wibbly-[i]")
+ animate(filter)
+ in_atom.remove_filter("wibbly-[i]")
diff --git a/code/__HELPERS/roundend.dm b/code/__HELPERS/roundend.dm
index a860a8cd3f..afd8a7c223 100644
--- a/code/__HELPERS/roundend.dm
+++ b/code/__HELPERS/roundend.dm
@@ -1,81 +1,102 @@
-#define POPCOUNT_SURVIVORS "survivors" //Not dead at roundend
-#define POPCOUNT_ESCAPEES "escapees" //Not dead and on centcom/shuttles marked as escaped
-#define POPCOUNT_SHUTTLE_ESCAPEES "shuttle_escapees" //Emergency shuttle only.
+#define POPCOUNT_SURVIVORS "survivors" //Not dead at roundend
+#define POPCOUNT_ESCAPEES "escapees" //Not dead and on centcom/shuttles marked as escaped
+#define POPCOUNT_SHUTTLE_ESCAPEES "shuttle_escapees" //Emergency shuttle only.
+#define PERSONAL_LAST_ROUND "personal last round"
+#define SERVER_LAST_ROUND "server last round"
/datum/controller/subsystem/ticker/proc/gather_roundend_feedback()
- var/datum/station_state/end_state = new /datum/station_state()
- end_state.count()
- station_integrity = min(PERCENT(GLOB.start_state.score(end_state)), 100)
gather_antag_data()
record_nuke_disk_location()
var/json_file = file("[GLOB.log_directory]/round_end_data.json")
+ // All but npcs sublists and ghost category contain only mobs with minds
var/list/file_data = list("escapees" = list("humans" = list(), "silicons" = list(), "others" = list(), "npcs" = list()), "abandoned" = list("humans" = list(), "silicons" = list(), "others" = list(), "npcs" = list()), "ghosts" = list(), "additional data" = list())
- var/num_survivors = 0
- var/num_escapees = 0
- var/num_shuttle_escapees = 0
+ var/num_survivors = 0 //Count of non-brain non-camera mobs with mind that are alive
+ var/num_escapees = 0 //Above and on centcom z
+ var/num_shuttle_escapees = 0 //Above and on escape shuttle
var/list/area/shuttle_areas
- if(SSshuttle && SSshuttle.emergency)
+ if(SSshuttle?.emergency)
shuttle_areas = SSshuttle.emergency.shuttle_areas
- for(var/mob/m in GLOB.mob_list)
- var/escaped
- var/category
+
+ for(var/mob/M in GLOB.mob_list)
var/list/mob_data = list()
- if(isnewplayer(m))
+ if(isnewplayer(M))
continue
- if (m.client && m.client.prefs && m.client.prefs.auto_ooc)
- if (!(m.client.prefs.chat_toggles & CHAT_OOC))
- m.client.prefs.chat_toggles ^= CHAT_OOC
- if(m.mind)
- if(m.stat != DEAD && !isbrain(m) && !iscameramob(m))
+ // enable their ooc?
+ if (M.client?.prefs?.auto_ooc)
+ if (!(M.client.prefs.chat_toggles & CHAT_OOC))
+ M.client.prefs.chat_toggles ^= CHAT_OOC
+
+ var/escape_status = "abandoned" //default to abandoned
+ var/category = "npcs" //Default to simple count only bracket
+ var/count_only = TRUE //Count by name only or full info
+
+ mob_data["name"] = M.name
+ if(M.mind)
+ count_only = FALSE
+ mob_data["ckey"] = M.mind.key
+ if(M.stat != DEAD && !isbrain(M) && !iscameramob(M))
num_survivors++
- mob_data += list("name" = m.name, "ckey" = ckey(m.mind.key))
- if(isobserver(m))
- escaped = "ghosts"
- else if(isliving(m))
- var/mob/living/L = m
- mob_data += list("location" = get_area(L), "health" = L.health)
+ if(EMERGENCY_ESCAPED_OR_ENDGAMED && (M.onCentCom() || M.onSyndieBase()))
+ num_escapees++
+ escape_status = "escapees"
+ if(shuttle_areas[get_area(M)])
+ num_shuttle_escapees++
+ if(isliving(M))
+ var/mob/living/L = M
+ mob_data["location"] = get_area(L)
+ mob_data["health"] = L.health
if(ishuman(L))
var/mob/living/carbon/human/H = L
category = "humans"
- mob_data += list("job" = H.mind.assigned_role, "species" = H.dna.species.name)
+ if(H.mind)
+ mob_data["job"] = H.mind.assigned_role
+ else
+ mob_data["job"] = "Unknown"
+ mob_data["species"] = H.dna.species.name
else if(issilicon(L))
category = "silicons"
if(isAI(L))
- mob_data += list("module" = "AI")
- if(isAI(L))
- mob_data += list("module" = "pAI")
- if(iscyborg(L))
+ mob_data["module"] = "AI"
+ else if(ispAI(L))
+ mob_data["module"] = "pAI"
+ else if(iscyborg(L))
var/mob/living/silicon/robot/R = L
- mob_data += list("module" = R.module)
- else
- category = "others"
- mob_data += list("typepath" = m.type)
- if(!escaped)
- if(EMERGENCY_ESCAPED_OR_ENDGAMED && (m.onCentCom() || m.onSyndieBase()))
- escaped = "escapees"
- num_escapees++
- if(shuttle_areas[get_area(m)])
- num_shuttle_escapees++
- else
- escaped = "abandoned"
- if(!m.mind && (!ishuman(m) || !issilicon(m)))
- var/list/npc_nest = file_data["[escaped]"]["npcs"]
- if(npc_nest.Find(initial(m.name)))
- file_data["[escaped]"]["npcs"]["[initial(m.name)]"] += 1
- else
- file_data["[escaped]"]["npcs"]["[initial(m.name)]"] = 1
- else
- if(isobserver(m))
- var/pos = length(file_data["[escaped]"]) + 1
- file_data["[escaped]"]["[pos]"] = mob_data
- else
- if(!category)
+ mob_data["module"] = R.module.name
+ else
category = "others"
- mob_data += list("name" = m.name, "typepath" = m.type)
- var/pos = length(file_data["[escaped]"]["[category]"]) + 1
- file_data["[escaped]"]["[category]"]["[pos]"] = mob_data
+ mob_data["typepath"] = M.type
+ //Ghosts don't care about minds, but we want to retain ckey data etc
+ if(isobserver(M))
+ count_only = FALSE
+ escape_status = "ghosts"
+ if(!M.mind)
+ mob_data["ckey"] = M.key
+ category = null //ghosts are one list deep
+ //All other mindless stuff just gets counts by name
+ if(count_only)
+ var/list/npc_nest = file_data["[escape_status]"]["npcs"]
+ var/name_to_use = initial(M.name)
+ if(ishuman(M))
+ name_to_use = "Unknown Human" //Monkeymen and other mindless corpses
+ if(npc_nest.Find(name_to_use))
+ file_data["[escape_status]"]["npcs"][name_to_use] += 1
+ else
+ file_data["[escape_status]"]["npcs"][name_to_use] = 1
+ else
+ //Mobs with minds and ghosts get detailed data
+ if(category)
+ var/pos = length(file_data["[escape_status]"]["[category]"]) + 1
+ file_data["[escape_status]"]["[category]"]["[pos]"] = mob_data
+ else
+ var/pos = length(file_data["[escape_status]"]) + 1
+ file_data["[escape_status]"]["[pos]"] = mob_data
+
+ var/datum/station_state/end_state = new /datum/station_state()
+ end_state.count()
+ station_integrity = min(PERCENT(GLOB.start_state.score(end_state)), 100)
file_data["additional data"]["station integrity"] = station_integrity
WRITE_FILE(json_file, json_encode(file_data))
+
SSblackbox.record_feedback("nested tally", "round_end_stats", num_survivors, list("survivors", "total"))
SSblackbox.record_feedback("nested tally", "round_end_stats", num_escapees, list("escapees", "total"))
SSblackbox.record_feedback("nested tally", "round_end_stats", GLOB.joined_player_list.len, list("players", "total"))
@@ -169,10 +190,34 @@
file_data["wanted"] = list("author" = "[GLOB.news_network.wanted_issue.scannedUser]", "criminal" = "[GLOB.news_network.wanted_issue.criminal]", "description" = "[GLOB.news_network.wanted_issue.body]", "photo file" = "[GLOB.news_network.wanted_issue.photo_file]")
WRITE_FILE(json_file, json_encode(file_data))
+///Handles random hardcore point rewarding if it applies.
+/datum/controller/subsystem/ticker/proc/HandleRandomHardcoreScore(client/player_client)
+ if(!ishuman(player_client.mob))
+ return FALSE
+ var/mob/living/carbon/human/human_mob = player_client.mob
+ if(!human_mob.hardcore_survival_score) ///no score no glory
+ return FALSE
+
+ if(human_mob.mind && (human_mob.mind.special_role || length(human_mob.mind.antag_datums) > 0))
+ var/didthegamerwin = TRUE
+ for(var/a in human_mob.mind.antag_datums)
+ var/datum/antagonist/antag_datum = a
+ for(var/i in antag_datum.objectives)
+ var/datum/objective/objective_datum = i
+ if(!objective_datum.check_completion())
+ didthegamerwin = FALSE
+ if(!didthegamerwin)
+ return FALSE
+ player_client.give_award(/datum/award/score/hardcore_random, human_mob, round(human_mob.hardcore_survival_score))
+ else if(human_mob.onCentCom())
+ player_client.give_award(/datum/award/score/hardcore_random, human_mob, round(human_mob.hardcore_survival_score))
+
+
/datum/controller/subsystem/ticker/proc/declare_completion()
set waitfor = FALSE
to_chat(world, "
The round has ended.")
+ log_game("The round has ended.")
if(LAZYLEN(GLOB.round_end_notifiees))
world.TgsTargetedChatBroadcast("[GLOB.round_end_notifiees.Join(", ")] the round has ended.", FALSE)
@@ -186,6 +231,19 @@
C.RollCredits()
C.playtitlemusic(40)
CONFIG_SET(flag/suicide_allowed,TRUE) // EORG suicides allowed
+
+ var/speed_round = FALSE
+ if(world.time - SSticker.round_start_time <= 300 SECONDS)
+ speed_round = TRUE
+
+ for(var/client/C in GLOB.clients)
+ if(!C.credits)
+ C.RollCredits()
+ C.playtitlemusic(40)
+ if(speed_round)
+ C.give_award(/datum/award/achievement/misc/speed_round, C.mob)
+ HandleRandomHardcoreScore(C)
+
var/popcount = gather_roundend_feedback()
display_report(popcount)
@@ -204,7 +262,7 @@
var/survival_rate = GLOB.joined_player_list.len ? "[PERCENT(popcount[POPCOUNT_SURVIVORS]/GLOB.joined_player_list.len)]%" : "there's literally no player"
- send2irc("Server", "A round of [mode.name] just ended[mode_result == "undefined" ? "." : " with a [mode_result]."] Survival rate: [survival_rate]")
+ send2adminchat("Server", "A round of [mode.name] just ended[mode_result == "undefined" ? "." : " with a [mode_result]."] Survival rate: [survival_rate]")
if(length(CONFIG_GET(keyed_list/cross_server)))
send_news_report()
@@ -215,6 +273,11 @@
CHECK_TICK
+ // handle_hearts()
+ set_observer_default_invisibility(0, "The round is over! You are now visible to the living.")
+
+ CHECK_TICK
+
//These need update to actually reflect the real antagonists
//Print a list of antagonists to the server log
var/list/total_antagonists = list()
@@ -233,16 +296,13 @@
for(var/antag_name in total_antagonists)
var/list/L = total_antagonists[antag_name]
log_game("[antag_name]s :[L.Join(", ")].")
- set_observer_default_invisibility(0, "The round is over! You are now visible to the living.")
CHECK_TICK
SSdbcore.SetRoundEnd()
//Collects persistence features
- if(mode.station_was_nuked)
- SSpersistence.station_was_destroyed = TRUE
- if(!mode.allow_persistence_save)
- SSpersistence.station_persistence_save_disabled = TRUE
- SSpersistence.CollectData()
+ if(mode.allow_persistence_save)
+ SSpersistence.SaveTCGCards()
+ SSpersistence.CollectData()
//stop collecting feedback during grifftime
SSblackbox.Seal()
@@ -277,11 +337,15 @@
//Antagonists
parts += antag_report()
+ parts += hardcore_random_report()
+
CHECK_TICK
//Medals
parts += medal_report()
//Station Goals
parts += goal_report()
+ //Economy & Money
+ parts += market_report()
listclearnulls(parts)
@@ -324,9 +388,9 @@
parts += "[FOURSPACES]Nobody died this shift!"
if(istype(SSticker.mode, /datum/game_mode/dynamic))
var/datum/game_mode/dynamic/mode = SSticker.mode
- mode.update_playercounts()
- parts += "[FOURSPACES]Final threat level: [mode.threat_level]"
- parts += "[FOURSPACES]Final threat: [mode.threat]"
+ mode.update_playercounts() // ?
+ parts += "[FOURSPACES]Threat level: [mode.threat_level]"
+ parts += "[FOURSPACES]Threat left: [mode.threat]"
parts += "[FOURSPACES]Average threat: [mode.threat_average]"
parts += "[FOURSPACES]Executed rules:"
for(var/datum/dynamic_ruleset/rule in mode.executed_rules)
@@ -343,20 +407,47 @@
/client/proc/roundend_report_file()
return "data/roundend_reports/[ckey].html"
-/datum/controller/subsystem/ticker/proc/show_roundend_report(client/C, previous = FALSE)
+/**
+ * Log the round-end report as an HTML file
+ *
+ * Composits the roundend report, and saves it in two locations.
+ * The report is first saved along with the round's logs
+ * Then, the report is copied to a fixed directory specifically for
+ * housing the server's last roundend report. In this location,
+ * the file will be overwritten at the end of each shift.
+ */
+/datum/controller/subsystem/ticker/proc/log_roundend_report()
+ var/roundend_file = file("[GLOB.log_directory]/round_end_data.html")
+ var/list/parts = list()
+ parts += "
"
+ parts += GLOB.survivor_report
+ parts += "
"
+ parts += GLOB.common_report
+ var/content = parts.Join()
+ //Log the rendered HTML in the round log directory
+ fdel(roundend_file)
+ WRITE_FILE(roundend_file, content)
+ //Place a copy in the root folder, to be overwritten each round.
+ roundend_file = file("data/server_last_roundend_report.html")
+ fdel(roundend_file)
+ WRITE_FILE(roundend_file, content)
+
+/datum/controller/subsystem/ticker/proc/show_roundend_report(client/C, report_type = null)
var/datum/browser/roundend_report = new(C, "roundend")
roundend_report.width = 800
roundend_report.height = 600
var/content
var/filename = C.roundend_report_file()
- if(!previous)
+ if(report_type == PERSONAL_LAST_ROUND) //Look at this player's last round
+ content = file2text(filename)
+ else if (report_type == SERVER_LAST_ROUND) //Look at the last round that this server has seen
+ content = file2text("data/server_last_roundend_report.html")
+ else //report_type is null, so make a new report based on the current round and show that to the player
var/list/report_parts = list(personal_report(C), GLOB.common_report)
content = report_parts.Join()
- remove_verb(C, /client/proc/show_previous_roundend_report)
fdel(filename)
text2file(content, filename)
- else
- content = file2text(filename)
+
roundend_report.set_content(content)
roundend_report.stylesheets = list()
roundend_report.add_stylesheet("roundend", 'html/browser/roundend.css')
@@ -393,8 +484,9 @@
/datum/controller/subsystem/ticker/proc/display_report(popcount)
GLOB.common_report = build_roundend_report()
GLOB.survivor_report = survivor_report(popcount)
+ log_roundend_report()
for(var/client/C in GLOB.clients)
- show_roundend_report(C, FALSE)
+ show_roundend_report(C)
give_show_report_button(C)
CHECK_TICK
@@ -412,12 +504,11 @@
if (aiPlayer.connected_robots.len)
var/borg_num = aiPlayer.connected_robots.len
- var/robolist = "
[aiPlayer.real_name]'s minions were: "
+ parts += "
[aiPlayer.real_name]'s minions were:"
for(var/mob/living/silicon/robot/robo in aiPlayer.connected_robots)
borg_num--
if(robo.mind)
- robolist += "[robo.name][robo.mind.hide_ckey ? "" : " (Played by: [robo.mind.key])"] [robo.stat == DEAD ? " (Deactivated)" : ""][borg_num ?", ":""]
"
- parts += "[robolist]"
+ parts += "[robo.name][robo.mind.hide_ckey ? "" : " (Played by: [robo.mind.key])"] [robo.stat == DEAD ? " (Deactivated)" : ""][borg_num ?", ":""]"
if(!borg_spacer)
borg_spacer = TRUE
@@ -444,6 +535,34 @@
parts += G.get_result()
return ""
+///Generate a report for how much money is on station, as well as the richest crewmember on the station.
+/datum/controller/subsystem/ticker/proc/market_report()
+ var/list/parts = list()
+ parts += ""
+ ///This is the richest account on station at roundend.
+ var/datum/bank_account/mr_moneybags
+ ///This is the station's total wealth at the end of the round.
+ var/station_vault = 0
+ ///How many players joined the round.
+ var/total_players = GLOB.joined_player_list.len
+ var/list/typecache_bank = typecacheof(list(/datum/bank_account/department, /datum/bank_account/remote))
+ for(var/i in SSeconomy.generated_accounts)
+ var/datum/bank_account/current_acc = SSeconomy.generated_accounts[i]
+ if(typecache_bank[current_acc.type])
+ continue
+ station_vault += current_acc.account_balance
+ if(!mr_moneybags || mr_moneybags.account_balance < current_acc.account_balance)
+ mr_moneybags = current_acc
+ parts += "There were [station_vault] credits collected by crew this shift.
"
+ if(total_players > 0)
+ parts += "An average of [station_vault/total_players] credits were collected.
"
+ // log_econ("Roundend credit total: [station_vault] credits. Average Credits: [station_vault/total_players]")
+ if(mr_moneybags)
+ parts += "The most affluent crew member at shift end was [mr_moneybags.account_holder] with [mr_moneybags.account_balance] cr!
"
+ else
+ parts += "Somehow, nobody made any money this shift! This'll result in some budget cuts..."
+ return parts
+
/datum/controller/subsystem/ticker/proc/medal_report()
if(GLOB.commendations.len)
var/list/parts = list()
@@ -453,16 +572,40 @@
return "[parts.Join("
")]
"
return ""
+///Generate a report for all players who made it out alive with a hardcore random character and prints their final score
+/datum/controller/subsystem/ticker/proc/hardcore_random_report()
+ . = list()
+ var/list/hardcores = list()
+ for(var/i in GLOB.player_list)
+ if(!ishuman(i))
+ continue
+ var/mob/living/carbon/human/human_player = i
+ if(!human_player.hardcore_survival_score || !human_player.onCentCom() || human_player.stat == DEAD) ///gotta escape nerd
+ continue
+ if(!human_player.mind)
+ continue
+ hardcores += human_player
+ if(!length(hardcores))
+ return
+ . += ""
+ . += "
"
+ for(var/mob/living/carbon/human/human_player in hardcores)
+ . += "- [printplayer(human_player.mind)] with a hardcore random score of [round(human_player.hardcore_survival_score)]
"
+ . += "
"
+
/datum/controller/subsystem/ticker/proc/antag_report()
var/list/result = list()
var/list/all_teams = list()
var/list/all_antagonists = list()
+ // for(var/datum/team/A in GLOB.antagonist_teams)
+ // all_teams |= A
+
for(var/datum/antagonist/A in GLOB.antagonists)
if(!A.owner)
continue
all_teams |= A.get_team()
- all_antagonists += A
+ all_antagonists |= A
for(var/datum/team/T in all_teams)
result += T.roundend_report()
@@ -515,9 +658,9 @@
/datum/action/report/Trigger()
if(owner && GLOB.common_report && SSticker.current_state == GAME_STATE_FINISHED)
- SSticker.show_roundend_report(owner.client, FALSE)
+ SSticker.show_roundend_report(owner.client)
-/datum/action/report/IsAvailable(silent = FALSE)
+/datum/action/report/IsAvailable()
return 1
/datum/action/report/Topic(href,href_list)
@@ -532,7 +675,9 @@
var/jobtext = ""
if(ply.assigned_role)
jobtext = " the [ply.assigned_role]"
- var/text = "[ply.hide_ckey ? "[ply.name][jobtext] " : "[ply.key] was [ply.name][jobtext] and "]"
+ var/text = (ply.hide_ckey ? \
+ "[ply.key] was [ply.name][jobtext] and" \
+ : "[ply.name][jobtext]")
if(ply.current)
if(ply.current.stat == DEAD)
text += " died"
@@ -589,11 +734,9 @@
var/list/sql_admins = list()
for(var/i in GLOB.protected_admins)
var/datum/admins/A = GLOB.protected_admins[i]
- var/sql_ckey = sanitizeSQL(A.target)
- var/sql_rank = sanitizeSQL(A.rank.name)
- sql_admins += list(list("ckey" = "'[sql_ckey]'", "rank" = "'[sql_rank]'"))
+ sql_admins += list(list("ckey" = A.target, "rank" = A.rank.name))
SSdbcore.MassInsert(format_table_name("admin"), sql_admins, duplicate_key = TRUE)
- var/datum/DBQuery/query_admin_rank_update = SSdbcore.NewQuery("UPDATE [format_table_name("player")] p INNER JOIN [format_table_name("admin")] a ON p.ckey = a.ckey SET p.lastadminrank = a.rank")
+ var/datum/db_query/query_admin_rank_update = SSdbcore.NewQuery("UPDATE [format_table_name("player")] p INNER JOIN [format_table_name("admin")] a ON p.ckey = a.ckey SET p.lastadminrank = a.rank")
query_admin_rank_update.Execute()
qdel(query_admin_rank_update)
@@ -626,15 +769,20 @@
flags += "can_edit_flags"
if(!flags.len)
continue
- var/sql_rank = sanitizeSQL(R.name)
var/flags_to_check = flags.Join(" != [R_EVERYTHING] AND ") + " != [R_EVERYTHING]"
- var/datum/DBQuery/query_check_everything_ranks = SSdbcore.NewQuery("SELECT flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")] WHERE rank = '[sql_rank]' AND ([flags_to_check])")
+ var/datum/db_query/query_check_everything_ranks = SSdbcore.NewQuery(
+ "SELECT flags, exclude_flags, can_edit_flags FROM [format_table_name("admin_ranks")] WHERE rank = :rank AND ([flags_to_check])",
+ list("rank" = R.name)
+ )
if(!query_check_everything_ranks.Execute())
qdel(query_check_everything_ranks)
return
if(query_check_everything_ranks.NextRow()) //no row is returned if the rank already has the correct flag value
var/flags_to_update = flags.Join(" = [R_EVERYTHING], ") + " = [R_EVERYTHING]"
- var/datum/DBQuery/query_update_everything_ranks = SSdbcore.NewQuery("UPDATE [format_table_name("admin_ranks")] SET [flags_to_update] WHERE rank = '[sql_rank]'")
+ var/datum/db_query/query_update_everything_ranks = SSdbcore.NewQuery(
+ "UPDATE [format_table_name("admin_ranks")] SET [flags_to_update] WHERE rank = :rank",
+ list("rank" = R.name)
+ )
if(!query_update_everything_ranks.Execute())
qdel(query_update_everything_ranks)
return
diff --git a/code/__HELPERS/text.dm b/code/__HELPERS/text.dm
index 7c251edd88..dec44653af 100644
--- a/code/__HELPERS/text.dm
+++ b/code/__HELPERS/text.dm
@@ -13,10 +13,6 @@
* SQL sanitization
*/
-// Run all strings to be used in an SQL query through this proc first to properly escape out injection attempts.
-/proc/sanitizeSQL(t)
- return SSdbcore.Quote("[t]")
-
/proc/format_table_name(table as text)
return CONFIG_GET(string/feedback_tableprefix) + table
@@ -670,7 +666,7 @@ GLOBAL_LIST_INIT(binary, list("0","1"))
if(fexists(log))
oldjson = json_decode(file2text(log))
oldentries = oldjson["data"]
- if(!isemptylist(oldentries))
+ if(length(oldentries))
for(var/string in accepted)
for(var/old in oldentries)
if(string == old)
@@ -680,7 +676,7 @@ GLOBAL_LIST_INIT(binary, list("0","1"))
var/list/finalized = list()
finalized = accepted.Copy() + oldentries.Copy() //we keep old and unreferenced phrases near the bottom for culling
listclearnulls(finalized)
- if(!isemptylist(finalized) && length(finalized) > storemax)
+ if(length(finalized) > storemax)
finalized.Cut(storemax + 1)
fdel(log)
diff --git a/code/__HELPERS/unsorted.dm b/code/__HELPERS/unsorted.dm
index 71bbfe64fe..ee4b09d42c 100644
--- a/code/__HELPERS/unsorted.dm
+++ b/code/__HELPERS/unsorted.dm
@@ -1247,28 +1247,40 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new)
#define FOR_DVIEW_END GLOB.dview_mob.loc = null
-//can a window be here, or is there a window blocking it?
-/proc/valid_window_location(turf/T, dir_to_check)
- if(!T)
+/**
+ * Checks whether the target turf is in a valid state to accept a directional window
+ * or other directional pseudo-dense object such as railings.
+ *
+ * Returns FALSE if the target turf cannot accept a directional window or railing.
+ * Returns TRUE otherwise.
+ *
+ * Arguments:
+ * * dest_turf - The destination turf to check for existing windows and railings
+ * * test_dir - The prospective dir of some atom you'd like to put on this turf.
+ * * is_fulltile - Whether the thing you're attempting to move to this turf takes up the entire tile or whether it supports multiple movable atoms on its tile.
+ */
+/proc/valid_window_location(turf/dest_turf, test_dir, is_fulltile = FALSE)
+ if(!dest_turf)
return FALSE
- for(var/obj/O in T)
- if(istype(O, /obj/machinery/door/window) && (O.dir == dir_to_check || dir_to_check == FULLTILE_WINDOW_DIR))
- return FALSE
- if(istype(O, /obj/structure/windoor_assembly))
- var/obj/structure/windoor_assembly/W = O
- if(W.ini_dir == dir_to_check || dir_to_check == FULLTILE_WINDOW_DIR)
+ for(var/obj/turf_content in dest_turf)
+ if(istype(turf_content, /obj/machinery/door/window))
+ if((turf_content.dir == test_dir) || is_fulltile)
return FALSE
- if(istype(O, /obj/structure/window))
- var/obj/structure/window/W = O
- if(W.ini_dir == dir_to_check || W.ini_dir == FULLTILE_WINDOW_DIR || dir_to_check == FULLTILE_WINDOW_DIR)
+ if(istype(turf_content, /obj/structure/windoor_assembly))
+ var/obj/structure/windoor_assembly/windoor_assembly = turf_content
+ if(windoor_assembly.dir == test_dir || is_fulltile)
return FALSE
- if(istype(O, /obj/structure/railing))
- var/obj/structure/railing/rail = O
- if(rail.ini_dir == dir_to_check || rail.ini_dir == FULLTILE_WINDOW_DIR || dir_to_check == FULLTILE_WINDOW_DIR)
+ if(istype(turf_content, /obj/structure/window))
+ var/obj/structure/window/window_structure = turf_content
+ if(window_structure.dir == test_dir || window_structure.fulltile || is_fulltile)
+ return FALSE
+ if(istype(turf_content, /obj/structure/railing))
+ var/obj/structure/railing/rail = turf_content
+ if(rail.dir == test_dir || is_fulltile)
return FALSE
return TRUE
-/proc/pass()
+/proc/pass(...)
return
/proc/get_mob_or_brainmob(occupant)
@@ -1579,33 +1591,6 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new)
for(var/i in 1 to items_list[each_item])
new each_item(where_to)
-//sends a message to chat
-//config_setting should be one of the following
-//null - noop
-//empty string - use TgsTargetBroadcast with admin_only = FALSE
-//other string - use TgsChatBroadcast with the tag that matches config_setting, only works with TGS4, if using TGS3 the above method is used
-/proc/send2chat(message, config_setting)
- if(config_setting == null)
- return
-
- UNTIL(GLOB.tgs_initialized)
- if(!world.TgsAvailable())
- return
-
- var/datum/tgs_version/version = world.TgsVersion()
- if(config_setting == "" || version.suite == 3)
- world.TgsTargetedChatBroadcast(message, FALSE)
- return
-
- var/list/channels_to_use = list()
- for(var/I in world.TgsChatChannelInfo())
- var/datum/tgs_chat_channel/channel = I
- if(channel.tag == config_setting)
- channels_to_use += channel
-
- if(channels_to_use.len)
- world.TgsChatBroadcast()
-
//Checks to see if either the victim has a garlic necklace or garlic in their blood
/proc/blood_sucking_checks(var/mob/living/carbon/target, check_neck, check_blood)
//Bypass this if the target isnt carbon.
diff --git a/code/_globalvars/lists/medals.dm b/code/_globalvars/lists/achievements.dm
old mode 100755
new mode 100644
similarity index 100%
rename from code/_globalvars/lists/medals.dm
rename to code/_globalvars/lists/achievements.dm
diff --git a/code/_globalvars/lists/flavor_misc.dm b/code/_globalvars/lists/flavor_misc.dm
index 41068048a1..ac86912c84 100644
--- a/code/_globalvars/lists/flavor_misc.dm
+++ b/code/_globalvars/lists/flavor_misc.dm
@@ -126,6 +126,7 @@ GLOBAL_LIST_INIT(ai_core_display_screens, list(
"Not Malf",
"Patriot",
"Pirate",
+ "Portrait",
"President",
"Rainbow",
"Clown",
@@ -152,14 +153,20 @@ GLOBAL_LIST_INIT(ai_core_display_screens, list(
"Yes-Man"
))
-/proc/resolve_ai_icon(input)
+/proc/resolve_ai_icon(input, radial_preview = FALSE)
if(!input || !(input in GLOB.ai_core_display_screens))
return "ai"
- else
- if(input == "Random")
- input = pick(GLOB.ai_core_display_screens - "Random")
+ if(radial_preview)
return "ai-[lowertext(input)]"
+ if(input == "Random")
+ input = pick(GLOB.ai_core_display_screens - "Random")
+ if(input == "Portrait")
+ var/datum/portrait_picker/tgui = new(usr)//create the datum
+ tgui.ui_interact(usr)//datum has a tgui component, here we open the window
+ return "ai-portrait" //just take this until they decide
+ return "ai-[lowertext(input)]"
+
GLOBAL_LIST_INIT(security_depts_prefs, list(SEC_DEPT_RANDOM, SEC_DEPT_NONE, SEC_DEPT_ENGINEERING, SEC_DEPT_MEDICAL, SEC_DEPT_SCIENCE, SEC_DEPT_SUPPLY))
//Backpacks
diff --git a/code/_onclick/ai.dm b/code/_onclick/ai.dm
index 59b78577af..f153b5965c 100644
--- a/code/_onclick/ai.dm
+++ b/code/_onclick/ai.dm
@@ -48,7 +48,7 @@
to_chat(src, "You're experiencing a bug. Reconnect immediately to fix it. Admins have been notified.")
if(REALTIMEOFDAY >= chnotify + 9000)
chnotify = REALTIMEOFDAY
- send2irc_adminless_only("NOCHEAT", message)
+ send2tgs_adminless_only("NOCHEAT", message)
return
var/list/modifiers = params2list(params)
@@ -113,7 +113,7 @@
A.AICtrlClick(src)
/mob/living/silicon/ai/AltClickOn(var/atom/A)
A.AIAltClick(src)
-
+
/*
The following criminally helpful code is just the previous code cleaned up;
diff --git a/code/_onclick/hud/_defines.dm b/code/_onclick/hud/_defines.dm
index 467c67e0c3..a1b7a74384 100644
--- a/code/_onclick/hud/_defines.dm
+++ b/code/_onclick/hud/_defines.dm
@@ -17,21 +17,6 @@
Therefore, the top right corner (except during admin shenanigans) is at "15,15"
*/
-//Lower left, persistent menu
-#define ui_inventory "WEST:6,SOUTH:5"
-
-//Middle left indicators
-#define ui_lingchemdisplay "WEST,CENTER-1:15"
-#define ui_lingstingdisplay "WEST:6,CENTER-3:11"
-
-#define ui_devilsouldisplay "WEST:6,CENTER-1:15"
-
-//Lower center, persistent menu
-#define ui_sstore1 "CENTER-5:10,SOUTH:5"
-#define ui_id "CENTER-4:12,SOUTH:5"
-#define ui_belt "CENTER-3:14,SOUTH:5"
-#define ui_back "CENTER-2:14,SOUTH:5"
-
/proc/ui_hand_position(i) //values based on old hand ui positions (CENTER:-/+16,SOUTH:5)
var/x_off = -(!(i % 2))
var/y_off = round((i-1) / 2)
@@ -46,35 +31,23 @@
var/y_off = round((M.held_items.len-1) / 2)
return "CENTER+[x_off]:16,SOUTH+[y_off+1]:5"
+//Lower left, persistent menu
+#define ui_inventory "WEST:6,SOUTH:5"
+
+//Middle left indicators
+#define ui_lingchemdisplay "WEST,CENTER-1:15"
+#define ui_lingstingdisplay "WEST:6,CENTER-3:11"
+
+#define ui_devilsouldisplay "WEST:6,CENTER-1:15"
+
+//Lower center, persistent menu
+#define ui_sstore1 "CENTER-5:10,SOUTH:5"
+#define ui_id "CENTER-4:12,SOUTH:5"
+#define ui_belt "CENTER-3:14,SOUTH:5"
+#define ui_back "CENTER-2:14,SOUTH:5"
#define ui_storage1 "CENTER+1:18,SOUTH:5"
#define ui_storage2 "CENTER+2:20,SOUTH:5"
-#define ui_borg_sensor "CENTER-3:15, SOUTH:5" //borgs
-#define ui_borg_lamp "CENTER-4:15, SOUTH:5" //borgs
-#define ui_borg_thrusters "CENTER-5:15, SOUTH:5" //borgs
-#define ui_inv1 "CENTER-2:16,SOUTH:5" //borgs
-#define ui_inv2 "CENTER-1 :16,SOUTH:5" //borgs
-#define ui_inv3 "CENTER :16,SOUTH:5" //borgs
-#define ui_borg_module "CENTER+1:16,SOUTH:5" //borgs
-#define ui_borg_store "CENTER+2:16,SOUTH:5" //borgs
-#define ui_borg_camera "CENTER+3:21,SOUTH:5" //borgs
-#define ui_borg_album "CENTER+4:21,SOUTH:5" //borgs
-#define ui_borg_language_menu "EAST-1:27,SOUTH+2:8" //borgs
-
-#define ui_monkey_head "CENTER-5:13,SOUTH:5" //monkey
-#define ui_monkey_mask "CENTER-4:14,SOUTH:5" //monkey
-#define ui_monkey_neck "CENTER-3:15,SOUTH:5" //monkey
-#define ui_monkey_back "CENTER-2:16,SOUTH:5" //monkey
-
-//#define ui_alien_storage_l "CENTER-2:14,SOUTH:5"//alien
-#define ui_alien_storage_r "CENTER+1:18,SOUTH:5"//alien
-#define ui_alien_language_menu "EAST-3:26,SOUTH:5" //alien
-
-#define ui_drone_drop "CENTER+1:18,SOUTH:5" //maintenance drones
-#define ui_drone_pull "CENTER+2:2,SOUTH:5" //maintenance drones
-#define ui_drone_storage "CENTER-2:14,SOUTH:5" //maintenance drones
-#define ui_drone_head "CENTER-3:14,SOUTH:5" //maintenance drones
-
//Lower right, persistent menu
#define ui_drop_throw "EAST-1:28,SOUTH+1:7"
#define ui_pull_resist "EAST-2:26,SOUTH+1:7"
@@ -88,11 +61,6 @@
#define ui_language_menu "EAST-5:4,SOUTH:21"//CIT CHANGE - ditto
#define ui_voremode "EAST-5:20,SOUTH:5"
-#define ui_borg_pull "EAST-2:26,SOUTH+1:7"
-#define ui_borg_radio "EAST-1:28,SOUTH+1:7"
-#define ui_borg_intents "EAST-2:26,SOUTH:5"
-
-
//Upper-middle right (alerts)
#define ui_alert1 "EAST-1:28,CENTER+5:27"
#define ui_alert2 "EAST-1:28,CENTER+4:25"
@@ -100,31 +68,70 @@
#define ui_alert4 "EAST-1:28,CENTER+2:21"
#define ui_alert5 "EAST-1:28,CENTER+1:19"
-
//Middle right (status indicators)
#define ui_healthdoll "EAST-1:28,CENTER-2:13"
#define ui_health "EAST-1:28,CENTER-1:15"
#define ui_internal "EAST-1:28,CENTER+1:19"//CIT CHANGE - moves internal icon up a little bit to accommodate for the stamina meter
#define ui_mood "EAST-1:28,CENTER-3:10"
+// #define ui_spacesuit "EAST-1:28,CENTER-4:10"
-//living
+//Pop-up inventory
+#define ui_shoes "WEST+1:8,SOUTH:5"
+#define ui_iclothing "WEST:6,SOUTH+1:7"
+#define ui_oclothing "WEST+1:8,SOUTH+1:7"
+#define ui_gloves "WEST+2:10,SOUTH+1:7"
+#define ui_glasses "WEST:6,SOUTH+3:11"
+#define ui_mask "WEST+1:8,SOUTH+2:9"
+#define ui_ears "WEST+2:10,SOUTH+2:9"
+#define ui_neck "WEST:6,SOUTH+2:9"
+#define ui_head "WEST+1:8,SOUTH+3:11"
+
+//Generic living
#define ui_living_pull "EAST-1:28,CENTER-2:15"
#define ui_living_health "EAST-1:28,CENTER:15"
-//borgs
-#define ui_borg_health "EAST-1:28,CENTER-1:15" //borgs have the health display where humans have the pressure damage indicator.
+//Monkeys
+#define ui_monkey_head "CENTER-5:13,SOUTH:5"
+#define ui_monkey_mask "CENTER-4:14,SOUTH:5"
+#define ui_monkey_neck "CENTER-3:15,SOUTH:5"
+#define ui_monkey_back "CENTER-2:16,SOUTH:5"
-//aliens
-#define ui_alien_health "EAST,CENTER-1:15" //aliens have the health display where humans have the pressure damage indicator.
+//Drones
+#define ui_drone_drop "CENTER+1:18,SOUTH:5"
+#define ui_drone_pull "CENTER+2:2,SOUTH:5"
+#define ui_drone_storage "CENTER-2:14,SOUTH:5"
+#define ui_drone_head "CENTER-3:14,SOUTH:5"
+
+//Cyborgs
+#define ui_borg_health "EAST-1:28,CENTER-1:15"
+#define ui_borg_pull "EAST-2:26,SOUTH+1:7"
+#define ui_borg_radio "EAST-1:28,SOUTH+1:7"
+#define ui_borg_intents "EAST-2:26,SOUTH:5"
+#define ui_borg_lamp "CENTER-3:16, SOUTH:5"
+#define ui_borg_tablet "CENTER-4:16, SOUTH:5"
+#define ui_inv1 "CENTER-2:16,SOUTH:5"
+#define ui_inv2 "CENTER-1 :16,SOUTH:5"
+#define ui_inv3 "CENTER :16,SOUTH:5"
+#define ui_borg_module "CENTER+1:16,SOUTH:5"
+#define ui_borg_store "CENTER+2:16,SOUTH:5"
+#define ui_borg_camera "CENTER+3:21,SOUTH:5"
+#define ui_borg_alerts "CENTER+4:21,SOUTH:5"
+#define ui_borg_language_menu "CENTER+4:21,SOUTH+1:5"
+#define ui_borg_sensor "CENTER-6:16, SOUTH:5" //LEGACY
+#define ui_borg_thrusters "CENTER-5:16, SOUTH:5" //LEGACY
+
+//Aliens
+#define ui_alien_health "EAST,CENTER-1:15"
#define ui_alienplasmadisplay "EAST,CENTER-2:15"
#define ui_alien_queen_finder "EAST,CENTER-3:15"
+#define ui_alien_storage_r "CENTER+1:18,SOUTH:5"
+#define ui_alien_language_menu "EAST-3:26,SOUTH:5"
-//constructs
+//Constructs
#define ui_construct_pull "EAST,CENTER-2:15"
-#define ui_construct_health "EAST,CENTER:15" //same as borgs and humans
+#define ui_construct_health "EAST,CENTER:15"
// AI
-
#define ui_ai_core "SOUTH:6,WEST"
#define ui_ai_camera_list "SOUTH:6,WEST+1"
#define ui_ai_track_with_camera "SOUTH:6,WEST+2"
@@ -143,26 +150,32 @@
#define ui_ai_multicam "SOUTH+1:6,WEST+13"
#define ui_ai_add_multicam "SOUTH+1:6,WEST+14"
-//Pop-up inventory
-#define ui_shoes "WEST+1:8,SOUTH:5"
-#define ui_iclothing "WEST:6,SOUTH+1:7"
-#define ui_oclothing "WEST+1:8,SOUTH+1:7"
-#define ui_gloves "WEST+2:10,SOUTH+1:7"
-
-#define ui_glasses "WEST:6,SOUTH+3:11"
-#define ui_mask "WEST+1:8,SOUTH+2:9"
-#define ui_ears "WEST+2:10,SOUTH+2:9"
-#define ui_neck "WEST:6,SOUTH+2:9"
-#define ui_head "WEST+1:8,SOUTH+3:11"
+// pAI
+// #define ui_pai_software "SOUTH:6,WEST"
+// #define ui_pai_shell "SOUTH:6,WEST+1"
+// #define ui_pai_chassis "SOUTH:6,WEST+2"
+// #define ui_pai_rest "SOUTH:6,WEST+3"
+// #define ui_pai_light "SOUTH:6,WEST+4"
+// #define ui_pai_newscaster "SOUTH:6,WEST+5"
+// #define ui_pai_host_monitor "SOUTH:6,WEST+6"
+// #define ui_pai_crew_manifest "SOUTH:6,WEST+7"
+// #define ui_pai_state_laws "SOUTH:6,WEST+8"
+// #define ui_pai_pda_send "SOUTH:6,WEST+9"
+// #define ui_pai_pda_log "SOUTH:6,WEST+10"
+// #define ui_pai_take_picture "SOUTH:6,WEST+12"
+// #define ui_pai_view_images "SOUTH:6,WEST+13"
//Ghosts
+#define ui_ghost_jumptomob "SOUTH:6,CENTER-3:24"
+#define ui_ghost_orbit "SOUTH:6,CENTER-2:24"
+#define ui_ghost_reenter_corpse "SOUTH:6,CENTER-1:24"
+#define ui_ghost_teleport "SOUTH:6,CENTER:24"
+#define ui_ghost_pai "SOUTH: 6, CENTER+1:24"
+#define ui_ghost_mafia "SOUTH: 6, CENTER+2:24"
+#define ui_ghost_spawners "SOUTH: 6, CENTER+1:24" // LEGACY. SAME LOC AS PAI
-#define ui_ghost_jumptomob "SOUTH:6,CENTER-2:24"
-#define ui_ghost_orbit "SOUTH:6,CENTER-1:24"
-#define ui_ghost_reenter_corpse "SOUTH:6,CENTER:24"
-#define ui_ghost_teleport "SOUTH:6,CENTER+1:24"
-#define ui_ghost_spawners "SOUTH: 6, CENTER+2:24"
+// #define ui_wanted_lvl "NORTH,11"
//UI position overrides for 1:1 screen layout. (default is 7:5)
diff --git a/code/_onclick/hud/plane_master.dm b/code/_onclick/hud/plane_master.dm
index f5b8991e20..4c736c9a2b 100644
--- a/code/_onclick/hud/plane_master.dm
+++ b/code/_onclick/hud/plane_master.dm
@@ -38,9 +38,6 @@
/obj/screen/plane_master/proc/shadow(_size, _offset = 0, _x = 0, _y = 0, _color = "#04080FAA")
filters += filter(type = "drop_shadow", x = _x, y = _y, color = _color, size = _size, offset = _offset)
-/obj/screen/plane_master/proc/clear_filters()
- filters = list()
-
///Contains just the floor
/obj/screen/plane_master/floor
name = "floor plane master"
diff --git a/code/_onclick/hud/robot.dm b/code/_onclick/hud/robot.dm
index 9859e7acdf..33a9cec80c 100644
--- a/code/_onclick/hud/robot.dm
+++ b/code/_onclick/hud/robot.dm
@@ -68,57 +68,17 @@
var/mob/living/silicon/robot/R = usr
R.uneq_active()
-/obj/screen/robot/lamp
- name = "headlamp"
- icon_state = "lamp0"
-
-/obj/screen/robot/lamp/Click()
- if(..())
- return
- var/mob/living/silicon/robot/R = usr
- R.control_headlamp()
-
-/obj/screen/robot/thrusters
- name = "ion thrusters"
- icon_state = "ionpulse0"
-
-/obj/screen/robot/thrusters/Click()
- if(..())
- return
- var/mob/living/silicon/robot/R = usr
- R.toggle_ionpulse()
-
-/obj/screen/robot/sensors
- name = "Sensor Augmentation"
- icon_state = "cyborg_sensor"
-
-/obj/screen/robot/sensors/Click()
- if(..())
- return
- var/mob/living/silicon/S = usr
- S.toggle_sensors()
-
-/obj/screen/robot/language_menu
- name = "silicon language selection"
- icon_state = "talk_wheel"
-
-/obj/screen/robot/language_menu/Click()
- if(..())
- return
- var/mob/living/silicon/S = usr
- S.open_language_menu(usr)
-
/datum/hud/robot
ui_style = 'icons/mob/screen_cyborg.dmi'
/datum/hud/robot/New(mob/owner)
..()
- var/mob/living/silicon/robot/mymobR = mymob
+ // i, Robit
+ var/mob/living/silicon/robot/robit = mymob
var/obj/screen/using
- using = new/obj/screen/robot/language_menu
+ using = new/obj/screen/language_menu
using.screen_loc = ui_borg_language_menu
- using.hud = src
static_inventory += using
//Radio
@@ -128,56 +88,72 @@
static_inventory += using
//Module select
- using = new /obj/screen/robot/module1()
- using.screen_loc = ui_inv1
- using.hud = src
- static_inventory += using
- mymobR.inv1 = using
+ if(!robit.inv1)
+ robit.inv1 = new /obj/screen/robot/module1()
- using = new /obj/screen/robot/module2()
- using.screen_loc = ui_inv2
- using.hud = src
- static_inventory += using
- mymobR.inv2 = using
+ robit.inv1.screen_loc = ui_inv1
+ robit.inv1.hud = src
+ static_inventory += robit.inv1
- using = new /obj/screen/robot/module3()
- using.screen_loc = ui_inv3
- using.hud = src
- static_inventory += using
- mymobR.inv3 = using
+ if(!robit.inv2)
+ robit.inv2 = new /obj/screen/robot/module2()
+
+ robit.inv2.screen_loc = ui_inv2
+ robit.inv2.hud = src
+ static_inventory += robit.inv2
+
+ if(!robit.inv3)
+ robit.inv3 = new /obj/screen/robot/module3()
+
+ robit.inv3.screen_loc = ui_inv3
+ robit.inv3.hud = src
+ static_inventory += robit.inv3
//End of module select
+ using = new /obj/screen/robot/lamp()
+ using.screen_loc = ui_borg_lamp
+ using.hud = src
+ static_inventory += using
+ robit.lampButton = using
+ var/obj/screen/robot/lamp/lampscreen = using
+ lampscreen.robot = robit
+
//Photography stuff
using = new /obj/screen/ai/image_take()
using.screen_loc = ui_borg_camera
using.hud = src
static_inventory += using
- using = new /obj/screen/ai/image_view()
- using.screen_loc = ui_borg_album
- using.hud = src
- static_inventory += using
-
//Sec/Med HUDs
using = new /obj/screen/robot/sensors()
using.screen_loc = ui_borg_sensor
using.hud = src
static_inventory += using
-//Headlamp control
- using = new /obj/screen/robot/lamp()
- using.screen_loc = ui_borg_lamp
+//Borg Integrated Tablet
+ using = new /obj/screen/robot/modPC()
+ using.screen_loc = ui_borg_tablet
+ using.hud = src
+ static_inventory += using
+ robit.interfaceButton = using
+ if(robit.modularInterface)
+ using.vis_contents += robit.modularInterface
+ var/obj/screen/robot/modPC/tabletbutton = using
+ tabletbutton.robot = robit
+
+//Alerts
+ using = new /obj/screen/robot/alerts()
+ using.screen_loc = ui_borg_alerts
using.hud = src
static_inventory += using
- mymobR.lamp_button = using
//Thrusters
using = new /obj/screen/robot/thrusters()
using.screen_loc = ui_borg_thrusters
using.hud = src
static_inventory += using
- mymobR.thruster_button = using
+ robit.thruster_button = using
//Intent
action_intent = new /obj/screen/act_intent/robot()
@@ -191,20 +167,21 @@
infodisplay += healths
//Installed Module
- mymobR.hands = new /obj/screen/robot/module()
- mymobR.hands.screen_loc = ui_borg_module
- static_inventory += mymobR.hands
+ robit.hands = new /obj/screen/robot/module()
+ robit.hands.screen_loc = ui_borg_module
+ robit.hands.hud = src
+ static_inventory += robit.hands
//Store
module_store_icon = new /obj/screen/robot/store()
- module_store_icon.hud = src
module_store_icon.screen_loc = ui_borg_store
+ module_store_icon.hud = src
pull_icon = new /obj/screen/pull()
pull_icon.icon = 'icons/mob/screen_cyborg.dmi'
+ pull_icon.screen_loc = ui_borg_pull
pull_icon.hud = src
pull_icon.update_icon()
- pull_icon.screen_loc = ui_borg_pull
hotkeybuttons += pull_icon
@@ -242,13 +219,13 @@
screenmob.client.screen += module_store_icon //"store" icon
if(!R.module.modules)
- to_chat(usr, "Selected module has no modules to select")
+ to_chat(usr, "Selected module has no modules to select!")
return
if(!R.robot_modules_background)
return
- var/display_rows = CEILING(length(R.module.get_inactive_modules()) / 8, 1)
+ var/display_rows = max(CEILING(length(R.module.get_inactive_modules()) / 8, 1),1)
R.robot_modules_background.screen_loc = "CENTER-4:16,SOUTH+1:7 to CENTER+3:16,SOUTH+[display_rows]:7"
screenmob.client.screen += R.robot_modules_background
@@ -305,3 +282,63 @@
else
for(var/obj/item/I in R.held_items)
screenmob.client.screen -= I
+
+/obj/screen/robot/lamp
+ name = "headlamp"
+ icon_state = "lamp_off"
+ var/mob/living/silicon/robot/robot
+
+/obj/screen/robot/lamp/Click()
+ . = ..()
+ if(.)
+ return
+ robot?.toggle_headlamp()
+ update_icon()
+
+/obj/screen/robot/lamp/update_icon()
+ if(robot?.lamp_enabled)
+ icon_state = "lamp_on"
+ else
+ icon_state = "lamp_off"
+
+/obj/screen/robot/alerts
+ name = "Alert Panel"
+ icon = 'icons/mob/screen_ai.dmi'
+ icon_state = "alerts"
+
+/obj/screen/robot/alerts/Click()
+ . = ..()
+ if(.)
+ return
+ var/mob/living/silicon/robot/borgo = usr
+ borgo.robot_alerts()
+
+/obj/screen/robot/thrusters
+ name = "ion thrusters"
+ icon_state = "ionpulse0"
+
+/obj/screen/robot/thrusters/Click()
+ if(..())
+ return
+ var/mob/living/silicon/robot/R = usr
+ R.toggle_ionpulse()
+
+/obj/screen/robot/sensors
+ name = "Sensor Augmentation"
+ icon_state = "cyborg_sensor"
+
+/obj/screen/robot/sensors/Click()
+ if(..())
+ return
+ var/mob/living/silicon/S = usr
+ S.toggle_sensors()
+/obj/screen/robot/modPC
+ name = "Modular Interface"
+ icon_state = "template"
+ var/mob/living/silicon/robot/robot
+
+/obj/screen/robot/modPC/Click()
+ . = ..()
+ if(.)
+ return
+ robot.modularInterface?.interact(robot)
diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm
index bedb0f6dec..77650be94b 100644
--- a/code/_onclick/item_attack.dm
+++ b/code/_onclick/item_attack.dm
@@ -97,6 +97,9 @@
M.lastattacker = user.real_name
M.lastattackerckey = user.ckey
+ if(force && M == user && user.client)
+ user.client.give_award(/datum/award/achievement/misc/selfouch, user)
+
user.do_attack_animation(M)
M.attacked_by(src, user, attackchain_flags, damage_multiplier)
diff --git a/code/controllers/configuration/configuration.dm b/code/controllers/configuration/configuration.dm
index 9d5110daf5..32da3b5938 100644
--- a/code/controllers/configuration/configuration.dm
+++ b/code/controllers/configuration/configuration.dm
@@ -415,7 +415,7 @@ Example config:
while(recent_round)
adjustment += repeated_mode_adjust[recent_round]
recent_round = SSpersistence.saved_modes.Find(name,recent_round+1,0)
- probability *= ((100-adjustment)/100)
+ probability *= max(0,((100-adjustment)/100))
runnable_storytellers[S] = probability
return runnable_storytellers
@@ -425,6 +425,7 @@ Example config:
var/list/min_pop = Get(/datum/config_entry/keyed_list/min_pop)
var/list/max_pop = Get(/datum/config_entry/keyed_list/max_pop)
var/list/repeated_mode_adjust = Get(/datum/config_entry/number_list/repeated_mode_adjust)
+ var/desired_chaos_level = 9 - SSpersistence.get_recent_chaos()
for(var/T in gamemode_cache)
var/datum/game_mode/M = new T()
if(!(M.config_tag in modes))
@@ -448,7 +449,18 @@ Example config:
while(recent_round)
adjustment += repeated_mode_adjust[recent_round]
recent_round = SSpersistence.saved_modes.Find(M.config_tag,recent_round+1,0)
- final_weight *= ((100-adjustment)/100)
+ final_weight *= max(0,((100-adjustment)/100))
+ if(Get(/datum/config_entry/flag/weigh_by_recent_chaos))
+ var/chaos_level = M.get_chaos()
+ var/exponent = Get(/datum/config_entry/number/chaos_exponent)
+ var/delta = chaos_level - desired_chaos_level
+ if(desired_chaos_level > 5)
+ delta = abs(min(delta, 0))
+ else if(desired_chaos_level < 5)
+ delta = max(delta, 0)
+ else
+ delta = abs(delta)
+ final_weight /= (delta + 1) ** exponent
runnable_modes[M] = final_weight
return runnable_modes
diff --git a/code/controllers/configuration/entries/comms.dm b/code/controllers/configuration/entries/comms.dm
index e56ff3f0d1..00f1cedf4f 100644
--- a/code/controllers/configuration/entries/comms.dm
+++ b/code/controllers/configuration/entries/comms.dm
@@ -22,11 +22,10 @@
/datum/config_entry/string/cross_comms_name
-/datum/config_entry/string/medal_hub_address
-
-/datum/config_entry/string/medal_hub_password
- protection = CONFIG_ENTRY_HIDDEN
+/datum/config_entry/string/cross_comms_network
+ protection = CONFIG_ENTRY_LOCKED
+/// cit config
/datum/config_entry/keyed_list/cross_server_bunker_override
key_mode = KEY_MODE_TEXT
value_mode = VALUE_MODE_TEXT
diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm
index f6a3388c01..08989469f5 100644
--- a/code/controllers/configuration/entries/game_options.dm
+++ b/code/controllers/configuration/entries/game_options.dm
@@ -7,6 +7,13 @@
/datum/config_entry/keyed_list/probability/ValidateListEntry(key_name)
return key_name in config.modes
+/datum/config_entry/keyed_list/chaos_level
+ key_mode = KEY_MODE_TEXT
+ value_mode = VALUE_MODE_NUM
+
+/datum/config_entry/keyed_list/chaos_level/ValidateListEntry(key_name)
+ return key_name in config.modes
+
/datum/config_entry/keyed_list/max_pop
key_mode = KEY_MODE_TEXT
value_mode = VALUE_MODE_NUM
@@ -605,3 +612,8 @@
/// Dirtyness multiplier for making turfs dirty
/datum/config_entry/number/turf_dirty_multiplier
config_entry_value = 1
+
+/datum/config_entry/flag/weigh_by_recent_chaos
+
+/datum/config_entry/number/chaos_exponent
+ config_entry_value = 1
diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index 4d985c7234..63da60d7b5 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -275,6 +275,11 @@
/datum/config_entry/flag/panic_bunker // prevents people the server hasn't seen before from connecting
+/datum/config_entry/number/panic_bunker_living // living time in minutes that a player needs to pass the panic bunker
+
+/datum/config_entry/string/panic_bunker_message
+ config_entry_value = "Sorry but the server is currently not accepting connections from never before seen players."
+
/datum/config_entry/number/notify_new_player_age // how long do we notify admins of a new player
min_val = -1
diff --git a/code/controllers/subsystem/achievements.dm b/code/controllers/subsystem/achievements.dm
new file mode 100644
index 0000000000..f0e10e6f03
--- /dev/null
+++ b/code/controllers/subsystem/achievements.dm
@@ -0,0 +1,74 @@
+SUBSYSTEM_DEF(achievements)
+ name = "Achievements"
+ flags = SS_NO_FIRE
+ init_order = INIT_ORDER_ACHIEVEMENTS
+ var/achievements_enabled = FALSE
+
+ ///List of achievements
+ var/list/datum/award/achievement/achievements = list()
+ ///List of scores
+ var/list/datum/award/score/scores = list()
+ ///List of all awards
+ var/list/datum/award/awards = list()
+
+/datum/controller/subsystem/achievements/Initialize(timeofday)
+ if(!SSdbcore.Connect())
+ return ..()
+ achievements_enabled = TRUE
+
+ for(var/T in subtypesof(/datum/award/achievement))
+ var/instance = new T
+ achievements[T] = instance
+ awards[T] = instance
+
+ for(var/T in subtypesof(/datum/award/score))
+ var/instance = new T
+ scores[T] = instance
+ awards[T] = instance
+
+ update_metadata()
+
+ for(var/i in GLOB.clients)
+ var/client/C = i
+ if(!C.player_details.achievements.initialized)
+ C.player_details.achievements.InitializeData()
+
+ return ..()
+
+/datum/controller/subsystem/achievements/Shutdown()
+ save_achievements_to_db()
+
+/datum/controller/subsystem/achievements/proc/save_achievements_to_db()
+ var/list/cheevos_to_save = list()
+ for(var/ckey in GLOB.player_details)
+ var/datum/player_details/PD = GLOB.player_details[ckey]
+ if(!PD || !PD.achievements)
+ continue
+ cheevos_to_save += PD.achievements.get_changed_data()
+ if(!length(cheevos_to_save))
+ return
+ SSdbcore.MassInsert(format_table_name("achievements"),cheevos_to_save,duplicate_key = TRUE)
+
+//Update the metadata if any are behind
+/datum/controller/subsystem/achievements/proc/update_metadata()
+ var/list/current_metadata = list()
+ //select metadata here
+ var/datum/db_query/Q = SSdbcore.NewQuery("SELECT achievement_key,achievement_version FROM [format_table_name("achievement_metadata")]")
+ if(!Q.Execute(async = TRUE))
+ qdel(Q)
+ return
+ else
+ while(Q.NextRow())
+ current_metadata[Q.item[1]] = text2num(Q.item[2])
+ qdel(Q)
+
+ var/list/to_update = list()
+ for(var/T in awards)
+ var/datum/award/A = awards[T]
+ if(!A.database_id)
+ continue
+ if(!current_metadata[A.database_id] || current_metadata[A.database_id] < A.achievement_version)
+ to_update += list(A.get_metadata_row())
+
+ if(to_update.len)
+ SSdbcore.MassInsert(format_table_name("achievement_metadata"),to_update,duplicate_key = TRUE)
diff --git a/code/controllers/subsystem/blackbox.dm b/code/controllers/subsystem/blackbox.dm
index bacccefc61..5d236045fd 100644
--- a/code/controllers/subsystem/blackbox.dm
+++ b/code/controllers/subsystem/blackbox.dm
@@ -5,10 +5,10 @@ SUBSYSTEM_DEF(blackbox)
runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME
init_order = INIT_ORDER_BLACKBOX
- var/list/feedback = list() //list of datum/feedback_variable
+ var/list/feedback = list() //list of datum/feedback_variable
var/list/first_death = list() //the first death of this round, assoc. vars keep track of different things
var/triggertime = 0
- var/sealed = FALSE //time to stop tracking stats?
+ var/sealed = FALSE //time to stop tracking stats?
var/list/versions = list("antagonists" = 3,
"admin_secrets_fun_used" = 2,
"explosion" = 2,
@@ -28,12 +28,12 @@ SUBSYSTEM_DEF(blackbox)
//poll population
/datum/controller/subsystem/blackbox/fire()
- set waitfor = FALSE //for population query
+ set waitfor = FALSE //for population query
CheckPlayerCount()
if(CONFIG_GET(flag/use_exp_tracking))
- if((triggertime < 0) || (world.time > (triggertime +3000))) //subsystem fires once at roundstart then once every 10 minutes. a 5 min check skips the first fire. The <0 is midnight rollover check
+ if((triggertime < 0) || (world.time > (triggertime +3000))) //subsystem fires once at roundstart then once every 10 minutes. a 5 min check skips the first fire. The <0 is midnight rollover check
update_exp(10,FALSE)
/datum/controller/subsystem/blackbox/proc/CheckPlayerCount()
@@ -43,7 +43,17 @@ SUBSYSTEM_DEF(blackbox)
return
var/playercount = LAZYLEN(GLOB.player_list)
var/admincount = GLOB.admins.len
- var/datum/DBQuery/query_record_playercount = SSdbcore.NewQuery("INSERT INTO [format_table_name("legacy_population")] (playercount, admincount, time, server_ip, server_port, round_id) VALUES ([playercount], [admincount], '[SQLtime()]', INET_ATON(IF('[world.internet_address]' LIKE '', '0', '[world.internet_address]')), '[world.port]', '[GLOB.round_id]')")
+ var/datum/db_query/query_record_playercount = SSdbcore.NewQuery({"
+ INSERT INTO [format_table_name("legacy_population")] (playercount, admincount, time, server_ip, server_port, round_id)
+ VALUES (:playercount, :admincount, :time, INET_ATON(:server_ip), :server_port, :round_id)
+ "}, list(
+ "playercount" = playercount,
+ "admincount" = admincount,
+ "time" = SQLtime(),
+ "server_ip" = world.internet_address || "0",
+ "server_port" = "[world.port]",
+ "round_id" = GLOB.round_id,
+ ))
query_record_playercount.Execute()
qdel(query_record_playercount)
@@ -87,24 +97,23 @@ SUBSYSTEM_DEF(blackbox)
if (!SSdbcore.Connect())
return
- // var/list/special_columns = list(
- // "datetime" = "NOW()"
- // )
+ var/list/special_columns = list(
+ "datetime" = "NOW()"
+ )
var/list/sqlrowlist = list()
for (var/datum/feedback_variable/FV in feedback)
sqlrowlist += list(list(
- "datetime" = "Now()", //legacy
"round_id" = GLOB.round_id,
- "key_name" = sanitizeSQL(FV.key),
+ "key_name" = FV.key,
"key_type" = FV.key_type,
"version" = versions[FV.key] || 1,
- "json" = sanitizeSQL(json_encode(FV.json))
+ "json" = json_encode(FV.json)
))
if (!length(sqlrowlist))
return
- SSdbcore.MassInsert(format_table_name("feedback"), sqlrowlist, ignore_errors = TRUE, delayed = TRUE)//, special_columns = special_columns)
+ SSdbcore.MassInsert(format_table_name("feedback"), sqlrowlist, ignore_errors = TRUE, delayed = TRUE, special_columns = special_columns)
/datum/controller/subsystem/blackbox/proc/Seal()
if(sealed)
@@ -162,13 +171,13 @@ feedback data can be recorded in 5 formats:
used for simple single-string records i.e. the current map
further calls to the same key will append saved data unless the overwrite argument is true or it already exists
when encoded calls made with overwrite will lack square brackets
- calls: SSblackbox.record_feedback("text", "example", 1, "sample text")
+ calls: SSblackbox.record_feedback("text", "example", 1, "sample text")
SSblackbox.record_feedback("text", "example", 1, "other text")
json: {"data":["sample text","other text"]}
"amount"
used to record simple counts of data i.e. the number of ahelps received
further calls to the same key will add or subtract (if increment argument is a negative) from the saved amount
- calls: SSblackbox.record_feedback("amount", "example", 8)
+ calls: SSblackbox.record_feedback("amount", "example", 8)
SSblackbox.record_feedback("amount", "example", 2)
json: {"data":10}
"tally"
@@ -176,7 +185,7 @@ feedback data can be recorded in 5 formats:
further calls to the same key will:
add or subtract from the saved value of the data key if it already exists
append the key and it's value if it doesn't exist
- calls: SSblackbox.record_feedback("tally", "example", 1, "sample data")
+ calls: SSblackbox.record_feedback("tally", "example", 1, "sample data")
SSblackbox.record_feedback("tally", "example", 4, "sample data")
SSblackbox.record_feedback("tally", "example", 2, "other data")
json: {"data":{"sample data":5,"other data":2}}
@@ -188,19 +197,19 @@ feedback data can be recorded in 5 formats:
further calls to the same key will:
add or subtract from the saved value of the data key if it already exists in the same multi-dimensional position
append the key and it's value if it doesn't exist
- calls: SSblackbox.record_feedback("nested tally", "example", 1, list("fruit", "orange", "apricot"))
+ calls: SSblackbox.record_feedback("nested tally", "example", 1, list("fruit", "orange", "apricot"))
SSblackbox.record_feedback("nested tally", "example", 2, list("fruit", "orange", "orange"))
SSblackbox.record_feedback("nested tally", "example", 3, list("fruit", "orange", "apricot"))
SSblackbox.record_feedback("nested tally", "example", 10, list("fruit", "red", "apple"))
SSblackbox.record_feedback("nested tally", "example", 1, list("vegetable", "orange", "carrot"))
json: {"data":{"fruit":{"orange":{"apricot":4,"orange":2},"red":{"apple":10}},"vegetable":{"orange":{"carrot":1}}}}
tracking values associated with a number can't merge with a nesting value, trying to do so will append the list
- call: SSblackbox.record_feedback("nested tally", "example", 3, list("fruit", "orange"))
+ call: SSblackbox.record_feedback("nested tally", "example", 3, list("fruit", "orange"))
json: {"data":{"fruit":{"orange":{"apricot":4,"orange":2},"red":{"apple":10},"orange":3},"vegetable":{"orange":{"carrot":1}}}}
"associative"
used to record text that's associated with a value i.e. coordinates
further calls to the same key will append a new list to existing data
- calls: SSblackbox.record_feedback("associative", "example", 1, list("text" = "example", "path" = /obj/item, "number" = 4))
+ calls: SSblackbox.record_feedback("associative", "example", 1, list("text" = "example", "path" = /obj/item, "number" = 4))
SSblackbox.record_feedback("associative", "example", 1, list("number" = 7, "text" = "example", "other text" = "sample"))
json: {"data":{"1":{"text":"example","path":"/obj/item","number":"4"},"2":{"number":"7","text":"example","other text":"sample"}}}
@@ -275,7 +284,7 @@ Versioning
/datum/feedback_variable/New(new_key, new_key_type)
key = new_key
key_type = new_key_type
-/*
+
/datum/controller/subsystem/blackbox/proc/LogAhelp(ticket, action, message, recipient, sender)
if(!SSdbcore.Connect())
return
@@ -286,7 +295,7 @@ Versioning
"}, list("ticket" = ticket, "action" = action, "message" = message, "recipient" = recipient, "sender" = sender, "server_ip" = world.internet_address || "0", "server_port" = world.port, "round_id" = GLOB.round_id, "time" = SQLtime()))
query_log_ahelp.Execute()
qdel(query_log_ahelp)
-*/
+
/datum/controller/subsystem/blackbox/proc/ReportDeath(mob/living/L)
set waitfor = FALSE
@@ -302,51 +311,39 @@ Versioning
first_death["area"] = "[AREACOORD(L)]"
first_death["damage"] = "[L.getBruteLoss()]/[L.getFireLoss()]/[L.getToxLoss()]/[L.getOxyLoss()]/[L.getCloneLoss()]"
first_death["last_words"] = L.last_words
- var/sqlname = L.real_name
- var/sqlkey = L.ckey
- var/sqljob = L.mind.assigned_role
- var/sqlspecial = L.mind.special_role
- var/sqlpod = get_area_name(L, TRUE)
- var/laname = L.lastattacker
- var/lakey = L.lastattackerckey
- var/sqlbrute = L.getBruteLoss()
- var/sqlfire = L.getFireLoss()
- var/sqlbrain = L.getOrganLoss(ORGAN_SLOT_BRAIN)
- var/sqloxy = L.getOxyLoss()
- var/sqltox = L.getToxLoss()
- var/sqlclone = L.getCloneLoss()
- var/sqlstamina = L.getStaminaLoss()
- var/x_coord = L.x
- var/y_coord = L.y
- var/z_coord = L.z
- var/last_words = L.last_words
- var/suicide = L.suiciding
- var/map = SSmapping.config.map_name
if(!SSdbcore.Connect())
return
- sqlname = sanitizeSQL(sqlname)
- sqlkey = sanitizeSQL(sqlkey)
- sqljob = sanitizeSQL(sqljob)
- sqlspecial = sanitizeSQL(sqlspecial)
- sqlpod = sanitizeSQL(sqlpod)
- laname = sanitizeSQL(laname)
- lakey = sanitizeSQL(lakey)
- sqlbrute = sanitizeSQL(sqlbrute)
- sqlfire = sanitizeSQL(sqlfire)
- sqlbrain = sanitizeSQL(sqlbrain)
- sqloxy = sanitizeSQL(sqloxy)
- sqltox = sanitizeSQL(sqltox)
- sqlclone = sanitizeSQL(sqlclone)
- sqlstamina = sanitizeSQL(sqlstamina)
- x_coord = sanitizeSQL(x_coord)
- y_coord = sanitizeSQL(y_coord)
- z_coord = sanitizeSQL(z_coord)
- last_words = sanitizeSQL(last_words)
- suicide = sanitizeSQL(suicide)
- map = sanitizeSQL(map)
- var/datum/DBQuery/query_report_death = SSdbcore.NewQuery("INSERT INTO [format_table_name("death")] (pod, x_coord, y_coord, z_coord, mapname, server_ip, server_port, round_id, tod, job, special, name, byondkey, laname, lakey, bruteloss, fireloss, brainloss, oxyloss, toxloss, cloneloss, staminaloss, last_words, suicide) VALUES ('[sqlpod]', '[x_coord]', '[y_coord]', '[z_coord]', '[map]', INET_ATON(IF('[world.internet_address]' LIKE '', '0', '[world.internet_address]')), '[world.port]', [GLOB.round_id], '[SQLtime()]', '[sqljob]', '[sqlspecial]', '[sqlname]', '[sqlkey]', '[laname]', '[lakey]', [sqlbrute], [sqlfire], [sqlbrain], [sqloxy], [sqltox], [sqlclone], [sqlstamina], '[last_words]', [suicide])")
+ var/datum/db_query/query_report_death = SSdbcore.NewQuery({"
+ INSERT INTO [format_table_name("death")] (pod, x_coord, y_coord, z_coord, mapname, server_ip, server_port, round_id, tod, job, special, name, byondkey, laname, lakey, bruteloss, fireloss, brainloss, oxyloss, toxloss, cloneloss, staminaloss, last_words, suicide)
+ VALUES (:pod, :x_coord, :y_coord, :z_coord, :map, INET_ATON(:internet_address), :port, :round_id, :time, :job, :special, :name, :key, :laname, :lakey, :brute, :fire, :brain, :oxy, :tox, :clone, :stamina, :last_words, :suicide)
+ "}, list(
+ "name" = L.real_name,
+ "key" = L.ckey,
+ "job" = L.mind.assigned_role,
+ "special" = L.mind.special_role,
+ "pod" = get_area_name(L, TRUE),
+ "laname" = L.lastattacker,
+ "lakey" = L.lastattackerckey,
+ "brute" = L.getBruteLoss(),
+ "fire" = L.getFireLoss(),
+ "brain" = L.getOrganLoss(ORGAN_SLOT_BRAIN) || BRAIN_DAMAGE_DEATH, //getOrganLoss returns null without a brain but a value is required for this column
+ "oxy" = L.getOxyLoss(),
+ "tox" = L.getToxLoss(),
+ "clone" = L.getCloneLoss(),
+ "stamina" = L.getStaminaLoss(),
+ "x_coord" = L.x,
+ "y_coord" = L.y,
+ "z_coord" = L.z,
+ "last_words" = L.last_words,
+ "suicide" = L.suiciding,
+ "map" = SSmapping.config.map_name,
+ "internet_address" = world.internet_address || "0",
+ "port" = "[world.port]",
+ "round_id" = GLOB.round_id,
+ "time" = SQLtime(),
+ ))
if(query_report_death)
query_report_death.Execute(async = TRUE)
qdel(query_report_death)
diff --git a/code/controllers/subsystem/dbcore.dm b/code/controllers/subsystem/dbcore.dm
index c779c9f26d..b6b750fbf4 100644
--- a/code/controllers/subsystem/dbcore.dm
+++ b/code/controllers/subsystem/dbcore.dm
@@ -3,7 +3,7 @@ SUBSYSTEM_DEF(dbcore)
flags = SS_BACKGROUND
wait = 1 MINUTES
init_order = INIT_ORDER_DBCORE
- var/const/FAILED_DB_CONNECTION_CUTOFF = 5
+ var/failed_connection_timeout = 0
var/schema_mismatch = 0
var/db_minor = 0
@@ -13,8 +13,7 @@ SUBSYSTEM_DEF(dbcore)
var/last_error
var/list/active_queries = list()
- var/datum/BSQL_Connection/connection
- var/datum/BSQL_Operation/connectOperation
+ var/connection // Arbitrary handle returned from rust_g.
/datum/controller/subsystem/dbcore/Initialize()
//We send warnings to the admins during subsystem init, as the clients will be New'd and messages
@@ -29,7 +28,7 @@ SUBSYSTEM_DEF(dbcore)
/datum/controller/subsystem/dbcore/fire()
for(var/I in active_queries)
- var/datum/DBQuery/Q = I
+ var/datum/db_query/Q = I
if(world.time - Q.last_activity_time > (5 MINUTES))
message_admins("Found undeleted query, please check the server logs and notify coders.")
log_sql("Undeleted query: \"[Q.sql]\" LA: [Q.last_activity] LAT: [Q.last_activity_time]")
@@ -39,24 +38,25 @@ SUBSYSTEM_DEF(dbcore)
/datum/controller/subsystem/dbcore/Recover()
connection = SSdbcore.connection
- connectOperation = SSdbcore.connectOperation
/datum/controller/subsystem/dbcore/Shutdown()
//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/DBQuery/query_round_shutdown = SSdbcore.NewQuery("UPDATE [format_table_name("round")] SET shutdown_datetime = Now(), end_state = '[sanitizeSQL(SSticker.end_state)]' WHERE id = [GLOB.round_id]")
+ var/datum/db_query/query_round_shutdown = SSdbcore.NewQuery(
+ "UPDATE [format_table_name("round")] SET shutdown_datetime = Now(), end_state = :end_state WHERE id = :round_id",
+ list("end_state" = SSticker.end_state, "round_id" = GLOB.round_id)
+ )
query_round_shutdown.Execute()
qdel(query_round_shutdown)
if(IsConnected())
Disconnect()
- world.BSQL_Shutdown()
//nu
/datum/controller/subsystem/dbcore/can_vv_get(var_name)
- return var_name != NAMEOF(src, connection) && var_name != NAMEOF(src, active_queries) && var_name != NAMEOF(src, connectOperation) && ..()
+ return var_name != NAMEOF(src, connection) && var_name != NAMEOF(src, active_queries) && ..()
/datum/controller/subsystem/dbcore/vv_edit_var(var_name, var_value)
- if(var_name == NAMEOF(src, connection) || var_name == NAMEOF(src, connectOperation))
+ if(var_name == NAMEOF(src, connection))
return FALSE
return ..()
@@ -64,7 +64,11 @@ SUBSYSTEM_DEF(dbcore)
if(IsConnected())
return TRUE
- if(failed_connections > FAILED_DB_CONNECTION_CUTOFF) //If it failed to establish a connection more than 5 times in a row, don't bother attempting to connect anymore.
+ if(failed_connection_timeout <= world.time) //it's been more than 5 seconds since we failed to connect, reset the counter
+ failed_connections = 0
+
+ if(failed_connections > 5) //If it failed to establish a connection more than 5 times in a row, don't bother attempting to connect for 5 seconds.
+ failed_connection_timeout = world.time + 50
return FALSE
if(!CONFIG_GET(flag/sql_enabled))
@@ -75,32 +79,33 @@ SUBSYSTEM_DEF(dbcore)
var/db = CONFIG_GET(string/feedback_database)
var/address = CONFIG_GET(string/address)
var/port = CONFIG_GET(number/port)
+ var/timeout = max(CONFIG_GET(number/async_query_timeout), CONFIG_GET(number/blocking_query_timeout))
+ var/thread_limit = CONFIG_GET(number/bsql_thread_limit)
- connection = new /datum/BSQL_Connection(BSQL_CONNECTION_TYPE_MARIADB, CONFIG_GET(number/async_query_timeout), CONFIG_GET(number/blocking_query_timeout), CONFIG_GET(number/bsql_thread_limit))
- var/error
- if(QDELETED(connection))
- connection = null
- error = last_error
+ var/result = json_decode(rustg_sql_connect_pool(json_encode(list(
+ "host" = address,
+ "port" = port,
+ "user" = user,
+ "pass" = pass,
+ "db_name" = db,
+ "read_timeout" = timeout,
+ "write_timeout" = timeout,
+ "max_threads" = thread_limit,
+ ))))
+ . = (result["status"] == "ok")
+ if (.)
+ connection = result["handle"]
else
- SSdbcore.last_error = null
- connectOperation = connection.BeginConnect(address, port, user, pass, db)
- if(SSdbcore.last_error)
- CRASH(SSdbcore.last_error)
- UNTIL(connectOperation.IsComplete())
- error = connectOperation.GetError()
- . = !error
- if (!.)
- last_error = error
- log_sql("Connect() failed | [error]")
+ connection = null
+ last_error = result["data"]
+ log_sql("Connect() failed | [last_error]")
++failed_connections
- QDEL_NULL(connection)
- QDEL_NULL(connectOperation)
/datum/controller/subsystem/dbcore/proc/CheckSchemaVersion()
if(CONFIG_GET(flag/sql_enabled))
if(Connect())
log_world("Database connection established.")
- var/datum/DBQuery/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 [format_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])
@@ -120,47 +125,46 @@ SUBSYSTEM_DEF(dbcore)
/datum/controller/subsystem/dbcore/proc/SetRoundID()
if(!Connect())
return
- var/datum/DBQuery/query_round_initialize = SSdbcore.NewQuery("INSERT INTO [format_table_name("round")] (initialize_datetime, server_ip, server_port) VALUES (Now(), INET_ATON(IF('[world.internet_address]' LIKE '', '0', '[world.internet_address]')), '[world.port]')")
- query_round_initialize.Execute()
+ 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)",
+ list("internet_address" = world.internet_address || "0", "port" = "[world.port]")
+ )
+ query_round_initialize.Execute(async = FALSE)
+ GLOB.round_id = "[query_round_initialize.last_insert_id]"
qdel(query_round_initialize)
- var/datum/DBQuery/query_round_last_id = SSdbcore.NewQuery("SELECT LAST_INSERT_ID()")
- query_round_last_id.Execute()
- if(query_round_last_id.NextRow())
- GLOB.round_id = query_round_last_id.item[1]
- qdel(query_round_last_id)
/datum/controller/subsystem/dbcore/proc/SetRoundStart()
if(!Connect())
return
- var/datum/DBQuery/query_round_start = SSdbcore.NewQuery("UPDATE [format_table_name("round")] SET start_datetime = Now() WHERE id = [GLOB.round_id]")
+ var/datum/db_query/query_round_start = SSdbcore.NewQuery(
+ "UPDATE [format_table_name("round")] SET start_datetime = Now() WHERE id = :round_id",
+ list("round_id" = GLOB.round_id)
+ )
query_round_start.Execute()
qdel(query_round_start)
/datum/controller/subsystem/dbcore/proc/SetRoundEnd()
if(!Connect())
return
- var/sql_station_name = sanitizeSQL(station_name())
- var/datum/DBQuery/query_round_end = SSdbcore.NewQuery("UPDATE [format_table_name("round")] SET end_datetime = Now(), game_mode_result = '[sanitizeSQL(SSticker.mode_result)]', station_name = '[sql_station_name]' WHERE id = [GLOB.round_id]")
+ var/datum/db_query/query_round_end = SSdbcore.NewQuery(
+ "UPDATE [format_table_name("round")] SET end_datetime = Now(), game_mode_result = :game_mode_result, station_name = :station_name WHERE id = :round_id",
+ list("game_mode_result" = SSticker.mode_result, "station_name" = station_name(), "round_id" = GLOB.round_id)
+ )
query_round_end.Execute()
qdel(query_round_end)
/datum/controller/subsystem/dbcore/proc/Disconnect()
failed_connections = 0
- QDEL_NULL(connectOperation)
- QDEL_NULL(connection)
+ if (connection)
+ rustg_sql_disconnect_pool(connection)
+ connection = null
/datum/controller/subsystem/dbcore/proc/IsConnected()
- if(!CONFIG_GET(flag/sql_enabled))
+ if (!CONFIG_GET(flag/sql_enabled))
return FALSE
- //block until any connect operations finish
- var/datum/BSQL_Connection/_connection = connection
- var/datum/BSQL_Operation/op = connectOperation
- UNTIL(QDELETED(_connection) || op.IsComplete())
- return !QDELETED(connection) && !op.GetError()
-
-/datum/controller/subsystem/dbcore/proc/Quote(str)
- if(connection)
- return connection.Quote(str)
+ if (!connection)
+ return FALSE
+ return json_decode(rustg_sql_connected(connection))["status"] == "online"
/datum/controller/subsystem/dbcore/proc/ErrorMsg()
if(!CONFIG_GET(flag/sql_enabled))
@@ -170,32 +174,34 @@ SUBSYSTEM_DEF(dbcore)
/datum/controller/subsystem/dbcore/proc/ReportError(error)
last_error = error
-/datum/controller/subsystem/dbcore/proc/NewQuery(sql_query)
+/datum/controller/subsystem/dbcore/proc/NewQuery(sql_query, arguments)
if(IsAdminAdvancedProcCall())
log_admin_private("ERROR: Advanced admin proc call led to sql query: [sql_query]. Query has been blocked")
message_admins("ERROR: Advanced admin proc call led to sql query. Query has been blocked")
return FALSE
- return new /datum/DBQuery(sql_query, connection)
+ return new /datum/db_query(connection, sql_query, arguments)
/datum/controller/subsystem/dbcore/proc/QuerySelect(list/querys, warn = FALSE, qdel = FALSE)
if (!islist(querys))
- if (!istype(querys, /datum/DBQuery))
+ if (!istype(querys, /datum/db_query))
CRASH("Invalid query passed to QuerySelect: [querys]")
querys = list(querys)
for (var/thing in querys)
- var/datum/DBQuery/query = thing
+ var/datum/db_query/query = thing
if (warn)
- INVOKE_ASYNC(query, /datum/DBQuery.proc/warn_execute)
+ INVOKE_ASYNC(query, /datum/db_query.proc/warn_execute)
else
- INVOKE_ASYNC(query, /datum/DBQuery.proc/Execute)
+ INVOKE_ASYNC(query, /datum/db_query.proc/Execute)
for (var/thing in querys)
- var/datum/DBQuery/query = thing
+ var/datum/db_query/query = thing
UNTIL(!query.in_progress)
if (qdel)
qdel(query)
+
+
/*
Takes a list of rows (each row being an associated list of column => value) and inserts them via a single mass query.
Rows missing columns present in other rows will resolve to SQL NULL
@@ -203,137 +209,135 @@ You are expected to do your own escaping of the data, and expected to provide yo
The duplicate_key arg can be true to automatically generate this part of the query
or set to a string that is appended to the end of the query
Ignore_errors instructes mysql to continue inserting rows if some of them have errors.
- the erroneous row(s) aren't inserted and there isn't really any way to know why or why errored
+ the erroneous row(s) aren't inserted and there isn't really any way to know why or why errored
Delayed insert mode was removed in mysql 7 and only works with MyISAM type tables,
It was included because it is still supported in mariadb.
It does not work with duplicate_key and the mysql server ignores it in those cases
*/
-/datum/controller/subsystem/dbcore/proc/MassInsert(table, list/rows, duplicate_key = FALSE, ignore_errors = FALSE, delayed = FALSE, warn = FALSE, async = TRUE)
+/datum/controller/subsystem/dbcore/proc/MassInsert(table, list/rows, duplicate_key = FALSE, ignore_errors = FALSE, delayed = FALSE, warn = FALSE, async = TRUE, special_columns = null)
if (!table || !rows || !istype(rows))
return
+
+ // Prepare column list
var/list/columns = list()
- var/list/sorted_rows = list()
-
+ var/list/has_question_mark = list()
for (var/list/row in rows)
- var/list/sorted_row = list()
- sorted_row.len = columns.len
for (var/column in row)
- var/idx = columns[column]
- if (!idx)
- idx = columns.len + 1
- columns[column] = idx
- sorted_row.len = columns.len
+ columns[column] = "?"
+ has_question_mark[column] = TRUE
+ for (var/column in special_columns)
+ columns[column] = special_columns[column]
+ has_question_mark[column] = findtext(special_columns[column], "?")
- sorted_row[idx] = row[column]
- sorted_rows[++sorted_rows.len] = sorted_row
+ // Prepare SQL query full of placeholders
+ var/list/query_parts = list("INSERT")
+ if (delayed)
+ query_parts += " DELAYED"
+ if (ignore_errors)
+ query_parts += " IGNORE"
+ query_parts += " INTO "
+ query_parts += table
+ query_parts += "\n([columns.Join(", ")])\nVALUES"
+
+ var/list/arguments = list()
+ var/has_row = FALSE
+ for (var/list/row in rows)
+ if (has_row)
+ query_parts += ","
+ query_parts += "\n ("
+ var/has_col = FALSE
+ for (var/column in columns)
+ if (has_col)
+ query_parts += ", "
+ if (has_question_mark[column])
+ var/name = "p[arguments.len]"
+ query_parts += replacetext(columns[column], "?", ":[name]")
+ arguments[name] = row[column]
+ else
+ query_parts += columns[column]
+ has_col = TRUE
+ query_parts += ")"
+ has_row = TRUE
if (duplicate_key == TRUE)
var/list/column_list = list()
for (var/column in columns)
column_list += "[column] = VALUES([column])"
- duplicate_key = "ON DUPLICATE KEY UPDATE [column_list.Join(", ")]\n"
- else if (duplicate_key == FALSE)
- duplicate_key = null
+ query_parts += "\nON DUPLICATE KEY UPDATE [column_list.Join(", ")]"
+ else if (duplicate_key != FALSE)
+ query_parts += duplicate_key
- if (ignore_errors)
- ignore_errors = " IGNORE"
- else
- ignore_errors = null
-
- if (delayed)
- delayed = " DELAYED"
- else
- delayed = null
-
- var/list/sqlrowlist = list()
- var/len = columns.len
- for (var/list/row in sorted_rows)
- if (length(row) != len)
- row.len = len
- for (var/value in row)
- if (value == null)
- value = "NULL"
- sqlrowlist += "([row.Join(", ")])"
-
- sqlrowlist = " [sqlrowlist.Join(",\n ")]"
- var/datum/DBQuery/Query = NewQuery("INSERT[delayed][ignore_errors] INTO [table]\n([columns.Join(", ")])\nVALUES\n[sqlrowlist]\n[duplicate_key]")
+ var/datum/db_query/Query = NewQuery(query_parts.Join(), arguments)
if (warn)
. = Query.warn_execute(async)
else
. = Query.Execute(async)
qdel(Query)
-/datum/DBQuery
- var/sql // The sql query being executed.
- var/list/item //list of data values populated by NextRow()
+/datum/db_query
+ // Inputs
+ var/connection
+ var/sql
+ var/arguments
+ // Status information
+ var/in_progress
+ var/last_error
var/last_activity
var/last_activity_time
- var/last_error
- var/skip_next_is_complete
- var/in_progress
- var/datum/BSQL_Connection/connection
- var/datum/BSQL_Operation/Query/query
+ // Output
+ var/list/list/rows
+ var/next_row_to_take = 1
+ var/affected
+ var/last_insert_id
-/datum/DBQuery/New(sql_query, datum/BSQL_Connection/connection)
+ var/list/item //list of data values populated by NextRow()
+
+/datum/db_query/New(connection, sql, arguments)
SSdbcore.active_queries[src] = TRUE
Activity("Created")
item = list()
- src.connection = connection
- sql = sql_query
-/datum/DBQuery/Destroy()
+ src.connection = connection
+ src.sql = sql
+ src.arguments = arguments
+
+/datum/db_query/Destroy()
Close()
SSdbcore.active_queries -= src
return ..()
-/datum/DBQuery/CanProcCall(proc_name)
+/datum/db_query/CanProcCall(proc_name)
//fuck off kevinz
return FALSE
-/datum/DBQuery/proc/SetQuery(new_sql)
- if(in_progress)
- CRASH("Attempted to set new sql while waiting on active query")
- Close()
- sql = new_sql
-
-/datum/DBQuery/proc/Activity(activity)
+/datum/db_query/proc/Activity(activity)
last_activity = activity
last_activity_time = world.time
-/datum/DBQuery/proc/warn_execute(async = FALSE)
+/datum/db_query/proc/warn_execute(async = TRUE)
. = Execute(async)
if(!.)
to_chat(usr, "A SQL error occurred during this operation, check the server logs.")
-/datum/DBQuery/proc/Execute(async = FALSE, log_error = TRUE)
+/datum/db_query/proc/Execute(async = TRUE, log_error = TRUE)
Activity("Execute")
if(in_progress)
CRASH("Attempted to start a new query while waiting on the old one")
- if(QDELETED(connection))
+ if(!SSdbcore.IsConnected())
last_error = "No connection!"
return FALSE
var/start_time
- var/timed_out
if(!async)
start_time = REALTIMEOFDAY
Close()
- query = connection.BeginQuery(sql)
- if(!async)
- timed_out = !query.WaitForCompletion()
- else
- in_progress = TRUE
- UNTIL(query.IsComplete())
- in_progress = FALSE
- skip_next_is_complete = TRUE
- var/error = QDELETED(query) ? "Query object deleted!" : query.GetError()
- last_error = error
- . = !error
+ . = run_query(async)
+ var/timed_out = !. && findtext(last_error, "Operation timed out")
if(!. && log_error)
- log_sql("[error] | Query used: [sql]")
+ log_sql("[last_error] | Query used: [sql] | Arguments: [json_encode(arguments)]")
if(!async && timed_out)
log_query_debug("Query execution started at [start_time]")
log_query_debug("Query execution ended at [REALTIMEOFDAY]")
@@ -341,44 +345,51 @@ Delayed insert mode was removed in mysql 7 and only works with MyISAM type table
log_query_debug("Query used: [sql]")
slow_query_check()
-/datum/DBQuery/proc/slow_query_check()
+/datum/db_query/proc/run_query(async)
+ var/job_result_str
+
+ if (async)
+ var/job_id = rustg_sql_query_async(connection, sql, json_encode(arguments))
+ in_progress = TRUE
+ UNTIL((job_result_str = rustg_sql_check_query(job_id)) != RUSTG_JOB_NO_RESULTS_YET)
+ in_progress = FALSE
+
+ if (job_result_str == RUSTG_JOB_ERROR)
+ last_error = job_result_str
+ return FALSE
+ else
+ job_result_str = rustg_sql_query_blocking(connection, sql, json_encode(arguments))
+
+ var/result = json_decode(job_result_str)
+ switch (result["status"])
+ if ("ok")
+ rows = result["rows"]
+ affected = result["affected"]
+ last_insert_id = result["last_insert_id"]
+ return TRUE
+ if ("err")
+ last_error = result["data"]
+ return FALSE
+ if ("offline")
+ last_error = "offline"
+ return FALSE
+
+/datum/db_query/proc/slow_query_check()
message_admins("HEY! A database query timed out. Did the server just hang? \[YES\]|\[NO\]")
-/datum/DBQuery/proc/NextRow(async)
+/datum/db_query/proc/NextRow(async = TRUE)
Activity("NextRow")
- UNTIL(!in_progress)
- if(!skip_next_is_complete)
- if(!async)
- query.WaitForCompletion()
- else
- in_progress = TRUE
- UNTIL(query.IsComplete())
- in_progress = FALSE
+
+ if (rows && next_row_to_take <= rows.len)
+ item = rows[next_row_to_take]
+ next_row_to_take++
+ return !!item
else
- skip_next_is_complete = FALSE
+ return FALSE
- last_error = query.GetError()
- var/list/results = query.CurrentRow()
- . = results != null
-
- item.Cut()
- //populate item array
- for(var/I in results)
- item += results[I]
-
-/datum/DBQuery/proc/ErrorMsg()
+/datum/db_query/proc/ErrorMsg()
return last_error
-/datum/DBQuery/proc/Close()
- item.Cut()
- QDEL_NULL(query)
-
-/world/BSQL_Debug(message)
- if(!CONFIG_GET(flag/bsql_debug))
- return
-
- //strip sensitive stuff
- if(findtext(message, ": CreateConnection("))
- message = "CreateConnection CENSORED"
-
- log_sql("BSQL_DEBUG: [message]")
+/datum/db_query/proc/Close()
+ rows = null
+ item = null
diff --git a/code/controllers/subsystem/job.dm b/code/controllers/subsystem/job.dm
index 0ba6076be2..c090d7367c 100644
--- a/code/controllers/subsystem/job.dm
+++ b/code/controllers/subsystem/job.dm
@@ -496,9 +496,15 @@ SUBSYSTEM_DEF(job)
H.equip_to_slot_if_possible(binder, SLOT_IN_BACKPACK, disable_warning = TRUE, bypass_equip_delay_self = TRUE)
for(var/card_type in H.client.prefs.tcg_cards)
if(card_type)
- var/obj/item/tcg_card/card = new(get_turf(H), card_type, H.client.prefs.tcg_cards[card_type])
- card.forceMove(binder)
- binder.cards.Add(card)
+ if(islist(H.client.prefs.tcg_cards[card_type]))
+ for(var/duplicate in H.client.prefs.tcg_cards[card_type])
+ var/obj/item/tcg_card/card = new(get_turf(H), card_type, duplicate)
+ card.forceMove(binder)
+ binder.cards.Add(card)
+ else
+ var/obj/item/tcg_card/card = new(get_turf(H), card_type, H.client.prefs.tcg_cards[card_type])
+ card.forceMove(binder)
+ binder.cards.Add(card)
binder.check_for_exodia()
if(length(H.client.prefs.tcg_decks))
binder.decks = H.client.prefs.tcg_decks
@@ -508,9 +514,15 @@ SUBSYSTEM_DEF(job)
H.equip_to_slot_if_possible(binder, SLOT_IN_BACKPACK, disable_warning = TRUE, bypass_equip_delay_self = TRUE)
for(var/card_type in N.client.prefs.tcg_cards)
if(card_type)
- var/obj/item/tcg_card/card = new(get_turf(H), card_type, N.client.prefs.tcg_cards[card_type])
- card.forceMove(binder)
- binder.cards.Add(card)
+ if(islist(H.client.prefs.tcg_cards[card_type]))
+ for(var/duplicate in N.client.prefs.tcg_cards[card_type])
+ var/obj/item/tcg_card/card = new(get_turf(H), card_type, duplicate)
+ card.forceMove(binder)
+ binder.cards.Add(card)
+ else
+ var/obj/item/tcg_card/card = new(get_turf(H), card_type, N.client.prefs.tcg_cards[card_type])
+ card.forceMove(binder)
+ binder.cards.Add(card)
binder.check_for_exodia()
if(length(N.client.prefs.tcg_decks))
binder.decks = N.client.prefs.tcg_decks
diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm
index 90908bbde0..b5dfec3c44 100644
--- a/code/controllers/subsystem/mapping.dm
+++ b/code/controllers/subsystem/mapping.dm
@@ -286,7 +286,9 @@ SUBSYSTEM_DEF(mapping)
setup_station_z_index()
if(SSdbcore.Connect())
- var/datum/DBQuery/query_round_map_name = SSdbcore.NewQuery("UPDATE [format_table_name("round")] SET map_name = '[config.map_name]' WHERE id = [GLOB.round_id]")
+ var/datum/db_query/query_round_map_name = SSdbcore.NewQuery({"
+ UPDATE [format_table_name("round")] SET map_name = :map_name WHERE id = :round_id
+ "}, list("map_name" = config.map_name, "round_id" = GLOB.round_id))
query_round_map_name.Execute()
qdel(query_round_map_name)
diff --git a/code/controllers/subsystem/materials.dm b/code/controllers/subsystem/materials.dm
index 23d5a7a2b7..2134be0176 100644
--- a/code/controllers/subsystem/materials.dm
+++ b/code/controllers/subsystem/materials.dm
@@ -1,6 +1,8 @@
/*! How material datums work
Materials are now instanced datums, with an associative list of them being kept in SSmaterials. We only instance the materials once and then re-use these instances for everything.
+
These materials call on_applied() on whatever item they are applied to, common effects are adding components, changing color and changing description. This allows us to differentiate items based on the material they are made out of.area
+
*/
SUBSYSTEM_DEF(materials)
@@ -14,12 +16,16 @@ SUBSYSTEM_DEF(materials)
var/list/materialtypes_by_category
///A cache of all material combinations that have been used
var/list/list/material_combos
- ///List of stackcrafting recipes for materials using rigid materials
+ ///List of stackcrafting recipes for materials using base recipes
+ var/list/base_stack_recipes = list(
+ new /datum/stack_recipe("Chair", /obj/structure/chair/greyscale, one_per_turf = TRUE, on_floor = TRUE, applies_mats = TRUE),
+ new /datum/stack_recipe("Toilet", /obj/structure/toilet/greyscale, one_per_turf = TRUE, on_floor = TRUE, applies_mats = TRUE),
+ new /datum/stack_recipe("Sink Frame", /obj/structure/sink/greyscale, one_per_turf = TRUE, on_floor = TRUE, applies_mats = TRUE),
+ new /datum/stack_recipe("Floor tile", /obj/item/stack/tile/material, 1, 4, 20, applies_mats = TRUE),
+ )
+ ///List of stackcrafting recipes for materials using rigid recipes
var/list/rigid_stack_recipes = list(
- new /datum/stack_recipe("chair", /obj/structure/chair/greyscale, one_per_turf = TRUE, on_floor = TRUE, applies_mats = TRUE),
- new /datum/stack_recipe("toilet", /obj/structure/toilet/greyscale, one_per_turf = TRUE, on_floor = TRUE, applies_mats = TRUE),
- new /datum/stack_recipe("sink", /obj/structure/sink/greyscale, one_per_turf = TRUE, on_floor = TRUE, applies_mats = TRUE),
- new /datum/stack_recipe("Floor tile", /obj/item/stack/tile/material, 1, 4, 20, applies_mats = TRUE)
+ // new /datum/stack_recipe("Carving block", /obj/structure/carving_block, 5, one_per_turf = TRUE, on_floor = TRUE, applies_mats = TRUE),
)
///Ran on initialize, populated the materials and materials_by_category dictionaries with their appropiate vars (See these variables for more info)
@@ -29,7 +35,11 @@ SUBSYSTEM_DEF(materials)
materialtypes_by_category = list()
material_combos = list()
for(var/type in subtypesof(/datum/material))
- var/datum/material/ref = new type
+ var/datum/material/ref = type
+ // if(!(initial(ref.init_flags) & MATERIAL_INIT_MAPLOAD))
+ // continue // Do not initialize
+
+ ref = new ref
materials[type] = ref
for(var/c in ref.categories)
materials_by_category[c] += list(ref)
@@ -40,7 +50,6 @@ SUBSYSTEM_DEF(materials)
InitializeMaterials()
return materials[fakemat] || fakemat
-
///Returns a list to be used as an object's custom_materials. Lists will be cached and re-used based on the parameters.
/datum/controller/subsystem/materials/proc/FindOrCreateMaterialCombo(list/materials_declaration, multiplier)
if(!material_combos)
diff --git a/code/controllers/subsystem/medals.dm b/code/controllers/subsystem/medals.dm
deleted file mode 100644
index 36be23973c..0000000000
--- a/code/controllers/subsystem/medals.dm
+++ /dev/null
@@ -1,87 +0,0 @@
-SUBSYSTEM_DEF(medals)
- name = "Medals"
- flags = SS_NO_FIRE
- var/hub_enabled = FALSE
-
-/datum/controller/subsystem/medals/Initialize(timeofday)
- if(CONFIG_GET(string/medal_hub_address) && CONFIG_GET(string/medal_hub_password))
- hub_enabled = TRUE
- return ..()
-
-/datum/controller/subsystem/medals/proc/UnlockMedal(medal, client/player)
- set waitfor = FALSE
- if(!medal || !hub_enabled)
- return
- if(isnull(world.SetMedal(medal, player, CONFIG_GET(string/medal_hub_address), CONFIG_GET(string/medal_hub_password))))
- hub_enabled = FALSE
- log_game("MEDAL ERROR: Could not contact hub to award medal:[medal] player:[player.key]")
- message_admins("Error! Failed to contact hub to award [medal] medal to [player.key]!")
- return
- to_chat(player, "Achievement unlocked: [medal]!")
-
-
-/datum/controller/subsystem/medals/proc/SetScore(score, client/player, increment, force)
- set waitfor = FALSE
- if(!score || !hub_enabled)
- return
-
- var/list/oldscore = GetScore(score, player, TRUE)
- if(increment)
- if(!oldscore[score])
- oldscore[score] = 1
- else
- oldscore[score] = (text2num(oldscore[score]) + 1)
- else
- oldscore[score] = force
-
- var/newscoreparam = list2params(oldscore)
-
- if(isnull(world.SetScores(player.ckey, newscoreparam, CONFIG_GET(string/medal_hub_address), CONFIG_GET(string/medal_hub_password))))
- hub_enabled = FALSE
- log_game("SCORE ERROR: Could not contact hub to set score. Score:[score] player:[player.key]")
- message_admins("Error! Failed to contact hub to set [score] score for [player.key]!")
-
-/datum/controller/subsystem/medals/proc/GetScore(score, client/player, returnlist)
- if(!score || !hub_enabled)
- return
-
- var/scoreget = world.GetScores(player.ckey, score, CONFIG_GET(string/medal_hub_address), CONFIG_GET(string/medal_hub_password))
- if(isnull(scoreget))
- hub_enabled = FALSE
- log_game("SCORE ERROR: Could not contact hub to get score. Score:[score] player:[player.key]")
- message_admins("Error! Failed to contact hub to get score: [score] for [player.key]!")
- return
- . = params2list(scoreget)
- if(!returnlist)
- return .[score]
-
-/datum/controller/subsystem/medals/proc/CheckMedal(medal, client/player)
- if(!medal || !hub_enabled)
- return
-
- if(isnull(world.GetMedal(medal, player, CONFIG_GET(string/medal_hub_address), CONFIG_GET(string/medal_hub_password))))
- hub_enabled = FALSE
- log_game("MEDAL ERROR: Could not contact hub to get medal:[medal] player: [player.key]")
- message_admins("Error! Failed to contact hub to get [medal] medal for [player.key]!")
- return
- to_chat(player, "[medal] is unlocked")
-
-/datum/controller/subsystem/medals/proc/LockMedal(medal, client/player)
- if(!player || !medal || !hub_enabled)
- return
- var/result = world.ClearMedal(medal, player, CONFIG_GET(string/medal_hub_address), CONFIG_GET(string/medal_hub_password))
- switch(result)
- if(null)
- hub_enabled = FALSE
- log_game("MEDAL ERROR: Could not contact hub to clear medal:[medal] player:[player.key]")
- message_admins("Error! Failed to contact hub to clear [medal] medal for [player.key]!")
- if(TRUE)
- message_admins("Medal: [medal] removed for [player.key]")
- if(FALSE)
- message_admins("Medal: [medal] was not found for [player.key]. Unable to clear.")
-
-
-/datum/controller/subsystem/medals/proc/ClearScore(client/player)
- if(isnull(world.SetScores(player.ckey, "", CONFIG_GET(string/medal_hub_address), CONFIG_GET(string/medal_hub_password))))
- log_game("MEDAL ERROR: Could not contact hub to clear scores for [player.key]!")
- message_admins("Error! Failed to contact hub to clear scores for [player.key]!")
diff --git a/code/controllers/subsystem/persistence/_persistence.dm b/code/controllers/subsystem/persistence/_persistence.dm
index 9b2c019db4..d494561d0f 100644
--- a/code/controllers/subsystem/persistence/_persistence.dm
+++ b/code/controllers/subsystem/persistence/_persistence.dm
@@ -88,7 +88,6 @@ SUBSYSTEM_DEF(persistence)
SavePhotoPersistence() //THIS IS PERSISTENCE, NOT THE LOGGING PORTION.
SavePaintings()
SaveScars()
- SaveTCGCards()
/**
* Loads persistent data relevant to the current map: Objects, etc.
diff --git a/code/controllers/subsystem/persistence/recent_votes_etc.dm b/code/controllers/subsystem/persistence/recent_votes_etc.dm
index f1b902d6ab..87f1ec0d4f 100644
--- a/code/controllers/subsystem/persistence/recent_votes_etc.dm
+++ b/code/controllers/subsystem/persistence/recent_votes_etc.dm
@@ -3,6 +3,7 @@
*/
/datum/controller/subsystem/persistence
var/list/saved_modes = list(1,2,3)
+ var/list/saved_chaos = list(5,5,5)
var/list/saved_dynamic_rules = list(list(),list(),list())
var/list/saved_storytellers = list("foo","bar","baz")
var/list/average_dynamic_threat = 50
@@ -20,6 +21,7 @@
/datum/controller/subsystem/persistence/LoadServerPersistence()
. = ..()
LoadRecentModes()
+ LoadRecentChaos()
LoadRecentStorytellers()
LoadRecentRulesets()
LoadRecentMaps()
@@ -33,6 +35,14 @@
file_data["data"] = saved_modes
fdel(json_file)
WRITE_FILE(json_file, json_encode(file_data))
+ saved_chaos[3] = saved_chaos[2]
+ saved_chaos[2] = saved_chaos[1]
+ saved_chaos[1] = SSticker.mode.get_chaos()
+ json_file = file("data/RecentChaos.json")
+ file_data = list()
+ file_data["data"] = saved_chaos
+ fdel(json_file)
+ WRITE_FILE(json_file, json_encode(file_data))
/datum/controller/subsystem/persistence/proc/CollectStoryteller(var/datum/game_mode/dynamic/mode)
saved_storytellers.len = 3
@@ -76,6 +86,15 @@
return
saved_modes = json["data"]
+/datum/controller/subsystem/persistence/proc/LoadRecentChaos()
+ var/json_file = file("data/RecentChaos.json")
+ if(!fexists(json_file))
+ return
+ var/list/json = json_decode(file2text(json_file))
+ if(!json)
+ return
+ saved_chaos = json["data"]
+
/datum/controller/subsystem/persistence/proc/LoadRecentRulesets()
var/json_file = file("data/RecentRulesets.json")
if(!fexists(json_file))
@@ -105,3 +124,9 @@
if(!json)
return
saved_maps = json["maps"]
+
+/datum/controller/subsystem/persistence/proc/get_recent_chaos()
+ var/sum = 0
+ for(var/n in saved_chaos)
+ sum += n
+ return sum/length(saved_chaos)
diff --git a/code/controllers/subsystem/processing/processing.dm b/code/controllers/subsystem/processing/processing.dm
index 5cefd3a148..f5a423b353 100644
--- a/code/controllers/subsystem/processing/processing.dm
+++ b/code/controllers/subsystem/processing/processing.dm
@@ -47,5 +47,5 @@ SUBSYSTEM_DEF(processing)
* If you override this do not call parent, as it will return PROCESS_KILL. This is done to prevent objects that dont override process() from staying in the processing list
*/
/datum/proc/process(delta_time)
- set waitfor = FALSE
+ // SHOULD_NOT_SLEEP(TRUE)
return PROCESS_KILL
diff --git a/code/controllers/subsystem/processing/weather.dm b/code/controllers/subsystem/processing/weather.dm
index ca067953cc..4035149ef2 100644
--- a/code/controllers/subsystem/processing/weather.dm
+++ b/code/controllers/subsystem/processing/weather.dm
@@ -70,3 +70,8 @@ PROCESSING_SUBSYSTEM_DEF(weather)
A = W
break
return A
+
+/datum/controller/subsystem/processing/weather/proc/get_weather_by_type(datum/weather/weather_datum_type)
+ for(var/V in processing)
+ if(istype(V,weather_datum_type))
+ return V
diff --git a/code/controllers/subsystem/shuttle.dm b/code/controllers/subsystem/shuttle.dm
index b74f1d46d3..889fdf35a1 100644
--- a/code/controllers/subsystem/shuttle.dm
+++ b/code/controllers/subsystem/shuttle.dm
@@ -12,6 +12,10 @@ SUBSYSTEM_DEF(shuttle)
var/list/beacons = list()
var/list/transit = list()
+ //Now it only for ID generation
+ var/list/assoc_mobile = list()
+ var/list/assoc_stationary = list()
+
var/list/transit_requesters = list()
var/list/transit_request_failures = list()
@@ -26,6 +30,7 @@ SUBSYSTEM_DEF(shuttle)
var/emergencyCallAmount = 0 //how many times the escape shuttle was called
var/emergencyNoEscape
var/emergencyNoRecall = FALSE
+ var/adminEmergencyNoRecall = FALSE
var/list/hostileEnvironments = list() //Things blocking escape shuttle from leaving
var/list/tradeBlockade = list() //Things blocking cargo from leaving.
var/supplyBlocked = FALSE
@@ -65,6 +70,8 @@ SUBSYSTEM_DEF(shuttle)
var/datum/turf_reservation/preview_reservation
+ var/shuttle_loading
+
/datum/controller/subsystem/shuttle/Initialize(timeofday)
ordernum = rand(1, 9000)
@@ -134,7 +141,7 @@ SUBSYSTEM_DEF(shuttle)
break
/datum/controller/subsystem/shuttle/proc/CheckAutoEvac()
- if(emergencyNoEscape || emergencyNoRecall || !emergency || !SSticker.HasRoundStarted())
+ if(emergencyNoEscape || adminEmergencyNoRecall || emergencyNoRecall || !emergency || !SSticker.HasRoundStarted())
return
var/threshold = CONFIG_GET(number/emergency_shuttle_autocall_threshold)
@@ -179,31 +186,26 @@ SUBSYSTEM_DEF(shuttle)
return S
WARNING("couldn't find dock with id: [id]")
+/// Check if we can call the evac shuttle.
+/// Returns TRUE if we can. Otherwise, returns a string detailing the problem.
/datum/controller/subsystem/shuttle/proc/canEvac(mob/user)
var/srd = CONFIG_GET(number/shuttle_refuel_delay)
if(world.time - SSticker.round_start_time < srd)
- to_chat(user, "The emergency shuttle is refueling. Please wait [DisplayTimeText(srd - (world.time - SSticker.round_start_time))] before trying again.")
- return FALSE
+ return "The emergency shuttle is refueling. Please wait [DisplayTimeText(srd - (world.time - SSticker.round_start_time))] before attempting to call."
switch(emergency.mode)
if(SHUTTLE_RECALL)
- to_chat(user, "The emergency shuttle may not be called while returning to CentCom.")
- return FALSE
+ return "The emergency shuttle may not be called while returning to CentCom."
if(SHUTTLE_CALL)
- to_chat(user, "The emergency shuttle is already on its way.")
- return FALSE
+ return "The emergency shuttle is already on its way."
if(SHUTTLE_DOCKED)
- to_chat(user, "The emergency shuttle is already here.")
- return FALSE
+ return "The emergency shuttle is already here."
if(SHUTTLE_IGNITING)
- to_chat(user, "The emergency shuttle is firing its engines to leave.")
- return FALSE
+ return "The emergency shuttle is firing its engines to leave."
if(SHUTTLE_ESCAPE)
- to_chat(user, "The emergency shuttle is moving away to a safe distance.")
- return FALSE
+ return "The emergency shuttle is moving away to a safe distance."
if(SHUTTLE_STRANDED)
- to_chat(user, "The emergency shuttle has been disabled by CentCom.")
- return FALSE
+ return "The emergency shuttle has been disabled by CentCom."
return TRUE
@@ -221,7 +223,9 @@ SUBSYSTEM_DEF(shuttle)
Good luck.")
emergency = backup_shuttle
- if(!canEvac(user))
+ var/can_evac_or_fail_reason = SSshuttle.canEvac(user)
+ if(can_evac_or_fail_reason != TRUE)
+ to_chat(user, "[can_evac_or_fail_reason]")
return
call_reason = trim(html_encode(call_reason))
@@ -250,10 +254,11 @@ SUBSYSTEM_DEF(shuttle)
var/area/A = get_area(user)
log_shuttle("[key_name(user)] has called the emergency shuttle.")
- deadchat_broadcast(" has called the shuttle at [A.name].", "[user.real_name]", user)
+ deadchat_broadcast(" has called the shuttle at [A.name].", "[user.real_name]", user) //, message_type=DEADCHAT_ANNOUNCEMENT)
if(call_reason)
SSblackbox.record_feedback("text", "shuttle_reason", 1, "[call_reason]")
log_shuttle("Shuttle call reason: [call_reason]")
+ SSticker.emergency_reason = call_reason
message_admins("[ADMIN_LOOKUPFLW(user)] has called the shuttle. (TRIGGER CENTCOM RECALL)")
/datum/controller/subsystem/shuttle/proc/centcom_recall(old_timer, admiral_message)
@@ -288,7 +293,7 @@ SUBSYSTEM_DEF(shuttle)
emergency.cancel(get_area(user))
log_shuttle("[key_name(user)] has recalled the shuttle.")
message_admins("[ADMIN_LOOKUPFLW(user)] has recalled the shuttle.")
- deadchat_broadcast(" has recalled the shuttle from [get_area_name(user, TRUE)].", "[user.real_name]", user)
+ deadchat_broadcast(" has recalled the shuttle from [get_area_name(user, TRUE)].", "[user.real_name]", user) //, message_type=DEADCHAT_ANNOUNCEMENT)
return 1
/datum/controller/subsystem/shuttle/proc/canRecall()
@@ -314,7 +319,7 @@ SUBSYSTEM_DEF(shuttle)
if (!SSticker.IsRoundInProgress())
return
- var/callShuttle = 1
+ var/callShuttle = TRUE
for(var/thing in GLOB.shuttle_caller_list)
if(isAI(thing))
@@ -330,7 +335,7 @@ SUBSYSTEM_DEF(shuttle)
var/turf/T = get_turf(thing)
if(T && is_station_level(T.z))
- callShuttle = 0
+ callShuttle = FALSE
break
if(callShuttle)
@@ -406,7 +411,7 @@ SUBSYSTEM_DEF(shuttle)
else
if(M.initiate_docking(getDock(destination)) != DOCKING_SUCCESS)
return 2
- return 0 //dock successful
+ return 0 //dock successful
/datum/controller/subsystem/shuttle/proc/moveShuttle(shuttleId, dockId, timed)
@@ -664,7 +669,7 @@ SUBSYSTEM_DEF(shuttle)
emergencyNoRecall = TRUE
endvote_passed = TRUE
-/datum/controller/subsystem/shuttle/proc/action_load(datum/map_template/shuttle/loading_template, obj/docking_port/stationary/destination_port)
+/datum/controller/subsystem/shuttle/proc/action_load(datum/map_template/shuttle/loading_template, obj/docking_port/stationary/destination_port, replace = FALSE)
// Check for an existing preview
if(preview_shuttle && (loading_template != preview_template))
preview_shuttle.jumpToNullSpace()
@@ -673,8 +678,8 @@ SUBSYSTEM_DEF(shuttle)
QDEL_NULL(preview_reservation)
if(!preview_shuttle)
- if(load_template(loading_template))
- preview_shuttle.linkup(loading_template, destination_port)
+ load_template(loading_template)
+ // preview_shuttle.linkup(loading_template, destination_port)
preview_template = loading_template
// get the existing shuttle information, if any
@@ -684,7 +689,7 @@ SUBSYSTEM_DEF(shuttle)
if(istype(destination_port))
D = destination_port
- else if(existing_shuttle)
+ else if(existing_shuttle && replace)
timer = existing_shuttle.timer
mode = existing_shuttle.mode
D = existing_shuttle.get_docked()
@@ -703,11 +708,12 @@ SUBSYSTEM_DEF(shuttle)
WARNING("Template shuttle [preview_shuttle] cannot dock at [D] ([result]).")
return
- if(existing_shuttle)
+ if(existing_shuttle && replace)
existing_shuttle.jumpToNullSpace()
var/list/force_memory = preview_shuttle.movement_force
preview_shuttle.movement_force = list("KNOCKDOWN" = 0, "THROW" = 0)
+ preview_shuttle.mode = SHUTTLE_PREARRIVAL//No idle shuttle moving. Transit dock get removed if shuttle moves too long.
preview_shuttle.initiate_docking(D)
preview_shuttle.movement_force = force_memory
@@ -718,7 +724,7 @@ SUBSYSTEM_DEF(shuttle)
preview_shuttle.timer = timer
preview_shuttle.mode = mode
- preview_shuttle.register()
+ preview_shuttle.register(replace)
// TODO indicate to the user that success happened, rather than just
// blanking the modification tab
@@ -848,7 +854,8 @@ SUBSYSTEM_DEF(shuttle)
return data
/datum/controller/subsystem/shuttle/ui_act(action, params)
- if(..())
+ . = ..()
+ if(.)
return
var/mob/user = usr
@@ -891,22 +898,10 @@ SUBSYSTEM_DEF(shuttle)
SSblackbox.record_feedback("text", "shuttle_manipulator", 1, "[M.name]")
break
- if("preview")
- if(S)
- . = TRUE
- unload_preview()
- load_template(S)
- if(preview_shuttle)
- preview_template = S
- user.forceMove(get_turf(preview_shuttle))
if("load")
- if(existing_shuttle == backup_shuttle)
- // TODO make the load button disabled
- WARNING("The shuttle that the selected shuttle will replace \
- is the backup shuttle. Backup shuttle is required to be \
- intact for round sanity.")
- else if(S)
+ if(S && !shuttle_loading)
. = TRUE
+ shuttle_loading = TRUE
// If successful, returns the mobile docking port
var/obj/docking_port/mobile/mdp = action_load(S)
if(mdp)
@@ -914,3 +909,38 @@ SUBSYSTEM_DEF(shuttle)
message_admins("[key_name_admin(usr)] loaded [mdp] with the shuttle manipulator.")
log_admin("[key_name(usr)] loaded [mdp] with the shuttle manipulator.")
SSblackbox.record_feedback("text", "shuttle_manipulator", 1, "[mdp.name]")
+ shuttle_loading = FALSE
+
+ if("preview")
+ //if(preview_shuttle && (loading_template != preview_template))
+ if(S && !shuttle_loading)
+ . = TRUE
+ shuttle_loading = TRUE
+ unload_preview()
+ load_template(S)
+ if(preview_shuttle)
+ preview_template = S
+ user.forceMove(get_turf(preview_shuttle))
+ shuttle_loading = FALSE
+
+ if("replace")
+ if(existing_shuttle == backup_shuttle)
+ // TODO make the load button disabled
+ WARNING("The shuttle that the selected shuttle will replace \
+ is the backup shuttle. Backup shuttle is required to be \
+ intact for round sanity.")
+ else if(S && !shuttle_loading)
+ . = TRUE
+ shuttle_loading = TRUE
+ // If successful, returns the mobile docking port
+ var/obj/docking_port/mobile/mdp = action_load(S, replace = TRUE)
+ if(mdp)
+ user.forceMove(get_turf(mdp))
+ message_admins("[key_name_admin(usr)] load/replaced [mdp] with the shuttle manipulator.")
+ log_admin("[key_name(usr)] load/replaced [mdp] with the shuttle manipulator.")
+ SSblackbox.record_feedback("text", "shuttle_manipulator", 1, "[mdp.name]")
+ shuttle_loading = FALSE
+ if(emergency == mdp) //you just changed the emergency shuttle, there are events in game + captains that can change your snowflake choice.
+ var/set_purchase = alert(usr, "Do you want to also disable shuttle purchases/random events that would change the shuttle?", "Butthurt Admin Prevention", "Yes, disable purchases/events", "No, I want to possibly get owned")
+ if(set_purchase == "Yes, disable purchases/events")
+ SSshuttle.shuttle_purchased = SHUTTLEPURCHASE_FORCED
diff --git a/code/controllers/subsystem/stickyban.dm b/code/controllers/subsystem/stickyban.dm
index 0c71777bc0..c61ea9943e 100644
--- a/code/controllers/subsystem/stickyban.dm
+++ b/code/controllers/subsystem/stickyban.dm
@@ -60,15 +60,10 @@ SUBSYSTEM_DEF(stickyban)
/datum/controller/subsystem/stickyban/proc/Populatedbcache()
var/newdbcache = list() //so if we runtime or the db connection dies we don't kill the existing cache
- // var/datum/db_query/query_stickybans = SSdbcore.NewQuery("SELECT ckey, reason, banning_admin, datetime FROM [format_table_name("stickyban")] ORDER BY ckey")
- // var/datum/db_query/query_ckey_matches = SSdbcore.NewQuery("SELECT stickyban, matched_ckey, first_matched, last_matched, exempt FROM [format_table_name("stickyban_matched_ckey")] ORDER BY first_matched")
- // var/datum/db_query/query_cid_matches = SSdbcore.NewQuery("SELECT stickyban, matched_cid, first_matched, last_matched FROM [format_table_name("stickyban_matched_cid")] ORDER BY first_matched")
- // var/datum/db_query/query_ip_matches = SSdbcore.NewQuery("SELECT stickyban, INET_NTOA(matched_ip), first_matched, last_matched FROM [format_table_name("stickyban_matched_ip")] ORDER BY first_matched")
-
- var/datum/DBQuery/query_stickybans = SSdbcore.NewQuery("SELECT ckey, reason, banning_admin, datetime FROM [format_table_name("stickyban")] ORDER BY ckey")
- var/datum/DBQuery/query_ckey_matches = SSdbcore.NewQuery("SELECT stickyban, matched_ckey, first_matched, last_matched, exempt FROM [format_table_name("stickyban_matched_ckey")] ORDER BY first_matched")
- var/datum/DBQuery/query_cid_matches = SSdbcore.NewQuery("SELECT stickyban, matched_cid, first_matched, last_matched FROM [format_table_name("stickyban_matched_cid")] ORDER BY first_matched")
- var/datum/DBQuery/query_ip_matches = SSdbcore.NewQuery("SELECT stickyban, INET_NTOA(matched_ip), first_matched, last_matched FROM [format_table_name("stickyban_matched_ip")] ORDER BY first_matched")
+ var/datum/db_query/query_stickybans = SSdbcore.NewQuery("SELECT ckey, reason, banning_admin, datetime FROM [format_table_name("stickyban")] ORDER BY ckey")
+ var/datum/db_query/query_ckey_matches = SSdbcore.NewQuery("SELECT stickyban, matched_ckey, first_matched, last_matched, exempt FROM [format_table_name("stickyban_matched_ckey")] ORDER BY first_matched")
+ var/datum/db_query/query_cid_matches = SSdbcore.NewQuery("SELECT stickyban, matched_cid, first_matched, last_matched FROM [format_table_name("stickyban_matched_cid")] ORDER BY first_matched")
+ var/datum/db_query/query_ip_matches = SSdbcore.NewQuery("SELECT stickyban, INET_NTOA(matched_ip), first_matched, last_matched FROM [format_table_name("stickyban_matched_ip")] ORDER BY first_matched")
SSdbcore.QuerySelect(list(query_stickybans, query_ckey_matches, query_cid_matches, query_ip_matches))
@@ -161,25 +156,15 @@ SUBSYSTEM_DEF(stickyban)
if (!ban["message"])
ban["message"] = "Evasion"
- // TODO: USE NEW DB IMPLEMENTATION
- var/datum/DBQuery/query_create_stickyban = SSdbcore.NewQuery(
- "INSERT IGNORE INTO [format_table_name("stickyban")] (ckey, reason, banning_admin) VALUES ([ckey], [ban["message"]], ban["admin"]))"
+ var/datum/db_query/query_create_stickyban = SSdbcore.NewQuery(
+ "INSERT IGNORE INTO [format_table_name("stickyban")] (ckey, reason, banning_admin) VALUES (:ckey, :message, :admin)",
+ list("ckey" = ckey, "message" = ban["message"], "admin" = ban["admin"])
)
-
- if (query_create_stickyban.warn_execute())
+ if (!query_create_stickyban.warn_execute())
qdel(query_create_stickyban)
return
qdel(query_create_stickyban)
- // var/datum/db_query/query_create_stickyban = SSdbcore.NewQuery(
- // "INSERT IGNORE INTO [format_table_name("stickyban")] (ckey, reason, banning_admin) VALUES (:ckey, :message, :admin)",
- // list("ckey" = ckey, "message" = ban["message"], "admin" = ban["admin"])
- // )
- // if (!query_create_stickyban.warn_execute())
- // qdel(query_create_stickyban)
- // return
- // qdel(query_create_stickyban)
-
var/list/sqlckeys = list()
var/list/sqlcids = list()
var/list/sqlips = list()
diff --git a/code/controllers/subsystem/sun.dm b/code/controllers/subsystem/sun.dm
index 442329cf46..746b1be7a9 100644
--- a/code/controllers/subsystem/sun.dm
+++ b/code/controllers/subsystem/sun.dm
@@ -1,32 +1,63 @@
+#define OCCLUSION_DISTANCE 20
+
+/datum/sun
+ var/azimuth = 0 // clockwise, top-down rotation from 0 (north) to 359
+ var/power_mod = 1 // how much power this sun is outputting relative to standard
+
+
+/datum/sun/vv_edit_var(var_name, var_value)
+ . = ..()
+ if(var_name == NAMEOF(src, azimuth))
+ SSsun.complete_movement()
+
+/atom/proc/check_obscured(datum/sun/sun, distance = OCCLUSION_DISTANCE)
+ var/target_x = round(sin(sun.azimuth), 0.01)
+ var/target_y = round(cos(sun.azimuth), 0.01)
+ var/x_hit = x
+ var/y_hit = y
+ var/turf/hit
+
+ for(var/run in 1 to distance)
+ x_hit += target_x
+ y_hit += target_y
+ hit = locate(round(x_hit, 1), round(y_hit, 1), z)
+ if(hit.opacity)
+ return TRUE
+ if(hit.x == 1 || hit.x == world.maxx || hit.y == 1 || hit.y == world.maxy) //edge of the map
+ break
+ return FALSE
+
SUBSYSTEM_DEF(sun)
name = "Sun"
wait = 1 MINUTES
flags = SS_NO_TICK_CHECK
- var/azimuth = 0 ///clockwise, top-down rotation from 0 (north) to 359
+ var/list/datum/sun/suns = list()
+ var/datum/sun/primary_sun
var/azimuth_mod = 1 ///multiplier against base_rotation
var/base_rotation = 6 ///base rotation in degrees per fire
/datum/controller/subsystem/sun/Initialize(start_timeofday)
- azimuth = rand(0, 359)
+ primary_sun = new
+ suns += primary_sun
+ primary_sun.azimuth = rand(0, 359)
azimuth_mod = round(rand(50, 200)/100, 0.01) // 50% - 200% of standard rotation
if(prob(50))
azimuth_mod *= -1
return ..()
/datum/controller/subsystem/sun/fire(resumed = FALSE)
- azimuth += azimuth_mod * base_rotation
- azimuth = round(azimuth, 0.01)
- if(azimuth >= 360)
- azimuth -= 360
- if(azimuth < 0)
- azimuth += 360
+ for(var/S in suns)
+ var/datum/sun/sun = S
+ sun.azimuth += azimuth_mod * base_rotation
+ sun.azimuth = round(sun.azimuth, 0.01)
+ if(sun.azimuth >= 360)
+ sun.azimuth -= 360
+ if(sun.azimuth < 0)
+ sun.azimuth += 360
complete_movement()
/datum/controller/subsystem/sun/proc/complete_movement()
- SEND_SIGNAL(src, COMSIG_SUN_MOVED, azimuth)
+ SEND_SIGNAL(src, COMSIG_SUN_MOVED, primary_sun, suns)
-/datum/controller/subsystem/sun/vv_edit_var(var_name, var_value)
- . = ..()
- if(var_name == NAMEOF(src, azimuth))
- complete_movement()
+#undef OCCLUSION_DISTANCE
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index 198c380f41..f37feeea34 100755
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -69,6 +69,7 @@ SUBSYSTEM_DEF(ticker)
var/modevoted = FALSE //Have we sent a vote for the gamemode?
var/station_integrity = 100 // stored at roundend for use in some antag goals
+ var/emergency_reason
/datum/controller/subsystem/ticker/Initialize(timeofday)
load_mode()
@@ -268,7 +269,7 @@ SUBSYSTEM_DEF(ticker)
if(!GLOB.Debug2)
if(!can_continue)
log_game("[mode.name] failed pre_setup, cause: [mode.setup_error]")
- send2irc("SSticker", "[mode.name] failed pre_setup, cause: [mode.setup_error]")
+ send2adminchat("SSticker", "[mode.name] failed pre_setup, cause: [mode.setup_error]")
message_admins("[mode.name] failed pre_setup, cause: [mode.setup_error]")
QDEL_NULL(mode)
to_chat(world, "Error setting up [GLOB.master_mode]. Reverting to pre-game lobby.")
@@ -334,7 +335,7 @@ SUBSYSTEM_DEF(ticker)
var/list/adm = get_admin_counts()
var/list/allmins = adm["present"]
- send2irc("Server", "Round [GLOB.round_id ? "#[GLOB.round_id]:" : "of"] [hide_mode ? "secret":"[mode.name]"] has started[allmins.len ? ".":" with no active admins online!"]")
+ send2adminchat("Server", "Round [GLOB.round_id ? "#[GLOB.round_id]:" : "of"] [hide_mode ? "secret":"[mode.name]"] has started[allmins.len ? ".":" with no active admins online!"]")
setup_done = TRUE
for(var/i in GLOB.start_landmarks_list)
@@ -563,7 +564,10 @@ SUBSYSTEM_DEF(ticker)
if(STATION_DESTROYED_NUKE)
news_message = "We would like to reassure all employees that the reports of a Syndicate backed nuclear attack on [station_name()] are, in fact, a hoax. Have a secure day!"
if(STATION_EVACUATED)
- news_message = "The crew of [station_name()] has been evacuated amid unconfirmed reports of enemy activity."
+ if(emergency_reason)
+ news_message = "[station_name()] has been evacuated after transmitting the following distress beacon:\n\n[emergency_reason]"
+ else
+ news_message = "The crew of [station_name()] has been evacuated amid unconfirmed reports of enemy activity."
if(BLOB_WIN)
news_message = "[station_name()] was overcome by an unknown biological outbreak, killing all crew on board. Don't let it happen to you! Remember, a clean work station is a safe work station."
if(BLOB_NUKE)
@@ -589,7 +593,7 @@ SUBSYSTEM_DEF(ticker)
if(WIZARD_KILLED)
news_message = "Tensions have flared with the Space Wizard Federation following the death of one of their members aboard [station_name()]."
if(STATION_NUKED)
- news_message = "[station_name()] activated its self destruct device for unknown reasons. Attempts to clone the Captain so he can be arrested and executed are underway."
+ news_message = "[station_name()] activated its self-destruct device for unknown reasons. Attempts to clone the Captain so he can be arrested and executed are underway."
if(CLOCK_SUMMON)
news_message = "The garbled messages about hailing a mouse and strange energy readings from [station_name()] have been discovered to be an ill-advised, if thorough, prank by a clown."
if(CLOCK_SILICONS)
@@ -604,7 +608,8 @@ SUBSYSTEM_DEF(ticker)
if(SSblackbox.first_death)
var/list/ded = SSblackbox.first_death
if(ded.len)
- news_message += " NT Sanctioned Psykers picked up faint traces of someone near the station, allegedly having had died. Their name was: [ded["name"]], [ded["role"]], at [ded["area"]].[ded["last_words"] ? " Their last words were: \"[ded["last_words"]]\"" : ""]"
+ var/last_words = ded["last_words"] ? " Their last words were: \"[ded["last_words"]]\"" : ""
+ news_message += " NT Sanctioned Psykers picked up faint traces of someone near the station, allegedly having had died. Their name was: [ded["name"]], [ded["role"]], at [ded["area"]].[last_words]"
else
news_message += " NT Sanctioned Psykers proudly confirm reports that nobody died this shift!"
diff --git a/code/datums/achievements/_achievement_data.dm b/code/datums/achievements/_achievement_data.dm
new file mode 100644
index 0000000000..80f544a965
--- /dev/null
+++ b/code/datums/achievements/_achievement_data.dm
@@ -0,0 +1,149 @@
+///Datum that handles
+/datum/achievement_data
+ ///Ckey of this achievement data's owner
+ var/owner_ckey
+ ///Up to date list of all achievements and their info.
+ var/data = list()
+ ///Original status of achievement.
+ var/original_cached_data = list()
+ ///Have we done our set-up yet?
+ var/initialized = FALSE
+
+/datum/achievement_data/New(ckey)
+ owner_ckey = ckey
+ if(SSachievements.initialized && !initialized)
+ InitializeData()
+
+/datum/achievement_data/proc/InitializeData()
+ initialized = TRUE
+ load_all_achievements() //So we know which achievements we have unlocked so far.
+
+///Gets list of changed rows in MassInsert format
+/datum/achievement_data/proc/get_changed_data()
+ . = list()
+ for(var/T in data)
+ var/datum/award/A = SSachievements.awards[T]
+ if(data[T] != original_cached_data[T])//If our data from before is not the same as now, save it to db.
+ var/deets = A.get_changed_rows(owner_ckey,data[T])
+ if(deets)
+ . += list(deets)
+
+/datum/achievement_data/proc/load_all_achievements()
+ set waitfor = FALSE
+
+ var/list/kv = list()
+ var/datum/db_query/Query = SSdbcore.NewQuery(
+ "SELECT achievement_key,value FROM [format_table_name("achievements")] WHERE ckey = :ckey",
+ list("ckey" = owner_ckey)
+ )
+ if(!Query.Execute())
+ qdel(Query)
+ return
+ while(Query.NextRow())
+ var/key = Query.item[1]
+ var/value = text2num(Query.item[2])
+ kv[key] = value
+ qdel(Query)
+
+ for(var/T in subtypesof(/datum/award))
+ var/datum/award/A = SSachievements.awards[T]
+ if(!A || !A.name) //Skip abstract achievements types
+ continue
+ if(!data[T])
+ data[T] = A.parse_value(kv[A.database_id])
+ original_cached_data[T] = data[T]
+
+///Updates local cache with db data for the given achievement type if it wasn't loaded yet.
+/datum/achievement_data/proc/get_data(achievement_type)
+ var/datum/award/A = SSachievements.awards[achievement_type]
+ if(!A.name)
+ return FALSE
+ if(!data[achievement_type])
+ data[achievement_type] = A.load(owner_ckey)
+ original_cached_data[achievement_type] = data[achievement_type]
+
+///Unlocks an achievement of a specific type. achievement type is a typepath to the award, user is the mob getting the award, and value is an optional value to be used for defining a score to add to the leaderboard
+/datum/achievement_data/proc/unlock(achievement_type, mob/user, value = 1)
+ set waitfor = FALSE
+
+ if(!SSachievements.achievements_enabled)
+ return
+ var/datum/award/A = SSachievements.awards[achievement_type]
+ get_data(achievement_type) //Get the current status first if necessary
+ if(istype(A, /datum/award/achievement))
+ if(data[achievement_type]) //You already unlocked it so don't bother running the unlock proc
+ return
+ data[achievement_type] = TRUE
+ A.on_unlock(user) //Only on default achievement, as scores keep going up.
+ else if(istype(A, /datum/award/score))
+ data[achievement_type] += value
+
+///Getter for the status/score of an achievement
+/datum/achievement_data/proc/get_achievement_status(achievement_type)
+ return data[achievement_type]
+
+///Resets an achievement to default values.
+/datum/achievement_data/proc/reset(achievement_type)
+ if(!SSachievements.achievements_enabled)
+ return
+ var/datum/award/A = SSachievements.awards[achievement_type]
+ get_data(achievement_type)
+ if(istype(A, /datum/award/achievement))
+ data[achievement_type] = FALSE
+ else if(istype(A, /datum/award/score))
+ data[achievement_type] = 0
+
+/datum/achievement_data/ui_assets(mob/user)
+ return list(
+ get_asset_datum(/datum/asset/spritesheet/simple/achievements),
+ )
+
+/datum/achievement_data/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/achievement_data/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "Achievements")
+ ui.open()
+
+/datum/achievement_data/ui_data(mob/user)
+ var/ret_data = list() // screw standards (qustinnus you must rename src.data ok)
+ ret_data["categories"] = list("Bosses", "Misc", "Mafia", "Scores")
+ ret_data["achievements"] = list()
+ ret_data["user_key"] = user.ckey
+
+ var/datum/asset/spritesheet/simple/assets = get_asset_datum(/datum/asset/spritesheet/simple/achievements)
+ //This should be split into static data later
+ for(var/achievement_type in SSachievements.awards)
+ if(!SSachievements.awards[achievement_type].name) //No name? we a subtype.
+ continue
+ if(isnull(data[achievement_type])) //We're still loading
+ continue
+ var/list/this = list(
+ "name" = SSachievements.awards[achievement_type].name,
+ "desc" = SSachievements.awards[achievement_type].desc,
+ "category" = SSachievements.awards[achievement_type].category,
+ "icon_class" = assets.icon_class_name(SSachievements.awards[achievement_type].icon),
+ "value" = data[achievement_type],
+ "score" = ispath(achievement_type,/datum/award/score)
+ )
+ ret_data["achievements"] += list(this)
+
+ return ret_data
+
+/datum/achievement_data/ui_static_data(mob/user)
+ . = ..()
+ .["highscore"] = list()
+ for(var/score in SSachievements.scores)
+ var/datum/award/score/S = SSachievements.scores[score]
+ if(!S.name || !S.track_high_scores || !S.high_scores.len)
+ continue
+ .["highscore"] += list(list("name" = S.name,"scores" = S.high_scores))
+
+/client/verb/checkachievements()
+ set category = "OOC"
+ set name = "Check achievements"
+ set desc = "See all of your achievements!"
+
+ player_details.achievements.ui_interact(usr)
diff --git a/code/datums/achievements/_awards.dm b/code/datums/achievements/_awards.dm
new file mode 100644
index 0000000000..77485de8ba
--- /dev/null
+++ b/code/datums/achievements/_awards.dm
@@ -0,0 +1,117 @@
+/datum/award
+ ///Name of the achievement, If null it won't show up in the achievement browser. (Handy for inheritance trees)
+ var/name
+ var/desc = "You did it."
+ ///Found in UI_Icons/Achievements
+ var/icon = "default"
+ var/category = "Normal"
+
+ ///What ID do we use in db, limited to 32 characters
+ var/database_id
+ //Bump this up if you're changing outdated table identifier and/or achievement type
+ var/achievement_version = 2
+
+ //Value returned on db connection failure, in case we want to differ 0 and nonexistent later on
+ var/default_value = FALSE
+
+///This proc loads the achievement data from the hub.
+/datum/award/proc/load(key)
+ if(!SSdbcore.Connect())
+ return default_value
+ if(!key || !database_id || !name)
+ return default_value
+ var/raw_value = get_raw_value(key)
+ return parse_value(raw_value)
+
+///This saves the changed data to the hub.
+/datum/award/proc/get_changed_rows(key, value)
+ if(!database_id || !key || !name)
+ return
+ return list(
+ "ckey" = key,
+ "achievement_key" = database_id,
+ "value" = value,
+ )
+
+/datum/award/proc/get_metadata_row()
+ return list(
+ "achievement_key" = database_id,
+ "achievement_version" = achievement_version,
+ "achievement_type" = "award",
+ "achievement_name" = name,
+ "achievement_description" = desc,
+ )
+
+///Get raw numerical achievement value from the database
+/datum/award/proc/get_raw_value(key)
+ var/datum/db_query/Q = SSdbcore.NewQuery(
+ "SELECT value FROM [format_table_name("achievements")] WHERE ckey = :ckey AND achievement_key = :achievement_key",
+ list("ckey" = key, "achievement_key" = database_id)
+ )
+ if(!Q.Execute(async = TRUE))
+ qdel(Q)
+ return 0
+ var/result = 0
+ if(Q.NextRow())
+ result = text2num(Q.item[1])
+ qdel(Q)
+ return result
+
+//Should return sanitized value for achievement cache
+/datum/award/proc/parse_value(raw_value)
+ return default_value
+
+///Can be overriden for achievement specific events
+/datum/award/proc/on_unlock(mob/user)
+ return
+
+///Achievements are one-off awards for usually doing cool things.
+/datum/award/achievement
+ desc = "Achievement for epic people"
+
+/datum/award/achievement/get_metadata_row()
+ . = ..()
+ .["achievement_type"] = "achievement"
+
+/datum/award/achievement/parse_value(raw_value)
+ return raw_value > 0
+
+/datum/award/achievement/on_unlock(mob/user)
+ . = ..()
+ to_chat(user, "Achievement unlocked: [name]!")
+
+///Scores are for leaderboarded things, such as killcount of a specific boss
+/datum/award/score
+ desc = "you did it sooo many times."
+ category = "Scores"
+ default_value = 0
+
+ var/track_high_scores = TRUE
+ var/list/high_scores = list()
+
+/datum/award/score/New()
+ . = ..()
+ if(track_high_scores)
+ LoadHighScores()
+
+/datum/award/score/get_metadata_row()
+ . = ..()
+ .["achievement_type"] = "score"
+
+/datum/award/score/proc/LoadHighScores()
+ var/datum/db_query/Q = SSdbcore.NewQuery(
+ "SELECT ckey,value FROM [format_table_name("achievements")] WHERE achievement_key = :achievement_key ORDER BY value DESC LIMIT 50",
+ list("achievement_key" = database_id)
+ )
+ if(!Q.Execute(async = TRUE))
+ qdel(Q)
+ return
+ else
+ while(Q.NextRow())
+ var/key = Q.item[1]
+ var/score = text2num(Q.item[2])
+ high_scores[key] = score
+ qdel(Q)
+
+/datum/award/score/parse_value(raw_value)
+ return isnum(raw_value) ? raw_value : 0
diff --git a/code/datums/achievements/boss_achievements.dm b/code/datums/achievements/boss_achievements.dm
new file mode 100644
index 0000000000..104a369405
--- /dev/null
+++ b/code/datums/achievements/boss_achievements.dm
@@ -0,0 +1,130 @@
+/datum/award/achievement/boss
+ category = "Bosses"
+ icon = "baseboss"
+
+/datum/award/achievement/boss/tendril_exterminator
+ name = "Tendril Exterminator"
+ desc = "Watch your step"
+ database_id = BOSS_MEDAL_TENDRIL
+ icon = "tendril"
+
+/datum/award/achievement/boss/boss_killer
+ name = "Boss Killer"
+ desc = "You've come a long ways from asking how to switch hands."
+ database_id = "Boss Killer"
+ // icon = "firstboss"
+
+/datum/award/achievement/boss/blood_miner_kill
+ name = "Blood-Drunk Miner Killer"
+ desc = "I guess he couldn't handle his drink that well."
+ database_id = BOSS_MEDAL_MINER
+ icon = "miner"
+
+/datum/award/achievement/boss/demonic_miner_kill
+ name = "Demonic-Frost Miner Killer"
+ desc = "Definitely harder than the Blood-Drunk Miner."
+ database_id = BOSS_MEDAL_FROSTMINER
+
+/datum/award/achievement/boss/bubblegum_kill
+ name = "Bubblegum Killer"
+ desc = "I guess he wasn't made of candy after all"
+ database_id = BOSS_MEDAL_BUBBLEGUM
+ icon = "bbgum"
+
+/datum/award/achievement/boss/colossus_kill
+ name = "Colossus Killer"
+ desc = "The bigger they are... the better the loot"
+ database_id = BOSS_MEDAL_COLOSSUS
+ icon = "colossus"
+
+/datum/award/achievement/boss/drake_kill
+ name = "Drake Killer"
+ desc = "Now I can wear Rune Platebodies!"
+ database_id = BOSS_MEDAL_DRAKE
+ icon = "drake"
+
+/datum/award/achievement/boss/hierophant_kill
+ name = "Hierophant Killer"
+ desc = "Hierophant, but not triumphant."
+ database_id = BOSS_MEDAL_HIEROPHANT
+ icon = "hierophant"
+
+/datum/award/achievement/boss/legion_kill
+ name = "Legion Killer"
+ desc = "We were many..now we are none."
+ database_id = BOSS_MEDAL_LEGION
+ icon = "legion"
+
+/datum/award/achievement/boss/swarmer_beacon_kill
+ name = "Swarm Beacon Killer"
+ desc = "GET THEM OFF OF ME!"
+ database_id = BOSS_MEDAL_SWARMERS
+ icon = "swarmer"
+
+/datum/award/achievement/boss/wendigo_kill
+ name = "Wendigo Killer"
+ desc = "You've now ruined years of mythical storytelling."
+ database_id = BOSS_MEDAL_WENDIGO
+
+/datum/award/achievement/boss/blood_miner_crusher
+ name = "Blood-Drunk Miner Crusher"
+ desc = "I guess he couldn't handle his drink that well."
+ database_id = BOSS_MEDAL_MINER_CRUSHER
+ icon = "miner"
+
+/datum/award/achievement/boss/demonic_miner_crusher
+ name = "Demonic-Frost Miner Crusher"
+ desc = "Definitely harder than the Blood-Drunk Miner."
+ database_id = BOSS_MEDAL_FROSTMINER_CRUSHER
+
+/datum/award/achievement/boss/bubblegum_crusher
+ name = "Bubblegum Crusher"
+ desc = "I guess he wasn't made of candy after all"
+ database_id = BOSS_MEDAL_BUBBLEGUM_CRUSHER
+ icon = "bbgum"
+
+/datum/award/achievement/boss/colossus_crusher
+ name = "Colossus Crusher"
+ desc = "The bigger they are... the better the loot"
+ database_id = BOSS_MEDAL_COLOSSUS_CRUSHER
+ icon = "colossus"
+
+/datum/award/achievement/boss/drake_crusher
+ name = "Drake Crusher"
+ desc = "Now I can wear Rune Platebodies!"
+ database_id = BOSS_MEDAL_DRAKE_CRUSHER
+ icon = "drake"
+
+/datum/award/achievement/boss/hierophant_crusher
+ name = "Hierophant Crusher"
+ desc = "Hierophant, but not triumphant."
+ database_id = BOSS_MEDAL_HIEROPHANT_CRUSHER
+ icon = "hierophant"
+
+/datum/award/achievement/boss/legion_crusher
+ name = "Legion Crusher"
+ desc = "We were many... now we are none."
+ database_id = BOSS_MEDAL_LEGION_CRUSHER
+
+/datum/award/achievement/boss/swarmer_beacon_crusher
+ name = "Swarm Beacon Crusher"
+ desc = "GET THEM OFF OF ME!"
+ database_id = BOSS_MEDAL_SWARMERS_CRUSHER
+
+/datum/award/achievement/boss/wendigo_crusher
+ name = "Wendigo Crusher"
+ desc = "You've now ruined years of mythical storytelling."
+ database_id = BOSS_MEDAL_WENDIGO_CRUSHER
+
+//should be removed soon
+// /datum/award/achievement/boss/king_goat_kill
+// name = "King Goat Killer"
+// desc = "The king is dead, long live the king!"
+// database_id = BOSS_MEDAL_KINGGOAT
+// icon = "goatboss"
+
+// /datum/award/achievement/boss/king_goat_crusher
+// name = "King Goat Crusher"
+// desc = "The king is dead, long live the king!"
+// database_id = BOSS_MEDAL_KINGGOAT_CRUSHER
+// icon = "goatboss"
diff --git a/code/datums/achievements/boss_scores.dm b/code/datums/achievements/boss_scores.dm
new file mode 100644
index 0000000000..fdb9efa7c7
--- /dev/null
+++ b/code/datums/achievements/boss_scores.dm
@@ -0,0 +1,54 @@
+/datum/award/score/tendril_score
+ name = "Tendril Score"
+ desc = "Watch your step"
+ database_id = TENDRIL_CLEAR_SCORE
+
+/datum/award/score/boss_score
+ name = "Bosses Killed"
+ desc = "You've killed HOW many?"
+ database_id = BOSS_SCORE
+
+/datum/award/score/blood_miner_score
+ name = "Blood-Drunk Miners Killed"
+ desc = "You've killed HOW many?"
+ database_id = MINER_SCORE
+
+/datum/award/score/demonic_miner_score
+ name = "Demonic-Frost Miners Killed"
+ desc = "You've killed HOW many?"
+ database_id = FROST_MINER_SCORE
+
+/datum/award/score/bubblegum_score
+ name = "Bubblegums Killed"
+ desc = "You've killed HOW many?"
+ database_id = BUBBLEGUM_SCORE
+
+/datum/award/score/colussus_score
+ name = "Colossus Killed"
+ desc = "You've killed HOW many?"
+ database_id = COLOSSUS_SCORE
+
+/datum/award/score/drake_score
+ name = "Drakes Killed"
+ desc = "You've killed HOW many?"
+ database_id = DRAKE_SCORE
+
+/datum/award/score/hierophant_score
+ name = "Hierophants Killed"
+ desc = "You've killed HOW many?"
+ database_id = HIEROPHANT_SCORE
+
+/datum/award/score/legion_score
+ name = "Legions Killed"
+ desc = "You've killed HOW many?"
+ database_id = LEGION_SCORE
+
+/datum/award/score/swarmer_beacon_score
+ name = "Swarmer Beacons Killed"
+ desc = "You've killed HOW many?"
+ database_id = SWARMER_BEACON_SCORE
+
+/datum/award/score/wendigo_score
+ name = "Wendigos Killed"
+ desc = "You've killed HOW many?"
+ database_id = WENDIGO_SCORE
diff --git a/code/datums/achievements/mafia_achievements.dm b/code/datums/achievements/mafia_achievements.dm
new file mode 100644
index 0000000000..fbe3486397
--- /dev/null
+++ b/code/datums/achievements/mafia_achievements.dm
@@ -0,0 +1,115 @@
+/datum/award/achievement/mafia
+ category = "Mafia"
+ icon = "basemafia"
+
+///ALL THE ACHIEVEMENTS FOR WINNING A ROUND AS A ROLE///
+
+/datum/award/achievement/mafia/assistant
+ name = "Assistant Victory"
+ desc = "If you got killed instead of someone more important, you just flexed the true strength of your \"\"\"\"role\"\"\"\"."
+ database_id = MAFIA_MEDAL_ASSISTANT
+ icon = "town"
+
+/datum/award/achievement/mafia/detective
+ name = "Detective Victory"
+ desc = "If you did this with a Medical Doctor in the game, i'm not really that impressed."
+ database_id = MAFIA_MEDAL_DETECTIVE
+ icon = "town"
+
+/datum/award/achievement/mafia/psychologist
+ name = "Psychologist Victory"
+ desc = "You learned how to not reveal someone random night one! Or... maybe you're just a lucky bastard."
+ database_id = MAFIA_MEDAL_PSYCHOLOGIST
+ icon = "town"
+
+/datum/award/achievement/mafia/chaplain
+ name = "Chaplain Victory"
+ desc = "Useless... until the one night the thoughtfeeder confidently claims themselves as detective. Mafia's true bullshit detector."
+ database_id = MAFIA_MEDAL_CHAPLAIN
+ icon = "town"
+
+/datum/award/achievement/mafia/md
+ name = "Medical Doctor Victory"
+ desc = "Congratulations on learning how to not talk!"
+ database_id = MAFIA_MEDAL_MD
+ icon = "town"
+
+/datum/award/achievement/mafia/officer
+ name = "Security Officer Victory"
+ desc = "Don't worry, you can win this if you're dead! You... did use your ability to become dead, right?"
+ database_id = MAFIA_MEDAL_OFFICER
+ icon = "town"
+
+/datum/award/achievement/mafia/lawyer
+ name = "Lawyer Victory"
+ desc = "Oh don't mind me, i'm just the worst rol- Oops, I just instantly ended the game."
+ database_id = MAFIA_MEDAL_LAWYER
+ icon = "town"
+
+/datum/award/achievement/mafia/hop
+ name = "Head of Personnel Victory"
+ desc = "King of Assistants, waster of a single mafia's night, thrower of games."
+ database_id = MAFIA_MEDAL_HOP
+ icon = "town"
+
+/datum/award/achievement/mafia/warden
+ name = "Warden Victory"
+ desc = "Make changelings think you're detective, go on lockdown, actual detective investigates you and dies. Cha cha real smooth!"
+ database_id = MAFIA_MEDAL_WARDEN
+ icon = "town"
+
+/datum/award/achievement/mafia/hos
+ name = "Head of Security Victory"
+ desc = "Certified not shitcurity."
+ database_id = MAFIA_MEDAL_HOS
+ icon = "town"
+
+/datum/award/achievement/mafia/changeling
+ name = "Changeling Victory"
+ desc = "I think the changelings are metacomming."
+ database_id = MAFIA_MEDAL_CHANGELING
+ icon = "mafia"
+
+/datum/award/achievement/mafia/thoughtfeeder
+ name = "Thoughtfeeder Victory"
+ desc = "Clown's best friend. And Obsessed. And fugitive? Whose side are you on?!"
+ database_id = MAFIA_MEDAL_THOUGHTFEEDER
+ icon = "mafia"
+
+/datum/award/achievement/mafia/traitor
+ name = "Traitor Victory"
+ desc = "Guys, we still have two more changelings to ki-!! TRAITOR VICTORY !!"
+ database_id = MAFIA_MEDAL_TRAITOR
+ icon = "neutral"
+
+/datum/award/achievement/mafia/nightmare
+ name = "Nightmare Victory"
+ desc = "DID YOUR LIGHT FLICKER?!"
+ database_id = MAFIA_MEDAL_NIGHTMARE
+ icon = "neutral"
+
+/datum/award/achievement/mafia/fugitive
+ name = "Fugitive Victory"
+ desc = "I'm just the description on an achievement, but if you end up having to choose between town and changelings, go changelings."
+ database_id = MAFIA_MEDAL_FUGITIVE
+ icon = "neutral"
+
+/datum/award/achievement/mafia/obsessed
+ name = "Obsessed Victory"
+ desc = "You got your target lynched, so instead of being spiteful and annoying, you're just smug and annoying."
+ database_id = MAFIA_MEDAL_OBSESSED
+ icon = "neutral"
+
+/datum/award/achievement/mafia/clown
+ name = "Clown Victory"
+ desc = "Did you know this works on traitors, despite their immunity? If you hit the jackpot and manage to kill one, they'll salt into the next dimension. Clown tips!"
+ database_id = MAFIA_MEDAL_CLOWN
+ icon = "neutral"
+
+///ALL THE ACHIEVEMENTS FOR MISC MAFIA ODDITIES///
+
+/datum/award/achievement/mafia/universally_hated
+ name = "Universally Hated"
+ desc = "Managed to get more than 12 votes when put up on trial, jesus christ."
+ database_id = MAFIA_MEDAL_HATED
+ icon = "hated"
diff --git a/code/datums/achievements/misc_achievements.dm b/code/datums/achievements/misc_achievements.dm
new file mode 100644
index 0000000000..0da38df8f3
--- /dev/null
+++ b/code/datums/achievements/misc_achievements.dm
@@ -0,0 +1,161 @@
+/datum/award/achievement/misc
+ category = "Misc"
+ icon = "basemisc"
+
+/datum/award/achievement/misc/meteor_examine
+ name = "Your Life Before Your Eyes"
+ desc = "Take a close look at hurtling space debris"
+ database_id = MEDAL_METEOR
+ icon = "meteors"
+
+/datum/award/achievement/misc/pulse
+ name = "Jackpot"
+ desc = "Win a pulse rifle from an arcade machine"
+ database_id = MEDAL_PULSE
+ icon = "jackpot"
+
+/datum/award/achievement/misc/time_waste
+ name = "Time waster"
+ desc = "Speak no evil, hear no evil, see just errors"
+ database_id = MEDAL_TIMEWASTE
+ icon = "timewaste"
+
+/datum/award/achievement/misc/feat_of_strength
+ name = "Feat of Strength"
+ desc = "If the rod is immovable, is it passing you or are you passing it?"
+ database_id = MEDAL_RODSUPLEX
+ icon = "featofstrength"
+
+/datum/award/achievement/misc/round_and_full
+ name = "Round and Full"
+ desc = "Well at least you aren't down the river, I hear they eat people there."
+ database_id = MEDAL_CLOWNCARKING
+ icon = "clownking"
+
+/datum/award/achievement/misc/the_best_driver
+ name = "The Best Driver"
+ desc = "100 honks later"
+ database_id = MEDAL_THANKSALOT
+ icon = "clownthanks"
+
+/datum/award/achievement/misc/helbitaljanken
+ name = "Helbitaljanken"
+ desc = "You janked hard"
+ database_id = MEDAL_HELBITALJANKEN
+ icon = "helbital"
+
+/datum/award/achievement/misc/getting_an_upgrade
+ name = "Getting an upgrade"
+ desc = "Make your first unique material item!"
+ database_id = MEDAL_MATERIALCRAFT
+
+/datum/award/achievement/misc/rocket_holdup
+ name = "Disk, Please!"
+ desc = "Is the man currently pointing a loaded rocket launcher at your head point blank really dumb enough to pull the trigger? Do you really want to find out?"
+ database_id = MEDAL_DISKPLEASE
+
+/datum/award/achievement/misc/gamer
+ name = "My Watchlist Status is Not Important"
+ desc = "You may be under the impression that violent video games are a harmless pastime, but the security and medical personnel swarming your location with batons and knockout gas look like they disagree."
+ database_id = MEDAL_GAMER
+
+/datum/award/achievement/misc/vendor_squish
+ name = "I Was a Teenage Anarchist"
+ desc = "You were doing a great job sticking it to the system until that vending machine decided to fight back."
+ database_id = MEDAL_VENDORSQUISH
+
+/datum/award/achievement/misc/swirlie
+ name = "A Bowl-d New World"
+ desc = "There's a lot of grisly ways to kick it on the Spinward Periphery, but drowning to death in a toilet probably wasn't what you had in mind. Probably."
+ database_id = MEDAL_SWIRLIE
+
+/datum/award/achievement/misc/selfouch
+ name = "How Do I Switch Hands???"
+ desc = "If you saw someone casually club themselves upside the head with a toolbox anywhere in the galaxy but here, you'd probably be pretty concerned for them."
+ database_id = MEDAL_SELFOUCH
+
+/datum/award/achievement/misc/sandman
+ name = "Mister Sandman"
+ desc = "Mechanically speaking, there's no real benefit to being unconscious during surgery. Weird how insistent this doctor is about using the N2O anyway though, huh?"
+ database_id = MEDAL_SANDMAN
+
+/datum/award/achievement/misc/cleanboss
+ name = "One Lean, Mean, Cleaning Machine"
+ desc = "How does it feel to know that your workplace values a mop bucket on wheels more than you?" // i can do better than this give me time
+ database_id = MEDAL_CLEANBOSS
+
+/datum/award/achievement/misc/rule8
+ name = "Rule 8"
+ desc = "Call an admin this is ILLEGAL!!"
+ database_id = MEDAL_RULE8
+ icon = "rule8"
+
+/datum/award/achievement/misc/speed_round
+ name = "Long shift"
+ desc = "Well, that didn't take long."
+ database_id = MEDAL_LONGSHIFT
+ icon = "longshift"
+
+/datum/award/achievement/misc/snail
+ name = "KKKiiilll mmmeee"
+ desc = "You were a little too ambitious, but hey, I guess you're still alive?"
+ database_id = MEDAL_SNAIL
+ icon = "snail"
+
+/datum/award/achievement/misc/lookoutsir
+ name = "Look Out, Sir!"
+ desc = "Either awarded for making the ultimate sacrifice for your comrades, or a really dumb attempt at grenade jumping."
+ database_id = MEDAL_LOOKOUTSIR
+
+/datum/award/achievement/misc/gottem
+ name = "HA, GOTTEM"
+ desc = "Made you look!"
+ database_id = MEDAL_GOTTEM
+
+/datum/award/achievement/misc/ascension
+ name = "Ascension"
+ desc = "Caedite eos. Novit enim Dominus qui sunt eius."
+ database_id = MEDAL_ASCENSION
+ icon = "ascension"
+
+/datum/award/achievement/misc/frenching
+ name = "Frenching"
+ desc = "Just a taste, for science!"
+ database_id = MEDAL_FRENCHING
+ icon = "frenching"
+
+/datum/award/achievement/misc/ash_ascension
+ name = "Nightwatcher's Eyes"
+ desc = "You've risen above the flames, became one with the ashes. You've been reborn as one with the Nightwatcher."
+ database_id = MEDAL_ASH_ASCENSION
+ icon = "ashascend"
+
+/datum/award/achievement/misc/flesh_ascension
+ name = "Vortex of Arms"
+ desc = "You've became something more, something greater. A piece of the emperor resides within you, and you within him."
+ database_id = MEDAL_FLESH_ASCENSION
+ icon = "fleshascend"
+
+/datum/award/achievement/misc/rust_ascension
+ name = "Hills of Rust"
+ desc = "You've summoned a piece of the Hill of rust, and so the Hills welcome you."
+ database_id = MEDAL_RUST_ASCENSION
+ icon = "rustascend"
+
+/datum/award/achievement/misc/void_ascension
+ name = "All that perish"
+ desc = "Place of a different being, different time. Everything ends there... but maybe it is just the beginning?"
+ database_id = MEDAL_VOID_ASCENSION
+ icon = "voidascend"
+
+/datum/award/achievement/misc/toolbox_soul
+ name = "SOUL'd Out"
+ desc = "My eternal soul was destroyed to make a toolbox look funny and all I got was this achievement..."
+ database_id = MEDAL_TOOLBOX_SOUL
+ icon = "toolbox_soul"
+
+/datum/award/achievement/misc/chemistry_tut
+ name = "Perfect chemistry blossom"
+ desc = "Passed the chemistry tutorial with perfect purity!"
+ database_id = MEDAL_CHEM_TUT
+ icon = "chem_tut"
diff --git a/code/datums/achievements/misc_scores.dm b/code/datums/achievements/misc_scores.dm
new file mode 100644
index 0000000000..7ffc50c015
--- /dev/null
+++ b/code/datums/achievements/misc_scores.dm
@@ -0,0 +1,11 @@
+///How many times did we survive being a cripple?
+/datum/award/score/hardcore_random
+ name = "Hardcore random points"
+ desc = "Well, I might be a blind, deaf, crippled guy, but hey, at least I'm alive."
+ database_id = HARDCORE_RANDOM_SCORE
+
+///How many maintenance pills did you eat?
+/datum/award/score/maintenance_pill
+ name = "Maintenance Pills Consumed"
+ desc = "Wait why?"
+ database_id = MAINTENANCE_PILL_SCORE
diff --git a/code/datums/achievements/skill_achievements.dm b/code/datums/achievements/skill_achievements.dm
new file mode 100644
index 0000000000..7da936c61f
--- /dev/null
+++ b/code/datums/achievements/skill_achievements.dm
@@ -0,0 +1,10 @@
+/datum/award/achievement/skill
+ category = "Skills"
+ icon = "baseskill"
+
+/datum/award/achievement/skill/legendary_miner
+ name = "Legendary miner"
+ desc = "No mere rock can stop me!"
+ database_id = MEDAL_LEGENDARY_MINER
+ icon = "mining"
+
diff --git a/code/datums/callback.dm b/code/datums/callback.dm
index 62e10922f3..b5baea28f1 100644
--- a/code/datums/callback.dm
+++ b/code/datums/callback.dm
@@ -1,53 +1,53 @@
/**
- *# Callback Datums
- *A datum that holds a proc to be called on another object, used to track proccalls to other objects
- *
- * ## USAGE
- *
- * ```
- * var/datum/callback/C = new(object|null, /proc/type/path|"procstring", arg1, arg2, ... argn)
- * var/timerid = addtimer(C, time, timertype)
- * you can also use the compiler define shorthand
- * var/timerid = addtimer(CALLBACK(object|null, /proc/type/path|procstring, arg1, arg2, ... argn), time, timertype)
- * ```
- *
- * Note: proc strings can only be given for datum proc calls, global procs must be proc paths
- *
- * Also proc strings are strongly advised against because they don't compile error if the proc stops existing
- *
- * In some cases you can provide a shortform of the procname, see the proc typepath shortcuts documentation below
- *
- * ## INVOKING THE CALLBACK
- *`var/result = C.Invoke(args, to, add)` additional args are added after the ones given when the callback was created
- *
- * `var/result = C.InvokeAsync(args, to, add)` Asyncronous - returns . on the first sleep then continues on in the background
- * after the sleep/block ends, otherwise operates normally.
- *
- * ## PROC TYPEPATH SHORTCUTS
- * (these operate on paths, not types, so to these shortcuts, datum is NOT a parent of atom, etc...)
- *
- * ### global proc while in another global proc:
- * .procname
- *
- * `CALLBACK(GLOBAL_PROC, .some_proc_here)`
- *
- * ### proc defined on current(src) object (when in a /proc/ and not an override) OR overridden at src or any of it's parents:
- * .procname
- *
- * `CALLBACK(src, .some_proc_here)`
- *
- * ### when the above doesn't apply:
- *.proc/procname
- *
- * `CALLBACK(src, .proc/some_proc_here)`
- *
- *
- * proc defined on a parent of a some type
- *
- * `/some/type/.proc/some_proc_here`
- *
- * Otherwise you must always provide the full typepath of the proc (/type/of/thing/proc/procname)
- */
+ *# Callback Datums
+ *A datum that holds a proc to be called on another object, used to track proccalls to other objects
+ *
+ * ## USAGE
+ *
+ * ```
+ * var/datum/callback/C = new(object|null, /proc/type/path|"procstring", arg1, arg2, ... argn)
+ * var/timerid = addtimer(C, time, timertype)
+ * you can also use the compiler define shorthand
+ * var/timerid = addtimer(CALLBACK(object|null, /proc/type/path|procstring, arg1, arg2, ... argn), time, timertype)
+ * ```
+ *
+ * Note: proc strings can only be given for datum proc calls, global procs must be proc paths
+ *
+ * Also proc strings are strongly advised against because they don't compile error if the proc stops existing
+ *
+ * In some cases you can provide a shortform of the procname, see the proc typepath shortcuts documentation below
+ *
+ * ## INVOKING THE CALLBACK
+ *`var/result = C.Invoke(args, to, add)` additional args are added after the ones given when the callback was created
+ *
+ * `var/result = C.InvokeAsync(args, to, add)` Asyncronous - returns . on the first sleep then continues on in the background
+ * after the sleep/block ends, otherwise operates normally.
+ *
+ * ## PROC TYPEPATH SHORTCUTS
+ * (these operate on paths, not types, so to these shortcuts, datum is NOT a parent of atom, etc...)
+ *
+ * ### global proc while in another global proc:
+ * .procname
+ *
+ * `CALLBACK(GLOBAL_PROC, .some_proc_here)`
+ *
+ * ### proc defined on current(src) object (when in a /proc/ and not an override) OR overridden at src or any of it's parents:
+ * .procname
+ *
+ * `CALLBACK(src, .some_proc_here)`
+ *
+ * ### when the above doesn't apply:
+ *.proc/procname
+ *
+ * `CALLBACK(src, .proc/some_proc_here)`
+ *
+ *
+ * proc defined on a parent of a some type
+ *
+ * `/some/type/.proc/some_proc_here`
+ *
+ * Otherwise you must always provide the full typepath of the proc (/type/of/thing/proc/procname)
+ */
/datum/callback
///The object we will be calling the proc on
@@ -60,13 +60,13 @@
var/datum/weakref/user
/**
- * Create a new callback datum
- *
- * Arguments
- * * thingtocall the object to call the proc on
- * * proctocall the proc to call on the target object
- * * ... an optional list of extra arguments to pass to the proc
- */
+ * Create a new callback datum
+ *
+ * Arguments
+ * * thingtocall the object to call the proc on
+ * * proctocall the proc to call on the target object
+ * * ... an optional list of extra arguments to pass to the proc
+ */
/datum/callback/New(thingtocall, proctocall, ...)
if (thingtocall)
object = thingtocall
@@ -76,13 +76,13 @@
if(usr)
user = WEAKREF(usr)
/**
- * Immediately Invoke proctocall on thingtocall, with waitfor set to false
- *
- * Arguments:
- * * thingtocall Object to call on
- * * proctocall Proc to call on that object
- * * ... optional list of arguments to pass as arguments to the proc being called
- */
+ * Immediately Invoke proctocall on thingtocall, with waitfor set to false
+ *
+ * Arguments:
+ * * thingtocall Object to call on
+ * * proctocall Proc to call on that object
+ * * ... optional list of arguments to pass as arguments to the proc being called
+ */
/world/proc/ImmediateInvokeAsync(thingtocall, proctocall, ...)
set waitfor = FALSE
@@ -97,13 +97,13 @@
call(thingtocall, proctocall)(arglist(calling_arguments))
/**
- * Invoke this callback
- *
- * Calls the registered proc on the registered object, if the user ref
- * can be resolved it also inclues that as an arg
- *
- * If the datum being called on is varedited, the call is wrapped via WrapAdminProcCall
- */
+ * Invoke this callback
+ *
+ * Calls the registered proc on the registered object, if the user ref
+ * can be resolved it also inclues that as an arg
+ *
+ * If the datum being called on is varedited, the call is wrapped via [WrapAdminProcCall][/proc/WrapAdminProcCall]
+ */
/datum/callback/proc/Invoke(...)
if(!usr)
var/datum/weakref/W = user
@@ -130,13 +130,13 @@
return call(object, delegate)(arglist(calling_arguments))
/**
- * Invoke this callback async (waitfor=false)
- *
- * Calls the registered proc on the registered object, if the user ref
- * can be resolved it also inclues that as an arg
- *
- * If the datum being called on is varedited, the call is wrapped via WrapAdminProcCall
- */
+ * Invoke this callback async (waitfor=false)
+ *
+ * Calls the registered proc on the registered object, if the user ref
+ * can be resolved it also inclues that as an arg
+ *
+ * If the datum being called on is varedited, the call is wrapped via WrapAdminProcCall
+ */
/datum/callback/proc/InvokeAsync(...)
set waitfor = FALSE
@@ -166,7 +166,7 @@
/**
Helper datum for the select callbacks proc
- */
+ */
/datum/callback_select
var/list/finished
var/pendingcount
@@ -192,16 +192,16 @@
finished[index] = rtn
/**
- * Runs a list of callbacks asyncronously, returning only when all have finished
- *
- * Callbacks can be repeated, to call it multiple times
- *
- * Arguments:
- * * list/callbacks the list of callbacks to be called
- * * list/callback_args the list of lists of arguments to pass into each callback
- * * savereturns Optionally save and return the list of returned values from each of the callbacks
- * * resolution The number of byond ticks between each time you check if all callbacks are complete
- */
+ * Runs a list of callbacks asyncronously, returning only when all have finished
+ *
+ * Callbacks can be repeated, to call it multiple times
+ *
+ * Arguments:
+ * * list/callbacks the list of callbacks to be called
+ * * list/callback_args the list of lists of arguments to pass into each callback
+ * * savereturns Optionally save and return the list of returned values from each of the callbacks
+ * * resolution The number of byond ticks between each time you check if all callbacks are complete
+ */
/proc/callback_select(list/callbacks, list/callback_args, savereturns = TRUE, resolution = 1)
if (!callbacks)
return
diff --git a/code/datums/components/material_container.dm b/code/datums/components/material_container.dm
index 7adc634621..cc988544b8 100644
--- a/code/datums/components/material_container.dm
+++ b/code/datums/components/material_container.dm
@@ -10,25 +10,37 @@
*/
/datum/component/material_container
+ /// The total amount of materials this material container contains
var/total_amount = 0
+ /// The maximum amount of materials this material container can contain
var/max_amount
- var/sheet_type
+ /// Map of material ref -> amount
var/list/materials //Map of key = material ref | Value = amount
+ /// The list of materials that this material container can accept
+ var/list/allowed_materials
var/show_on_examine
var/disable_attackby
var/list/allowed_typecache
+ /// The last main material that was inserted into this container
var/last_inserted_id
+ /// Whether or not this material container allows specific amounts from sheets to be inserted
var/precise_insertion = FALSE
+ /// A callback invoked before materials are inserted into this container
var/datum/callback/precondition
+ /// A callback invoked after materials are inserted into this container
var/datum/callback/after_insert
/// Sets up the proper signals and fills the list of materials with the appropriate references.
/datum/component/material_container/Initialize(list/mat_list, max_amt = 0, _show_on_examine = FALSE, list/allowed_types, datum/callback/_precondition, datum/callback/_after_insert, _disable_attackby)
+ if(!isatom(parent))
+ return COMPONENT_INCOMPATIBLE
+
materials = list()
max_amount = max(0, max_amt)
show_on_examine = _show_on_examine
disable_attackby = _disable_attackby
+ allowed_materials = mat_list || list()
if(allowed_types)
if(ispath(allowed_types) && allowed_types == /obj/item/stack)
allowed_typecache = GLOB.typecache_stack
@@ -38,14 +50,32 @@
precondition = _precondition
after_insert = _after_insert
- RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/OnAttackBy)
- RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/OnExamine)
+ RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/on_attackby)
+ RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/on_examine)
- for(var/mat in mat_list) //Make the assoc list ref | amount
- var/datum/material/M = SSmaterials.GetMaterialRef(mat)
- materials[M] = 0
+ for(var/mat in mat_list) //Make the assoc list material reference -> amount
+ var/mat_ref = SSmaterials.GetMaterialRef(mat)
+ if(isnull(mat_ref))
+ continue
+ var/mat_amt = mat_list[mat]
+ if(isnull(mat_amt))
+ mat_amt = 0
+ materials[mat_ref] += mat_amt
+
+/datum/component/material_container/Destroy(force, silent)
+ materials = null
+ allowed_typecache = null
+ // if(insertion_check)
+ // QDEL_NULL(insertion_check)
+ if(precondition)
+ QDEL_NULL(precondition)
+ if(after_insert)
+ QDEL_NULL(after_insert)
+ return ..()
+
+/datum/component/material_container/proc/on_examine(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
-/datum/component/material_container/proc/OnExamine(datum/source, mob/user, list/examine_list)
if(show_on_examine)
for(var/I in materials)
var/datum/material/M = I
@@ -54,7 +84,9 @@
examine_list += "It has [amt] units of [lowertext(M.name)] stored."
/// Proc that allows players to fill the parent with mats
-/datum/component/material_container/proc/OnAttackBy(datum/source, obj/item/I, mob/living/user)
+/datum/component/material_container/proc/on_attackby(datum/source, obj/item/I, mob/living/user)
+ SIGNAL_HANDLER
+
var/list/tc = allowed_typecache
if(disable_attackby)
return
@@ -63,48 +95,104 @@
if(I.item_flags & ABSTRACT)
return
if((I.flags_1 & HOLOGRAM_1) || (I.item_flags & NO_MAT_REDEMPTION) || (tc && !is_type_in_typecache(I, tc)))
+ // if(!(mat_container_flags & MATCONTAINER_SILENT))
to_chat(user, "[parent] won't accept [I]!")
return
. = COMPONENT_NO_AFTERATTACK
var/datum/callback/pc = precondition
if(pc && !pc.Invoke(user))
return
- var/material_amount = get_item_material_amount(I)
+ var/material_amount = get_item_material_amount(I) //, mat_container_flags)
if(!material_amount)
to_chat(user, "[I] does not contain sufficient materials to be accepted by [parent].")
return
if((!precise_insertion || !GLOB.typecache_stack[I.type]) && !has_space(material_amount))
- to_chat(user, "[parent] has not enough space. Please remove materials from [parent] in order to insert more.")
+ to_chat(user, "[parent] is full. Please remove materials from [parent] in order to insert more.")
return
- user_insert(I, user)
+ user_insert(I, user) //, mat_container_flags)
/// Proc used for when player inserts materials
-/datum/component/material_container/proc/user_insert(obj/item/I, mob/living/user)
+/datum/component/material_container/proc/user_insert(obj/item/I, mob/living/user, datum/component/remote_materials/remote = null)
set waitfor = FALSE
- var/requested_amount
var/active_held = user.get_active_held_item() // differs from I when using TK
- if(istype(I, /obj/item/stack) && precise_insertion)
- var/atom/current_parent = parent
+ var/inserted = 0
+
+ //handle stacks specially
+ if(istype(I, /obj/item/stack))
+ var/atom/current_parent = remote ? remote.parent : parent //is the user using a remote materials component?
var/obj/item/stack/S = I
- requested_amount = input(user, "How much do you want to insert?", "Inserting [S.singular_name]s") as num|null
+
+ //try to get ammount to use
+ var/requested_amount
+ if(precise_insertion)
+ requested_amount = input(user, "How much do you want to insert?", "Inserting [S.singular_name]s") as num|null
+ else
+ requested_amount= S.amount
+
if(isnull(requested_amount) || (requested_amount <= 0))
return
- if(QDELETED(I) || QDELETED(user) || QDELETED(src) || parent != current_parent || user.physical_can_use_topic(current_parent) < UI_INTERACTIVE || user.get_active_held_item() != active_held)
+ if(QDELETED(I) || QDELETED(user) || QDELETED(src) || user.get_active_held_item() != active_held)
return
- if(!user.temporarilyRemoveItemFromInventory(I))
- to_chat(user, "[I] is stuck to you and cannot be placed into [parent].")
- return
- var/inserted = insert_item(I, stack_amt = requested_amount)
+ //are we still in range after the user input?
+ if((remote ? remote.parent : parent) != current_parent || user.physical_can_use_topic(current_parent) < UI_INTERACTIVE)
+ return
+ inserted = insert_stack(S, requested_amount)
+ else
+ if(!user.temporarilyRemoveItemFromInventory(I))
+ to_chat(user, "[I] is stuck to you and cannot be placed into [parent].")
+ return
+ inserted = insert_item(I)
+ qdel(I)
+
if(inserted)
to_chat(user, "You insert a material total of [inserted] into [parent].")
- qdel(I)
if(after_insert)
after_insert.Invoke(I, last_inserted_id, inserted)
- else if(I == active_held)
- user.put_in_active_hand(I)
+ if(remote && remote.after_insert)
+ remote.after_insert.Invoke(I, last_inserted_id, inserted)
+
+//Inserts a number of sheets from a stack, returns the amount of sheets used.
+/datum/component/material_container/proc/insert_stack(obj/item/stack/S, amt, multiplier = 1)
+ if(isnull(amt))
+ amt = S.amount
+
+ if(amt <= 0)
+ return FALSE
+
+ if(amt > S.amount)
+ amt = S.amount
+
+ var/material_amt = get_item_material_amount(S)
+ if(!material_amt)
+ return FALSE
+
+ //get max number of sheets we have room to add
+ var/mat_per_sheet = material_amt/S.amount
+ amt = min(amt, round((max_amount - total_amount) / (mat_per_sheet)))
+ if(!amt)
+ return FALSE
+
+ //add the mats and keep track of how much was added
+ var/starting_total = total_amount
+ for(var/MAT in materials)
+ materials[MAT] += S.mats_per_unit[MAT] * amt * multiplier
+ total_amount += S.mats_per_unit[MAT] * amt * multiplier
+ var/total_added = total_amount - starting_total
+
+ //update last_inserted_id with mat making up majority of the stack
+ var/primary_mat
+ var/max_mat_value = 0
+ for(var/MAT in materials)
+ if(S.mats_per_unit[MAT] > max_mat_value)
+ max_mat_value = S.mats_per_unit[MAT]
+ primary_mat = MAT
+ last_inserted_id = primary_mat
+
+ S.use(amt)
+ return total_added
/// Proc specifically for inserting items, returns the amount of materials entered.
-/datum/component/material_container/proc/insert_item(obj/item/I, var/multiplier = 1, stack_amt)
+/datum/component/material_container/proc/insert_item(obj/item/I, var/multiplier = 1)
if(QDELETED(I))
return FALSE
@@ -117,16 +205,46 @@
last_inserted_id = insert_item_materials(I, multiplier)
return material_amount
+/**
+ * Inserts the relevant materials from an item into this material container.
+ *
+ * Arguments:
+ * - [source][/obj/item]: The source of the materials we are inserting.
+ * - multiplier: The multiplier for the materials being inserted.
+ * - breakdown_flags: The breakdown bitflags that will be used to retrieve the materials from the source
+ */
/datum/component/material_container/proc/insert_item_materials(obj/item/I, multiplier = 1)
var/primary_mat
var/max_mat_value = 0
- for(var/MAT in materials)
- materials[MAT] += I.custom_materials[MAT] * multiplier
- total_amount += I.custom_materials[MAT] * multiplier
- if(I.custom_materials[MAT] > max_mat_value)
+ var/list/item_materials = I.custom_materials
+ for(var/MAT in item_materials)
+ if(!can_hold_material(MAT))
+ continue
+ materials[MAT] += item_materials[MAT] * multiplier
+ total_amount += item_materials[MAT] * multiplier
+ if(item_materials[MAT] > max_mat_value)
+ max_mat_value = item_materials[MAT]
primary_mat = MAT
+
return primary_mat
+/**
+ * The default check for whether we can add materials to this material container.
+ *
+ * Arguments:
+ * - [mat][/atom/material]: The material we are checking for insertability.
+ */
+/datum/component/material_container/proc/can_hold_material(datum/material/mat)
+ if(mat in allowed_typecache)
+ return TRUE
+ if(istype(mat) && ((mat.id in allowed_typecache) || (mat.type in allowed_materials)))
+ allowed_materials += mat // This could get messy with passing lists by ref... but if you're doing that the list expansion is probably being taken care of elsewhere anyway...
+ return TRUE
+ // if(insertion_check?.Invoke(mat))
+ // allowed_materials += mat
+ // return TRUE
+ return FALSE
+
/// For inserting an amount of material
/datum/component/material_container/proc/insert_amount_mat(amt, var/datum/material/mat)
if(!istype(mat))
@@ -135,6 +253,7 @@
var/total_amount_saved = total_amount
if(mat)
materials[mat] += amt
+ total_amount += amt
else
for(var/i in materials)
materials[i] += amt
diff --git a/code/datums/components/pellet_cloud.dm b/code/datums/components/pellet_cloud.dm
index 6404be94c4..f88b9e1869 100644
--- a/code/datums/components/pellet_cloud.dm
+++ b/code/datums/components/pellet_cloud.dm
@@ -275,6 +275,11 @@
else
target.visible_message("[target] is hit by a [proj_name][hit_part ? " in the [hit_part.name]" : ""]!", null, null, COMBAT_MESSAGE_RANGE, target)
to_chat(target, "You're hit by a [proj_name][hit_part ? " in the [hit_part.name]" : ""]!")
+
+ for(var/M in purple_hearts)
+ var/mob/living/martyr = M
+ if(martyr.stat == DEAD && martyr.client)
+ martyr.client.give_award(/datum/award/achievement/misc/lookoutsir, martyr)
UnregisterSignal(parent, COMSIG_PARENT_PREQDELETED)
if(queued_delete)
qdel(parent)
diff --git a/code/datums/components/remote_materials.dm b/code/datums/components/remote_materials.dm
index 01038c11d3..b1d23ea3a8 100644
--- a/code/datums/components/remote_materials.dm
+++ b/code/datums/components/remote_materials.dm
@@ -15,13 +15,15 @@ handles linking back and forth.
var/category
var/allow_standalone
var/local_size = INFINITY
+ var/datum/callback/after_insert
-/datum/component/remote_materials/Initialize(category, mapload, allow_standalone = TRUE, force_connect = FALSE)
+/datum/component/remote_materials/Initialize(category, mapload, allow_standalone = TRUE, force_connect = FALSE, _after_insert)
if (!isatom(parent))
return COMPONENT_INCOMPATIBLE
src.category = category
src.allow_standalone = allow_standalone
+ after_insert = _after_insert
RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/OnAttackBy)
@@ -67,7 +69,7 @@ handles linking back and forth.
/datum/material/plastic,
)
- mat_container = parent.AddComponent(/datum/component/material_container, allowed_mats, local_size, allowed_types=/obj/item/stack)
+ mat_container = parent.AddComponent(/datum/component/material_container, allowed_mats, local_size, allowed_types=/obj/item/stack, _after_insert = after_insert)
/datum/component/remote_materials/proc/set_local_size(size)
local_size = size
@@ -103,7 +105,7 @@ handles linking back and forth.
return COMPONENT_NO_AFTERATTACK
else if(silo && istype(I, /obj/item/stack))
- if(silo.remote_attackby(parent, user, I))
+ if(silo.remote_attackby(parent, user, I, src))
return COMPONENT_NO_AFTERATTACK
/datum/component/remote_materials/proc/on_hold()
diff --git a/code/datums/components/storage/concrete/stack.dm b/code/datums/components/storage/concrete/stack.dm
index a3f1e526a0..d7b118cee4 100644
--- a/code/datums/components/storage/concrete/stack.dm
+++ b/code/datums/components/storage/concrete/stack.dm
@@ -40,7 +40,8 @@
_S.add(can_insert)
S.use(can_insert, TRUE)
return TRUE
- return ..(S.change_stack(null, can_insert), override)
+ I = S.split_stack(null, can_insert)
+ return ..()
/datum/component/storage/concrete/stack/remove_from_storage(obj/item/I, atom/new_location)
var/atom/real_location = real_location()
diff --git a/code/datums/looping_sounds/machinery_sounds.dm b/code/datums/looping_sounds/machinery_sounds.dm
index f7a3b46118..79810284cd 100644
--- a/code/datums/looping_sounds/machinery_sounds.dm
+++ b/code/datums/looping_sounds/machinery_sounds.dm
@@ -99,18 +99,18 @@
falloff_exponent = 5
volume = 50
*/
-// /datum/looping_sound/computer
-// start_sound = 'sound/machines/computer/computer_start.ogg'
-// start_length = 7.2 SECONDS
-// start_volume = 10
-// mid_sounds = list('sound/machines/computer/computer_mid1.ogg'=1, 'sound/machines/computer/computer_mid2.ogg'=1)
-// mid_length = 1.8 SECONDS
-// end_sound = 'sound/machines/computer/computer_end.ogg'
-// end_volume = 10
-// volume = 2
-// falloff_exponent = 5 //Ultra quiet very fast
-// extra_range = -12
-// falloff_distance = 1 //Instant falloff after initial tile
+/datum/looping_sound/computer
+ start_sound = 'sound/machines/computer/computer_start.ogg'
+ start_length = 7.2 SECONDS
+ start_volume = 10
+ mid_sounds = list('sound/machines/computer/computer_mid1.ogg'=1, 'sound/machines/computer/computer_mid2.ogg'=1)
+ mid_length = 1.8 SECONDS
+ end_sound = 'sound/machines/computer/computer_end.ogg'
+ end_volume = 10
+ volume = 2
+ falloff_exponent = 5 //Ultra quiet very fast
+ extra_range = -12
+ falloff_distance = 1 //Instant falloff after initial tile
// /datum/looping_sound/gravgen
// mid_sounds = list('sound/machines/gravgen/gravgen_mid1.ogg'=1,'sound/machines/gravgen/gravgen_mid2.ogg'=1,'sound/machines/gravgen/gravgen_mid3.ogg'=1,'sound/machines/gravgen/gravgen_mid4.ogg'=1,)
diff --git a/code/datums/looping_sounds/weather.dm b/code/datums/looping_sounds/weather.dm
index d8ed8d123b..ea34fbb693 100644
--- a/code/datums/looping_sounds/weather.dm
+++ b/code/datums/looping_sounds/weather.dm
@@ -8,7 +8,7 @@
start_sound = 'sound/weather/ashstorm/outside/active_start.ogg'
start_length = 130
end_sound = 'sound/weather/ashstorm/outside/active_end.ogg'
- volume = 80
+ volume = 60
/datum/looping_sound/active_inside_ashstorm
mid_sounds = list(
@@ -20,7 +20,7 @@
start_sound = 'sound/weather/ashstorm/inside/active_start.ogg'
start_length = 130
end_sound = 'sound/weather/ashstorm/inside/active_end.ogg'
- volume = 60
+ volume = 20
/datum/looping_sound/weak_outside_ashstorm
mid_sounds = list(
diff --git a/code/datums/martial/cqc.dm b/code/datums/martial/cqc.dm
index 96ba7bf965..4277cb1316 100644
--- a/code/datums/martial/cqc.dm
+++ b/code/datums/martial/cqc.dm
@@ -97,7 +97,7 @@
D.visible_message("[A] locks [D] into a restraining position!", \
"[A] locks you into a restraining position!")
D.apply_damage(damage, STAMINA)
- D.Stun(100)
+ D.Stun(10)
restraining = TRUE
addtimer(VARSET_CALLBACK(src, restraining, FALSE), 50, TIMER_UNIQUE)
return TRUE
@@ -175,7 +175,7 @@
return TRUE
if(CHECK_MOBILITY(D, MOBILITY_MOVE) || !restraining)
A.do_attack_animation(D, ATTACK_EFFECT_PUNCH)
- if(damage >= stunthreshold)
+ if(damage >= stunthreshold)
I = D.get_active_held_item()
D.visible_message("[A] strikes [D]'s jaw with their hand!", \
"[A] strikes your jaw, disorienting you!")
@@ -196,7 +196,8 @@
log_combat(A, D, "knocked out (Chokehold)(CQC)")
D.visible_message("[A] puts [D] into a chokehold!", \
"[A] puts you into a chokehold!")
- D.SetSleeping(400)
+ if(D.silent <= 10)
+ D.silent = clamp(D.silent + 10, 0, 10)
restraining = FALSE
if(A.grab_state < GRAB_NECK)
A.setGrabState(GRAB_NECK)
@@ -213,7 +214,7 @@
to_chat(usr, "Slam: Grab Harm. Slam opponent into the ground, knocking them down.")
to_chat(usr, "CQC Kick: Harm Harm. Knocks opponent away. Knocks out stunned or knocked down opponents.")
- to_chat(usr, "Restrain: Grab Grab. Locks opponents into a restraining position, disarm to knock them out with a chokehold.")
+ to_chat(usr, "Restrain: Grab Grab. Locks opponents into a restraining position, disarm to mute them with a chokehold.")
to_chat(usr, "Pressure: Disarm Grab. Decent stamina damage.")
to_chat(usr, "Consecutive CQC: Disarm Disarm Harm. Mainly offensive move, huge damage and decent stamina damage.")
diff --git a/code/datums/materials/_material.dm b/code/datums/materials/_material.dm
index 5148aab4ac..658f22a107 100644
--- a/code/datums/materials/_material.dm
+++ b/code/datums/materials/_material.dm
@@ -1,15 +1,24 @@
/*! Material datum
+
Simple datum which is instanced once per type and is used for every object of said material. It has a variety of variables that define behavior. Subtyping from this makes it easier to create your own materials.
+
*/
/datum/material
+ /// What the material is referred to as IC.
var/name = "material"
+ /// A short description of the material. Not used anywhere, yet...
var/desc = "its..stuff."
+ /// What the material is indexed by in the SSmaterials.materials list. Defaults to the type of the material.
+ var/id
+
///Base color of the material, is used for greyscale. Item isn't changed in color if this is null.
var/color
///Base alpha of the material, is used for greyscale icons.
var/alpha
+ ///Bitflags that influence how SSmaterials handles this material.
+ // var/init_flags = MATERIAL_INIT_MAPLOAD
///Materials "Traits". its a map of key = category | Value = Bool. Used to define what it can be used for
var/list/categories = list()
///The type of sheet this material creates. This should be replaced as soon as possible by greyscale sheets
@@ -22,7 +31,7 @@ Simple datum which is instanced once per type and is used for every object of sa
var/value_per_unit = 0
///Armor modifiers, multiplies an items normal armor vars by these amounts.
var/armor_modifiers = list("melee" = 1, "bullet" = 1, "laser" = 1, "energy" = 1, "bomb" = 1, "bio" = 1, "rad" = 1, "fire" = 1, "acid" = 1)
- ///How beautiful is this material per unit?
+ ///How beautiful is this material per unit.
var/beauty_modifier = 0
///Can be used to override the sound items make, lets add some SLOSHing.
var/item_sound_override
@@ -30,14 +39,31 @@ Simple datum which is instanced once per type and is used for every object of sa
var/turf_sound_override
///what texture icon state to overlay
var/texture_layer_icon_state
- ///a cached filter for the texture icon
+ ///a cached icon for the texture filter
var/cached_texture_filter
+ ///What type of shard the material will shatter to
+ var/obj/item/shard_type
+
+
+/** Handles initializing the material.
+ *
+ * Arugments:
+ * - _id: The ID the material should use. Overrides the existing ID.
+ */
+/datum/material/proc/Initialize(_id, ...)
+ if(_id)
+ id = _id
+ else if(isnull(id))
+ id = type
+
+ if(texture_layer_icon_state)
+ cached_texture_filter = icon('icons/materials/composite.dmi', texture_layer_icon_state)
+
+ return TRUE
/datum/material/New()
. = ..()
- if(texture_layer_icon_state)
- var/texture_icon = icon('icons/materials/composite.dmi', texture_layer_icon_state)
- cached_texture_filter = filter(type="layer", icon=texture_icon, blend_mode = BLEND_INSET_OVERLAY)
+ Initialize()
///This proc is called when the material is added to an object.
/datum/material/proc/on_applied(atom/source, amount, material_flags)
@@ -48,18 +74,20 @@ Simple datum which is instanced once per type and is used for every object of sa
source.alpha = alpha
if(texture_layer_icon_state)
ADD_KEEP_TOGETHER(source, MATERIAL_SOURCE(src))
- source.filters += cached_texture_filter
+ source.add_filter("material_texture_[name]",1,layering_filter(icon=cached_texture_filter,blend_mode=BLEND_INSET_OVERLAY))
+ if(alpha < 255)
+ source.opacity = FALSE
if(material_flags & MATERIAL_ADD_PREFIX)
source.name = "[name] [source.name]"
- if(beauty_modifier)
- addtimer(CALLBACK(source, /datum.proc/_AddElement, list(/datum/element/beauty, beauty_modifier * amount)), 0)
+ // if(beauty_modifier) returnign in hardsync2 if i ever port ebeauty cmp
+ // addtimer(CALLBACK(source, /datum.proc/_AddElement, list(/datum/element/beauty, beauty_modifier * amount)), 0)
if(istype(source, /obj)) //objs
on_applied_obj(source, amount, material_flags)
- else if(isturf(source, /turf)) //turfs
+ if(istype(source, /turf)) //turfs
on_applied_turf(source, amount, material_flags)
source.mat_update_desc(src)
@@ -67,8 +95,9 @@ Simple datum which is instanced once per type and is used for every object of sa
///This proc is called when a material updates an object's description
/atom/proc/mat_update_desc(/datum/material/mat)
return
+
///This proc is called when the material is added to an object specifically.
-/datum/material/proc/on_applied_obj(var/obj/o, amount, material_flags)
+/datum/material/proc/on_applied_obj(obj/o, amount, material_flags)
if(material_flags & MATERIAL_AFFECT_STATISTICS)
var/new_max_integrity = CEILING(o.max_integrity * integrity_modifier, 1)
o.modify_max_integrity(new_max_integrity)
@@ -92,43 +121,73 @@ Simple datum which is instanced once per type and is used for every object of sa
I.hitsound = item_sound_override
I.usesound = item_sound_override
I.throwhitsound = item_sound_override
+ // I.mob_throw_hit_sound = item_sound_override
+ // I.equip_sound = item_sound_override
+ // I.pickup_sound = item_sound_override
+ // I.drop_sound = item_sound_override
-/datum/material/proc/on_applied_turf(var/turf/T, amount, material_flags)
+/datum/material/proc/on_applied_turf(turf/T, amount, material_flags)
if(isopenturf(T))
- if(!turf_sound_override)
- return
- var/turf/open/O = T
- O.footstep = turf_sound_override
- O.barefootstep = turf_sound_override
- O.clawfootstep = turf_sound_override
- O.heavyfootstep = turf_sound_override
+ if(turf_sound_override)
+ var/turf/open/O = T
+ O.footstep = turf_sound_override
+ O.barefootstep = turf_sound_override
+ O.clawfootstep = turf_sound_override
+ O.heavyfootstep = turf_sound_override
+ // if(alpha < 255)
+ // T.AddElement(/datum/element/turf_z_transparency, TRUE)
+ return
///This proc is called when the material is removed from an object.
-/datum/material/proc/on_removed(atom/source, material_flags)
+/datum/material/proc/on_removed(atom/source, amount, material_flags)
if(material_flags & MATERIAL_COLOR) //Prevent changing things with pre-set colors, to keep colored toolboxes their looks for example
if(color)
source.remove_atom_colour(FIXED_COLOUR_PRIORITY, color)
- source.alpha = initial(source.alpha)
if(texture_layer_icon_state)
- source.filters -= cached_texture_filter
+ source.remove_filter("material_texture_[name]")
REMOVE_KEEP_TOGETHER(source, MATERIAL_SOURCE(src))
+ source.alpha = initial(source.alpha)
if(material_flags & MATERIAL_ADD_PREFIX)
source.name = initial(source.name)
- if(istype(source, /obj)) //objs
- on_removed_obj(source, material_flags)
+ // if(beauty_modifier) //component/beauty/InheritComponent() will handle the removal.
+ // addtimer(CALLBACK(source, /datum.proc/_AddElement, list(/datum/element/beauty, -beauty_modifier * amount)), 0)
- else if(istype(source, /turf)) //turfs
- on_removed_turf(source, material_flags)
+ if(istype(source, /obj)) //objs
+ on_removed_obj(source, amount, material_flags)
+
+ if(istype(source, /turf)) //turfs
+ on_removed_turf(source, amount, material_flags)
///This proc is called when the material is removed from an object specifically.
-/datum/material/proc/on_removed_obj(obj/o, material_flags)
+/datum/material/proc/on_removed_obj(obj/o, amount, material_flags)
if(material_flags & MATERIAL_AFFECT_STATISTICS)
var/new_max_integrity = initial(o.max_integrity)
o.modify_max_integrity(new_max_integrity)
o.force = initial(o.force)
o.throwforce = initial(o.throwforce)
-/datum/material/proc/on_removed_turf(turf/T, material_flags)
- return
+/datum/material/proc/on_removed_turf(turf/T, amount, material_flags)
+ // if(alpha)
+ // RemoveElement(/datum/element/turf_z_transparency, FALSE)
+
+/**
+ * This proc is called when the mat is found in an item that's consumed by accident. see /obj/item/proc/on_accidental_consumption.
+ * Arguments
+ * * M - person consuming the mat
+ * * S - (optional) item the mat is contained in (NOT the item with the mat itself)
+ */
+/datum/material/proc/on_accidental_mat_consumption(mob/living/carbon/M, obj/item/S)
+ return FALSE
+
+/** Returns the composition of this material.
+ *
+ * Mostly used for alloys when breaking down materials.
+ *
+ * Arguments:
+ * - amount: The amount of the material to break down.
+ * - breakdown_flags: Some flags dictating how exactly this material is being broken down.
+ */
+/datum/material/proc/return_composition(amount=1, breakdown_flags=NONE)
+ return list((src) = amount) // Yes we need the parenthesis, without them BYOND stringifies src into "src" and things break.
diff --git a/code/datums/shuttles.dm b/code/datums/shuttles.dm
index e2e6a05b08..62f8cef5dc 100644
--- a/code/datums/shuttles.dm
+++ b/code/datums/shuttles.dm
@@ -8,12 +8,16 @@
var/description
var/prerequisites
var/admin_notes
-
+ /// How much does this shuttle cost the cargo budget to purchase? Put in terms of CARGO_CRATE_VALUE to properly scale the cost with the current balance of cargo's income.
var/credit_cost = INFINITY
+ /// Can the be legitimately purchased by the station? Used by hardcoded or pre-mapped shuttles like the lavaland or cargo shuttle.
var/can_be_bought = TRUE
+ /// If set, overrides default movement_force on shuttle
+ var/list/movement_force
var/port_x_offset
var/port_y_offset
+ var/extra_desc = ""
/datum/map_template/shuttle/proc/prerequisites_met()
return TRUE
@@ -23,7 +27,7 @@
mappath = "[prefix][shuttle_id].dmm"
. = ..()
-/datum/map_template/shuttle/preload_size(path = mappath, force_cache = FALSE)
+/datum/map_template/shuttle/preload_size(path, force_cache)
. = ..(path, TRUE) // Done this way because we still want to know if someone actualy wanted to cache the map
if(!cached_map)
return
@@ -64,6 +68,9 @@
continue
if(length(place.baseturfs) < 2) // Some snowflake shuttle shit
continue
+ // var/list/sanity = place.baseturfs.Copy() // we do not have new baseturfs yet
+ // sanity.Insert(3, /turf/baseturf_skipover/shuttle)
+ // place.baseturfs = baseturfs_string_list(sanity, place)
place.baseturfs.Insert(3, /turf/baseturf_skipover/shuttle)
for(var/obj/docking_port/mobile/port in place)
@@ -93,6 +100,7 @@
port.dwidth = port_y_offset - 1
port.dheight = width - port_x_offset
+ // these three for loops are cit specific.
for(var/obj/structure/closet/closet in place)
if(closet.anchorable)
closet.anchored = TRUE
@@ -104,11 +112,10 @@
rack.AddComponent(/datum/component/magnetic_catch)
//Whatever special stuff you want
-/datum/map_template/shuttle/proc/post_load(obj/docking_port/mobile/M)
- return
-
-/datum/map_template/shuttle/proc/on_bought()
- return
+/datum/map_template/shuttle/post_load(obj/docking_port/mobile/M)
+ if(movement_force)
+ M.movement_force = movement_force.Copy()
+ M.linkup()
/datum/map_template/shuttle/emergency
port_id = "emergency"
@@ -117,6 +124,7 @@
/datum/map_template/shuttle/cargo
port_id = "cargo"
name = "Base Shuttle Template (Cargo)"
+ can_be_bought = FALSE
/datum/map_template/shuttle/ferry
port_id = "ferry"
@@ -137,10 +145,6 @@
port_id = "mining_common"
can_be_bought = FALSE
-/datum/map_template/shuttle/cargo
- port_id = "cargo"
- can_be_bought = FALSE
-
/datum/map_template/shuttle/arrival
port_id = "arrival"
can_be_bought = FALSE
@@ -189,21 +193,23 @@
name = "Backup Shuttle"
can_be_bought = FALSE
-/datum/map_template/shuttle/emergency/airless
- suffix = "airless"
+/datum/map_template/shuttle/emergency/construction
+ suffix = "construction"
name = "Build your own shuttle kit"
- description = "Save money by building your own shuttle! The chassis will dock upon purchase, but launch will have to be authorized as usual via shuttle call. Interior and lighting not included."
+ description = "For the enterprising shuttle engineer! The chassis will dock upon purchase, but launch will have to be authorized as usual via shuttle call. Comes stocked with construction materials. Unlocks the ability to buy shuttle engine crates from cargo."
admin_notes = "No brig, no medical facilities, just an empty box."
credit_cost = -7500
-/datum/map_template/shuttle/emergency/airless/prerequisites_met()
+/datum/map_template/shuttle/emergency/construction/prerequisites_met()
// first 10 minutes only
return world.time - SSticker.round_start_time < 6000
-/datum/map_template/shuttle/emergency/airless/on_bought()
- //enable buying engines from cargo
- var/datum/supply_pack/P = SSshuttle.supply_packs[/datum/supply_pack/engineering/shuttle_engine]
- P.special_enabled = TRUE
+// this is broken and does not work. Thanks TG
+// /datum/map_template/shuttle/emergency/airless/post_load()
+// . = ..()
+// //enable buying engines from cargo
+// var/datum/supply_pack/P = SSshuttle.supply_packs[/datum/supply_pack/engineering/shuttle_engine]
+// P.special_enabled = TRUE
/datum/map_template/shuttle/emergency/asteroid
@@ -220,6 +226,13 @@
Has medical facilities."
credit_cost = 5000
+// /datum/map_template/shuttle/emergency/pod
+// suffix = "pod"
+// name = "Emergency Pods"
+// description = "We did not expect an evacuation this quickly. All we have available is two escape pods."
+// admin_notes = "For player punishment."
+// can_be_bought = FALSE
+
/datum/map_template/shuttle/emergency/russiafightpit
suffix = "russiafightpit"
name = "Mother Russia Bleeds"
@@ -230,9 +243,10 @@
/datum/map_template/shuttle/emergency/meteor
suffix = "meteor"
name = "Asteroid With Engines Strapped To It"
- description = "A hollowed out asteroid with engines strapped to it. Due to its size and difficulty in steering it, this shuttle may damage the docking area."
+ description = "A hollowed out asteroid with engines strapped to it, the hollowing procedure makes it very difficult to hijack but is very expensive. Due to its size and difficulty in steering it, this shuttle may damage the docking area."
admin_notes = "This shuttle will likely crush escape, killing anyone there."
credit_cost = -5000
+ movement_force = list("KNOCKDOWN" = 3, "THROW" = 2)
/datum/map_template/shuttle/emergency/luxury
suffix = "luxury"
@@ -247,18 +261,30 @@
description = "The glorious results of centuries of plasma research done by Nanotrasen employees. This is the reason why you are here. Get on and dance like you're on fire, burn baby burn!"
admin_notes = "Flaming hot. The main area has a dance machine as well as plasma floor tiles that will be ignited by players every single time."
credit_cost = 10000
+ // can_be_bought = FALSE
-/datum/map_template/shuttle/emergency/arena
- suffix = "arena"
- name = "The Arena"
- description = "The crew must pass through an otherworldy arena to board this shuttle. Expect massive casualties. The source of the Bloody Signal must be tracked down and eliminated to unlock this shuttle."
- admin_notes = "RIP AND TEAR."
- credit_cost = 10000
+// /datum/map_template/shuttle/emergency/arena
+// suffix = "arena"
+// name = "The Arena"
+// description = "The crew must pass through an otherworldy arena to board this shuttle. Expect massive casualties. The source of the Bloody Signal must be tracked down and eliminated to unlock this shuttle."
+// admin_notes = "RIP AND TEAR."
+// credit_cost = 10000
+// /// Whether the arena z-level has been created
+// var/arena_loaded = FALSE
-/datum/map_template/shuttle/emergency/arena/prerequisites_met()
- if("bubblegum" in SSshuttle.shuttle_purchase_requirements_met)
- return TRUE
- return FALSE
+// /datum/map_template/shuttle/emergency/arena/prerequisites_met()
+// return SSshuttle.shuttle_purchase_requirements_met["bubblegum"]
+
+// /datum/map_template/shuttle/emergency/arena/post_load(obj/docking_port/mobile/M)
+// . = ..()
+// if(!arena_loaded)
+// arena_loaded = TRUE
+// var/datum/map_template/arena/arena_template = new()
+// arena_template.load_new_z()
+
+// /datum/map_template/arena
+// name = "The Arena"
+// mappath = "_maps/templates/the_arena.dmm"
/datum/map_template/shuttle/emergency/birdboat
suffix = "birdboat"
@@ -272,6 +298,13 @@
credit_cost = 2000
description = "The gold standard in emergency exfiltration, this tried and true design is equipped with everything the crew needs for a safe flight home."
+// /datum/map_template/shuttle/emergency/donut
+// suffix = "donut"
+// name = "Donutstation Emergency Shuttle"
+// description = "The perfect spearhead for any crude joke involving the station's shape, this shuttle supports a separate containment cell for prisoners and a compact medical wing."
+// admin_notes = "Has airlocks on both sides of the shuttle and will probably intersect near the front on some stations that build past departures."
+// credit_cost = 2500
+
/datum/map_template/shuttle/emergency/clown
suffix = "clown"
name = "Snappop(tm)!"
@@ -316,7 +349,9 @@
credit_cost = -1000
description = "Due to a lack of functional emergency shuttles, we bought this second hand from a scrapyard and pressed it into service. Please do not lean too heavily on the exterior windows, they are fragile."
admin_notes = "An abomination with no functional medbay, sections missing, and some very fragile windows. Surprisingly airtight."
+ movement_force = list("KNOCKDOWN" = 3, "THROW" = 2)
+// CIT SPECIFIC
/datum/map_template/shuttle/emergency/syndicate
suffix = "syndicate"
name = "Syndicate GM Battlecruiser"
@@ -325,9 +360,7 @@
admin_notes = "An emag exclusive, stocked with syndicate equipment and turrets that will target any simplemob."
/datum/map_template/shuttle/emergency/syndicate/prerequisites_met()
- if("emagged" in SSshuttle.shuttle_purchase_requirements_met)
- return TRUE
- return FALSE
+ return SSshuttle.shuttle_purchase_requirements_met["emagged"]
/datum/map_template/shuttle/emergency/narnar
suffix = "narnar"
@@ -335,6 +368,10 @@
description = "Looks like this shuttle may have wandered into the darkness between the stars on route to the station. Let's not think too hard about where all the bodies came from."
admin_notes = "Contains real cult ruins, mob eyeballs, and inactive constructs. Cult mobs will automatically be sentienced by fun balloon. \
Cloning pods in 'medbay' area are showcases and nonfunctional."
+ credit_cost = 6667 ///The joke is the number so no defines
+
+/datum/map_template/shuttle/emergency/narnar/prerequisites_met()
+ return SSshuttle.shuttle_purchase_requirements_met["narsie"]
/datum/map_template/shuttle/emergency/pubby
suffix = "pubby"
@@ -354,7 +391,7 @@
/datum/map_template/shuttle/emergency/supermatter
suffix = "supermatter"
name = "Hyperfractal Gigashuttle"
- description = "(Emag only) \"I dunno, this seems kinda needlessly complicated.\"\n\
+ description = "\"I dunno, this seems kinda needlessly complicated.\"\n\
\"This shuttle has very a very high safety record, according to CentCom Officer Cadet Yins.\"\n\
\"Are you sure?\"\n\
\"Yes, it has a safety record of N-A-N, which is apparently larger than 100%.\""
@@ -363,19 +400,19 @@
It does, however, still dust anything on contact, emits high levels of radiation, and induce hallucinations in anyone looking at it without protective goggles. \
Emitters spawn powered on, expect admin notices, they are harmless."
credit_cost = 15000
+ movement_force = list("KNOCKDOWN" = 3, "THROW" = 2)
/datum/map_template/shuttle/emergency/supermatter/prerequisites_met()
- if("emagged" in SSshuttle.shuttle_purchase_requirements_met)
- return TRUE
- return FALSE
-
+ return SSshuttle.shuttle_purchase_requirements_met["emagged"]
/datum/map_template/shuttle/emergency/imfedupwiththisworld
suffix = "imfedupwiththisworld"
name = "Oh, Hi Daniel"
description = "How was space work today? Oh, pretty good. We got a new space station and the company will make a lot of money. What space station? I cannot tell you; it's space confidential. \
Aw, come space on. Why not? No, I can't. Anyway, how is your space roleplay life?"
admin_notes = "Tiny, with a single airlock and wooden walls. What could go wrong?"
+ // can_be_bought = FALSE
credit_cost = -5000
+ movement_force = list("KNOCKDOWN" = 3, "THROW" = 2)
/datum/map_template/shuttle/emergency/goon
suffix = "goon"
@@ -383,6 +420,14 @@
description = "The Nanotrasen Emergency Shuttle Port(NES Port for short) is a shuttle used at other less known Nanotrasen facilities and has a more open inside for larger crowds, but fewer onboard shuttle facilities."
credit_cost = 500
+// /datum/map_template/shuttle/emergency/rollerdome
+// suffix = "rollerdome"
+// name = "Uncle Pete's Rollerdome"
+// description = "Developed by a member of Nanotrasen's R&D crew that claims to have travelled from the year 2028.
+// He says this shuttle is based off an old entertainment complex from the 1990s, though our database has no records on anything pertaining to that decade."
+// admin_notes = "ONLY NINETIES KIDS REMEMBER. Uses the fun balloon and drone from the Emergency Bar."
+// credit_cost = 500 * 5
+
/datum/map_template/shuttle/emergency/wabbajack
suffix = "wabbajack"
name = "NT Lepton Violet"
@@ -398,6 +443,7 @@
description = "On the smaller size with a modern design, this shuttle is for the crew who like the cosier things, while still being able to stretch their legs."
credit_cost = 1000
+// CIT SPECIFIC
/datum/map_template/shuttle/emergency/gorilla
suffix = "gorilla"
name = "Gorilla Cargo Freighter"
@@ -405,11 +451,17 @@
credit_cost = 2000
/datum/map_template/shuttle/emergency/gorilla/prerequisites_met()
- if("emagged" in SSshuttle.shuttle_purchase_requirements_met)
- return TRUE
- return FALSE
+ return SSshuttle.shuttle_purchase_requirements_met["emagged"]
-/datum/map_template/shuttle/emergency/cruise
+
+// /datum/map_template/shuttle/emergency/cruise
+// suffix = "cruise"
+// name = "The NTSS Independence"
+// description = "Ordinarily reserved for special functions and events, the Cruise Shuttle Independence can bring a summery cheer to your next station evacuation for a 'modest' fee!"
+// admin_notes = "This motherfucker is BIG. You might need to force dock it."
+// credit_cost = 8000
+
+/datum/map_template/shuttle/emergency/monkey
suffix = "nature"
name = "Dynamic Environmental Interaction Shuttle"
description = "A large shuttle with a center biodome that is flourishing with life. Frolick with the monkeys! (Extra monkeys are stored on the bridge.)"
@@ -441,7 +493,7 @@
/datum/map_template/shuttle/ferry/fancy
suffix = "fancy"
name = "fancy transport ferry"
- description = "At some point, someone upgraded the ferry to have fancier flooring... and less seats."
+ description = "At some point, someone upgraded the ferry to have fancier flooring... and fewer seats."
/datum/map_template/shuttle/ferry/kilo
suffix = "kilo"
@@ -464,6 +516,14 @@
suffix = "cere"
name = "NT Construction Vessel"
+// /datum/map_template/shuttle/whiteship/kilo
+// suffix = "kilo"
+// name = "NT Mining Shuttle"
+
+// /datum/map_template/shuttle/whiteship/donut
+// suffix = "donut"
+// name = "NT Long-Distance Bluespace Jumper"
+
/datum/map_template/shuttle/whiteship/delta
suffix = "delta"
name = "NT Frigate"
@@ -476,10 +536,6 @@
suffix = "cog"
name = "NT Prisoner Transport"
-/datum/map_template/shuttle/cargo/box
- suffix = "box"
- name = "supply shuttle (Box)"
-
/datum/map_template/shuttle/cargo/kilo
suffix = "kilo"
name = "supply shuttle (Kilo)"
@@ -488,6 +544,14 @@
suffix = "birdboat"
name = "supply shuttle (Birdboat)"
+// /datum/map_template/shuttle/cargo/donut
+// suffix = "donut"
+// name = "supply shuttle (Donut)"
+
+// /datum/map_template/shuttle/cargo/pubby
+// suffix = "pubby"
+// name = "supply shuttle (Pubby)"
+
/datum/map_template/shuttle/emergency/delta
suffix = "delta"
name = "Delta Station Emergency Shuttle"
@@ -497,11 +561,24 @@
/datum/map_template/shuttle/emergency/raven
suffix = "raven"
- name = "CentCom Raven Battlecruiser"
- description = "The CentCom Raven Battlecruiser is currently docked at the CentCom ship bay awaiting a mission, this Battlecruiser has been reassigned as an emergency escape shuttle for currently unknown reasons. The CentCom Raven Battlecruiser should comfortably fit a medium to large crew size crew and is complete with all required facitlities including a top of the range CentCom Medical Bay."
- admin_notes = "Comes with turrets that will target any simplemob."
+ name = "CentCom Raven Cruiser"
+ description = "The CentCom Raven Cruiser is a former high-risk salvage vessel, now repurposed into an emergency escape shuttle. \
+ Once first to the scene to pick through warzones for valuable remains, it now serves as an excellent escape option for stations under heavy fire from outside forces. \
+ This escape shuttle boasts shields and numerous anti-personnel turrets guarding its perimeter to fend off meteors and enemy boarding attempts."
+ admin_notes = "Comes with turrets that will target anything without the neutral faction (nuke ops, xenos etc, but not pets)."
credit_cost = 12500
+// /datum/map_template/shuttle/emergency/zeta
+// suffix = "zeta"
+// name = "Tr%nPo2r& Z3TA"
+// description = "A glitch appears on your monitor, flickering in and out of the options laid before you.
+// It seems strange and alien, you may need a special technology to access the signal.."
+// admin_notes = "Has alien surgery tools, and a void core that provides unlimited power."
+// credit_cost = CARGO_CRATE_VALUE * 16
+
+// /datum/map_template/shuttle/emergency/zeta/prerequisites_met()
+// return SSshuttle.shuttle_purchase_requirements_met[SHUTTLE_UNLOCK_ALIENTECH]
+
/datum/map_template/shuttle/emergency/cog
suffix = "cog"
name = "NES Classic"
@@ -524,18 +601,22 @@
suffix = "box"
name = "labour shuttle (Box)"
-/datum/map_template/shuttle/labour/kilo
- suffix = "kilo"
- name = "labour shuttle (Kilo)"
-
/datum/map_template/shuttle/labour/cog
suffix = "cog"
name = "labour shuttle (Cog)"
+// /datum/map_template/shuttle/arrival/donut
+// suffix = "donut"
+// name = "arrival shuttle (Donut)"
+
/datum/map_template/shuttle/infiltrator/basic
suffix = "basic"
name = "basic syndicate infiltrator"
+// /datum/map_template/shuttle/infiltrator/advanced
+// suffix = "advanced"
+// name = "advanced syndicate infiltrator"
+
/datum/map_template/shuttle/cargo/delta
suffix = "delta"
name = "cargo ferry (Delta)"
@@ -548,17 +629,25 @@
suffix = "kilo"
name = "mining shuttle (Kilo)"
+// /datum/map_template/shuttle/mining/large
+// suffix = "large"
+// name = "mining shuttle (Large)"
+
/datum/map_template/shuttle/labour/delta
suffix = "delta"
name = "labour shuttle (Delta)"
+/datum/map_template/shuttle/labour/kilo
+ suffix = "kilo"
+ name = "labour shuttle (Kilo)"
+
/datum/map_template/shuttle/mining_common/meta
suffix = "meta"
name = "lavaland shuttle (Meta)"
-/datum/map_template/shuttle/labour/kilo
- suffix = "kilo"
- name = "labour shuttle (Kilo)"
+// /datum/map_template/shuttle/mining_common/kilo
+// suffix = "kilo"
+// name = "lavaland shuttle (Kilo)"
/datum/map_template/shuttle/arrival/delta
suffix = "delta"
@@ -608,6 +697,18 @@
suffix = "default"
name = "pirate ship (Default)"
+/datum/map_template/shuttle/hunter/space_cop
+ suffix = "space_cop"
+ name = "Police Spacevan"
+
+/datum/map_template/shuttle/hunter/russian
+ suffix = "russian"
+ name = "Russian Cargo Ship"
+
+/datum/map_template/shuttle/hunter/bounty
+ suffix = "bounty"
+ name = "Bounty Hunter Ship"
+
/datum/map_template/shuttle/ruin/caravan_victim
suffix = "caravan_victim"
name = "Small Freighter"
@@ -631,15 +732,3 @@
/datum/map_template/shuttle/snowdin/excavation
suffix = "excavation"
name = "Snowdin Excavation Elevator"
-
-/datum/map_template/shuttle/hunter/space_cop
- suffix = "space_cop"
- name = "Police Spacevan"
-
-/datum/map_template/shuttle/hunter/russian
- suffix = "russian"
- name = "Russian Cargo Ship"
-
-/datum/map_template/shuttle/hunter/bounty
- suffix = "bounty"
- name = "Bounty Hunter Ship"
diff --git a/code/datums/tgs_event_handler.dm b/code/datums/tgs_event_handler.dm
index 731be64183..434450b9be 100644
--- a/code/datums/tgs_event_handler.dm
+++ b/code/datums/tgs_event_handler.dm
@@ -5,8 +5,8 @@
switch(event_code)
if(TGS_EVENT_REBOOT_MODE_CHANGE)
var/list/reboot_mode_lookup = list ("[TGS_REBOOT_MODE_NORMAL]" = "be normal", "[TGS_REBOOT_MODE_SHUTDOWN]" = "shutdown the server", "[TGS_REBOOT_MODE_RESTART]" = "hard restart the server")
- var old_reboot_mode = args[2]
- var new_reboot_mode = args[3]
+ var/old_reboot_mode = args[2]
+ var/new_reboot_mode = args[3]
message_admins("TGS: Reboot will no longer [reboot_mode_lookup["[old_reboot_mode]"]], it will instead [reboot_mode_lookup["[new_reboot_mode]"]]")
if(TGS_EVENT_PORT_SWAP)
message_admins("TGS: Changing port from [world.port] to [args[2]]")
@@ -28,12 +28,14 @@
var/datum/tgs_version/old_version = world.TgsVersion()
var/datum/tgs_version/new_version = args[2]
if(!old_version.Equals(new_version))
- to_chat(world, "TGS updated to v[old_version.deprefixed_parameter]")
+ to_chat(world, "TGS updated to v[new_version.deprefixed_parameter]")
else
message_admins("TGS: Back online")
if(reattach_timer)
deltimer(reattach_timer)
reattach_timer = null
+ if(TGS_EVENT_WATCHDOG_SHUTDOWN)
+ to_chat_immediate(world, "Server is shutting down!")
/datum/tgs_event_handler/impl/proc/LateOnReattach()
message_admins("Warning: TGS hasn't notified us of it coming back for a full minute! Is there a problem?")
diff --git a/code/datums/traits/negative.dm b/code/datums/traits/negative.dm
index cce138e82c..38007023b5 100644
--- a/code/datums/traits/negative.dm
+++ b/code/datums/traits/negative.dm
@@ -153,7 +153,7 @@ GLOBAL_LIST_EMPTY(family_heirlooms)
name = "Light Sensitivity"
desc = "Bright lights irritate you. Your eyes start to water, your skin feels itchy against the photon radiation, and your hair gets dry and frizzy. Maybe it's a medical condition. If only Nanotrasen was more considerate of your needs..."
value = -1
- gain_text = "The safty of light feels off..."
+ gain_text = "Bright lights seem irritating."
lose_text = "Enlightening."
medical_record_text = "Despite my warnings, the patient refuses turn on the lights, only to end up rolling down a full flight of stairs and into the cellar."
diff --git a/code/datums/view.dm b/code/datums/view.dm
index 8eb06c2bd2..5610fb040e 100644
--- a/code/datums/view.dm
+++ b/code/datums/view.dm
@@ -83,8 +83,6 @@
/datum/viewData/proc/apply()
chief.change_view(getView())
safeApplyFormat()
- if(chief.prefs.auto_fit_viewport)
- chief.fit_viewport()
/datum/viewData/proc/supress()
is_suppressed = TRUE
diff --git a/code/datums/weather/weather_types/radiation_storm.dm b/code/datums/weather/weather_types/radiation_storm.dm
index 4638508d1c..337be3e005 100644
--- a/code/datums/weather/weather_types/radiation_storm.dm
+++ b/code/datums/weather/weather_types/radiation_storm.dm
@@ -22,7 +22,7 @@
target_trait = ZTRAIT_STATION
immunity_type = "rad"
-
+
var/radiation_intensity = 100
/datum/weather/rad_storm/telegraph()
diff --git a/code/datums/world_topic.dm b/code/datums/world_topic.dm
index 7a1f0f6980..946090c571 100644
--- a/code/datums/world_topic.dm
+++ b/code/datums/world_topic.dm
@@ -90,7 +90,7 @@
if(!is_new_ckey)
log_admin("AUTO BUNKER: [ckeytobypass] given access (incoming comms from [sender]).")
message_admins("AUTO BUNKER: [ckeytobypass] given access (incoming comms from [sender]).")
- send2irc("Panic Bunker", "AUTO BUNKER: [ckeytobypass] given access (incoming comms from [sender]).")
+ send2adminchat("Panic Bunker", "AUTO BUNKER: [ckeytobypass] given access (incoming comms from [sender]).")
return "Success"
/datum/world_topic/ahelp_relay
@@ -106,8 +106,8 @@
/datum/world_topic/comms_console/Run(list/input, addr)
minor_announce(input["message"], "Incoming message from [input["message_sender"]]")
- for(var/obj/machinery/computer/communications/CM in GLOB.machines)
- CM.overrideCooldown()
+ for(var/obj/machinery/computer/communications/console in GLOB.machines)
+ console.override_cooldown()
/datum/world_topic/news_report
keyword = "News_Report"
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index bd2e515562..18674cdb17 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -885,6 +885,9 @@
VV_DROPDOWN_OPTION(VV_HK_ADD_REAGENT, "Add Reagent")
VV_DROPDOWN_OPTION(VV_HK_TRIGGER_EMP, "EMP Pulse")
VV_DROPDOWN_OPTION(VV_HK_TRIGGER_EXPLOSION, "Explosion")
+ // VV_DROPDOWN_OPTION(VV_HK_RADIATE, "Radiate")
+ VV_DROPDOWN_OPTION(VV_HK_EDIT_FILTERS, "Edit Filters")
+ // VV_DROPDOWN_OPTION(VV_HK_ADD_AI, "Add AI controller")
/atom/vv_do_topic(list/href_list)
. = ..()
@@ -928,6 +931,9 @@
var/newname = input(usr, "What do you want to rename this to?", "Automatic Rename") as null|text
if(newname)
vv_auto_rename(newname)
+ if(href_list[VV_HK_EDIT_FILTERS] && check_rights(R_VAREDIT))
+ var/client/C = usr.client
+ C?.open_filter_editor(src)
/atom/vv_get_header()
. = ..()
@@ -1148,7 +1154,6 @@
victim.log_message(message, LOG_ATTACK, color="blue")
-// Filter stuff
/atom/proc/add_filter(name,priority,list/params)
LAZYINITLIST(filter_data)
var/list/p = params.Copy()
@@ -1164,26 +1169,64 @@
var/list/arguments = data.Copy()
arguments -= "priority"
filters += filter(arglist(arguments))
+ UNSETEMPTY(filter_data)
+
+/atom/proc/transition_filter(name, time, list/new_params, easing, loop)
+ var/filter = get_filter(name)
+ if(!filter)
+ return
+
+ var/list/old_filter_data = filter_data[name]
+
+ var/list/params = old_filter_data.Copy()
+ for(var/thing in new_params)
+ params[thing] = new_params[thing]
+
+ animate(filter, new_params, time = time, easing = easing, loop = loop)
+ for(var/param in params)
+ filter_data[name][param] = params[param]
+
+/atom/proc/change_filter_priority(name, new_priority)
+ if(!filter_data || !filter_data[name])
+ return
+
+ filter_data[name]["priority"] = new_priority
+ update_filters()
+
+/obj/item/update_filters()
+ . = ..()
+ for(var/X in actions)
+ var/datum/action/A = X
+ A.UpdateButtonIcon()
/atom/proc/get_filter(name)
if(filter_data && filter_data[name])
return filters[filter_data.Find(name)]
-/atom/proc/remove_filter(name)
- if(filter_data && filter_data[name])
- filter_data -= name
- update_filters()
- return TRUE
+/atom/proc/remove_filter(name_or_names)
+ if(!filter_data)
+ return
+
+ var/list/names = islist(name_or_names) ? name_or_names : list(name_or_names)
+
+ for(var/name in names)
+ if(filter_data[name])
+ filter_data -= name
+ update_filters()
+
+/atom/proc/clear_filters()
+ filter_data = null
+ filters = null
/atom/proc/intercept_zImpact(atom/movable/AM, levels = 1)
. |= SEND_SIGNAL(src, COMSIG_ATOM_INTERCEPT_Z_FALL, AM, levels)
///Sets the custom materials for an item.
-/atom/proc/set_custom_materials(var/list/materials, multiplier = 1)
+/atom/proc/set_custom_materials(list/materials, multiplier = 1)
if(custom_materials) //Only runs if custom materials existed at first. Should usually be the case but check anyways
for(var/i in custom_materials)
var/datum/material/custom_material = SSmaterials.GetMaterialRef(i)
- custom_material.on_removed(src, material_flags) //Remove the current materials
+ custom_material.on_removed(src, custom_materials[i], material_flags) //Remove the current materials
if(!length(materials))
custom_materials = null
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index a582fa03e4..fbda722e4f 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -331,6 +331,7 @@
return
. = anchored
anchored = anchorvalue
+ SEND_SIGNAL(src, COMSIG_OBJ_SETANCHORED, anchorvalue)
// SEND_SIGNAL(src, COMSIG_MOVABLE_SET_ANCHORED, anchorvalue)
/atom/movable/proc/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
diff --git a/code/game/gamemodes/bloodsucker/bloodsucker.dm b/code/game/gamemodes/bloodsucker/bloodsucker.dm
index c54de16e2e..65321f5820 100644
--- a/code/game/gamemodes/bloodsucker/bloodsucker.dm
+++ b/code/game/gamemodes/bloodsucker/bloodsucker.dm
@@ -24,6 +24,7 @@
traitor_name = "Bloodsucker"
antag_flag = ROLE_BLOODSUCKER
false_report_weight = 1
+ chaos = 4
restricted_jobs = list("AI","Cyborg")
protected_jobs = list("Chaplain", "Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster")
required_players = 20
@@ -87,7 +88,7 @@
// Init Sunlight (called from datum_bloodsucker.on_gain(), in case game mode isn't even Bloodsucker
/datum/game_mode/proc/check_start_sunlight()
// Already Sunlight (and not about to cancel)
- if(istype(bloodsucker_sunlight) && !bloodsucker_sunlight.cancel_me)
+ if(istype(bloodsucker_sunlight))
return
bloodsucker_sunlight = new ()
@@ -97,7 +98,6 @@
if(!istype(bloodsucker_sunlight))
return
if(bloodsuckers.len <= 0)
- bloodsucker_sunlight.cancel_me = TRUE
qdel(bloodsucker_sunlight)
bloodsucker_sunlight = null
diff --git a/code/game/gamemodes/brother/traitor_bro.dm b/code/game/gamemodes/brother/traitor_bro.dm
index 718ed2c103..eda6b5f9e2 100644
--- a/code/game/gamemodes/brother/traitor_bro.dm
+++ b/code/game/gamemodes/brother/traitor_bro.dm
@@ -6,6 +6,7 @@
name = "traitor+brothers"
config_tag = "traitorbro"
required_players = 25
+ chaos = 5
restricted_jobs = list("AI", "Cyborg")
protected_jobs = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster")
diff --git a/code/game/gamemodes/changeling/changeling.dm b/code/game/gamemodes/changeling/changeling.dm
index 8029685bf7..20ef83a54c 100644
--- a/code/game/gamemodes/changeling/changeling.dm
+++ b/code/game/gamemodes/changeling/changeling.dm
@@ -10,6 +10,7 @@ GLOBAL_VAR(changeling_team_objective_type) //If this is not null, we hand our th
config_tag = "changeling"
antag_flag = ROLE_CHANGELING
false_report_weight = 10
+ chaos = 5
restricted_jobs = list("AI", "Cyborg")
protected_jobs = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") //citadel change - adds HoP, CE, CMO, and RD to ling role blacklist
required_players = 15
diff --git a/code/game/gamemodes/changeling/traitor_chan.dm b/code/game/gamemodes/changeling/traitor_chan.dm
index b010b08bc3..88a1cde8ce 100644
--- a/code/game/gamemodes/changeling/traitor_chan.dm
+++ b/code/game/gamemodes/changeling/traitor_chan.dm
@@ -2,6 +2,7 @@
name = "traitor+changeling"
config_tag = "traitorchan"
false_report_weight = 10
+ chaos = 6
traitors_possible = 3 //hard limit on traitors if scaling is turned off
restricted_jobs = list("AI", "Cyborg")
required_players = 25
diff --git a/code/game/gamemodes/clock_cult/clock_cult.dm b/code/game/gamemodes/clock_cult/clock_cult.dm
index 29455afe56..d8ebf6f20c 100644
--- a/code/game/gamemodes/clock_cult/clock_cult.dm
+++ b/code/game/gamemodes/clock_cult/clock_cult.dm
@@ -134,6 +134,7 @@ Credit where due:
config_tag = "clockwork_cult"
antag_flag = ROLE_SERVANT_OF_RATVAR
false_report_weight = 10
+ chaos = 8
required_players = 24 //Fixing this directly for now since apparently config machine for forcing modes broke.
required_enemies = 3
recommended_enemies = 5
diff --git a/code/game/gamemodes/clown_ops/clown_ops.dm b/code/game/gamemodes/clown_ops/clown_ops.dm
index 108c67ad27..659d2de105 100644
--- a/code/game/gamemodes/clown_ops/clown_ops.dm
+++ b/code/game/gamemodes/clown_ops/clown_ops.dm
@@ -1,7 +1,7 @@
/datum/game_mode/nuclear/clown_ops
name = "clown ops"
config_tag = "clownops"
-
+ chaos = 8
announce_span = "danger"
announce_text = "Clown empire forces are approaching the station in an attempt to HONK it!\n\
Operatives: Secure the nuclear authentication disk and use your bananium fission explosive to HONK the station.\n\
diff --git a/code/game/gamemodes/cult/cult.dm b/code/game/gamemodes/cult/cult.dm
index 8ec4123201..ba9fad7a84 100644
--- a/code/game/gamemodes/cult/cult.dm
+++ b/code/game/gamemodes/cult/cult.dm
@@ -38,6 +38,7 @@
config_tag = "cult"
antag_flag = ROLE_CULTIST
false_report_weight = 10
+ chaos = 8
restricted_jobs = list("AI", "Cyborg")
protected_jobs = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster")
required_players = 30
diff --git a/code/game/gamemodes/devil/devil agent/devil_agent.dm b/code/game/gamemodes/devil/devil agent/devil_agent.dm
index 789cff5c8f..ccfd6f1dd5 100644
--- a/code/game/gamemodes/devil/devil agent/devil_agent.dm
+++ b/code/game/gamemodes/devil/devil agent/devil_agent.dm
@@ -1,6 +1,7 @@
/datum/game_mode/devil/devil_agents
name = "Devil Agents"
config_tag = "devil_agents"
+ chaos = 5
required_players = 25
required_enemies = 3
recommended_enemies = 8
diff --git a/code/game/gamemodes/devil/devil_game_mode.dm b/code/game/gamemodes/devil/devil_game_mode.dm
index 0f2e8f7858..9bf7fc0e82 100644
--- a/code/game/gamemodes/devil/devil_game_mode.dm
+++ b/code/game/gamemodes/devil/devil_game_mode.dm
@@ -3,6 +3,7 @@
config_tag = "devil"
antag_flag = ROLE_DEVIL
false_report_weight = 1
+ chaos = 3
protected_jobs = list("Lawyer", "Curator", "Chaplain", "Head of Security", "Captain", "AI")
required_players = 0
required_enemies = 1
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets.dm b/code/game/gamemodes/dynamic/dynamic_rulesets.dm
index 3e7e504130..f1e48eb31c 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets.dm
@@ -106,6 +106,7 @@
for(var/i in 1 to 3)
if(config_tag in saved_dynamic_rules[i])
weight_mult -= (repeated_mode_adjust[i]/100)
+ weight_mult = max(0,weight_mult)
if(config_tag in costs)
cost = costs[config_tag]
if(config_tag in requirementses)
diff --git a/code/game/gamemodes/eldritch_cult/eldritch_cult.dm b/code/game/gamemodes/eldritch_cult/eldritch_cult.dm
index a3e3c54dce..1693163fa2 100644
--- a/code/game/gamemodes/eldritch_cult/eldritch_cult.dm
+++ b/code/game/gamemodes/eldritch_cult/eldritch_cult.dm
@@ -3,6 +3,7 @@
config_tag = "heresy"
antag_flag = ROLE_HERETIC
false_report_weight = 5
+ chaos = 5
restricted_jobs = list("AI", "Cyborg")
protected_jobs = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster") //citadel change - adds HoP, CE, CMO, and RD to heretic role blacklist
required_players = 15
diff --git a/code/game/gamemodes/extended/extended.dm b/code/game/gamemodes/extended/extended.dm
index 976f3e3eca..a6abcbefbd 100644
--- a/code/game/gamemodes/extended/extended.dm
+++ b/code/game/gamemodes/extended/extended.dm
@@ -3,6 +3,7 @@
config_tag = "secret_extended"
false_report_weight = 5
required_players = 0
+ chaos = 0
announce_span = "notice"
announce_text = "Just have fun and enjoy the game!"
diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm
index 3d7eeb7a8a..10c0154412 100644
--- a/code/game/gamemodes/game_mode.dm
+++ b/code/game/gamemodes/game_mode.dm
@@ -17,6 +17,7 @@
var/config_tag = null
var/votable = 1
var/probability = 0
+ var/chaos = 5 // 0-9, used for weighting round-to-round
var/false_report_weight = 0 //How often will this show up incorrectly in a centcom report?
var/station_was_nuked = 0 //see nuclearbomb.dm and malfunction.dm
var/nuke_off_station = 0 //Used for tracking where the nuke hit
@@ -81,30 +82,43 @@
///Everyone should now be on the station and have their normal gear. This is the place to give the special roles extra things
/datum/game_mode/proc/post_setup(report) //Gamemodes can override the intercept report. Passing TRUE as the argument will force a report.
- //finalize_monster_hunters() Disabled for now
if(!report)
report = !CONFIG_GET(flag/no_intercept_report)
addtimer(CALLBACK(GLOBAL_PROC, .proc/display_roundstart_logout_report), ROUNDSTART_LOGOUT_REPORT_TIME)
- if(prob(20)) //CIT CHANGE - adds a 20% chance for the security level to be the opposite of what it normally is
+ if(prob(20)) //cit-change
flipseclevel = TRUE
+
+ // if(CONFIG_GET(flag/reopen_roundstart_suicide_roles))
+ // var/delay = CONFIG_GET(number/reopen_roundstart_suicide_roles_delay)
+ // if(delay)
+ // delay = (delay SECONDS)
+ // else
+ // delay = (4 MINUTES) //default to 4 minutes if the delay isn't defined.
+ // addtimer(CALLBACK(GLOBAL_PROC, .proc/reopen_roundstart_suicide_roles), delay)
+
if(SSdbcore.Connect())
- var/sql
+ var/list/to_set = list()
+ var/arguments = list()
if(SSticker.mode)
- sql += "game_mode = '[SSticker.mode]'"
+ to_set += "game_mode = :game_mode"
+ arguments["game_mode"] = SSticker.mode
if(GLOB.revdata.originmastercommit)
- if(sql)
- sql += ", "
- sql += "commit_hash = '[GLOB.revdata.originmastercommit]'"
- if(sql)
- var/datum/DBQuery/query_round_game_mode = SSdbcore.NewQuery("UPDATE [format_table_name("round")] SET [sql] WHERE id = [GLOB.round_id]")
+ to_set += "commit_hash = :commit_hash"
+ arguments["commit_hash"] = GLOB.revdata.originmastercommit
+ if(to_set.len)
+ arguments["round_id"] = GLOB.round_id
+ var/datum/db_query/query_round_game_mode = SSdbcore.NewQuery(
+ "UPDATE [format_table_name("round")] SET [to_set.Join(", ")] WHERE id = :round_id",
+ arguments
+ )
query_round_game_mode.Execute()
qdel(query_round_game_mode)
if(report)
addtimer(CALLBACK(src, .proc/send_intercept, 0), rand(waittime_l, waittime_h))
generate_station_goals()
gamemode_ready = TRUE
- return 1
+ return TRUE
///Handles late-join antag assignments
@@ -610,3 +624,10 @@
/// Mode specific info for ghost game_info
/datum/game_mode/proc/ghost_info()
return
+
+/datum/game_mode/proc/get_chaos()
+ var/chaos_levels = CONFIG_GET(keyed_list/chaos_level)
+ if(config_tag in chaos_levels)
+ return chaos_levels[config_tag]
+ else
+ return chaos
diff --git a/code/game/gamemodes/gangs/gangs.dm b/code/game/gamemodes/gangs/gangs.dm
index 0dc4a520ef..100669c487 100644
--- a/code/game/gamemodes/gangs/gangs.dm
+++ b/code/game/gamemodes/gangs/gangs.dm
@@ -6,6 +6,7 @@ GLOBAL_LIST_EMPTY(gangs)
name = "gang war"
config_tag = "gang"
antag_flag = ROLE_GANG
+ chaos = 9
restricted_jobs = list("AI", "Cyborg")
protected_jobs = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster")
required_players = 15
diff --git a/code/game/gamemodes/meteor/meteor.dm b/code/game/gamemodes/meteor/meteor.dm
index afeebb770b..78fe6d9324 100644
--- a/code/game/gamemodes/meteor/meteor.dm
+++ b/code/game/gamemodes/meteor/meteor.dm
@@ -2,6 +2,7 @@
name = "meteor"
config_tag = "meteor"
false_report_weight = 1
+ chaos = 9
var/meteordelay = 2000
var/nometeors = 0
var/rampupdelta = 5
diff --git a/code/game/gamemodes/meteor/meteors.dm b/code/game/gamemodes/meteor/meteors.dm
index 5cfec2376a..9953b593d9 100644
--- a/code/game/gamemodes/meteor/meteors.dm
+++ b/code/game/gamemodes/meteor/meteors.dm
@@ -1,4 +1,6 @@
#define DEFAULT_METEOR_LIFETIME 1800
+#define MAP_EDGE_PAD 5
+
GLOBAL_VAR_INIT(meteor_wave_delay, 625) //minimum wait between waves in tenths of seconds
//set to at least 100 unless you want evarr ruining every round
@@ -30,7 +32,7 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust)) //for space dust event
var/turf/pickedgoal
var/max_i = 10//number of tries to spawn meteor.
while(!isspaceturf(pickedstart))
- var/startSide = dir || pick(GLOB.cardinals)
+ var/startSide = (dir ? dir : pick(GLOB.cardinals))
var/startZ = pick(SSmapping.levels_by_trait(ZTRAIT_STATION))
pickedstart = spaceDebrisStartLoc(startSide, startZ)
pickedgoal = spaceDebrisFinishLoc(startSide, startZ)
@@ -46,17 +48,17 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust)) //for space dust event
var/startx
switch(startSide)
if(NORTH)
- starty = world.maxy-(TRANSITIONEDGE+2)
- startx = rand((TRANSITIONEDGE+2), world.maxx-(TRANSITIONEDGE+2))
+ starty = world.maxy-(TRANSITIONEDGE + MAP_EDGE_PAD)
+ startx = rand((TRANSITIONEDGE + MAP_EDGE_PAD), world.maxx-(TRANSITIONEDGE + MAP_EDGE_PAD))
if(EAST)
- starty = rand((TRANSITIONEDGE+2),world.maxy-(TRANSITIONEDGE+2))
- startx = world.maxx-(TRANSITIONEDGE+2)
+ starty = rand((TRANSITIONEDGE + MAP_EDGE_PAD),world.maxy-(TRANSITIONEDGE + MAP_EDGE_PAD))
+ startx = world.maxx-(TRANSITIONEDGE + MAP_EDGE_PAD)
if(SOUTH)
- starty = (TRANSITIONEDGE+2)
- startx = rand((TRANSITIONEDGE+2), world.maxx-(TRANSITIONEDGE+2))
+ starty = (TRANSITIONEDGE + MAP_EDGE_PAD)
+ startx = rand((TRANSITIONEDGE + MAP_EDGE_PAD), world.maxx-(TRANSITIONEDGE + MAP_EDGE_PAD))
if(WEST)
- starty = rand((TRANSITIONEDGE+2), world.maxy-(TRANSITIONEDGE+2))
- startx = (TRANSITIONEDGE+2)
+ starty = rand((TRANSITIONEDGE + MAP_EDGE_PAD), world.maxy-(TRANSITIONEDGE + MAP_EDGE_PAD))
+ startx = (TRANSITIONEDGE + MAP_EDGE_PAD)
. = locate(startx, starty, Z)
/proc/spaceDebrisFinishLoc(startSide, Z)
@@ -64,17 +66,17 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust)) //for space dust event
var/endx
switch(startSide)
if(NORTH)
- endy = (TRANSITIONEDGE+1)
- endx = rand((TRANSITIONEDGE+1), world.maxx-(TRANSITIONEDGE+1))
+ endy = (TRANSITIONEDGE + MAP_EDGE_PAD)
+ endx = rand((TRANSITIONEDGE + MAP_EDGE_PAD), world.maxx-(TRANSITIONEDGE + MAP_EDGE_PAD))
if(EAST)
- endy = rand((TRANSITIONEDGE+1), world.maxy-(TRANSITIONEDGE+1))
- endx = (TRANSITIONEDGE+1)
+ endy = rand((TRANSITIONEDGE + MAP_EDGE_PAD), world.maxy-(TRANSITIONEDGE + MAP_EDGE_PAD))
+ endx = (TRANSITIONEDGE + MAP_EDGE_PAD)
if(SOUTH)
- endy = world.maxy-(TRANSITIONEDGE+1)
- endx = rand((TRANSITIONEDGE+1), world.maxx-(TRANSITIONEDGE+1))
+ endy = world.maxy-(TRANSITIONEDGE + MAP_EDGE_PAD)
+ endx = rand((TRANSITIONEDGE + MAP_EDGE_PAD), world.maxx-(TRANSITIONEDGE + MAP_EDGE_PAD))
if(WEST)
- endy = rand((TRANSITIONEDGE+1),world.maxy-(TRANSITIONEDGE+1))
- endx = world.maxx-(TRANSITIONEDGE+1)
+ endy = rand((TRANSITIONEDGE + MAP_EDGE_PAD),world.maxy-(TRANSITIONEDGE + MAP_EDGE_PAD))
+ endx = world.maxx-(TRANSITIONEDGE + MAP_EDGE_PAD)
. = locate(endx, endy, Z)
///////////////////////
@@ -82,7 +84,7 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust)) //for space dust event
//////////////////////
/obj/effect/meteor
- name = "the concept of meteor"
+ name = "\proper the concept of meteor"
desc = "You should probably run instead of gawking at this."
icon = 'icons/obj/meteor.dmi'
icon_state = "small"
@@ -92,7 +94,7 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust)) //for space dust event
var/hitpwr = 2 //Level of ex_act to be called on hit.
var/dest
pass_flags = PASSTABLE
- var/heavy = 0
+ var/heavy = FALSE
var/meteorsound = 'sound/effects/meteorimpact.ogg'
var/z_original
var/threat = 0 // used for determining which meteors are most interesting
@@ -108,12 +110,12 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust)) //for space dust event
. = ..() //process movement...
+ var/turf/T = get_turf(loc)
if(.)//.. if did move, ram the turf we get in
- var/turf/T = get_turf(loc)
ram_turf(T)
- if(prob(10) && !isspaceturf(T) && !istype(T, /turf/closed/mineral) && !istype(T, /turf/open/floor/plating/asteroid))//randomly takes a 'hit' from ramming
- get_hit()
+ if(prob(10) && !isspaceturf(T) && !istype(T, /turf/closed/mineral) && !istype(T, /turf/open/floor/plating/asteroid))//randomly takes a 'hit' from ramming, and ignore spare ruin aseroids
+ get_hit()
/obj/effect/meteor/Destroy()
if (timerid)
@@ -135,24 +137,25 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust)) //for space dust event
/obj/effect/meteor/Bump(atom/A)
if(A)
ram_turf(get_turf(A))
- playsound(src.loc, meteorsound, 40, 1)
- if(!istype(A, /turf/closed/mineral) && !istype(A, /turf/open/floor/plating/asteroid))
+ playsound(src.loc, meteorsound, 40, TRUE)
+ if(!istype(A, /turf/closed/mineral) && !istype(A, /turf/open/floor/plating/asteroid)) // ignore localstation ruins
get_hit()
/obj/effect/meteor/proc/ram_turf(turf/T)
//first bust whatever is in the turf
- for(var/atom/A in T)
- if(A != src)
- if(isliving(A))
- A.visible_message("[src] slams into [A].", "[src] slams into you!.")
- A.ex_act(hitpwr)
+ for(var/thing in T)
+ if(thing == src)
+ continue
+ if(isliving(thing))
+ var/mob/living/living_thing = thing
+ living_thing.visible_message("[src] slams into [living_thing].", "[src] slams into you!.")
+ living_thing.ex_act(hitpwr)
//then, ram the turf if it still exists
if(T)
T.ex_act(hitpwr)
-
//process getting 'hit' by colliding with a dense object
//or randomly when ramming turfs
/obj/effect/meteor/proc/get_hit()
@@ -162,13 +165,10 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust)) //for space dust event
meteor_effect()
qdel(src)
-/obj/effect/meteor/ex_act()
- return
-
/obj/effect/meteor/examine(mob/user)
+ . = ..()
if(!(flags_1 & ADMIN_SPAWNED_1) && isliving(user))
- SSmedals.UnlockMedal(MEDAL_METEOR, user.client)
- return ..()
+ user.client.give_award(/datum/award/achievement/misc/meteor_examine, user)
/obj/effect/meteor/attackby(obj/item/I, mob/user, params)
if(I.tool_behaviour == TOOL_MINING)
@@ -232,7 +232,7 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust)) //for space dust event
name = "big meteor"
icon_state = "large"
hits = 6
- heavy = 1
+ heavy = TRUE
dropamt = 4
threat = 10
@@ -245,7 +245,7 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust)) //for space dust event
name = "flaming meteor"
icon_state = "flaming"
hits = 5
- heavy = 1
+ heavy = TRUE
meteorsound = 'sound/effects/bamf.ogg'
meteordrop = list(/obj/item/stack/ore/plasma)
threat = 20
@@ -258,7 +258,7 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust)) //for space dust event
/obj/effect/meteor/irradiated
name = "glowing meteor"
icon_state = "glowing"
- heavy = 1
+ heavy = TRUE
meteordrop = list(/obj/item/stack/ore/uranium)
threat = 15
@@ -275,7 +275,7 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust)) //for space dust event
icon_state = "meateor"
desc = "Just... don't think too hard about where this thing came from."
hits = 2
- heavy = 1
+ heavy = TRUE
meteorsound = 'sound/effects/blobattack.ogg'
meteordrop = list(/obj/item/reagent_containers/food/snacks/meat/slab/human, /obj/item/reagent_containers/food/snacks/meat/slab/human/mutant, /obj/item/organ/heart, /obj/item/organ/lungs, /obj/item/organ/tongue, /obj/item/organ/appendix/)
var/meteorgibs = /obj/effect/gibspawner/generic
@@ -327,7 +327,7 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust)) //for space dust event
desc = "Your life briefly passes before your eyes the moment you lay them on this monstrosity."
hits = 30
hitpwr = 1
- heavy = 1
+ heavy = TRUE
meteorsound = 'sound/effects/bamf.ogg'
meteordrop = list(/obj/item/stack/ore/plasma)
threat = 50
@@ -358,7 +358,7 @@ GLOBAL_LIST_INIT(meteorsSPOOKY, list(/obj/effect/meteor/pumpkin))
icon = 'icons/obj/meteor_spooky.dmi'
icon_state = "pumpkin"
hits = 10
- heavy = 1
+ heavy = TRUE
dropamt = 1
meteordrop = list(/obj/item/clothing/head/hardhat/pumpkinhead, /obj/item/reagent_containers/food/snacks/grown/pumpkin)
threat = 100
@@ -368,3 +368,4 @@ GLOBAL_LIST_INIT(meteorsSPOOKY, list(/obj/effect/meteor/pumpkin))
meteorsound = pick('sound/hallucinations/im_here1.ogg','sound/hallucinations/im_here2.ogg')
//////////////////////////
#undef DEFAULT_METEOR_LIFETIME
+#undef MAP_EDGE_PAD
diff --git a/code/game/gamemodes/monkey/monkey.dm b/code/game/gamemodes/monkey/monkey.dm
index 76460ffbb8..c91252258a 100644
--- a/code/game/gamemodes/monkey/monkey.dm
+++ b/code/game/gamemodes/monkey/monkey.dm
@@ -11,6 +11,7 @@
required_players = 20
required_enemies = 1
recommended_enemies = 1
+ chaos = 9
restricted_jobs = list("Cyborg", "AI")
diff --git a/code/game/gamemodes/nuclear/nuclear.dm b/code/game/gamemodes/nuclear/nuclear.dm
index 48a298984c..dcf84e84db 100644
--- a/code/game/gamemodes/nuclear/nuclear.dm
+++ b/code/game/gamemodes/nuclear/nuclear.dm
@@ -2,6 +2,7 @@
name = "nuclear emergency"
config_tag = "nuclear"
false_report_weight = 10
+ chaos = 9
required_players = 28 // 30 players - 3 players to be the nuke ops = 25 players remaining
required_enemies = 2
recommended_enemies = 5
diff --git a/code/game/gamemodes/overthrow/overthrow.dm b/code/game/gamemodes/overthrow/overthrow.dm
index dca0c1ade1..6a118567df 100644
--- a/code/game/gamemodes/overthrow/overthrow.dm
+++ b/code/game/gamemodes/overthrow/overthrow.dm
@@ -3,6 +3,7 @@
name = "overthrow"
config_tag = "overthrow"
antag_flag = ROLE_OVERTHROW
+ chaos = 5
restricted_jobs = list("AI", "Cyborg")
protected_jobs = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster")
required_players = 20 // the core idea is of a swift, bloodless coup, so it shouldn't be as chaotic as revs.
diff --git a/code/game/gamemodes/revolution/revolution.dm b/code/game/gamemodes/revolution/revolution.dm
index 9eed18d906..419a74d616 100644
--- a/code/game/gamemodes/revolution/revolution.dm
+++ b/code/game/gamemodes/revolution/revolution.dm
@@ -12,6 +12,7 @@
config_tag = "revolution"
antag_flag = ROLE_REV
false_report_weight = 10
+ chaos = 8
restricted_jobs = list("AI", "Cyborg")
protected_jobs = list("Security Officer", "Warden", "Detective", "Head of Security", "Captain", "Head of Personnel", "Chief Engineer", "Chief Medical Officer", "Research Director", "Quartermaster")
required_players = 20
diff --git a/code/game/gamemodes/traitor/double_agents.dm b/code/game/gamemodes/traitor/double_agents.dm
index fc669d4855..c64e508cef 100644
--- a/code/game/gamemodes/traitor/double_agents.dm
+++ b/code/game/gamemodes/traitor/double_agents.dm
@@ -10,6 +10,7 @@
required_enemies = 5
recommended_enemies = 8
reroll_friendly = 0
+ chaos = 7
traitor_name = "Nanotrasen Internal Affairs Agent"
antag_flag = ROLE_INTERNAL_AFFAIRS
diff --git a/code/game/gamemodes/traitor/traitor.dm b/code/game/gamemodes/traitor/traitor.dm
index 8b1b18660a..d42fe615cd 100644
--- a/code/game/gamemodes/traitor/traitor.dm
+++ b/code/game/gamemodes/traitor/traitor.dm
@@ -17,6 +17,7 @@
recommended_enemies = 4
reroll_friendly = 1
enemy_minimum_age = 0
+ chaos = 2
announce_span = "danger"
announce_text = "There are Syndicate agents on the station!\n\
diff --git a/code/game/gamemodes/wizard/wizard.dm b/code/game/gamemodes/wizard/wizard.dm
index 23f065318e..d8cb851aad 100644
--- a/code/game/gamemodes/wizard/wizard.dm
+++ b/code/game/gamemodes/wizard/wizard.dm
@@ -12,6 +12,7 @@
recommended_enemies = 1
enemy_minimum_age = 7
round_ends_with_antag_death = 1
+ chaos = 9
announce_span = "danger"
announce_text = "There is a space wizard attacking the station!\n\
Wizard: Accomplish your objectives and cause mayhem on the station.\n\
diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm
index 8d7a8c047a..e37fd13106 100644
--- a/code/game/machinery/_machinery.dm
+++ b/code/game/machinery/_machinery.dm
@@ -137,7 +137,7 @@ Class Procs:
GLOB.machines += src
if(ispath(circuit, /obj/item/circuitboard))
- circuit = new circuit
+ circuit = new circuit(src)
circuit.apply_default_parts(src)
if(!speed_process && init_process)
@@ -361,11 +361,11 @@ Class Procs:
/obj/machinery/deconstruct(disassembled = TRUE)
if(!(flags_1 & NODECONSTRUCT_1))
on_deconstruction()
- if(component_parts && component_parts.len)
+ if(LAZYLEN(component_parts))
spawn_frame(disassembled)
for(var/obj/item/I in component_parts)
I.forceMove(loc)
- component_parts.Cut()
+ LAZYCLEARLIST(component_parts)
qdel(src)
/obj/machinery/proc/spawn_frame(disassembled)
@@ -539,12 +539,17 @@ Class Procs:
/obj/machinery/Exited(atom/movable/AM, atom/newloc)
. = ..()
+ // if(AM == occupant)
+ // set_occupant(null)
if (AM == occupant)
SEND_SIGNAL(src, COMSIG_MACHINE_EJECT_OCCUPANT, occupant)
occupant = null
+ if(AM == circuit && circuit.loc != src)
+ component_parts -= AM //TODO: make the cmp part functions use lazyX
+ circuit = null
-/obj/machinery/proc/adjust_item_drop_location(atom/movable/AM) // Adjust item drop location to a 3x3 grid inside the tile, returns slot id from 0 to 8
- var/md5 = md5(AM.name) // Oh, and it's deterministic too. A specific item will always drop from the same slot.
+/obj/machinery/proc/adjust_item_drop_location(atom/movable/AM) // Adjust item drop location to a 3x3 grid inside the tile, returns slot id from 0 to 8
+ var/md5 = md5(AM.name) // Oh, and it's deterministic too. A specific item will always drop from the same slot.
for (var/i in 1 to 32)
. += hex2num(md5[i])
. = . % 9
diff --git a/code/game/machinery/aug_manipulator.dm b/code/game/machinery/aug_manipulator.dm
index 6fdeae61a1..e496973d75 100644
--- a/code/game/machinery/aug_manipulator.dm
+++ b/code/game/machinery/aug_manipulator.dm
@@ -8,7 +8,19 @@
max_integrity = 200
var/obj/item/bodypart/storedpart
var/initial_icon_state
- var/static/list/style_list_icons = list("standard" = 'icons/mob/augmentation/augments.dmi', "engineer" = 'icons/mob/augmentation/augments_engineer.dmi', "security" = 'icons/mob/augmentation/augments_security.dmi', "mining" = 'icons/mob/augmentation/augments_mining.dmi')
+ var/static/list/style_list_icons = list("standard" = 'icons/mob/augmentation/augments.dmi',
+ "engineer" = 'icons/mob/augmentation/augments_engineer.dmi',
+ "security" = 'icons/mob/augmentation/augments_security.dmi',
+ "mining" = 'icons/mob/augmentation/augments_mining.dmi',
+ "Talon" = 'icons/mob/augmentation/cosmetic_prosthetic/talon.dmi',
+ "Nanotrasen" = 'icons/mob/augmentation/cosmetic_prosthetic/nanotrasen.dmi',
+ "Hephaesthus" = 'icons/mob/augmentation/cosmetic_prosthetic/hephaestus.dmi',
+ "Bishop" = 'icons/mob/augmentation/cosmetic_prosthetic/bishop.dmi',
+ "Xion" = 'icons/mob/augmentation/cosmetic_prosthetic/xion.dmi',
+ "Grayson" = 'icons/mob/augmentation/cosmetic_prosthetic/grayson.dmi',
+ "Cybersolutions" = 'icons/mob/augmentation/cosmetic_prosthetic/cybersolutions.dmi',
+ "Ward" = 'icons/mob/augmentation/cosmetic_prosthetic/ward.dmi'
+ )
/obj/machinery/aug_manipulator/examine(mob/user)
. = ..()
diff --git a/code/game/machinery/autolathe.dm b/code/game/machinery/autolathe.dm
index 1f0687151d..d8a5f7d2c7 100644
--- a/code/game/machinery/autolathe.dm
+++ b/code/game/machinery/autolathe.dm
@@ -1,6 +1,6 @@
-#define AUTOLATHE_MAIN_MENU 1
-#define AUTOLATHE_CATEGORY_MENU 2
-#define AUTOLATHE_SEARCH_MENU 3
+#define AUTOLATHE_MAIN_MENU 1
+#define AUTOLATHE_CATEGORY_MENU 2
+#define AUTOLATHE_SEARCH_MENU 3
/obj/machinery/autolathe
name = "autolathe"
@@ -17,7 +17,7 @@
var/list/L = list()
var/list/LL = list()
var/hacked = FALSE
- var/disabled = 0
+ var/disabled = FALSE
var/shocked = FALSE
var/hack_wire
var/disable_wire
@@ -27,13 +27,13 @@
var/prod_coeff = 1
var/datum/design/being_built
+ var/datum/techweb/stored_research
var/list/datum/design/matching_designs
var/selected_category
var/screen = 1
var/base_price = 25
var/hacked_price = 50
- var/datum/techweb/specialized/autounlocking/stored_research = /datum/techweb/specialized/autounlocking/autolathe
var/list/categories = list(
"Tools",
"Electronics",
@@ -46,19 +46,13 @@
"Dinnerware",
"Imported"
)
- var/list/allowed_materials
-
- /// Base print speed
- var/base_print_speed = 10
/obj/machinery/autolathe/Initialize()
- var/list/mats = allowed_materials
- if(!mats)
- mats = SSmaterials.materialtypes_by_category[MAT_CATEGORY_RIGID]
- AddComponent(/datum/component/material_container, mats, _show_on_examine=TRUE, _after_insert=CALLBACK(src, .proc/AfterMaterialInsert))
+ AddComponent(/datum/component/material_container, SSmaterials.materialtypes_by_category[MAT_CATEGORY_RIGID], 0, TRUE, null, null, CALLBACK(src, .proc/AfterMaterialInsert))
. = ..()
+
wires = new /datum/wires/autolathe(src)
- stored_research = new stored_research
+ stored_research = new /datum/techweb/specialized/autounlocking/autolathe
matching_designs = list()
/obj/machinery/autolathe/Destroy()
@@ -83,7 +77,7 @@
if(AUTOLATHE_SEARCH_MENU)
dat = search_win(user)
- var/datum/browser/popup = new(user, name, name, 400, 500)
+ var/datum/browser/popup = new(user, "autolathe", name, 400, 500)
popup.set_content(dat)
popup.open()
@@ -114,9 +108,9 @@
return TRUE
if(istype(O, /obj/item/disk/design_disk))
- user.visible_message("[user] begins to load \the [O] in \the [src]...",
- "You begin to load a design from \the [O]...",
- "You hear the chatter of a floppy drive.")
+ user.visible_message("[user] begins to load \the [O] in \the [src]...",
+ "You begin to load a design from \the [O]...",
+ "You hear the chatter of a floppy drive.")
busy = TRUE
var/obj/item/disk/design_disk/D = O
if(do_after(user, 14.4, target = src))
@@ -128,14 +122,16 @@
return ..()
-/obj/machinery/autolathe/proc/AfterMaterialInsert(obj/item/item_inserted, id_inserted, amount_inserted)
+
+/obj/machinery/autolathe/proc/AfterMaterialInsert(item_inserted, id_inserted, amount_inserted)
if(istype(item_inserted, /obj/item/stack/ore/bluespace_crystal))
use_power(MINERAL_MATERIAL_AMOUNT / 10)
- else if(item_inserted.custom_materials?.len && item_inserted.custom_materials[SSmaterials.GetMaterialRef(/datum/material/glass)])
+ else if(custom_materials && custom_materials.len && custom_materials[SSmaterials.GetMaterialRef(/datum/material/glass)])
flick("autolathe_r",src)//plays glass insertion animation by default otherwise
else
flick("autolathe_o",src)//plays metal insertion animation
+
use_power(min(1000, amount_inserted / 100))
updateUsrDialog()
@@ -187,7 +183,7 @@
if(materials.materials[i] > 0)
list_to_show += i
- used_material = input("Choose [used_material]", "Custom Material") as null|anything in list_to_show
+ used_material = input("Choose [used_material]", "Custom Material") as null|anything in sortList(list_to_show, /proc/cmp_typepaths_asc)
if(!used_material)
return //Didn't pick any material, so you can't build shit either.
custom_materials[used_material] += amount_needed
@@ -198,8 +194,8 @@
busy = TRUE
use_power(power)
icon_state = "autolathe_n"
- var/time = is_stack ? 10 : base_print_speed * coeff * multiplier
- addtimer(CALLBACK(src, .proc/make_item, power, materials_used, custom_materials, multiplier, coeff, is_stack), time)
+ var/time = is_stack ? 32 : (32 * coeff * multiplier) ** 0.8
+ addtimer(CALLBACK(src, .proc/make_item, power, materials_used, custom_materials, multiplier, coeff, is_stack, usr), time)
else
to_chat(usr, "Not enough materials for this operation.")
@@ -218,10 +214,11 @@
return
-/obj/machinery/autolathe/proc/make_item(power, var/list/materials_used, var/list/picked_materials, multiplier, coeff, is_stack)
+/obj/machinery/autolathe/proc/make_item(power, list/materials_used, list/picked_materials, multiplier, coeff, is_stack, mob/user)
var/datum/component/material_container/materials = GetComponent(/datum/component/material_container)
var/atom/A = drop_location()
use_power(power)
+
materials.use_materials(materials_used)
if(is_stack)
@@ -235,6 +232,11 @@
if(length(picked_materials))
new_item.set_custom_materials(picked_materials, 1 / multiplier) //Ensure we get the non multiplied amount
+ for(var/x in picked_materials)
+ var/datum/material/M = x
+ if(!istype(M, /datum/material/glass) && !istype(M, /datum/material/iron))
+ user.client.give_award(/datum/award/achievement/misc/getting_an_upgrade, user)
+
icon_state = "autolathe"
busy = FALSE
@@ -246,12 +248,10 @@
T += MB.rating*75000
var/datum/component/material_container/materials = GetComponent(/datum/component/material_container)
materials.max_amount = T
- var/manips = 0
- var/total_manip_rating = 0
+ T=1.2
for(var/obj/item/stock_parts/manipulator/M in component_parts)
- total_manip_rating += M.rating
- manips++
- prod_coeff = STANDARD_PART_LEVEL_LATHE_COEFFICIENT(total_manip_rating / (manips? manips : 1))
+ T -= M.rating*0.2
+ prod_coeff = min(1,max(0,T)) // Coeff going 1 -> 0,8 -> 0,6 -> 0,4
/obj/machinery/autolathe/examine(mob/user)
. += ..()
@@ -376,6 +376,7 @@
return materials.has_materials(required_materials)
+
/obj/machinery/autolathe/proc/get_design_cost(datum/design/D)
var/coeff = (ispath(D.build_path, /obj/item/stack) ? 1 : prod_coeff)
var/dat
@@ -416,9 +417,7 @@
hacked = state
for(var/id in SSresearch.techweb_designs)
var/datum/design/D = SSresearch.techweb_design_by_id(id)
- if(D.build_type & stored_research.design_autounlock_skip_types)
- continue
- if((D.build_type & stored_research.design_autounlock_buildtypes) && ("hacked" in D.category))
+ if((D.build_type & AUTOLATHE) && ("hacked" in D.category))
if(hacked)
stored_research.add_design(D)
else
@@ -428,19 +427,24 @@
. = ..()
adjust_hacked(TRUE)
+//Called when the object is constructed by an autolathe
+//Has a reference to the autolathe so you can do !!FUN!! things with hacked lathes
+/obj/item/proc/autolathe_crafted(obj/machinery/autolathe/A)
+ return
+
/obj/machinery/autolathe/secure
name = "secured autolathe"
desc = "It produces items using metal and glass. This model was reprogrammed without some of the more hazardous designs."
circuit = /obj/item/circuitboard/machine/autolathe/secure
- stored_research = /datum/techweb/specialized/autounlocking/autolathe/public
- base_print_speed = 20
+
+/obj/machinery/autolathe/secure/Initialize()
+ . = ..()
+ stored_research = new /datum/techweb/specialized/autounlocking/autolathe/public
/obj/machinery/autolathe/toy
name = "autoylathe"
desc = "It produces toys using plastic, metal and glass."
circuit = /obj/item/circuitboard/machine/autolathe/toy
-
- stored_research = /datum/techweb/specialized/autounlocking/autolathe/toy
categories = list(
"Toys",
"Figurines",
@@ -453,12 +457,8 @@
"Misc",
"Imported"
)
- allowed_materials = list(
- /datum/material/iron,
- /datum/material/glass,
- /datum/material/plastic
- )
/obj/machinery/autolathe/toy/hacked/Initialize()
. = ..()
adjust_hacked(TRUE)
+ stored_research = new /datum/techweb/specialized/autounlocking/autolathe/toy
diff --git a/code/game/machinery/colormate.dm b/code/game/machinery/colormate.dm
index 1f1c16248c..d059d492ae 100644
--- a/code/game/machinery/colormate.dm
+++ b/code/game/machinery/colormate.dm
@@ -44,7 +44,8 @@
icon_state = "colormate"
/obj/machinery/gear_painter/Destroy()
- inserted.forceMove(drop_location())
+ if(inserted) //please i beg you do not drop nulls
+ inserted.forceMove(drop_location())
return ..()
/obj/machinery/gear_painter/attackby(obj/item/I, mob/living/user)
diff --git a/code/game/machinery/computer/_computer.dm b/code/game/machinery/computer/_computer.dm
index 16081db2c1..0c628ba43e 100644
--- a/code/game/machinery/computer/_computer.dm
+++ b/code/game/machinery/computer/_computer.dm
@@ -17,20 +17,16 @@
/obj/machinery/computer/Initialize(mapload, obj/item/circuitboard/C)
. = ..()
+
power_change()
- if(!QDELETED(C))
- qdel(circuit)
- circuit = C
- C.moveToNullspace()
/obj/machinery/computer/Destroy()
- QDEL_NULL(circuit)
- return ..()
+ . = ..()
/obj/machinery/computer/process()
if(stat & (NOPOWER|BROKEN))
- return 0
- return 1
+ return FALSE
+ return TRUE
/obj/machinery/computer/ratvar_act()
if(!clockwork)
@@ -87,25 +83,32 @@
switch(damage_type)
if(BRUTE)
if(stat & BROKEN)
- playsound(src.loc, 'sound/effects/hit_on_shattered_glass.ogg', 70, 1)
+ playsound(src.loc, 'sound/effects/hit_on_shattered_glass.ogg', 70, TRUE)
else
- playsound(src.loc, 'sound/effects/glasshit.ogg', 75, 1)
+ playsound(src.loc, 'sound/effects/glasshit.ogg', 75, TRUE)
if(BURN)
- playsound(src.loc, 'sound/items/welder.ogg', 100, 1)
+ playsound(src.loc, 'sound/items/welder.ogg', 100, TRUE)
/obj/machinery/computer/obj_break(damage_flag)
- if(circuit && !(flags_1 & NODECONSTRUCT_1)) //no circuit, no breaking
- if(!(stat & BROKEN))
- playsound(loc, 'sound/effects/glassbr3.ogg', 100, 1)
- stat |= BROKEN
- update_icon()
- set_light(0)
+ if(!circuit) //no circuit, no breaking
+ return
+ . = ..()
+ if(. && !(stat & BROKEN))
+ stat |= BROKEN
+ playsound(loc, 'sound/effects/glassbr3.ogg', 100, TRUE)
+ set_light(0)
+ update_icon()
/obj/machinery/computer/emp_act(severity)
. = ..()
if (!(. & EMP_PROTECT_SELF))
- if(prob(severity/1.8))
- obj_break("energy")
+ switch(severity)
+ if(1)
+ if(prob(50))
+ obj_break("energy")
+ if(2)
+ if(prob(10))
+ obj_break("energy")
/obj/machinery/computer/deconstruct(disassembled = TRUE, mob/user)
on_deconstruction()
@@ -114,12 +117,14 @@
var/obj/structure/frame/computer/A = new /obj/structure/frame/computer(src.loc)
A.setDir(dir)
A.circuit = circuit
- A.setAnchored(TRUE)
+ // Circuit removal code is handled in /obj/machinery/Exited()
+ circuit.forceMove(A)
+ A.set_anchored(TRUE)
if(stat & BROKEN)
if(user)
to_chat(user, "The broken glass falls out.")
else
- playsound(src, 'sound/effects/hit_on_shattered_glass.ogg', 70, 1)
+ playsound(src, 'sound/effects/hit_on_shattered_glass.ogg', 70, TRUE)
new /obj/item/shard(drop_location())
new /obj/item/shard(drop_location())
A.state = 3
@@ -129,8 +134,11 @@
to_chat(user, "You disconnect the monitor.")
A.state = 4
A.icon_state = "4"
- circuit = null
for(var/obj/C in src)
C.forceMove(loc)
-
qdel(src)
+
+/obj/machinery/computer/AltClick(mob/user)
+ . = ..()
+ if(!user.canUseTopic(src, !issilicon(user)) || !(stat & (NOPOWER|BROKEN)))
+ return
diff --git a/code/game/machinery/computer/arcade.dm b/code/game/machinery/computer/arcade.dm
index 077571b931..dd2ebf287f 100644
--- a/code/game/machinery/computer/arcade.dm
+++ b/code/game/machinery/computer/arcade.dm
@@ -3,14 +3,7 @@
#define ARCADE_WEIGHT_RARE 1
#define ARCADE_RATIO_PLUSH 0.20 // average 1 out of 6 wins is a plush.
-/obj/machinery/computer/arcade
- name = "random arcade"
- desc = "random arcade machine"
- icon_state = "arcade"
- icon_keyboard = null
- icon_screen = "invaders"
- clockwork = TRUE //it'd look weird
- var/list/prizes = list(
+GLOBAL_LIST_INIT(arcade_prize_pool, list(
/obj/item/toy/balloon = ARCADE_WEIGHT_USELESS,
/obj/item/toy/beach_ball = ARCADE_WEIGHT_USELESS,
/obj/item/toy/cattoy = ARCADE_WEIGHT_USELESS,
@@ -70,9 +63,16 @@
/obj/item/clothing/mask/fakemoustache/italian = ARCADE_WEIGHT_RARE,
/obj/item/clothing/suit/hooded/wintercoat/ratvar/fake = ARCADE_WEIGHT_TRICK,
/obj/item/clothing/suit/hooded/wintercoat/narsie/fake = ARCADE_WEIGHT_TRICK
- )
+))
+/obj/machinery/computer/arcade
+ name = "random arcade"
+ desc = "random arcade machine"
+ icon_state = "arcade"
+ icon_keyboard = "no_keyboard"
+ icon_screen = "invaders"
light_color = LIGHT_COLOR_GREEN
+ var/list/prize_override
/obj/machinery/computer/arcade/proc/Reset()
return
@@ -91,44 +91,54 @@
var/obj/machinery/computer/arcade/A = new CB.build_path(loc, CB)
A.setDir(dir)
return INITIALIZE_HINT_QDEL
- //The below object acts as a spawner with a wide array of possible picks, most being uninspired references to past/current player characters.
- //Nevertheless, this keeps its ratio constant with the sum of all the others prizes.
- prizes[/obj/item/toy/plush/random] = counterlist_sum(prizes) * ARCADE_RATIO_PLUSH
+
Reset()
-/obj/machinery/computer/arcade/proc/prizevend(mob/user, list/rarity_classes)
- SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "arcade", /datum/mood_event/arcade)
+/obj/machinery/computer/arcade/proc/prizevend(mob/user, prizes = 1)
+ // if(user.mind?.get_skill_level(/datum/skill/gaming) >= SKILL_LEVEL_LEGENDARY && HAS_TRAIT(user, TRAIT_GAMERGOD))
+ // visible_message("[user] inputs an intense cheat code!",
+ // "You hear a flurry of buttons being pressed.")
+ // say("CODE ACTIVATED: EXTRA PRIZES.")
+ // prizes *= 2
+ for(var/i = 0, i < prizes, i++)
+ SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "arcade", /datum/mood_event/arcade)
+ if(prob(0.0001)) //1 in a million
+ new /obj/item/gun/energy/pulse/prize(src)
+ visible_message("[src] dispenses.. woah, a gun! Way past cool.", "You hear a chime and a shot.")
+ user.client.give_award(/datum/award/achievement/misc/pulse, user)
+ return
- if(prob(1) && prob(1) && prob(1)) //Proper 1 in a million
- new /obj/item/gun/energy/pulse/prize(src)
- SSmedals.UnlockMedal(MEDAL_PULSE, usr.client)
+ var/prizeselect
+ if(prize_override)
+ prizeselect = pickweight(prize_override)
+ else
+ prizeselect = pickweight(GLOB.arcade_prize_pool)
+ var/atom/movable/the_prize = new prizeselect(get_turf(src))
+ playsound(src, 'sound/machines/machine_vend.ogg', 50, TRUE, extrarange = -3)
+ visible_message("[src] dispenses [the_prize]!", "You hear a chime and a clunk.")
- if(!contents.len)
- var/list/toy_raffle
- if(rarity_classes)
- for(var/A in prizes)
- if(prizes[A] in rarity_classes)
- LAZYSET(toy_raffle, A, prizes[A])
- if(!toy_raffle)
- toy_raffle = prizes
- var/prizeselect = pickweight(toy_raffle)
- new prizeselect(src)
-
- var/atom/movable/prize = pick(contents)
- visible_message("[src] dispenses [prize]!", "You hear a chime and a clunk.")
-
- prize.forceMove(get_turf(src))
/obj/machinery/computer/arcade/emp_act(severity)
. = ..()
+ var/override = FALSE
+ if(prize_override)
+ override = TRUE
if(stat & (NOPOWER|BROKEN) || . & EMP_PROTECT_SELF)
return
var/empprize = null
- var/num_of_prizes = rand(round(severity/50),round(severity/100))
+ var/num_of_prizes = 0
+ switch(severity)
+ if(1)
+ num_of_prizes = rand(1,4)
+ if(2)
+ num_of_prizes = rand(0,2)
for(var/i = num_of_prizes; i > 0; i--)
- empprize = pickweight(prizes)
+ if(override)
+ empprize = pickweight(prize_override)
+ else
+ empprize = pickweight(GLOB.arcade_prize_pool)
new empprize(loc)
explosion(loc, -1, 0, 1+num_of_prizes, flame_range = 1+num_of_prizes)
diff --git a/code/game/machinery/computer/arcade/battle.dm b/code/game/machinery/computer/arcade/battle.dm
index 96f224f4cf..8240a22290 100644
--- a/code/game/machinery/computer/arcade/battle.dm
+++ b/code/game/machinery/computer/arcade/battle.dm
@@ -1,130 +1,399 @@
// ** BATTLE ** //
-
-
/obj/machinery/computer/arcade/battle
name = "arcade machine"
desc = "Does not support Pinball."
icon_state = "arcade"
circuit = /obj/item/circuitboard/computer/arcade/battle
- var/enemy_name = "Space Villain"
- var/temp = "Winners don't use space drugs" //Temporary message, for attack messages, etc
- var/player_hp = 30 //Player health/attack points
- var/player_mp = 10
- var/enemy_hp = 45 //Enemy health/attack points
- var/enemy_mp = 20
- var/gameover = FALSE
- var/blocked = FALSE //Player cannot attack/heal while set
- var/turtle = 0
- var/turn_speed = 5 //Measured in deciseconds.
+ var/enemy_name = "Space Villain"
+ ///Enemy health/attack points
+ var/enemy_hp = 100
+ var/enemy_mp = 40
+ ///Temporary message, for attack messages, etc
+ var/temp = "
Winners don't use space drugs"
+ ///the list of passive skill the enemy currently has. the actual passives are added in the enemy_setup() proc
+ var/list/enemy_passive
+ ///if all the enemy's weakpoints have been triggered becomes TRUE
+ var/finishing_move = FALSE
+ ///linked to passives, when it's equal or above the max_passive finishing move will become TRUE
+ var/pissed_off = 0
+ ///the number of passives the enemy will start with
+ var/max_passive = 3
+ ///weapon wielded by the enemy, the shotgun doesn't count.
+ var/chosen_weapon
+
+ ///Player health
+ var/player_hp = 85
+ ///player magic points
+ var/player_mp = 20
+ ///used to remember the last three move of the player before this turn.
+ var/list/last_three_move
+ ///if the enemy or player died. restart the game when TRUE
+ var/gameover = FALSE
+ ///the player cannot make any move while this is set to TRUE. should only TRUE during enemy turns.
+ var/blocked = FALSE
+ ///used to clear the enemy_action proc timer when the game is restarted
+ var/timer_id
+ ///weapon used by the enemy, pure fluff.for certain actions
+ var/list/weapons
+ ///unique to the emag mode, acts as a time limit where the player dies when it reaches 0.
+ var/bomb_cooldown = 19
+
+///creates the enemy base stats for a new round along with the enemy passives
+/obj/machinery/computer/arcade/battle/proc/enemy_setup(player_skill)
+ player_hp = 85
+ player_mp = 20
+ enemy_hp = 100
+ enemy_mp = 40
+ gameover = FALSE
+ blocked = FALSE
+ finishing_move = FALSE
+ pissed_off = 0
+ last_three_move = null
+
+ enemy_passive = list("short_temper" = TRUE, "poisonous" = TRUE, "smart" = TRUE, "shotgun" = TRUE, "magical" = TRUE, "chonker" = TRUE)
+ for(var/i = LAZYLEN(enemy_passive); i > max_passive; i--) //we'll remove passives from the list until we have the number of passive we want
+ var/picked_passive = pick(enemy_passive)
+ LAZYREMOVE(enemy_passive, picked_passive)
+
+ if(LAZYACCESS(enemy_passive, "chonker"))
+ enemy_hp += 20
+
+ if(LAZYACCESS(enemy_passive, "shotgun"))
+ chosen_weapon = "shotgun"
+ else if(weapons)
+ chosen_weapon = pick(weapons)
+ else
+ chosen_weapon = "null gun" //if the weapons list is somehow empty, shouldn't happen but runtimes are sneaky bastards.
+
+ if(player_skill)
+ player_hp += player_skill * 2
/obj/machinery/computer/arcade/battle/Reset()
+ max_passive = 3
var/name_action
var/name_part1
var/name_part2
- name_action = pick("Defeat ", "Annihilate ", "Save ", "Strike ", "Stop ", "Destroy ", "Robust ", "Romance ", "Pwn ", "Own ", "Ban ")
+ if(SSevents.holidays && SSevents.holidays[HALLOWEEN])
+ name_action = pick_list(ARCADE_FILE, "rpg_action_halloween")
+ name_part1 = pick_list(ARCADE_FILE, "rpg_adjective_halloween")
+ name_part2 = pick_list(ARCADE_FILE, "rpg_enemy_halloween")
+ weapons = strings(ARCADE_FILE, "rpg_weapon_halloween")
+ else if(SSevents.holidays && SSevents.holidays[CHRISTMAS])
+ name_action = pick_list(ARCADE_FILE, "rpg_action_xmas")
+ name_part1 = pick_list(ARCADE_FILE, "rpg_adjective_xmas")
+ name_part2 = pick_list(ARCADE_FILE, "rpg_enemy_xmas")
+ weapons = strings(ARCADE_FILE, "rpg_weapon_xmas")
+ else if(SSevents.holidays && SSevents.holidays[VALENTINES])
+ name_action = pick_list(ARCADE_FILE, "rpg_action_valentines")
+ name_part1 = pick_list(ARCADE_FILE, "rpg_adjective_valentines")
+ name_part2 = pick_list(ARCADE_FILE, "rpg_enemy_valentines")
+ weapons = strings(ARCADE_FILE, "rpg_weapon_valentines")
+ else
+ name_action = pick_list(ARCADE_FILE, "rpg_action")
+ name_part1 = pick_list(ARCADE_FILE, "rpg_adjective")
+ name_part2 = pick_list(ARCADE_FILE, "rpg_enemy")
+ weapons = strings(ARCADE_FILE, "rpg_weapon")
- name_part1 = pick("the Automatic ", "Farmer ", "Lord ", "Professor ", "the Cuban ", "the Evil ", "the Dread King ", "the Space ", "Lord ", "the Great ", "Duke ", "General ")
- name_part2 = pick("Melonoid", "Murdertron", "Sorcerer", "Ruin", "Jeff", "Ectoplasm", "Crushulon", "Uhangoid", "Vhakoid", "Peteoid", "slime", "Griefer", "ERPer", "Lizard Man", "Unicorn", "Bloopers")
+ enemy_name = ("The " + name_part1 + " " + name_part2)
+ name = (name_action + " " + enemy_name)
- enemy_name = replacetext((name_part1 + name_part2), "the ", "")
- name = (name_action + name_part1 + name_part2)
+ enemy_setup(0) //in the case it's reset we assume the player skill is 0 because the VOID isn't a gamer
/obj/machinery/computer/arcade/battle/ui_interact(mob/user)
. = ..()
+ screen_setup(user)
+
+///sets up the main screen for the user
+/obj/machinery/computer/arcade/battle/proc/screen_setup(mob/user)
var/dat = "Close"
dat += "[enemy_name]
"
- dat += "
[temp]
"
+ dat += "[temp]"
dat += "
Health: [player_hp] | Magic: [player_mp] | Enemy Health: [enemy_hp]"
if (gameover)
dat += "New Game"
else
- dat += "Attack | "
- dat += "Heal | "
- dat += "Recharge Power"
+ dat += "Light attack"
+ dat += "Defend"
+ dat += "Counter attack"
+ dat += "Power attack"
dat += ""
- var/datum/browser/popup = new(user, "arcade", "Space Villain 2000")
- popup.set_content(dat)
- popup.open()
+ if(user.client) //mainly here to avoid a runtime when the player gets gibbed when losing the emag mode.
+ var/datum/browser/popup = new(user, "arcade", "Space Villain 2000")
+ popup.set_content(dat)
+ popup.open()
/obj/machinery/computer/arcade/battle/Topic(href, href_list)
if(..())
return
+ var/gamerSkill = 0
+ // if(usr?.mind)
+ // gamerSkill = usr.mind.get_skill_level(/datum/skill/gaming)
if (!blocked && !gameover)
+ var/attackamt = rand(5,7) + rand(0, gamerSkill)
+
+ if(finishing_move) //time to bonk that fucker,cuban pete will sometime survive a finishing move.
+ attackamt *= 100
+
+ //light attack suck absolute ass but it doesn't cost any MP so it's pretty good to finish an enemy off
if (href_list["attack"])
- blocked = TRUE
- var/attackamt = rand(2,6)
- temp = "You attack for [attackamt] damage!"
- playsound(loc, 'sound/arcade/hit.ogg', 50, TRUE, extrarange = -3)
- updateUsrDialog()
- if(turtle > 0)
- turtle--
-
- sleep(turn_speed)
+ temp = "
you do quick jab for [attackamt] of damage!
"
enemy_hp -= attackamt
- arcade_action(usr)
+ arcade_action(usr,"attack",attackamt)
- else if (href_list["heal"])
- blocked = TRUE
- var/pointamt = rand(1,3)
- var/healamt = rand(6,8)
- temp = "You use [pointamt] magic to heal for [healamt] damage!"
- playsound(loc, 'sound/arcade/heal.ogg', 50, TRUE, extrarange = -3)
- updateUsrDialog()
- turtle++
+ //defend lets you gain back MP and take less damage from non magical attack.
+ else if(href_list["defend"])
+ temp = "
you take a defensive stance and gain back 10 mp!
"
+ player_mp += 10
+ arcade_action(usr,"defend",attackamt)
+ playsound(src, 'sound/arcade/mana.ogg', 50, TRUE, extrarange = -3)
- sleep(turn_speed)
- player_mp -= pointamt
- player_hp += healamt
- blocked = TRUE
- updateUsrDialog()
- arcade_action(usr)
+ //mainly used to counter short temper and their absurd damage, will deal twice the damage the player took of a non magical attack.
+ else if(href_list["counter_attack"] && player_mp >= 10)
+ temp = "
you prepare yourself to counter the next attack!
"
+ player_mp -= 10
+ arcade_action(usr,"counter_attack",attackamt)
+ playsound(src, 'sound/arcade/mana.ogg', 50, TRUE, extrarange = -3)
- else if (href_list["charge"])
- blocked = TRUE
- var/chargeamt = rand(4,7)
- temp = "You regain [chargeamt] points"
- playsound(loc, 'sound/arcade/mana.ogg', 50, TRUE, extrarange = -3)
- player_mp += chargeamt
- if(turtle > 0)
- turtle--
+ else if(href_list["counter_attack"] && player_mp < 10)
+ temp = "
you don't have the mp necessary to counter attack and defend yourself instead
"
+ player_mp += 10
+ arcade_action(usr,"defend",attackamt)
+ playsound(src, 'sound/arcade/mana.ogg', 50, TRUE, extrarange = -3)
- updateUsrDialog()
- sleep(turn_speed)
- arcade_action(usr)
+ //power attack deals twice the amount of damage but is really expensive MP wise, mainly used with combos to get weakpoints.
+ else if (href_list["power_attack"] && player_mp >= 20)
+ temp = "
You attack [enemy_name] with all your might for [attackamt * 2] damage!
"
+ enemy_hp -= attackamt * 2
+ player_mp -= 20
+ arcade_action(usr,"power_attack",attackamt)
+
+ else if(href_list["power_attack"] && player_mp < 20)
+ temp = "
You don't have the mp necessary for a power attack and settle for a light attack!
"
+ enemy_hp -= attackamt
+ arcade_action(usr,"attack",attackamt)
if (href_list["close"])
usr.unset_machine()
usr << browse(null, "window=arcade")
else if (href_list["newgame"]) //Reset everything
- temp = "New Round"
- player_hp = initial(player_hp)
- player_mp = initial(player_mp)
- enemy_hp = initial(enemy_hp)
- enemy_mp = initial(enemy_mp)
- gameover = FALSE
- turtle = 0
+ temp = "
New Round"
if(obj_flags & EMAGGED)
Reset()
obj_flags &= ~EMAGGED
+ enemy_setup(gamerSkill)
+ screen_setup(usr)
+
+
add_fingerprint(usr)
updateUsrDialog()
return
-/obj/machinery/computer/arcade/battle/proc/arcade_action(mob/user)
- if ((enemy_mp <= 0) || (enemy_hp <= 0))
+///happens after a player action and before the enemy turn. the enemy turn will be cancelled if there's a gameover.
+/obj/machinery/computer/arcade/battle/proc/arcade_action(mob/user,player_stance,attackamt)
+ screen_setup(user)
+ blocked = TRUE
+ if(player_stance == "attack" || player_stance == "power_attack")
+ if(attackamt > 40)
+ playsound(src, 'sound/arcade/boom.ogg', 50, TRUE, extrarange = -3)
+ else
+ playsound(src, 'sound/arcade/hit.ogg', 50, TRUE, extrarange = -3)
+
+ timer_id = addtimer(CALLBACK(src, .proc/enemy_action,player_stance,user),1 SECONDS,TIMER_STOPPABLE)
+ gameover_check(user)
+
+///the enemy turn, the enemy's action entirely depend on their current passive and a teensy tiny bit of randomness
+/obj/machinery/computer/arcade/battle/proc/enemy_action(player_stance,mob/user)
+ var/list/list_temp = list()
+
+ switch(LAZYLEN(last_three_move)) //we keep the last three action of the player in a list here
+ if(0 to 2)
+ LAZYADD(last_three_move, player_stance)
+ if(3)
+ for(var/i in 1 to 2)
+ last_three_move[i] = last_three_move[i + 1]
+ last_three_move[3] = player_stance
+
+ if(4 to INFINITY)
+ last_three_move = null //this shouldn't even happen but we empty the list if it somehow goes above 3
+
+ var/enemy_stance
+ var/attack_amount = rand(8,10) //making the attack amount not vary too much so that it's easier to see if the enemy has a shotgun
+
+ if(player_stance == "defend")
+ attack_amount -= 5
+
+ //if emagged, cuban pete will set up a bomb acting up as a timer. when it reaches 0 the player fucking dies
+ if(obj_flags & EMAGGED)
+ switch(bomb_cooldown--)
+ if(18)
+ list_temp += "
[enemy_name] takes two valve tank and links them together, what's he planning?"
+ if(15)
+ list_temp += "
[enemy_name] adds a remote control to the tan- ho god is that a bomb?"
+ if(12)
+ list_temp += "
[enemy_name] throws the bomb next to you, you'r too scared to pick it up. "
+ if(6)
+ list_temp += "
[enemy_name]'s hand brushes the remote linked to the bomb, your heart skipped a beat. "
+ if(2)
+ list_temp += "
[enemy_name] is going to press the button! It's now or never! "
+ if(0)
+ player_hp -= attack_amount * 1000 //hey it's a maxcap we might as well go all in
+
+ //yeah I used the shotgun as a passive, you know why? because the shotgun gives +5 attack which is pretty good
+ if(LAZYACCESS(enemy_passive, "shotgun"))
+ if(weakpoint_check("shotgun","defend","defend","power_attack"))
+ list_temp += "
You manage to disarm [enemy_name] with a surprise power attack and shoot him with his shotgun until it runs out of ammo! "
+ enemy_hp -= 10
+ chosen_weapon = "empty shotgun"
+ else
+ attack_amount += 5
+
+ //heccing chonker passive, only gives more HP at the start of a new game but has one of the hardest weakpoint to trigger.
+ if(LAZYACCESS(enemy_passive, "chonker"))
+ if(weakpoint_check("chonker","power_attack","power_attack","power_attack"))
+ list_temp += "
After a lot of power attacks you manage to tip over [enemy_name] as they fall over their enormous weight "
+ enemy_hp -= 30
+
+ //smart passive trait, mainly works in tandem with other traits, makes the enemy unable to be counter_attacked
+ if(LAZYACCESS(enemy_passive, "smart"))
+ if(weakpoint_check("smart","defend","defend","attack"))
+ list_temp += "
[enemy_name] is confused by your illogical strategy! "
+ attack_amount -= 5
+
+ else if(attack_amount >= player_hp)
+ player_hp -= attack_amount
+ list_temp += "
[enemy_name] figures out you are really close to death and finishes you off with their [chosen_weapon]!"
+ enemy_stance = "attack"
+
+ else if(player_stance == "counter_attack")
+ list_temp += "
[enemy_name] is not taking your bait. "
+ if(LAZYACCESS(enemy_passive, "short_temper"))
+ list_temp += "However controlling their hatred of you still takes a toll on their mental and physical health!"
+ enemy_hp -= 5
+ enemy_mp -= 5
+ enemy_stance = "defensive"
+
+ //short temper passive trait, gets easily baited into being counter attacked but will bypass your counter when low on HP
+ if(LAZYACCESS(enemy_passive, "short_temper"))
+ if(weakpoint_check("short_temper","counter_attack","counter_attack","counter_attack"))
+ list_temp += "
[enemy_name] is getting frustrated at all your counter attacks and throws a tantrum!"
+ enemy_hp -= attack_amount
+
+ else if(player_stance == "counter_attack")
+ if(!(LAZYACCESS(enemy_passive, "smart")) && enemy_hp > 30)
+ list_temp += "
[enemy_name] took the bait and allowed you to counter attack for [attack_amount * 2] damage!"
+ player_hp -= attack_amount
+ enemy_hp -= attack_amount * 2
+ enemy_stance = "attack"
+
+ else if(enemy_hp <= 30) //will break through the counter when low enough on HP even when smart.
+ list_temp += "
[enemy_name] is getting tired of your tricks and breaks through your counter with their [chosen_weapon]!"
+ player_hp -= attack_amount
+ enemy_stance = "attack"
+
+ else if(!enemy_stance)
+ var/added_temp
+
+ if(rand())
+ added_temp = "you for [attack_amount + 5] damage!"
+ player_hp -= attack_amount + 5
+ enemy_stance = "attack"
+ else
+ added_temp = "the wall, breaking their skull in the process and losing [attack_amount] hp!" //[enemy_name] you have a literal dent in your skull
+ enemy_hp -= attack_amount
+ enemy_stance = "attack"
+
+ list_temp += "
[enemy_name] grits their teeth and charge right into [added_temp]"
+
+ //in the case none of the previous passive triggered, Mainly here to set an enemy stance for passives that needs it like the magical passive.
+ if(!enemy_stance)
+ enemy_stance = pick("attack","defensive")
+ if(enemy_stance == "attack")
+ player_hp -= attack_amount
+ list_temp += "
[enemy_name] attacks you for [attack_amount] points of damage with their [chosen_weapon]"
+ if(player_stance == "counter_attack")
+ enemy_hp -= attack_amount * 2
+ list_temp += "
You counter [enemy_name]'s attack and deal [attack_amount * 2] points of damage!"
+
+ if(enemy_stance == "defensive" && enemy_mp < 15)
+ list_temp += "
[enemy_name] take some time to get some mp back! "
+ enemy_mp += attack_amount
+
+ else if (enemy_stance == "defensive" && enemy_mp >= 15 && !(LAZYACCESS(enemy_passive, "magical")))
+ list_temp += "
[enemy_name] quickly heal themselves for 5 hp! "
+ enemy_mp -= 15
+ enemy_hp += 5
+
+ //magical passive trait, recharges MP nearly every turn it's not blasting you with magic.
+ if(LAZYACCESS(enemy_passive, "magical"))
+ if(player_mp >= 50)
+ list_temp += "
the huge amount of magical energy you have acumulated throws [enemy_name] off balance!"
+ enemy_mp = 0
+ LAZYREMOVE(enemy_passive, "magical")
+ pissed_off++
+
+ else if(LAZYACCESS(enemy_passive, "smart") && player_stance == "counter_attack" && enemy_mp >= 20)
+ list_temp += "
[enemy_name] blasts you with magic from afar for 10 points of damage before you can counter!"
+ player_hp -= 10
+ enemy_mp -= 20
+
+ else if(enemy_hp >= 20 && enemy_mp >= 40 && enemy_stance == "defensive")
+ list_temp += "
[enemy_name] Blasts you with magic from afar!"
+ enemy_mp -= 40
+ player_hp -= 30
+ enemy_stance = "attack"
+
+ else if(enemy_hp < 20 && enemy_mp >= 20 && enemy_stance == "defensive") //it's a pretty expensive spell so they can't spam it that much
+ list_temp += "
[enemy_name] heal themselves with magic and gain back 20 hp!"
+ enemy_hp += 20
+ enemy_mp -= 30
+ else
+ list_temp += "
[enemy_name]'s magical nature lets them get some mp back!"
+ enemy_mp += attack_amount
+
+ //poisonous passive trait, while it's less damage added than the shotgun it acts up even when the enemy doesn't attack at all.
+ if(LAZYACCESS(enemy_passive, "poisonous"))
+ if(weakpoint_check("poisonous","attack","attack","attack"))
+ list_temp += "
your flurry of attack throws back the poisonnous gas at [enemy_name] and makes them choke on it! "
+ enemy_hp -= 5
+ else
+ list_temp += "
the stinky breath of [enemy_name] hurts you for 3 hp! "
+ player_hp -= 3
+
+ //if all passive's weakpoint have been triggered, set finishing_move to TRUE
+ if(pissed_off >= max_passive && !finishing_move)
+ list_temp += "
You have weakened [enemy_name] enough for them to show their weak point, you will do 10 times as much damage with your next attack! "
+ finishing_move = TRUE
+
+ playsound(src, 'sound/arcade/heal.ogg', 50, TRUE, extrarange = -3)
+
+ temp = list_temp.Join()
+ gameover_check(user)
+ screen_setup(user)
+ blocked = FALSE
+
+
+/obj/machinery/computer/arcade/battle/proc/gameover_check(mob/user)
+ var/xp_gained = 0
+ if(enemy_hp <= 0)
if(!gameover)
+ if(timer_id)
+ deltimer(timer_id)
+ timer_id = null
+ if(player_hp <= 0)
+ player_hp = 1 //let's just pretend the enemy didn't kill you so not both the player and enemy look dead.
gameover = TRUE
- temp = "[enemy_name] has fallen! Rejoice!"
- playsound(loc, 'sound/arcade/win.ogg', 50, TRUE, extrarange = -3)
+ blocked = FALSE
+ temp = "
[enemy_name] has fallen! Rejoice!"
+ playsound(loc, 'sound/arcade/win.ogg', 50, TRUE)
if(obj_flags & EMAGGED)
new /obj/effect/spawner/newbomb/timer/syndicate(loc)
@@ -133,78 +402,76 @@
log_game("[key_name(usr)] has outbombed Cuban Pete and been awarded a bomb.")
Reset()
obj_flags &= ~EMAGGED
+ xp_gained += 100
else
prizevend(user)
+ xp_gained += 50
SSblackbox.record_feedback("nested tally", "arcade_results", 1, list("win", (obj_flags & EMAGGED ? "emagged":"normal")))
-
- else if ((obj_flags & EMAGGED) && (turtle >= 4))
- var/boomamt = rand(5,10)
- temp = "[enemy_name] throws a bomb, exploding you for [boomamt] damage!"
- playsound(loc, 'sound/arcade/boom.ogg', 50, TRUE, extrarange = -3)
- player_hp -= boomamt
-
- else if ((enemy_mp <= 5) && (prob(70)))
- var/stealamt = rand(2,3)
- temp = "[enemy_name] steals [stealamt] of your power!"
- playsound(loc, 'sound/arcade/steal.ogg', 50, TRUE, extrarange = -3)
- player_mp -= stealamt
- updateUsrDialog()
-
- if (player_mp <= 0)
- gameover = TRUE
- sleep(turn_speed)
- temp = "You have been drained! GAME OVER"
- playsound(loc, 'sound/arcade/lose.ogg', 50, TRUE, extrarange = -3)
- if(obj_flags & EMAGGED)
- usr.gib()
- SSblackbox.record_feedback("nested tally", "arcade_results", 1, list("loss", "mana", (obj_flags & EMAGGED ? "emagged":"normal")))
-
- else if ((enemy_hp <= 10) && (enemy_mp > 4))
- temp = "[enemy_name] heals for 4 health!"
- playsound(loc, 'sound/arcade/heal.ogg', 50, TRUE, extrarange = -3)
- enemy_hp += 4
- enemy_mp -= 4
-
- else
- var/attackamt = rand(3,6)
- temp = "[enemy_name] attacks for [attackamt] damage!"
- playsound(loc, 'sound/arcade/hit.ogg', 50, TRUE, extrarange = -3)
- player_hp -= attackamt
-
- if ((player_mp <= 0) || (player_hp <= 0))
+ else if(player_hp <= 0)
+ if(timer_id)
+ deltimer(timer_id)
+ timer_id = null
gameover = TRUE
- temp = "You have been crushed! GAME OVER"
- playsound(loc, 'sound/arcade/lose.ogg', 50, TRUE, extrarange = -3)
+ temp = "
You have been crushed! GAME OVER"
+ playsound(loc, 'sound/arcade/lose.ogg', 50, TRUE)
+ xp_gained += 10//pity points
if(obj_flags & EMAGGED)
- usr.gib()
+ var/mob/living/living_user = user
+ if (istype(living_user))
+ living_user.gib()
SSblackbox.record_feedback("nested tally", "arcade_results", 1, list("loss", "hp", (obj_flags & EMAGGED ? "emagged":"normal")))
- blocked = FALSE
- return
+ // if(gameover)
+ // user?.mind?.adjust_experience(/datum/skill/gaming, xp_gained+1)//always gain at least 1 point of XP
+
+
+///used to check if the last three move of the player are the one we want in the right order and if the passive's weakpoint has been triggered yet
+/obj/machinery/computer/arcade/battle/proc/weakpoint_check(passive,first_move,second_move,third_move)
+ if(LAZYLEN(last_three_move) < 3)
+ return FALSE
+
+ if(last_three_move[1] == first_move && last_three_move[2] == second_move && last_three_move[3] == third_move && LAZYACCESS(enemy_passive, passive))
+ LAZYREMOVE(enemy_passive, passive)
+ pissed_off++
+ return TRUE
+ else
+ return FALSE
+
+
+/obj/machinery/computer/arcade/battle/Destroy()
+ enemy_passive = null
+ weapons = null
+ last_three_move = null
+ return ..() //well boys we did it, lists are no more
/obj/machinery/computer/arcade/battle/examine_more(mob/user)
- to_chat(user, "Scribbled on the side of the Arcade Machine you notice some writing...\
- \nmagical -> >=50 power\
- \nsmart -> defend, defend, light attack\
- \nshotgun -> defend, defend, power attack\
- \nshort temper -> counter, counter, counter\
- \npoisonous -> light attack, light attack, light attack\
- \nchonker -> power attack, power attack, power attack")
- return ..()
+ var/list/msg = list("You notice some writing scribbled on the side of [src]...")
+ msg += "\tsmart -> defend, defend, light attack"
+ msg += "\tshotgun -> defend, defend, power attack"
+ msg += "\tshort temper -> counter, counter, counter"
+ msg += "\tpoisonous -> light attack, light attack, light attack"
+ msg += "\tchonker -> power attack, power attack, power attack"
+ return msg
/obj/machinery/computer/arcade/battle/emag_act(mob/user)
. = ..()
if(obj_flags & EMAGGED)
return
+
to_chat(user, "A mesmerizing Rhumba beat starts playing from the arcade machine's speakers!")
- temp = "If you die in the game, you die for real!"
- player_hp = 30
- player_mp = 10
- enemy_hp = 45
- enemy_mp = 20
+ temp = "
If you die in the game, you die for real!"
+ max_passive = 6
+ bomb_cooldown = 18
+ var/gamerSkill = 0
+ // if(usr?.mind)
+ // gamerSkill = usr.mind.get_skill_level(/datum/skill/gaming)
+ enemy_setup(gamerSkill)
+ enemy_hp += 100 //extra HP just to make cuban pete even more bullshit
+ player_hp += 30 //the player will also get a few extra HP in order to have a fucking chance
+
+ screen_setup(user)
gameover = FALSE
- blocked = FALSE
obj_flags |= EMAGGED
diff --git a/code/game/machinery/computer/arcade/minesweeper.dm b/code/game/machinery/computer/arcade/minesweeper.dm
index 3f7ef778b0..dbd9890c81 100644
--- a/code/game/machinery/computer/arcade/minesweeper.dm
+++ b/code/game/machinery/computer/arcade/minesweeper.dm
@@ -268,7 +268,7 @@
visible_message("[src] dispenses [itemname]!", "You hear a chime and a clunk.")
DISABLE_BITFIELD(obj_flags, EMAGGED)
else
- var/dope_prizes = (area >= 480) ? list(ARCADE_WEIGHT_RARE) : (area >= 256) ? list(ARCADE_WEIGHT_RARE, ARCADE_WEIGHT_TRICK) : null
+ var/dope_prizes = (area >= 480) ? 6 : (area >= 256) ? 4 : 2
prizevend(user, dope_prizes)
if(game_status == MINESWEEPER_GAME_WON)
@@ -311,12 +311,12 @@
var/new_rows = input(user, "How many rows do you want? (Minimum: 4, Maximum: 30)", "Minesweeper Rows") as null|num
if(!new_rows || !user.canUseTopic(src, !hasSiliconAccessInArea(user)))
return FALSE
- new_rows = clamp(new_rows + 1, 4, 30)
+ new_rows = clamp(new_rows + 1, 4, 20)
playsound(loc, 'sound/arcade/minesweeper_menuselect.ogg', 50, FALSE, extrarange = -3)
var/new_columns = input(user, "How many columns do you want? (Minimum: 4, Maximum: 50)", "Minesweeper Squares") as null|num
if(!new_columns || !user.canUseTopic(src, !hasSiliconAccessInArea(user)))
return FALSE
- new_columns = clamp(new_columns + 1, 4, 50)
+ new_columns = clamp(new_columns + 1, 4, 30)
playsound(loc, 'sound/arcade/minesweeper_menuselect.ogg', 50, FALSE, extrarange = -3)
var/grid_area = (new_rows - 1) * (new_columns - 1)
var/lower_limit = round(grid_area*0.156)
diff --git a/code/game/machinery/computer/arcade/misc_arcade.dm b/code/game/machinery/computer/arcade/misc_arcade.dm
index 50633192ce..5c887e3726 100644
--- a/code/game/machinery/computer/arcade/misc_arcade.dm
+++ b/code/game/machinery/computer/arcade/misc_arcade.dm
@@ -8,7 +8,7 @@
icon_state = "arcade"
circuit = /obj/item/circuitboard/computer/arcade/amputation
-/obj/machinery/computer/arcade/amputation/on_attack_hand(mob/user, act_intent = user.a_intent, unarmed_attack_flags)
+/obj/machinery/computer/arcade/amputation/on_attack_hand(mob/user)
if(!iscarbon(user))
return
var/mob/living/carbon/c_user = user
@@ -24,8 +24,13 @@
var/obj/item/bodypart/chopchop = c_user.get_bodypart(which_hand)
chopchop.dismember()
qdel(chopchop)
- playsound(loc, 'sound/arcade/win.ogg', 50, TRUE, extrarange = -3)
- for(var/i=1; i<=rand(3,5); i++)
- prizevend(user)
+ // user.mind?.adjust_experience(/datum/skill/gaming, 100)
+ playsound(loc, 'sound/arcade/win.ogg', 50, TRUE)
+ prizevend(user, rand(3,5))
else
to_chat(c_user, "You (wisely) decide against putting your hand in the machine.")
+
+/obj/machinery/computer/arcade/amputation/festive //dispenses wrapped gifts instead of arcade prizes, also known as the ancap christmas tree
+ name = "Mediborg's Festive Amputation Adventure"
+ desc = "A picture of a blood-soaked medical cyborg wearing a Santa hat flashes on the screen. The mediborg has a speech bubble that says, \"Put your hand in the machine if you aren't a coward!\""
+ prize_override = list(/obj/item/a_gift/anything = 1)
diff --git a/code/game/machinery/computer/arcade/orion_trail.dm b/code/game/machinery/computer/arcade/orion_trail.dm
index 441010906c..58617a0cf3 100644
--- a/code/game/machinery/computer/arcade/orion_trail.dm
+++ b/code/game/machinery/computer/arcade/orion_trail.dm
@@ -1,5 +1,3 @@
-
-
// *** THE ORION TRAIL ** //
#define ORION_TRAIL_WINTURN 9
@@ -15,6 +13,8 @@
#define ORION_TRAIL_COLLISION "Collision"
#define ORION_TRAIL_SPACEPORT "Spaceport"
#define ORION_TRAIL_BLACKHOLE "BlackHole"
+#define ORION_TRAIL_OLDSHIP "Old Ship"
+#define ORION_TRAIL_SEARCH "Old Ship Search"
#define ORION_STATUS_START 1
#define ORION_STATUS_NORMAL 2
@@ -44,7 +44,8 @@
ORION_TRAIL_LING = 3,
ORION_TRAIL_MALFUNCTION = 2,
ORION_TRAIL_COLLISION = 1,
- ORION_TRAIL_SPACEPORT = 2
+ ORION_TRAIL_SPACEPORT = 2,
+ ORION_TRAIL_OLDSHIP = 2
)
var/list/stops = list()
var/list/stopblurbs = list()
@@ -55,13 +56,27 @@
var/gameStatus = ORION_STATUS_START
var/canContinueEvent = 0
+ var/obj/item/radio/Radio
+ var/list/gamers = list()
+ var/killed_crew = 0
+
+
+/obj/machinery/computer/arcade/orion_trail/Initialize()
+ . = ..()
+ Radio = new /obj/item/radio(src)
+ Radio.listening = 0
+
+/obj/machinery/computer/arcade/orion_trail/Destroy()
+ QDEL_NULL(Radio)
+ return ..()
+
/obj/machinery/computer/arcade/orion_trail/kobayashi
name = "Kobayashi Maru control computer"
desc = "A test for cadets"
icon = 'icons/obj/machines/particle_accelerator.dmi'
icon_state = "control_boxp"
events = list("Raiders" = 3, "Interstellar Flux" = 1, "Illness" = 3, "Breakdown" = 2, "Malfunction" = 2, "Collision" = 1, "Spaceport" = 2)
- prizes = list(/obj/item/paper/fluff/holodeck/trek_diploma = 1)
+ prize_override = list(/obj/item/paper/fluff/holodeck/trek_diploma = 1)
settlers = list("Kirk","Worf","Gene")
/obj/machinery/computer/arcade/orion_trail/Reset()
@@ -96,14 +111,52 @@
event = null
gameStatus = ORION_STATUS_NORMAL
lings_aboard = 0
+ killed_crew = 0
//spaceport junk
spaceport_raided = 0
spaceport_freebie = 0
last_spaceport_action = ""
-/obj/machinery/computer/arcade/orion_trail/ui_interact(mob/user)
+/obj/machinery/computer/arcade/orion_trail/proc/report_player(mob/gamer)
+ if(gamers[gamer] == -2)
+ return // enough harassing them
+
+ if(gamers[gamer] == -1)
+ say("WARNING: Continued antisocial behavior detected: Dispensing self-help literature.")
+ new /obj/item/paper/pamphlet/violent_video_games(drop_location())
+ gamers[gamer]--
+ return
+
+ if(!(gamer in gamers))
+ gamers[gamer] = 0
+
+ gamers[gamer]++ // How many times the player has 'prestiged' (massacred their crew)
+
+ if(gamers[gamer] > 2 && prob(20 * gamers[gamer]))
+
+ Radio.set_frequency(FREQ_SECURITY)
+ Radio.talk_into(src, "SECURITY ALERT: Crewmember [gamer] recorded displaying antisocial tendencies in [get_area(src)]. Please watch for violent behavior.", FREQ_SECURITY)
+
+ Radio.set_frequency(FREQ_MEDICAL)
+ Radio.talk_into(src, "PSYCH ALERT: Crewmember [gamer] recorded displaying antisocial tendencies in [get_area(src)]. Please schedule psych evaluation.", FREQ_MEDICAL)
+
+ gamers[gamer] = -1
+
+ gamer.client.give_award(/datum/award/achievement/misc/gamer, gamer) // PSYCH REPORT NOTE: patient kept rambling about how they did it for an "achievement", recommend continued holding for observation
+ // gamer.mind?.adjust_experience(/datum/skill/gaming, 50) // cheevos make u better
+
+ if(!isnull(GLOB.data_core.general))
+ for(var/datum/data/record/R in GLOB.data_core.general)
+ if(R.fields["name"] == gamer.name)
+ R.fields["m_stat"] = "*Unstable*"
+ return
+
+/obj/machinery/computer/arcade/orion_trail/ui_interact(mob/_user)
. = ..()
+ if (!isliving(_user))
+ return
+ var/mob/living/user = _user
if(fuel <= 0 || food <=0 || settlers.len == 0)
gameStatus = ORION_STATUS_GAMEOVER
event = null
@@ -136,6 +189,8 @@
desc = "Learn how our ancestors got to Orion, and have fun in the process!"
dat += "
May They Rest In Peace
"
+ // user?.mind?.adjust_experience(/datum/skill/gaming, 10)//learning from your mistakes is the first rule of roguelikes
+
else if(event)
dat = eventdat
else if(gameStatus == ORION_STATUS_NORMAL)
@@ -174,20 +229,32 @@
return
busy = TRUE
+ // var/gamerSkillLevel = 0
+ var/gamerSkill = 0
+ var/gamerSkillRands = 0
+
+ // if(usr?.mind)
+ // gamerSkillLevel = usr.mind.get_skill_level(/datum/skill/gaming)
+ // gamerSkill = usr.mind.get_skill_modifier(/datum/skill/gaming, SKILL_PROBS_MODIFIER)
+ // gamerSkillRands = usr.mind.get_skill_modifier(/datum/skill/gaming, SKILL_RANDS_MODIFIER)
+
+
+ var/xp_gained = 0
if (href_list["continue"]) //Continue your travels
if(gameStatus == ORION_STATUS_NORMAL && !event && turns != 7)
if(turns >= ORION_TRAIL_WINTURN)
win(usr)
+ xp_gained += 34
else
food -= (alive+lings_aboard)*2
fuel -= 5
- if(turns == 2 && prob(30))
+ if(turns == 2 && prob(30-gamerSkill))
event = ORION_TRAIL_COLLISION
event()
- else if(prob(75))
+ else if(prob(75-gamerSkill))
event = pickweight(events)
if(lings_aboard)
- if(event == ORION_TRAIL_LING || prob(55))
+ if(event == ORION_TRAIL_LING || prob(55-gamerSkill))
event = ORION_TRAIL_LING_ATTACK
event()
turns += 1
@@ -195,15 +262,18 @@
var/mob/living/carbon/M = usr //for some vars
switch(event)
if(ORION_TRAIL_RAIDERS)
- if(prob(50))
+ if(prob(50-gamerSkill))
to_chat(usr, "You hear battle shouts. The tramping of boots on cold metal. Screams of agony. The rush of venting air. Are you going insane?")
M.hallucination += 30
else
to_chat(usr, "Something strikes you from behind! It hurts like hell and feel like a blunt weapon, but nothing is there...")
M.take_bodypart_damage(30)
- playsound(loc, 'sound/weapons/genhit2.ogg', 100, 1)
+ playsound(loc, 'sound/weapons/genhit2.ogg', 100, TRUE)
if(ORION_TRAIL_ILLNESS)
- var/severity = rand(1,3) //pray to RNGesus. PRAY, PIGS
+ var/maxSeverity = 3
+ // if(gamerSkillLevel >= SKILL_LEVEL_EXPERT)
+ // maxSeverity = 2 //part of gitting gud is rng mitigation
+ var/severity = rand(1,maxSeverity) //pray to RNGesus. PRAY, PIGS
if(severity == 1)
to_chat(M, "You suddenly feel slightly nauseated." )
if(severity == 2)
@@ -215,16 +285,16 @@
sleep(30)
M.vomit(10, distance = 5)
if(ORION_TRAIL_FLUX)
- if(prob(75))
+ if(prob(75-gamerSkill))
M.DefaultCombatKnockdown(60)
say("A sudden gust of powerful wind slams [M] into the floor!")
M.take_bodypart_damage(25)
- playsound(loc, 'sound/weapons/genhit.ogg', 100, 1)
+ playsound(loc, 'sound/weapons/genhit.ogg', 100, TRUE)
else
to_chat(M, "A violent gale blows past you, and you barely manage to stay standing!")
if(ORION_TRAIL_COLLISION) //by far the most damaging event
- if(prob(90))
- playsound(loc, 'sound/effects/bang.ogg', 100, 1)
+ if(prob(90-gamerSkill))
+ playsound(loc, 'sound/effects/bang.ogg', 100, TRUE)
var/turf/open/floor/F
for(F in orange(1, src))
F.ScrapeAway()
@@ -232,15 +302,15 @@
if(hull)
sleep(10)
say("A new floor suddenly appears around [src]. What the hell?")
- playsound(loc, 'sound/weapons/genhit.ogg', 100, 1)
+ playsound(loc, 'sound/weapons/genhit.ogg', 100, TRUE)
var/turf/open/space/T
for(T in orange(1, src))
T.PlaceOnTop(/turf/open/floor/plating)
else
say("Something slams into the floor around [src] - luckily, it didn't get through!")
- playsound(loc, 'sound/effects/bang.ogg', 50, 1)
+ playsound(loc, 'sound/effects/bang.ogg', 50, TRUE)
if(ORION_TRAIL_MALFUNCTION)
- playsound(loc, 'sound/effects/empulse.ogg', 50, 1)
+ playsound(loc, 'sound/effects/empulse.ogg', 50, TRUE)
visible_message("[src] malfunctions, randomizing in-game stats!")
var/oldfood = food
var/oldfuel = fuel
@@ -254,7 +324,7 @@
audible_message("[src] lets out a somehow ominous chime.")
food = oldfood
fuel = oldfuel
- playsound(loc, 'sound/machines/chime.ogg', 50, 1)
+ playsound(loc, 'sound/machines/chime.ogg', 50, TRUE)
else if(href_list["newgame"]) //Reset everything
if(gameStatus == ORION_STATUS_START)
@@ -266,6 +336,10 @@
food = 80
fuel = 60
settlers = list("Harry","Larry","Bob")
+ else if(href_list["search"]) //search old ship
+ if(event == ORION_TRAIL_OLDSHIP)
+ event = ORION_TRAIL_SEARCH
+ event()
else if(href_list["slow"]) //slow down
if(event == ORION_TRAIL_FLUX)
food -= (alive+lings_aboard)*2
@@ -302,11 +376,11 @@
event = null
else if(href_list["blackhole"]) //keep speed past a black hole
if(turns == 7)
- if(prob(75))
+ if(prob(75-gamerSkill))
event = ORION_TRAIL_BLACKHOLE
event()
if(obj_flags & EMAGGED)
- playsound(loc, 'sound/effects/supermatter.ogg', 100, 1)
+ playsound(loc, 'sound/effects/supermatter.ogg', 100, TRUE)
say("A miniature black hole suddenly appears in front of [src], devouring [usr] alive!")
if(isliving(usr))
var/mob/living/L = usr
@@ -328,22 +402,29 @@
else if(href_list["killcrew"]) //shoot a crewmember
if(gameStatus == ORION_STATUS_NORMAL || event == ORION_TRAIL_LING)
var/sheriff = remove_crewmember() //I shot the sheriff
- playsound(loc,'sound/weapons/gunshot.ogg', 100, 1)
+ playsound(loc,'sound/weapons/gunshot.ogg', 100, TRUE)
+ killed_crew++
+
+ var/mob/living/user = usr
if(settlers.len == 0 || alive == 0)
say("The last crewmember [sheriff], shot themselves, GAME OVER!")
if(obj_flags & EMAGGED)
- usr.death(0)
- obj_flags &= EMAGGED
+ user.death(FALSE)
gameStatus = ORION_STATUS_GAMEOVER
event = null
+
+ if(killed_crew >= 4)
+ xp_gained -= 15//no cheating by spamming game overs
+ report_player(usr)
else if(obj_flags & EMAGGED)
if(usr.name == sheriff)
say("The crew of the ship chose to kill [usr.name]!")
- usr.death(0)
+ user.death(FALSE)
if(event == ORION_TRAIL_LING) //only ends the ORION_TRAIL_LING event, since you can do this action in multiple places
event = null
+ killed_crew-- // the kill was valid
//Spaceport specific interactions
//they get a header because most of them don't reset event (because it's a shop, you leave when you want to)
@@ -356,6 +437,7 @@
fuel -= 10
food -= 10
event()
+ killed_crew-- // I mean not really but you know
else if(href_list["sellcrew"]) //sell a crewmember
if(gameStatus == ORION_STATUS_MARKET)
@@ -377,15 +459,16 @@
else if(href_list["raid_spaceport"])
if(gameStatus == ORION_STATUS_MARKET)
if(!spaceport_raided)
- var/success = min(15 * alive,100) //default crew (4) have a 60% chance
+ var/success = min(15 * alive + gamerSkill,100) //default crew (4) have a 60% chance
spaceport_raided = 1
var/FU = 0
var/FO = 0
if(prob(success))
- FU = rand(5,15)
- FO = rand(5,15)
+ FU = rand(5 + gamerSkillRands,15 + gamerSkillRands)
+ FO = rand(5 + gamerSkillRands,15 + gamerSkillRands)
last_spaceport_action = "You successfully raided the spaceport! You gained [FU] Fuel and [FO] Food! (+[FU]FU,+[FO]FO)"
+ xp_gained += 10
else
FU = rand(-5,-15)
FO = rand(-5,-15)
@@ -444,8 +527,7 @@
add_fingerprint(usr)
updateUsrDialog()
busy = FALSE
- return
-
+ // usr?.mind?.adjust_experience(/datum/skill/gaming, xp_gained+1)
/obj/machinery/computer/arcade/orion_trail/proc/event()
eventdat = "[event]
"
@@ -474,6 +556,38 @@
eventdat += "Slow Down Continue
"
eventdat += "Close
"
+ if(ORION_TRAIL_OLDSHIP)
+ eventdat += "
Your crew spots an old ship floating through space. It might have some supplies, but then again it looks rather unsafe."
+ eventdat += "Search itLeave it
Close
"
+ canContinueEvent = 1
+
+ if(ORION_TRAIL_SEARCH)
+ switch(rand(100))
+ if(0 to 15)
+ var/rescued = add_crewmember()
+ var/oldfood = rand(1,7)
+ var/oldfuel = rand(4,10)
+ food += oldfood
+ fuel += oldfuel
+ eventdat += "
As you look through it you find some supplies and a living person!"
+ eventdat += "
[rescued] was rescued from the abandoned ship!"
+ eventdat += "
You found [oldfood] Food and [oldfuel] Fuel."
+ if(15 to 35)
+ var/lfuel = rand(4,7)
+ var/deadname = remove_crewmember()
+ fuel -= lfuel
+ eventdat += "
[deadname] was lost deep in the wreckage, and your own vessel lost [lfuel] Fuel maneuvering to the the abandoned ship."
+ if(35 to 65)
+ var/oldfood = rand(5,11)
+ food += oldfood
+ engine++
+ eventdat += "
You found [oldfood] Food and some parts amongst the wreck."
+ else
+ eventdat += "
As you look through the wreck you cannot find much of use."
+ eventdat += "Continue
"
+ eventdat += "Close
"
+ canContinueEvent = 1
+
if(ORION_TRAIL_ILLNESS)
eventdat += "A deadly illness has been contracted!"
var/deadname = remove_crewmember()
@@ -687,7 +801,7 @@
//Add Random/Specific crewmember
-/obj/machinery/computer/arcade/orion_trail/proc/add_crewmember(var/specific = "")
+/obj/machinery/computer/arcade/orion_trail/proc/add_crewmember(specific = "")
var/newcrew = ""
if(specific)
newcrew = specific
@@ -703,7 +817,7 @@
//Remove Random/Specific crewmember
-/obj/machinery/computer/arcade/orion_trail/proc/remove_crewmember(var/specific = "", var/dont_remove = "")
+/obj/machinery/computer/arcade/orion_trail/proc/remove_crewmember(specific = "", dont_remove = "")
var/list/safe2remove = settlers
var/removed = ""
if(dont_remove)
@@ -779,14 +893,14 @@
to_chat(user, "You flip the switch on the underside of [src].")
active = 1
visible_message("[src] softly beeps and whirs to life!")
- playsound(loc, 'sound/machines/defib_SaftyOn.ogg', 25, 1)
+ playsound(loc, 'sound/machines/defib_SaftyOn.ogg', 25, TRUE)
say("This is ship ID #[rand(1,1000)] to Orion Port Authority. We're coming in for landing, over.")
sleep(20)
visible_message("[src] begins to vibrate...")
say("Uh, Port? Having some issues with our reactor, could you check it out? Over.")
sleep(30)
say("Oh, God! Code Eight! CODE EIGHT! IT'S GONNA BL-")
- playsound(loc, 'sound/machines/buzz-sigh.ogg', 25, 1)
+ playsound(loc, 'sound/machines/buzz-sigh.ogg', 25, TRUE)
sleep(3.6)
visible_message("[src] explodes!")
explosion(loc, 2,4,8, flame_range = 16)
diff --git a/code/game/machinery/computer/atmos_control.dm b/code/game/machinery/computer/atmos_control.dm
index 75a181b922..98d8811948 100644
--- a/code/game/machinery/computer/atmos_control.dm
+++ b/code/game/machinery/computer/atmos_control.dm
@@ -92,6 +92,7 @@ GLOBAL_LIST_EMPTY(atmos_air_controllers)
icon_screen = "tank"
icon_keyboard = "atmos_key"
circuit = /obj/item/circuitboard/computer/atmos_control
+ light_color = LIGHT_COLOR_CYAN
var/frequency = FREQ_ATMOS_STORAGE
var/list/sensors = list(
@@ -102,6 +103,20 @@ GLOBAL_LIST_EMPTY(atmos_air_controllers)
ATMOS_GAS_MONITOR_SENSOR_N2O = "Nitrous Oxide Tank",
ATMOS_GAS_MONITOR_SENSOR_AIR = "Mixed Air Tank",
ATMOS_GAS_MONITOR_SENSOR_MIX = "Mix Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_BZ = "BZ Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_FREON = "Freon Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_HALON = "Halon Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_HEALIUM = "Healium Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_H2 = "Hydrogen Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_HYPERNOBLIUM = "Hypernoblium Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_MIASMA = "Miasma Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_NO2 = "Nitryl Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_PLUOXIUM = "Pluoxium Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_PROTO_NITRATE = "Proto-Nitrate Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_STIMULUM = "Stimulum Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_TRITIUM = "Tritium Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_H2O = "Water Vapor Tank",
+ // ATMOS_GAS_MONITOR_SENSOR_ZAUKER = "Zauker Tank",
ATMOS_GAS_MONITOR_LOOP_DISTRIBUTION = "Distribution Loop",
ATMOS_GAS_MONITOR_LOOP_ATMOS_WASTE = "Atmos Waste Loop",
ATMOS_GAS_MONITOR_SENSOR_INCINERATOR = "Incinerator Chamber",
@@ -110,7 +125,6 @@ GLOBAL_LIST_EMPTY(atmos_air_controllers)
var/list/sensor_information = list()
var/datum/radio_frequency/radio_connection
- light_color = LIGHT_COLOR_CYAN
/obj/machinery/computer/atmos_control/Initialize()
. = ..()
@@ -165,15 +179,10 @@ GLOBAL_LIST_EMPTY(atmos_air_controllers)
/obj/machinery/computer/atmos_control/incinerator
name = "Incinerator Air Control"
sensors = list(ATMOS_GAS_MONITOR_SENSOR_INCINERATOR = "Incinerator Chamber")
- ui_x = 400
- ui_y = 300
-
//Toxins mix sensor only
/obj/machinery/computer/atmos_control/toxinsmix
name = "Toxins Mixing Air Control"
sensors = list(ATMOS_GAS_MONITOR_SENSOR_TOXINS_LAB = "Toxins Mixing Chamber")
- ui_x = 400
- ui_y = 300
/////////////////////////////////////////////////////////////
// LARGE TANK CONTROL
@@ -184,13 +193,9 @@ GLOBAL_LIST_EMPTY(atmos_air_controllers)
var/output_tag
frequency = FREQ_ATMOS_STORAGE
circuit = /obj/item/circuitboard/computer/atmos_control/tank
-
var/list/input_info
var/list/output_info
- ui_x = 500
- ui_y = 315
-
/obj/machinery/computer/atmos_control/tank/oxygen_tank
name = "Oxygen Supply Control"
input_tag = ATMOS_GAS_MONITOR_INPUT_O2
@@ -236,7 +241,7 @@ GLOBAL_LIST_EMPTY(atmos_air_controllers)
// This hacky madness is the evidence of the fact that a lot of machines were never meant to be constructable, im so sorry you had to see this
/obj/machinery/computer/atmos_control/tank/proc/reconnect(mob/user)
var/list/IO = list()
- var/datum/radio_frequency/freq = SSradio.return_frequency(FREQ_ATMOS_STORAGE)
+ var/datum/radio_frequency/freq = SSradio.return_frequency(frequency)
var/list/devices = freq.devices["_default"]
for(var/obj/machinery/atmospherics/components/unary/vent_pump/U in devices)
var/list/text = splittext(U.id_tag, "_")
@@ -252,10 +257,11 @@ GLOBAL_LIST_EMPTY(atmos_air_controllers)
src.output_tag = "[S]_out"
name = "[uppertext(S)] Supply Control"
var/list/new_devices = freq.devices["4"]
+ sensors.Cut()
for(var/obj/machinery/air_sensor/U in new_devices)
var/list/text = splittext(U.id_tag, "_")
if(text[1] == S)
- sensors = list("[S]_sensor" = "Tank")
+ sensors = list("[S]_sensor" = "[S] Tank")
break
for(var/obj/machinery/atmospherics/components/unary/outlet_injector/U in devices)
@@ -268,13 +274,16 @@ GLOBAL_LIST_EMPTY(atmos_air_controllers)
data["tank"] = TRUE
data["inputting"] = input_info ? input_info["power"] : FALSE
data["inputRate"] = input_info ? input_info["volume_rate"] : 0
+ data["maxInputRate"] = input_info ? MAX_TRANSFER_RATE : 0
data["outputting"] = output_info ? output_info["power"] : FALSE
data["outputPressure"] = output_info ? output_info["internal"] : 0
-
+ data["maxOutputPressure"] = output_info ? MAX_OUTPUT_PRESSURE : 0
return data
/obj/machinery/computer/atmos_control/tank/ui_act(action, params)
- if(..() || !radio_connection)
+ . = ..()
+
+ if(. || !radio_connection)
return
var/datum/signal/signal = new(list("sigtype" = "command", "user" = usr))
switch(action)
diff --git a/code/game/machinery/computer/camera.dm b/code/game/machinery/computer/camera.dm
index 03a5a17493..a62d2cd102 100644
--- a/code/game/machinery/computer/camera.dm
+++ b/code/game/machinery/computer/camera.dm
@@ -1,3 +1,5 @@
+#define DEFAULT_MAP_SIZE 15
+
/obj/machinery/computer/security
name = "security camera console"
desc = "Used to access the various cameras on the station."
@@ -8,15 +10,19 @@
var/list/network = list("ss13")
var/obj/machinery/camera/active_camera
+ /// The turf where the camera was last updated.
+ var/turf/last_camera_turf
var/list/concurrent_users = list()
// Stuff needed to render the map
var/map_name
- var/const/default_map_size = 15
- var/obj/screen/cam_screen
- var/obj/screen/plane_master/lighting/cam_plane_master
+ var/obj/screen/map_view/cam_screen
+ /// All the plane masters that need to be applied.
+ var/list/cam_plane_masters
var/obj/screen/background/cam_background
+ interaction_flags_machine = INTERACT_MACHINE_ALLOW_SILICON | INTERACT_MACHINE_SET_MACHINE //| INTERACT_MACHINE_REQUIRES_SIGHT
+
/obj/machinery/computer/security/Initialize()
. = ..()
// Map name has to start and end with an A-Z character,
@@ -33,18 +39,20 @@
cam_screen.assigned_map = map_name
cam_screen.del_on_map_removal = FALSE
cam_screen.screen_loc = "[map_name]:1,1"
- cam_plane_master = new
- cam_plane_master.name = "plane_master"
- cam_plane_master.assigned_map = map_name
- cam_plane_master.del_on_map_removal = FALSE
- cam_plane_master.screen_loc = "[map_name]:CENTER"
+ cam_plane_masters = list()
+ for(var/plane in subtypesof(/obj/screen/plane_master))
+ var/obj/screen/instance = new plane()
+ instance.assigned_map = map_name
+ instance.del_on_map_removal = FALSE
+ instance.screen_loc = "[map_name]:CENTER"
+ cam_plane_masters += instance
cam_background = new
cam_background.assigned_map = map_name
cam_background.del_on_map_removal = FALSE
/obj/machinery/computer/security/Destroy()
qdel(cam_screen)
- qdel(cam_plane_master)
+ QDEL_LIST(cam_plane_masters)
qdel(cam_background)
return ..()
@@ -56,9 +64,10 @@
/obj/machinery/computer/security/ui_interact(mob/user, datum/tgui/ui)
// Update UI
ui = SStgui.try_update_ui(user, src, ui)
- // Show static if can't use the camera
- if(!active_camera?.can_use())
- show_camera_static()
+
+ // Update the camera, showing static if necessary and updating data if the location has moved.
+ update_active_camera_screen()
+
if(!ui)
var/user_ref = REF(user)
var/is_living = isliving(user)
@@ -72,7 +81,7 @@
use_power(active_power_usage)
// Register map objects
user.client.register_map_obj(cam_screen)
- for(var/plane in cam_plane_master)
+ for(var/plane in cam_plane_masters)
user.client.register_map_obj(plane)
user.client.register_map_obj(cam_background)
// Open UI
@@ -100,6 +109,7 @@
data["cameras"] += list(list(
name = C.c_tag,
))
+
return data
/obj/machinery/computer/security/ui_act(action, params)
@@ -110,31 +120,51 @@
if(action == "switch_camera")
var/c_tag = params["name"]
var/list/cameras = get_available_cameras()
- var/obj/machinery/camera/C = cameras[c_tag]
- active_camera = C
+ var/obj/machinery/camera/selected_camera = cameras[c_tag]
+ active_camera = selected_camera
playsound(src, get_sfx("terminal_type"), 25, FALSE)
- // Show static if can't use the camera
- if(!active_camera?.can_use())
- show_camera_static()
+ if(!selected_camera)
return TRUE
- var/list/visible_turfs = list()
- for(var/turf/T in (C.isXRay() \
- ? range(C.view_range, C) \
- : view(C.view_range, C)))
- visible_turfs += T
-
- var/list/bbox = get_bbox_of_atoms(visible_turfs)
- var/size_x = bbox[3] - bbox[1] + 1
- var/size_y = bbox[4] - bbox[2] + 1
-
- cam_screen.vis_contents = visible_turfs
- cam_background.icon_state = "clear"
- cam_background.fill_rect(1, 1, size_x, size_y)
+ update_active_camera_screen()
return TRUE
+/obj/machinery/computer/security/proc/update_active_camera_screen()
+ // Show static if can't use the camera
+ if(!active_camera?.can_use())
+ show_camera_static()
+ return
+
+ var/list/visible_turfs = list()
+
+ // Is this camera located in or attached to a living thing? If so, assume the camera's loc is the living thing.
+ var/cam_location = isliving(active_camera.loc) ? active_camera.loc : active_camera
+
+ // If we're not forcing an update for some reason and the cameras are in the same location,
+ // we don't need to update anything.
+ // Most security cameras will end here as they're not moving.
+ var/newturf = get_turf(cam_location)
+ if(last_camera_turf == newturf)
+ return
+
+ // Cameras that get here are moving, and are likely attached to some moving atom such as cyborgs.
+ last_camera_turf = get_turf(cam_location)
+
+ var/list/visible_things = active_camera.isXRay() ? range(active_camera.view_range, cam_location) : view(active_camera.view_range, cam_location)
+
+ for(var/turf/visible_turf in visible_things)
+ visible_turfs += visible_turf
+
+ var/list/bbox = get_bbox_of_atoms(visible_turfs)
+ var/size_x = bbox[3] - bbox[1] + 1
+ var/size_y = bbox[4] - bbox[2] + 1
+
+ cam_screen.vis_contents = visible_turfs
+ cam_background.icon_state = "clear"
+ cam_background.fill_rect(1, 1, size_x, size_y)
+
/obj/machinery/computer/security/ui_close(mob/user)
var/user_ref = REF(user)
var/is_living = isliving(user)
@@ -151,7 +181,7 @@
/obj/machinery/computer/security/proc/show_camera_static()
cam_screen.vis_contents.Cut()
cam_background.icon_state = "scanline2"
- cam_background.fill_rect(1, 1, default_map_size, default_map_size)
+ cam_background.fill_rect(1, 1, DEFAULT_MAP_SIZE, DEFAULT_MAP_SIZE)
// Returns the list of cameras accessible from this computer
/obj/machinery/computer/security/proc/get_available_cameras()
@@ -179,7 +209,7 @@
name = "security camera monitor"
desc = "An old TV hooked into the station's camera network."
icon_state = "television"
- icon_keyboard = null
+ icon_keyboard = "no_keyboard"
icon_screen = "detective_tv"
pass_flags = PASSTABLE
@@ -211,7 +241,7 @@
/obj/machinery/computer/security/qm
name = "\improper Quartermaster's camera console"
- desc = "A console with access to the mining, auxillary base and vault camera networks."
+ desc = "A console with access to the mining, auxiliary base and vault camera networks."
network = list("mine", "auxbase", "vault")
circuit = null
@@ -222,17 +252,12 @@
desc = "Used for watching an empty arena."
icon = 'icons/obj/stationobjs.dmi'
icon_state = "telescreen"
+ layer = SIGN_LAYER
network = list("thunder")
density = FALSE
circuit = null
light_power = 0
-/obj/machinery/computer/security/telescreen/Initialize()
- . = ..()
- var/turf/T = get_turf_pixel(src)
- if(iswallturf(T))
- plane = ABOVE_WALL_PLANE
-
/obj/machinery/computer/security/telescreen/update_icon_state()
icon_state = initial(icon_state)
if(stat & BROKEN)
@@ -246,21 +271,19 @@
network = list("thunder")
density = FALSE
circuit = null
- //interaction_flags_atom = NONE // interact() is called by BigClick()
+ interaction_flags_atom = NONE // interact() is called by BigClick()
var/icon_state_off = "entertainment_blank"
var/icon_state_on = "entertainment"
-/* If someone would like to try to get this long-distance viewing thing working, be my guest. I tried everything I could possibly think of and it just refused to operate correctly.
-
/obj/machinery/computer/security/telescreen/entertainment/Initialize()
. = ..()
RegisterSignal(src, COMSIG_CLICK, .proc/BigClick)
// Bypass clickchain to allow humans to use the telescreen from a distance
/obj/machinery/computer/security/telescreen/entertainment/proc/BigClick()
- interact(usr)
+ SIGNAL_HANDLER
-*/
+ INVOKE_ASYNC(src, /atom.proc/interact, usr)
/obj/machinery/computer/security/telescreen/entertainment/proc/notify(on)
if(on && icon_state == icon_state_off)
@@ -279,8 +302,8 @@
network = list("rd", "aicore", "aiupload", "minisat", "xeno", "test")
/obj/machinery/computer/security/telescreen/circuitry
- name = "circuitry telescreen"
- desc = "Used for watching the other eggheads from the safety of the circuitry lab."
+ name = "research telescreen"
+ desc = "A telescreen with access to the research division's camera network."
network = list("rd")
/obj/machinery/computer/security/telescreen/ce
@@ -324,8 +347,8 @@
network = list("prison")
/obj/machinery/computer/security/telescreen/auxbase
- name = "auxillary base monitor"
- desc = "A telescreen that connects to the auxillary base's camera."
+ name = "auxiliary base monitor"
+ desc = "A telescreen that connects to the auxiliary base's camera."
network = list("auxbase")
/obj/machinery/computer/security/telescreen/minisat
@@ -346,3 +369,5 @@
for(var/i in network)
network -= i
network += "[idnum][i]"
+
+#undef DEFAULT_MAP_SIZE
diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm
index 3ef887b156..ff65a6e159 100755
--- a/code/game/machinery/computer/communications.dm
+++ b/code/game/machinery/computer/communications.dm
@@ -1,14 +1,10 @@
-#define STATE_DEFAULT 1
-#define STATE_CALLSHUTTLE 2
-#define STATE_CANCELSHUTTLE 3
-#define STATE_MESSAGELIST 4
-#define STATE_VIEWMESSAGE 5
-#define STATE_DELMESSAGE 6
-#define STATE_STATUSDISPLAY 7
-#define STATE_ALERT_LEVEL 8
-#define STATE_CONFIRM_LEVEL 9
-#define STATE_TOGGLE_EMERGENCY 10
-#define STATE_PURCHASE 11
+#define IMPORTANT_ACTION_COOLDOWN (60 SECONDS)
+#define MAX_STATUS_LINE_LENGTH 40
+
+#define STATE_BUYING_SHUTTLE "buying_shuttle"
+#define STATE_CHANGING_STATUS "changing_status"
+#define STATE_MAIN "main"
+#define STATE_MESSAGES "messages"
// The communications computer
/obj/machinery/computer/communications
@@ -19,423 +15,53 @@
req_access = list(ACCESS_HEADS)
circuit = /obj/item/circuitboard/computer/communications
light_color = LIGHT_COLOR_BLUE
- var/auth_id = "Unknown" //Who is currently logged in?
- var/list/datum/comm_message/messages = list()
- var/datum/comm_message/currmsg
- var/datum/comm_message/aicurrmsg
- var/state = STATE_DEFAULT
- var/aistate = STATE_DEFAULT
- var/message_cooldown = 0
- var/ai_message_cooldown = 0
- var/tmp_alertlevel = 0
- var/static/security_level_cd // used to stop mass spam.
- var/stat_msg1
- var/stat_msg2
+ /// Cooldown for important actions, such as messaging CentCom or other sectors
+ COOLDOWN_DECLARE(static/important_action_cooldown)
+ /// The current state of the UI
+ var/state = STATE_MAIN
-/obj/machinery/computer/communications/proc/checkCCcooldown()
- var/obj/item/circuitboard/computer/communications/CM = circuit
- if(CM.lastTimeUsed + 600 > world.time)
- return FALSE
- return TRUE
+ /// The current state of the UI for AIs
+ var/cyborg_state = STATE_MAIN
+
+ /// The name of the user who logged in
+ var/authorize_name
+
+ /// The access that the card had on login
+ var/list/authorize_access
+
+ /// The messages this console has been sent
+ var/list/datum/comm_message/messages
+
+ /// How many times the alert level has been changed
+ /// Used to clear the modal to change alert level
+ var/alert_level_tick = 0
+
+ /// The last lines used for changing the status display
+ var/static/last_status_display
/obj/machinery/computer/communications/Initialize()
. = ..()
GLOB.shuttle_caller_list += src
-/obj/machinery/computer/communications/Topic(href, href_list)
- if(..())
- return
- if(!usr.canUseTopic(src, !issilicon(usr)))
- return
- if(!is_station_level(z) && !is_reserved_level(z)) //Can only use in transit and on SS13
- to_chat(usr, "Unable to establish a connection: \black You're too far away from the station!")
- return
- usr.set_machine(src)
+/// Are we NOT a silicon, AND we're logged in as the captain?
+/obj/machinery/computer/communications/proc/authenticated_as_non_silicon_captain(mob/user)
+ if (issilicon(user))
+ return FALSE
+ return ACCESS_CAPTAIN in authorize_access
+/// Are we a silicon, OR we're logged in as the captain?
+/obj/machinery/computer/communications/proc/authenticated_as_silicon_or_captain(mob/user)
+ if (issilicon(user))
+ return TRUE
+ return ACCESS_CAPTAIN in authorize_access
- if(!href_list["operation"])
- return
- var/obj/item/circuitboard/computer/communications/CM = circuit
- switch(href_list["operation"])
- // main interface
- if("main")
- state = STATE_DEFAULT
- playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, FALSE)
- if("login")
- var/mob/M = usr
-
- var/obj/item/card/id/I = M.get_idcard(TRUE)
-
- if(I && istype(I))
- if(check_access(I))
- authenticated = 1
- auth_id = "[I.registered_name] ([I.assignment])"
- if((ACCESS_CAPTAIN in I.access))
- authenticated = 2
- playsound(src, 'sound/machines/terminal_on.ogg', 50, FALSE)
- if(obj_flags & EMAGGED)
- authenticated = 2
- auth_id = "Unknown"
- to_chat(M, "[src] lets out a quiet alarm as its login is overridden.")
- playsound(src, 'sound/machines/terminal_on.ogg', 50, FALSE)
- playsound(src, 'sound/machines/terminal_alert.ogg', 25, FALSE)
- if(prob(25))
- for(var/mob/living/silicon/ai/AI in active_ais())
- SEND_SOUND(AI, sound('sound/machines/terminal_alert.ogg', volume = 10)) //Very quiet for balance reasons
- if("logout")
- authenticated = 0
- playsound(src, 'sound/machines/terminal_off.ogg', 50, FALSE)
-
- if("swipeidseclevel")
- var/mob/M = usr
- var/obj/item/card/id/I = M.get_active_held_item()
- if (istype(I, /obj/item/pda))
- var/obj/item/pda/pda = I
- I = pda.id
- if (I && istype(I))
- if(ACCESS_CAPTAIN in I.access)
- if(security_level_cd > world.time)
- to_chat(usr, "Security level protocols are currently on cooldown. Please stand by.")
- return
- var/old_level = GLOB.security_level
- if(!tmp_alertlevel)
- tmp_alertlevel = SEC_LEVEL_GREEN
- if(tmp_alertlevel < SEC_LEVEL_GREEN)
- tmp_alertlevel = SEC_LEVEL_GREEN
- if(tmp_alertlevel > SEC_LEVEL_AMBER)
- tmp_alertlevel = SEC_LEVEL_AMBER //Cannot engage delta with this
- set_security_level(tmp_alertlevel)
- security_level_cd = world.time + 15 SECONDS
- if(GLOB.security_level != old_level)
- to_chat(usr, "Authorization confirmed. Modifying security level.")
- playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
- //Only notify people if an actual change happened
- var/security_level = NUM2SECLEVEL(GLOB.security_level)
- log_game("[key_name(usr)] has changed the security level to [security_level] with [src] at [AREACOORD(usr)].")
- message_admins("[ADMIN_LOOKUPFLW(usr)] has changed the security level to [security_level] with [src] at [AREACOORD(usr)].")
- deadchat_broadcast("[usr.real_name] has changed the security level to [security_level] with [src] at [get_area_name(usr, TRUE)].", usr)
- tmp_alertlevel = 0
- else
- to_chat(usr, "You are not authorized to do this!")
- playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, FALSE)
- tmp_alertlevel = 0
- state = STATE_DEFAULT
- else
- to_chat(usr, "You need to swipe your ID!")
- playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, FALSE)
-
- if("announce")
- if(authenticated==2)
- playsound(src, 'sound/machines/terminal_prompt.ogg', 50, FALSE)
- make_announcement(usr)
-
- if("crossserver")
- if(authenticated==2)
- var/dest = href_list["cross_dest"]
- if(!checkCCcooldown())
- to_chat(usr, "Arrays recycling. Please stand by.")
- playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, FALSE)
- return
- var/warning = dest == "all" ? "Please choose a message to transmit to allied stations." : "Please choose a message to transmit to [dest] sector station."
- var/input = stripped_multiline_input(usr, "[warning] Please be aware that this process is very expensive, and abuse will lead to... termination.", "Send a message to an allied station.", "")
- if(!input || !(usr in view(1,src)) || !checkCCcooldown())
- return
- playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
- if(dest == "all")
- send2otherserver("[station_name()]", input,"Comms_Console")
- else
- send2otherserver("[station_name()]", input,"Comms_Console", list(dest))
- minor_announce(input, title = "Outgoing message to allied station")
- usr.log_talk(input, LOG_SAY, tag="message to the other server")
- message_admins("[ADMIN_LOOKUPFLW(usr)] has sent a message to the other server.")
- deadchat_broadcast("[usr.real_name] has sent an outgoing message to the other station(s).", usr)
- CM.lastTimeUsed = world.time
-
- if("purchase_menu")
- state = STATE_PURCHASE
-
- if("buyshuttle")
- if(authenticated==2)
- var/list/shuttles = flatten_list(SSmapping.shuttle_templates)
- var/datum/map_template/shuttle/S = locate(href_list["chosen_shuttle"]) in shuttles
- if(S && istype(S))
- if(SSshuttle.emergency.mode != SHUTTLE_RECALL && SSshuttle.emergency.mode != SHUTTLE_IDLE)
- to_chat(usr, "It's a bit late to buy a new shuttle, don't you think?")
- return
- if(SSshuttle.shuttle_purchased)
- to_chat(usr, "A replacement shuttle has already been purchased.")
- else if(!S.prerequisites_met())
- to_chat(usr, "You have not met the requirements for purchasing this shuttle.")
- else
- var/points_to_check
- var/datum/bank_account/D = SSeconomy.get_dep_account(ACCOUNT_CAR)
- if(D)
- points_to_check = D.account_balance
- if(points_to_check >= S.credit_cost)
- SSshuttle.shuttle_purchased = TRUE
- SSshuttle.unload_preview()
- SSshuttle.load_template(S)
- SSshuttle.existing_shuttle = SSshuttle.emergency
- SSshuttle.action_load(S)
- D.adjust_money(-S.credit_cost)
- minor_announce("[usr.real_name] has purchased [S.name] for [S.credit_cost] credits." , "Shuttle Purchase")
- message_admins("[ADMIN_LOOKUPFLW(usr)] purchased [S.name].")
- log_shuttle("[key_name(usr)] has purchased [S.name].")
- SSblackbox.record_feedback("text", "shuttle_purchase", 1, "[S.name]")
- else
- to_chat(usr, "Insufficient credits.")
-
- if("callshuttle")
- state = STATE_DEFAULT
- if(authenticated && SSshuttle.canEvac(usr))
- state = STATE_CALLSHUTTLE
- if("callshuttle2")
- if(authenticated)
- SSshuttle.requestEvac(usr, href_list["call"])
- if(SSshuttle.emergency.timer)
- post_status("shuttle")
- state = STATE_DEFAULT
- if("cancelshuttle")
- state = STATE_DEFAULT
- if(authenticated)
- state = STATE_CANCELSHUTTLE
- if("cancelshuttle2")
- if(authenticated)
- if(SSshuttle.endvote_passed) //Citadel Edit - endvote passing = no recalls
- say("Warning: Emergency shuttle recalls have been blocked by Central Command due to ongoing crew transfer procedures.")
- else
- SSshuttle.cancelEvac(usr)
- state = STATE_DEFAULT
- if("messagelist")
- currmsg = 0
- state = STATE_MESSAGELIST
- if("viewmessage")
- state = STATE_VIEWMESSAGE
- if (!currmsg)
- if(href_list["message-num"])
- var/msgnum = text2num(href_list["message-num"])
- currmsg = messages[msgnum]
- else
- state = STATE_MESSAGELIST
- if("delmessage")
- state = currmsg ? STATE_DELMESSAGE : STATE_MESSAGELIST
- if("delmessage2")
- if(authenticated)
- if(currmsg)
- if(aicurrmsg == currmsg)
- aicurrmsg = null
- messages -= currmsg
- currmsg = null
- state = STATE_MESSAGELIST
- else
- state = STATE_VIEWMESSAGE
- if("respond")
- var/answer = text2num(href_list["answer"])
- if(!currmsg || !answer || currmsg.possible_answers.len < answer)
- state = STATE_MESSAGELIST
- else
- if(!currmsg.answered)
- currmsg.answered = answer
- log_game("[key_name(usr)] answered [currmsg.title] comm message. Answer : [currmsg.answered]")
- if(currmsg)
- currmsg.answer_callback.InvokeAsync()
- state = STATE_VIEWMESSAGE
- updateDialog()
- if("status")
- state = STATE_STATUSDISPLAY
- if("securitylevel")
- tmp_alertlevel = text2num( href_list["newalertlevel"] )
- if(!tmp_alertlevel)
- tmp_alertlevel = 0
- state = STATE_CONFIRM_LEVEL
- if("changeseclevel")
- state = STATE_ALERT_LEVEL
-
- if("emergencyaccess")
- state = STATE_TOGGLE_EMERGENCY
- if("enableemergency")
- make_maint_all_access()
- log_game("[key_name(usr)] enabled emergency maintenance access.")
- message_admins("[ADMIN_LOOKUPFLW(usr)] enabled emergency maintenance access.")
- deadchat_broadcast("[usr.real_name] enabled emergency maintenance access at [get_area_name(usr, TRUE)].", usr)
- state = STATE_DEFAULT
- if("disableemergency")
- revoke_maint_all_access()
- log_game("[key_name(usr)] disabled emergency maintenance access.")
- message_admins("[ADMIN_LOOKUPFLW(usr)] disabled emergency maintenance access.")
- deadchat_broadcast("[usr.real_name] disabled emergency maintenance access at [get_area_name(usr, TRUE)].", usr)
- state = STATE_DEFAULT
-
- // Status display stuff
- if("setstat")
- playsound(src, "terminal_type", 50, FALSE)
- switch(href_list["statdisp"])
- if("message")
- post_status("message", stat_msg1, stat_msg2)
- if("alert")
- post_status("alert", href_list["alert"])
- else
- post_status(href_list["statdisp"])
-
- if("setmsg1")
- stat_msg1 = reject_bad_text(input("Line 1", "Enter Message Text", stat_msg1) as text|null, 40)
- updateDialog()
- if("setmsg2")
- stat_msg2 = reject_bad_text(input("Line 2", "Enter Message Text", stat_msg2) as text|null, 40)
- updateDialog()
-
- // OMG CENTCOM LETTERHEAD
- if("MessageCentCom")
- if(authenticated)
- if(!checkCCcooldown())
- to_chat(usr, "Arrays recycling. Please stand by.")
- return
- var/input = stripped_input(usr, "Please choose a message to transmit to CentCom via quantum entanglement. Please be aware that this process is very expensive, and abuse will lead to... termination. Transmission does not guarantee a response.", "Send a message to CentCom.", "")
- if(!input || !(usr in view(1,src)) || !checkCCcooldown())
- return
- playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
- CentCom_announce(input, usr)
- to_chat(usr, "Message transmitted to Central Command.")
- for(var/client/X in GLOB.admins)
- if(X.prefs.toggles & SOUND_ADMINHELP)
- SEND_SOUND(X, sound('sound/effects/printer.ogg'))
- window_flash(X, ignorepref = FALSE)
- usr.log_talk(input, LOG_SAY, tag="CentCom announcement")
- deadchat_broadcast("[usr.real_name] has messaged CentCom, \"[input]\" at [get_area_name(usr, TRUE)].", usr)
- CM.lastTimeUsed = world.time
-
- // OMG SYNDICATE ...LETTERHEAD
- if("MessageSyndicate")
- if((authenticated) && (obj_flags & EMAGGED))
- if(!checkCCcooldown())
- to_chat(usr, "Arrays recycling. Please stand by.")
- playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, FALSE)
- return
- var/input = stripped_input(usr, "Please choose a message to transmit to \[ABNORMAL ROUTING COORDINATES\] via quantum entanglement. Please be aware that this process is very expensive, and abuse will lead to... termination. Transmission does not guarantee a response.", "Send a message to /??????/.", "")
- if(!input || !(usr in view(1,src)) || !checkCCcooldown())
- return
- playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
- Syndicate_announce(input, usr)
- to_chat(usr, "SYSERR @l(19833)of(transmit.dm): !@$ MESSAGE TRANSMITTED TO SYNDICATE COMMAND.")
- for(var/client/X in GLOB.admins)
- if(X.prefs.toggles & SOUND_ADMINHELP)
- SEND_SOUND(X, sound('sound/effects/printer.ogg'))
- window_flash(X, ignorepref = FALSE)
- usr.log_talk(input, LOG_SAY, tag="Syndicate announcement")
- deadchat_broadcast("[usr.real_name] has messaged the Syndicate, \"[input]\" at [get_area_name(usr, TRUE)].", usr)
- CM.lastTimeUsed = world.time
-
- if("RestoreBackup")
- to_chat(usr, "Backup routing data restored!")
- playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
- obj_flags &= ~EMAGGED
- updateDialog()
-
- if("nukerequest") //When there's no other way
- if(authenticated==2)
- if(!checkCCcooldown())
- to_chat(usr, "Arrays recycling. Please stand by.")
- return
- var/input = stripped_input(usr, "Please enter the reason for requesting the nuclear self-destruct codes. Misuse of the nuclear request system will not be tolerated under any circumstances. Transmission does not guarantee a response.", "Self-Destruct Code Request.","")
- if(!input || !(usr in view(1,src)) || !checkCCcooldown())
- return
- Nuke_request(input, usr)
- to_chat(usr, "Request sent.")
- usr.log_message("has requested the nuclear codes from CentCom with reason \"[input]\"", LOG_SAY)
- priority_announce("The codes for the on-station nuclear self-destruct have been requested by [usr]. Confirmation or denial of this request will be sent shortly.", "Nuclear Self Destruct Codes Requested","commandreport")
- CM.lastTimeUsed = world.time
-
-
- // AI interface
- if("ai-main")
- aicurrmsg = null
- aistate = STATE_DEFAULT
- if("ai-callshuttle")
- aistate = STATE_DEFAULT
- if(SSshuttle.canEvac(usr))
- aistate = STATE_CALLSHUTTLE
- if("ai-callshuttle2")
- SSshuttle.requestEvac(usr, href_list["call"])
- aistate = STATE_DEFAULT
- if("ai-messagelist")
- aicurrmsg = null
- aistate = STATE_MESSAGELIST
- if("ai-viewmessage")
- aistate = STATE_VIEWMESSAGE
- if (!aicurrmsg)
- if(href_list["message-num"])
- var/msgnum = text2num(href_list["message-num"])
- aicurrmsg = messages[msgnum]
- else
- aistate = STATE_MESSAGELIST
- if("ai-delmessage")
- aistate = aicurrmsg ? STATE_DELMESSAGE : STATE_MESSAGELIST
- if("ai-delmessage2")
- if(aicurrmsg)
- if(currmsg == aicurrmsg)
- currmsg = null
- messages -= aicurrmsg
- aicurrmsg = null
- aistate = STATE_MESSAGELIST
- if("ai-respond")
- var/answer = text2num(href_list["answer"])
- if(!aicurrmsg || !answer || aicurrmsg.possible_answers.len < answer)
- aistate = STATE_MESSAGELIST
- else
- if(!aicurrmsg.answered)
- aicurrmsg.answered = answer
- log_game("[key_name(usr)] answered [aicurrmsg.title] comm message. Answer : [aicurrmsg.answered]")
- if(aicurrmsg.answer_callback)
- aicurrmsg.answer_callback.InvokeAsync()
- aistate = STATE_VIEWMESSAGE
- if("ai-status")
- aistate = STATE_STATUSDISPLAY
- if("ai-announce")
- make_announcement(usr, 1)
- if("ai-securitylevel")
- if(security_level_cd > world.time)
- to_chat(usr, "Security level protocols are currently on cooldown. Please stand by.")
- return
- tmp_alertlevel = text2num( href_list["newalertlevel"] )
- if(!tmp_alertlevel)
- tmp_alertlevel = SEC_LEVEL_GREEN
- var/old_level = GLOB.security_level
- if(!tmp_alertlevel)
- tmp_alertlevel = SEC_LEVEL_GREEN
- if(tmp_alertlevel < SEC_LEVEL_GREEN)
- tmp_alertlevel = SEC_LEVEL_GREEN
- if(tmp_alertlevel > SEC_LEVEL_AMBER)
- tmp_alertlevel = SEC_LEVEL_AMBER //Cannot engage delta with this
- set_security_level(tmp_alertlevel)
- security_level_cd = world.time + 15 SECONDS
- if(GLOB.security_level != old_level)
- //Only notify people if an actual change happened
- var/security_level = NUM2SECLEVEL(GLOB.security_level)
- log_game("[key_name(usr)] has changed the security level to [security_level] from [src] at [AREACOORD(usr)].")
- message_admins("[ADMIN_LOOKUPFLW(usr)] has changed the security level to [security_level] from [src] at [AREACOORD(usr)].")
- deadchat_broadcast("[usr.real_name] has changed the security level to [security_level] from [src] at [get_area_name(usr, TRUE)].", usr)
- tmp_alertlevel = 0
- aistate = STATE_DEFAULT
- if("ai-changeseclevel")
- aistate = STATE_ALERT_LEVEL
- if("ai-emergencyaccess")
- aistate = STATE_TOGGLE_EMERGENCY
- if("ai-enableemergency")
- make_maint_all_access()
- log_game("[key_name(usr)] enabled emergency maintenance access.")
- message_admins("[ADMIN_LOOKUPFLW(usr)] enabled emergency maintenance access.")
- deadchat_broadcast("[usr.real_name] enabled emergency maintenance access.", usr)
- aistate = STATE_DEFAULT
- if("ai-disableemergency")
- revoke_maint_all_access()
- log_game("[key_name(usr)] disabled emergency maintenance access.")
- message_admins("[ADMIN_LOOKUPFLW(usr)] disabled emergency maintenance access.")
- deadchat_broadcast("[usr.real_name] disabled emergency maintenance access.", usr)
- aistate = STATE_DEFAULT
-
- updateUsrDialog()
+/// Are we a silicon, OR logged in?
+/obj/machinery/computer/communications/proc/authenticated(mob/user)
+ if (issilicon(user))
+ return TRUE
+ return authenticated
/obj/machinery/computer/communications/attackby(obj/I, mob/user, params)
if(istype(I, /obj/item/card/id))
@@ -445,292 +71,413 @@
/obj/machinery/computer/communications/emag_act(mob/user)
. = ..()
- if(obj_flags & EMAGGED)
+ if (obj_flags & EMAGGED)
return
obj_flags |= EMAGGED
- SSshuttle.shuttle_purchase_requirements_met |= "emagged"
- if(authenticated == 1)
- authenticated = 2
+ if (authenticated)
+ authorize_access = get_all_accesses()
to_chat(user, "You scramble the communication routing circuits!")
playsound(src, 'sound/machines/terminal_alert.ogg', 50, FALSE)
+ return
+
+/obj/machinery/computer/communications/ui_act(action, list/params)
+ var/static/list/approved_states = list(STATE_BUYING_SHUTTLE, STATE_CHANGING_STATUS, STATE_MAIN, STATE_MESSAGES)
+ var/static/list/approved_status_pictures = list("biohazard", "blank", "default", "lockdown", "redalert", "shuttle")
+
+ . = ..()
+ if (.)
+ return
+
+ if (!has_communication())
+ return
+
+ . = TRUE
+
+ switch (action)
+ if ("answerMessage")
+ if (!authenticated(usr))
+ return
+ var/answer_index = text2num(params["answer"])
+ var/message_index = text2num(params["message"])
+ if (!answer_index || !message_index || answer_index < 1 || message_index < 1)
+ return
+ var/datum/comm_message/message = messages[message_index]
+ if (message.answered)
+ return
+ message.answered = answer_index
+ message.answer_callback.InvokeAsync()
+ if ("callShuttle")
+ if (!authenticated(usr) || !SSshuttle.canEvac(usr, TRUE))
+ return
+ var/reason = trim(params["reason"], MAX_MESSAGE_LEN)
+ if (length(reason) < CALL_SHUTTLE_REASON_LENGTH)
+ return
+ SSshuttle.requestEvac(usr, reason)
+ post_status("shuttle")
+ if ("changeSecurityLevel")
+ if (!authenticated_as_silicon_or_captain(usr))
+ return
+
+ // Check if they have
+ if (!issilicon(usr))
+ var/obj/item/held_item = usr.get_active_held_item()
+ var/obj/item/card/id/id_card = held_item?.GetID()
+ if (!istype(id_card))
+ to_chat(usr, "You need to swipe your ID!")
+ playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, FALSE)
+ return
+ if (!(ACCESS_CAPTAIN in id_card.access))
+ to_chat(usr, "You are not authorized to do this!")
+ playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, FALSE)
+ return
+
+ var/new_sec_level = SECLEVEL2NUM(params["newSecurityLevel"])
+ if (new_sec_level != SEC_LEVEL_GREEN && new_sec_level != SEC_LEVEL_BLUE && new_sec_level != SEC_LEVEL_AMBER)
+ return
+ if (GLOB.security_level == new_sec_level)
+ return
+
+ set_security_level(new_sec_level)
+
+ to_chat(usr, "Authorization confirmed. Modifying security level.")
+ playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
+
+ // Only notify people if an actual change happened
+ log_game("[key_name(usr)] has changed the security level to [params["newSecurityLevel"]] with [src] at [AREACOORD(usr)].")
+ message_admins("[ADMIN_LOOKUPFLW(usr)] has changed the security level to [params["newSecurityLevel"]] with [src] at [AREACOORD(usr)].")
+ deadchat_broadcast(" has changed the security level to [params["newSecurityLevel"]] with [src] at [get_area_name(usr, TRUE)].", "[usr.real_name]", usr)
+
+ alert_level_tick += 1
+ if ("deleteMessage")
+ if (!authenticated(usr))
+ return
+ var/message_index = text2num(params["message"])
+ if (!message_index)
+ return
+ LAZYREMOVE(messages, LAZYACCESS(messages, message_index))
+ if ("makePriorityAnnouncement")
+ if (!authenticated_as_silicon_or_captain(usr))
+ return
+ make_announcement(usr)
+ if ("messageAssociates")
+ if (!authenticated_as_non_silicon_captain(usr))
+ return
+ if (!COOLDOWN_FINISHED(src, important_action_cooldown))
+ return
+
+ playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
+ var/message = trim(html_encode(params["message"]), MAX_MESSAGE_LEN)
+
+ var/emagged = obj_flags & EMAGGED
+ if (emagged)
+ message_syndicate(message, usr)
+ to_chat(usr, "SYSERR @l(19833)of(transmit.dm): !@$ MESSAGE TRANSMITTED TO SYNDICATE COMMAND.")
+ else
+ message_centcom(message, usr)
+ to_chat(usr, "Message transmitted to Central Command.")
+
+ var/associates = emagged ? "the Syndicate": "CentCom"
+ usr.log_talk(message, LOG_SAY, tag = "message to [associates]")
+ deadchat_broadcast(" has messaged [associates], \"[message]\" at [get_area_name(usr, TRUE)].", "[usr.real_name]", usr)
+ COOLDOWN_START(src, important_action_cooldown, IMPORTANT_ACTION_COOLDOWN)
+ if ("purchaseShuttle")
+ var/can_buy_shuttles_or_fail_reason = can_buy_shuttles(usr)
+ if (can_buy_shuttles_or_fail_reason != TRUE)
+ if (can_buy_shuttles_or_fail_reason != FALSE)
+ to_chat(usr, "[can_buy_shuttles_or_fail_reason]")
+ return
+ var/list/shuttles = flatten_list(SSmapping.shuttle_templates)
+ var/datum/map_template/shuttle/shuttle = locate(params["shuttle"]) in shuttles
+ if (!istype(shuttle))
+ return
+ if (!shuttle.prerequisites_met())
+ to_chat(usr, "You have not met the requirements for purchasing this shuttle.")
+ return
+ var/datum/bank_account/bank_account = SSeconomy.get_dep_account(ACCOUNT_CAR)
+ if (bank_account.account_balance < shuttle.credit_cost)
+ return
+ SSshuttle.shuttle_purchased = SHUTTLEPURCHASE_PURCHASED
+ SSshuttle.unload_preview()
+ SSshuttle.existing_shuttle = SSshuttle.emergency
+ SSshuttle.action_load(shuttle, replace = TRUE)
+ bank_account.adjust_money(-shuttle.credit_cost)
+ minor_announce("[usr.real_name] has purchased [shuttle.name] for [shuttle.credit_cost] credits.[shuttle.extra_desc ? " [shuttle.extra_desc]" : ""]" , "Shuttle Purchase")
+ message_admins("[ADMIN_LOOKUPFLW(usr)] purchased [shuttle.name].")
+ log_shuttle("[key_name(usr)] has purchased [shuttle.name].")
+ SSblackbox.record_feedback("text", "shuttle_purchase", 1, shuttle.name)
+ state = STATE_MAIN
+ if ("recallShuttle")
+ // AIs cannot recall the shuttle
+ if (!authenticated(usr) || issilicon(usr))
+ return
+ SSshuttle.cancelEvac(usr)
+ if ("requestNukeCodes")
+ if (!authenticated_as_non_silicon_captain(usr))
+ return
+ if (!COOLDOWN_FINISHED(src, important_action_cooldown))
+ return
+ var/reason = trim(html_encode(params["reason"]), MAX_MESSAGE_LEN)
+ nuke_request(reason, usr)
+ to_chat(usr, "Request sent.")
+ usr.log_message("has requested the nuclear codes from CentCom with reason \"[reason]\"", LOG_SAY)
+ priority_announce("The codes for the on-station nuclear self-destruct have been requested by [usr]. Confirmation or denial of this request will be sent shortly.", "Nuclear Self-Destruct Codes Requested", "commandreport")
+ playsound(src, 'sound/machines/terminal_prompt.ogg', 50, FALSE)
+ COOLDOWN_START(src, important_action_cooldown, IMPORTANT_ACTION_COOLDOWN)
+ if ("restoreBackupRoutingData")
+ if (!authenticated_as_non_silicon_captain(usr))
+ return
+ if (!(obj_flags & EMAGGED))
+ return
+ to_chat(usr, "Backup routing data restored.")
+ playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
+ obj_flags &= ~EMAGGED
+ if ("sendToOtherSector")
+ if (!authenticated_as_non_silicon_captain(usr))
+ return
+ if (!can_send_messages_to_other_sectors(usr))
+ return
+ if (!COOLDOWN_FINISHED(src, important_action_cooldown))
+ return
+
+ var/message = trim(html_encode(params["message"]), MAX_MESSAGE_LEN)
+ if (!message)
+ return
+
+ playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
+
+ var/destination = params["destination"]
+ var/list/payload = list()
+
+ var/network_name = CONFIG_GET(string/cross_comms_network)
+ if (network_name)
+ payload["network"] = network_name
+
+ send2otherserver(station_name(), message, "Comms_Console", destination == "all" ? null : list(destination), additional_data = payload)
+ minor_announce(message, title = "Outgoing message to allied station")
+ usr.log_talk(message, LOG_SAY, tag = "message to the other server")
+ message_admins("[ADMIN_LOOKUPFLW(usr)] has sent a message to the other server\[s].")
+ deadchat_broadcast(" has sent an outgoing message to the other station(s).", "[usr.real_name]", usr)
+
+ COOLDOWN_START(src, important_action_cooldown, IMPORTANT_ACTION_COOLDOWN)
+ if ("setState")
+ if (!authenticated(usr))
+ return
+ if (!(params["state"] in approved_states))
+ return
+ if (state == STATE_BUYING_SHUTTLE && can_buy_shuttles(usr) != TRUE)
+ return
+ set_state(usr, params["state"])
+ playsound(src, "terminal_type", 50, FALSE)
+ if ("setStatusMessage")
+ if (!authenticated(usr))
+ return
+ var/line_one = reject_bad_text(params["lineOne"] || "", MAX_STATUS_LINE_LENGTH)
+ var/line_two = reject_bad_text(params["lineTwo"] || "", MAX_STATUS_LINE_LENGTH)
+ post_status("alert", "blank")
+ post_status("message", line_one, line_two)
+ last_status_display = list(line_one, line_two)
+ playsound(src, "terminal_type", 50, FALSE)
+ if ("setStatusPicture")
+ if (!authenticated(usr))
+ return
+ var/picture = params["picture"]
+ if (!(picture in approved_status_pictures))
+ return
+ post_status("alert", picture)
+ playsound(src, "terminal_type", 50, FALSE)
+ if ("toggleAuthentication")
+ // Log out if we're logged in
+ if (authorize_name)
+ authenticated = FALSE
+ authorize_access = null
+ authorize_name = null
+ playsound(src, 'sound/machines/terminal_off.ogg', 50, FALSE)
+ return
+
+ if (obj_flags & EMAGGED)
+ authenticated = TRUE
+ authorize_access = get_all_accesses()
+ authorize_name = "Unknown"
+ to_chat(usr, "[src] lets out a quiet alarm as its login is overridden.")
+ playsound(src, 'sound/machines/terminal_alert.ogg', 25, FALSE)
+ else if(isliving(usr))
+ var/mob/living/L = usr
+ var/obj/item/card/id/id_card = L.get_idcard(hand_first = TRUE)
+ if (check_access(id_card))
+ authenticated = TRUE
+ authorize_access = id_card.access
+ authorize_name = "[id_card.registered_name] - [id_card.assignment]"
+
+ state = STATE_MAIN
+ playsound(src, 'sound/machines/terminal_on.ogg', 50, FALSE)
+ if ("toggleEmergencyAccess")
+ if (!authenticated_as_silicon_or_captain(usr))
+ return
+ if (GLOB.emergency_access)
+ revoke_maint_all_access()
+ log_game("[key_name(usr)] disabled emergency maintenance access.")
+ message_admins("[ADMIN_LOOKUPFLW(usr)] disabled emergency maintenance access.")
+ deadchat_broadcast(" disabled emergency maintenance access at [get_area_name(usr, TRUE)].", "[usr.real_name]", usr)
+ else
+ make_maint_all_access()
+ log_game("[key_name(usr)] enabled emergency maintenance access.")
+ message_admins("[ADMIN_LOOKUPFLW(usr)] enabled emergency maintenance access.")
+ deadchat_broadcast(" enabled emergency maintenance access at [get_area_name(usr, TRUE)].", "[usr.real_name]", usr)
+
+/obj/machinery/computer/communications/ui_data(mob/user)
+ var/list/data = list(
+ "authenticated" = FALSE,
+ "emagged" = FALSE,
+ "hasConnection" = has_communication(),
+ )
+
+ var/ui_state = issilicon(user) ? cyborg_state : state
+
+ if (authenticated || issilicon(user))
+ data["authenticated"] = TRUE
+ data["canLogOut"] = !issilicon(user)
+ data["page"] = ui_state
+
+ if (obj_flags & EMAGGED)
+ data["emagged"] = TRUE
+
+ switch (ui_state)
+ if (STATE_MAIN)
+ data["canBuyShuttles"] = can_buy_shuttles(user)
+ data["canMakeAnnouncement"] = FALSE
+ data["canMessageAssociates"] = FALSE
+ data["canRecallShuttles"] = !issilicon(user)
+ data["canRequestNuke"] = FALSE
+ data["canSendToSectors"] = FALSE
+ data["canSetAlertLevel"] = FALSE
+ data["canToggleEmergencyAccess"] = FALSE
+ data["importantActionReady"] = COOLDOWN_FINISHED(src, important_action_cooldown)
+ data["shuttleCalled"] = FALSE
+ data["shuttleLastCalled"] = FALSE
+
+ data["alertLevel"] = NUM2SECLEVEL(GLOB.security_level)
+ data["authorizeName"] = authorize_name
+ data["canLogOut"] = !issilicon(user)
+ data["shuttleCanEvacOrFailReason"] = SSshuttle.canEvac(user, TRUE)
+
+ if (authenticated_as_non_silicon_captain(user))
+ data["canMessageAssociates"] = TRUE
+ data["canRequestNuke"] = TRUE
+
+ if (can_send_messages_to_other_sectors(user))
+ data["canSendToSectors"] = TRUE
+
+ var/list/sectors = list()
+ var/our_id = CONFIG_GET(string/cross_comms_name)
+
+ for (var/server in CONFIG_GET(keyed_list/cross_server))
+ if (server == our_id)
+ continue
+ sectors += server
+
+ data["sectors"] = sectors
+
+ if (authenticated_as_silicon_or_captain(user))
+ data["canToggleEmergencyAccess"] = TRUE
+ data["emergencyAccess"] = GLOB.emergency_access
+
+ data["alertLevelTick"] = alert_level_tick
+ data["canMakeAnnouncement"] = TRUE
+ data["canSetAlertLevel"] = issilicon(user) ? "NO_SWIPE_NEEDED" : "SWIPE_NEEDED"
+
+ if (SSshuttle.emergency.mode != SHUTTLE_IDLE && SSshuttle.emergency.mode != SHUTTLE_RECALL)
+ data["shuttleCalled"] = TRUE
+ data["shuttleRecallable"] = SSshuttle.canRecall()
+
+ if (SSshuttle.emergencyCallAmount)
+ data["shuttleCalledPreviously"] = TRUE
+ if (SSshuttle.emergencyLastCallLoc)
+ data["shuttleLastCalled"] = format_text(SSshuttle.emergencyLastCallLoc.name)
+ if (STATE_MESSAGES)
+ data["messages"] = list()
+
+ if (messages)
+ for (var/_message in messages)
+ var/datum/comm_message/message = _message
+ data["messages"] += list(list(
+ "answered" = message.answered,
+ "content" = message.content,
+ "title" = message.title,
+ "possibleAnswers" = message.possible_answers,
+ ))
+ if (STATE_BUYING_SHUTTLE)
+ var/datum/bank_account/bank_account = SSeconomy.get_dep_account(ACCOUNT_CAR)
+ var/list/shuttles = list()
+
+ for (var/shuttle_id in SSmapping.shuttle_templates)
+ var/datum/map_template/shuttle/shuttle_template = SSmapping.shuttle_templates[shuttle_id]
+ if (!shuttle_template.can_be_bought || shuttle_template.credit_cost == INFINITY)
+ continue
+ shuttles += list(list(
+ "name" = shuttle_template.name,
+ "description" = shuttle_template.description,
+ "creditCost" = shuttle_template.credit_cost,
+ "prerequisites" = shuttle_template.prerequisites,
+ "ref" = REF(shuttle_template),
+ ))
+
+ data["budget"] = bank_account.account_balance
+ data["shuttles"] = shuttles
+ if (STATE_CHANGING_STATUS)
+ data["lineOne"] = last_status_display ? last_status_display[1] : ""
+ data["lineTwo"] = last_status_display ? last_status_display[2] : ""
+
+ return data
+
+/obj/machinery/computer/communications/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if (!ui)
+ ui = new(user, src, "CommunicationsConsole")
+ ui.open()
+
+/obj/machinery/computer/communications/ui_static_data(mob/user)
+ return list(
+ "callShuttleReasonMinLength" = CALL_SHUTTLE_REASON_LENGTH,
+ "maxStatusLineLength" = MAX_STATUS_LINE_LENGTH,
+ "maxMessageLength" = MAX_MESSAGE_LEN,
+ )
+
+/// Returns whether or not the communications console can communicate with the station
+/obj/machinery/computer/communications/proc/has_communication()
+ var/turf/current_turf = get_turf(src)
+ var/z_level = current_turf.z
+ return is_station_level(z_level) || is_centcom_level(z_level)
+
+/obj/machinery/computer/communications/proc/set_state(mob/user, new_state)
+ if (issilicon(user))
+ cyborg_state = new_state
+ else
+ state = new_state
+
+/// Returns TRUE if the user can buy shuttles.
+/// If they cannot, returns FALSE or a string detailing why.
+/obj/machinery/computer/communications/proc/can_buy_shuttles(mob/user)
+ if (!SSmapping.config.allow_custom_shuttles)
+ return FALSE
+ if (!authenticated_as_non_silicon_captain(user))
+ return FALSE
+ if (SSshuttle.emergency.mode != SHUTTLE_RECALL && SSshuttle.emergency.mode != SHUTTLE_IDLE)
+ return "The shuttle is already in transit."
+ if (SSshuttle.shuttle_purchased == SHUTTLEPURCHASE_PURCHASED)
+ return "A replacement shuttle has already been purchased."
+ if (SSshuttle.shuttle_purchased == SHUTTLEPURCHASE_FORCED)
+ return "Due to unforseen circumstances, shuttle purchasing is no longer available."
return TRUE
-/obj/machinery/computer/communications/ui_interact(mob/user)
- . = ..()
- if (z > 6)
- to_chat(user, "Unable to establish a connection: \black You're too far away from the station!")
+/obj/machinery/computer/communications/proc/can_send_messages_to_other_sectors(mob/user)
+ if (!authenticated_as_non_silicon_captain(user))
return
- var/dat = ""
- if(SSshuttle.emergency.mode == SHUTTLE_CALL)
- var/timeleft = SSshuttle.emergency.timeLeft()
- dat += "Emergency shuttle\n
\nETA: [timeleft / 60 % 60]:[add_leading(num2text(timeleft % 60), 2, "0")]"
+ return length(CONFIG_GET(keyed_list/cross_server)) > 0
-
- var/datum/browser/popup = new(user, "communications", "Communications Console", 400, 500)
-
- if(issilicon(user))
- var/dat2 = interact_ai(user) // give the AI a different interact proc to limit its access
- if(dat2)
- dat += dat2
- popup.set_content(dat)
- popup.open()
- return
-
- switch(state)
- if(STATE_DEFAULT)
- if (authenticated)
- if(SSshuttle.emergencyCallAmount)
- if(SSshuttle.emergencyLastCallLoc)
- dat += "Most recent shuttle call/recall traced to: [format_text(SSshuttle.emergencyLastCallLoc.name)]
"
- else
- dat += "Unable to trace most recent shuttle call/recall signal.
"
- dat += "Logged in as: [auth_id]"
- dat += "
"
- dat += "
\[ Log Out \]
"
- dat += "
General Functions"
- dat += "
\[ Message List \]"
- switch(SSshuttle.emergency.mode)
- if(SHUTTLE_IDLE, SHUTTLE_RECALL)
- dat += "
\[ Call Emergency Shuttle \]"
- else
- dat += "
\[ Cancel Shuttle Call \]"
-
- dat += "
\[ Set Status Display \]"
- if (authenticated==2)
- dat += "
Captain Functions"
- dat += "
\[ Make a Captain's Announcement \]"
- var/list/cross_servers = CONFIG_GET(keyed_list/cross_server)
- var/our_id = CONFIG_GET(string/cross_comms_name)
- if(cross_servers.len)
- for(var/server in cross_servers)
- if(server == our_id)
- continue
- dat += "
\[ Send a message to station in [server] sector. \]"
- if(cross_servers.len > 2)
- dat += "
\[ Send a message to all allied stations \]"
- if(SSmapping.config.allow_custom_shuttles)
- dat += "
\[ Purchase Shuttle \]"
- dat += "
\[ Change Alert Level \]"
- dat += "
\[ Emergency Maintenance Access \]"
- dat += "
\[ Request Nuclear Authentication Codes \]"
- if(!(obj_flags & EMAGGED))
- dat += "
\[ Send Message to CentCom \]"
- else
- dat += "
\[ Send Message to \[UNKNOWN\] \]"
- dat += "
\[ Restore Backup Routing Data \]"
- else
- dat += "
\[ Log In \]"
- if(STATE_CALLSHUTTLE)
- dat += get_call_shuttle_form()
- playsound(src, 'sound/machines/terminal_prompt.ogg', 50, FALSE)
- if(STATE_CANCELSHUTTLE)
- dat += get_cancel_shuttle_form()
- playsound(src, 'sound/machines/terminal_prompt.ogg', 50, FALSE)
- if(STATE_MESSAGELIST)
- dat += "Messages:"
- for(var/i in 1 to messages.len)
- var/datum/comm_message/M = messages[i]
- dat += "
[M.title]"
- playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
- if(STATE_VIEWMESSAGE)
- if (currmsg)
- dat += "[currmsg.title]
[currmsg.content]"
- if(!currmsg.answered && currmsg.possible_answers.len)
- for(var/i in 1 to currmsg.possible_answers.len)
- var/answer = currmsg.possible_answers[i]
- dat += "
\[ Answer : [answer] \]"
- else if(currmsg.answered)
- var/answered = currmsg.possible_answers[currmsg.answered]
- dat += "
Archived Answer : [answered]"
- dat += "
\[ Delete \]"
- else
- aistate = STATE_MESSAGELIST
- attack_hand(user)
- return
- if(STATE_DELMESSAGE)
- if (currmsg)
- dat += "Are you sure you want to delete this message? \[ OK | Cancel \]"
- else
- state = STATE_MESSAGELIST
- attack_hand(user)
- return
- if(STATE_STATUSDISPLAY)
- dat += "Set Status Displays
"
- dat += "\[ Clear \]
"
- dat += "\[ Shuttle ETA \]
"
- dat += "\[ Message \]"
- dat += "
"
- dat += "\[ Alert: None |"
- dat += " Red Alert |"
- dat += " Lockdown |"
- dat += " Biohazard \]
"
- playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
- if(STATE_ALERT_LEVEL)
- dat += "Current alert level: [NUM2SECLEVEL(GLOB.security_level)]
"
- if(GLOB.security_level == SEC_LEVEL_DELTA)
- dat += "The self-destruct mechanism is active. Find a way to deactivate the mechanism to lower the alert level or evacuate."
- else
- dat += "Amber
"
- dat += "Blue
"
- dat += "Green"
- if(STATE_CONFIRM_LEVEL)
- dat += "Current alert level: [NUM2SECLEVEL(GLOB.security_level)]
"
- dat += "Confirm the change to: [NUM2SECLEVEL(tmp_alertlevel)]
"
- dat += "Swipe ID to confirm change.
"
- if(STATE_TOGGLE_EMERGENCY)
- playsound(src, 'sound/machines/terminal_prompt.ogg', 50, FALSE)
- if(GLOB.emergency_access == 1)
- dat += "Emergency Maintenance Access is currently ENABLED"
- dat += "
Restore maintenance access restrictions?
\[ OK | Cancel \]"
- else
- dat += "Emergency Maintenance Access is currently DISABLED"
- dat += "
Lift access restrictions on maintenance and external airlocks?
\[ OK | Cancel \]"
-
- if(STATE_PURCHASE)
- var/datum/bank_account/D = SSeconomy.get_dep_account(ACCOUNT_CAR)
- dat += "Budget: [D.account_balance] Credits.
"
- dat += "
"
- dat += "Caution: Purchasing dangerous shuttles may lead to mutiny and/or death.
"
- dat += "
"
- for(var/shuttle_id in SSmapping.shuttle_templates)
- var/datum/map_template/shuttle/S = SSmapping.shuttle_templates[shuttle_id]
- if(S.can_be_bought && S.credit_cost < INFINITY)
- dat += "[S.name] | [S.credit_cost] Credits
"
- dat += "[S.description]
"
- if(S.prerequisites)
- dat += "Prerequisites: [S.prerequisites]
"
- dat += "(Purchase)
"
-
- dat += "
\[ [(state != STATE_DEFAULT) ? "Main Menu | " : ""]Close \]"
-
- popup.set_content(dat)
- popup.open()
-
-/obj/machinery/computer/communications/proc/get_javascript_header(form_id)
- var/dat = {""}
- return dat
-
-/obj/machinery/computer/communications/proc/get_call_shuttle_form(ai_interface = 0)
- var/form_id = "callshuttle"
- var/dat = get_javascript_header(form_id)
- dat += ""
output += ruler
- var/datum/DBQuery/query_get_type_messages = SSdbcore.NewQuery("SELECT id, IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey), targetckey), targetckey, IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), adminckey), text, timestamp, server, IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = lasteditor), lasteditor), expire_timestamp FROM [format_table_name("messages")] WHERE type = '[type]' AND deleted = 0 AND (expire_timestamp > NOW() OR expire_timestamp IS NULL)")
+ var/datum/db_query/query_get_type_messages = SSdbcore.NewQuery({"
+ SELECT
+ id,
+ IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey), targetckey),
+ targetckey,
+ IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), adminckey),
+ text,
+ timestamp,
+ server,
+ IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = lasteditor), lasteditor),
+ expire_timestamp
+ FROM [format_table_name("messages")]
+ WHERE type = :type AND deleted = 0 AND (expire_timestamp > NOW() OR expire_timestamp IS NULL)
+ "}, list("type" = type))
if(!query_get_type_messages.warn_execute())
qdel(query_get_type_messages)
return
@@ -362,9 +443,24 @@
output += "
[text]
"
qdel(query_get_type_messages)
if(target_ckey)
- target_ckey = sanitizeSQL(target_ckey)
var/target_key
- var/datum/DBQuery/query_get_messages = SSdbcore.NewQuery("SELECT type, secret, id, IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), adminckey), text, timestamp, server, IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = lasteditor), lasteditor), DATEDIFF(NOW(), timestamp), IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey), targetckey), expire_timestamp, severity FROM [format_table_name("messages")] WHERE type <> 'memo' AND targetckey = '[target_ckey]' AND deleted = 0 AND (expire_timestamp > NOW() OR expire_timestamp IS NULL) ORDER BY timestamp DESC")
+ var/datum/db_query/query_get_messages = SSdbcore.NewQuery({"
+ SELECT
+ type,
+ secret,
+ id,
+ IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), adminckey),
+ text,
+ timestamp,
+ server,
+ IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = lasteditor), lasteditor),
+ DATEDIFF(NOW(), timestamp),
+ IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey), targetckey),
+ expire_timestamp, severity
+ FROM [format_table_name("messages")]
+ WHERE type <> 'memo' AND targetckey = :targetckey AND deleted = 0 AND (expire_timestamp > NOW() OR expire_timestamp IS NULL)
+ ORDER BY timestamp DESC
+ "}, list("targetckey" = target_ckey))
if(!query_get_messages.warn_execute())
qdel(query_get_messages)
return
@@ -442,7 +538,9 @@
notedata += data
qdel(query_get_messages)
if(!target_key)
- var/datum/DBQuery/query_get_message_key = SSdbcore.NewQuery("SELECT byond_key FROM [format_table_name("player")] WHERE ckey = '[target_ckey]'")
+ var/datum/db_query/query_get_message_key = SSdbcore.NewQuery({"
+ SELECT byond_key FROM [format_table_name("player")] WHERE ckey = :ckey
+ "}, list("ckey" = target_ckey))
if(!query_get_message_key.warn_execute())
qdel(query_get_message_key)
return
@@ -479,8 +577,6 @@
var/search
output += "Add messageAdd watchlist entryAdd note"
output += ruler
- if(!isnum(index))
- index = sanitizeSQL(index)
switch(index)
if(1)
search = "^."
@@ -488,7 +584,17 @@
search = "^\[^\[:alpha:\]\]"
else
search = "^[index]"
- var/datum/DBQuery/query_list_messages = SSdbcore.NewQuery("SELECT DISTINCT targetckey, (SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey) FROM [format_table_name("messages")] WHERE type <> 'memo' AND targetckey REGEXP '[search]' AND deleted = 0 AND (expire_timestamp > NOW() OR expire_timestamp IS NULL) ORDER BY targetckey")
+ var/datum/db_query/query_list_messages = SSdbcore.NewQuery({"
+ SELECT DISTINCT
+ targetckey,
+ (SELECT byond_key FROM [format_table_name("player")] WHERE ckey = targetckey)
+ FROM [format_table_name("messages")]
+ WHERE type <> 'memo'
+ AND targetckey REGEXP :search
+ AND deleted = 0
+ AND (expire_timestamp > NOW() OR expire_timestamp IS NULL)
+ ORDER BY targetckey
+ "}, list("search" = search))
if(!query_list_messages.warn_execute())
qdel(query_list_messages)
return
@@ -512,17 +618,24 @@
/proc/get_message_output(type, target_ckey)
if(!SSdbcore.Connect())
- to_chat(usr, "Failed to establish database connection.")
+ to_chat(usr, "Failed to establish database connection.", confidential = TRUE)
return
if(!type)
return
var/output
- if(target_ckey)
- target_ckey = sanitizeSQL(target_ckey)
- var/query = "SELECT id, IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), adminckey), text, timestamp, IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = lasteditor), lasteditor) FROM [format_table_name("messages")] WHERE type = '[type]' AND deleted = 0 AND (expire_timestamp > NOW() OR expire_timestamp IS NULL)"
- if(type == "message" || type == "watchlist entry")
- query += " AND targetckey = '[target_ckey]'"
- var/datum/DBQuery/query_get_message_output = SSdbcore.NewQuery(query)
+ var/datum/db_query/query_get_message_output = SSdbcore.NewQuery({"
+ SELECT
+ id,
+ IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = adminckey), adminckey),
+ text,
+ timestamp,
+ IFNULL((SELECT byond_key FROM [format_table_name("player")] WHERE ckey = lasteditor), lasteditor)
+ FROM [format_table_name("messages")]
+ WHERE type = :type
+ AND deleted = 0
+ AND (expire_timestamp > NOW() OR expire_timestamp IS NULL)
+ AND ((type != 'message' AND type != 'watchlist entry') OR targetckey = :targetckey)
+ "}, list("targetckey" = target_ckey, "type" = type))
if(!query_get_message_output.warn_execute())
qdel(query_get_message_output)
return
@@ -536,7 +649,10 @@
if("message")
output += "Admin message left by [admin_key] on [timestamp]"
output += "
[text]
"
- var/datum/DBQuery/query_message_read = SSdbcore.NewQuery("UPDATE [format_table_name("messages")] SET type = 'message sent' WHERE id = [message_id]")
+ var/datum/db_query/query_message_read = SSdbcore.NewQuery(
+ "UPDATE [format_table_name("messages")] SET type = 'message sent' WHERE id = :id",
+ list("id" = message_id)
+ )
if(!query_message_read.warn_execute())
qdel(query_get_message_output)
qdel(query_message_read)
@@ -544,7 +660,7 @@
qdel(query_message_read)
if("watchlist entry")
message_admins("Notice: [key_name_admin(target_ckey)] has been on the watchlist since [timestamp] and has just connected - Reason: [text]")
- send2irc_adminless_only("Watchlist", "[key_name(target_ckey)] is on the watchlist and has just connected - Reason: [text]")
+ send2tgs_adminless_only("Watchlist", "[key_name(target_ckey)] is on the watchlist and has just connected - Reason: [text]")
if("memo")
output += "Memo by [admin_key] on [timestamp]"
if(editor_key)
@@ -576,7 +692,7 @@
var/timestamp = note.group[1]
notetext = note.group[2]
var/admin_ckey = note.group[3]
- var/datum/DBQuery/query_convert_time = SSdbcore.NewQuery("SELECT ADDTIME(STR_TO_DATE('[timestamp]','%d-%b-%Y'), '0')")
+ var/datum/db_query/query_convert_time = SSdbcore.NewQuery("SELECT ADDTIME(STR_TO_DATE(:timestamp,'%d-%b-%Y'), '0')", list("timestamp" = timestamp))
if(!query_convert_time.Execute())
qdel(query_convert_time)
return
@@ -591,7 +707,7 @@
/*alternatively this proc can be run once to pass through every note and attempt to convert it before deleting the file, if done then AUTOCONVERT_NOTES should be turned off
this proc can take several minutes to execute fully if converting and cause DD to hang if converting a lot of notes; it's not advised to do so while a server is live
/proc/mass_convert_notes()
- to_chat(world, "Beginning mass note conversion")
+ to_chat(world, "Beginning mass note conversion", confidential = TRUE)
var/savefile/notesfile = new(NOTESFILE)
if(!notesfile)
log_game("Error: Cannot access [NOTESFILE]")
@@ -599,7 +715,7 @@ this proc can take several minutes to execute fully if converting and cause DD t
notesfile.cd = "/"
for(var/ckey in notesfile.dir)
convert_notes_sql(ckey)
- to_chat(world, "Deleting NOTESFILE")
+ to_chat(world, "Deleting NOTESFILE", confidential = TRUE)
fdel(NOTESFILE)
- to_chat(world, "Finished mass note conversion, remember to turn off AUTOCONVERT_NOTES")*/
+ to_chat(world, "Finished mass note conversion, remember to turn off AUTOCONVERT_NOTES", confidential = TRUE)*/
#undef NOTESFILE
diff --git a/code/modules/admin/stickyban.dm b/code/modules/admin/stickyban.dm
index ef0bfe8e70..57487f3144 100644
--- a/code/modules/admin/stickyban.dm
+++ b/code/modules/admin/stickyban.dm
@@ -32,15 +32,11 @@
return
ban["message"] = "[reason]"
- if(SSdbcore.Connect()) // todo: second wave
- // var/datum/db_query/query_create_stickyban = SSdbcore.NewQuery({"
- // INSERT INTO [format_table_name("stickyban")] (ckey, reason, banning_admin)
- // VALUES (:ckey, :message, :banning_admin)
- // "}, list("ckey" = ckey, "message" = ban["message"], "banning_admin" = usr.ckey))
- var/datum/DBQuery/query_create_stickyban = SSdbcore.NewQuery({"
+ if(SSdbcore.Connect())
+ var/datum/db_query/query_create_stickyban = SSdbcore.NewQuery({"
INSERT INTO [format_table_name("stickyban")] (ckey, reason, banning_admin)
- VALUES ([ckey], [ban["message"]], [usr.ckey])
- "})
+ VALUES (:ckey, :message, :banning_admin)
+ "}, list("ckey" = ckey, "message" = ban["message"], "banning_admin" = usr.ckey))
if (query_create_stickyban.warn_execute())
ban["fromdb"] = TRUE
qdel(query_create_stickyban)
@@ -74,19 +70,14 @@
SSstickyban.cache -= ckey
if (SSdbcore.Connect())
- // SSdbcore.QuerySelect(list(
- // SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban")] WHERE ckey = :ckey", list("ckey" = ckey)),
- // SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban_matched_ckey")] WHERE stickyban = :ckey", list("ckey" = ckey)),
- // SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban_matched_cid")] WHERE stickyban = :ckey", list("ckey" = ckey)),
- // SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban_matched_ip")] WHERE stickyban = :ckey", list("ckey" = ckey))
- // ), warn = TRUE, qdel = TRUE)
SSdbcore.QuerySelect(list(
- SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban")] WHERE ckey = [ckey]"),
- SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban_matched_ckey")] WHERE stickyban = [ckey]"),
- SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban_matched_cid")] WHERE stickyban = [ckey]"),
- SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban_matched_ip")] WHERE stickyban = [ckey]")
+ SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban")] WHERE ckey = :ckey", list("ckey" = ckey)),
+ SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban_matched_ckey")] WHERE stickyban = :ckey", list("ckey" = ckey)),
+ SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban_matched_cid")] WHERE stickyban = :ckey", list("ckey" = ckey)),
+ SSdbcore.NewQuery("DELETE FROM [format_table_name("stickyban_matched_ip")] WHERE stickyban = :ckey", list("ckey" = ckey))
), warn = TRUE, qdel = TRUE)
+
log_admin_private("[key_name(usr)] removed [ckey]'s stickyban")
message_admins("[key_name_admin(usr)] removed [ckey]'s stickyban")
@@ -128,12 +119,9 @@
SSstickyban.cache[ckey] = ban
if (SSdbcore.Connect())
- // var/datum/db_query/query_remove_stickyban_alt = SSdbcore.NewQuery(
- // "DELETE FROM [format_table_name("stickyban_matched_ckey")] WHERE stickyban = :ckey AND matched_ckey = :alt",
- // list("ckey" = ckey, "alt" = alt)
- // )
- var/datum/DBQuery/query_remove_stickyban_alt = SSdbcore.NewQuery(
- "DELETE FROM [format_table_name("stickyban_matched_ckey")] WHERE stickyban = [ckey] AND matched_ckey = [alt]"
+ var/datum/db_query/query_remove_stickyban_alt = SSdbcore.NewQuery(
+ "DELETE FROM [format_table_name("stickyban_matched_ckey")] WHERE stickyban = :ckey AND matched_ckey = :alt",
+ list("ckey" = ckey, "alt" = alt)
)
query_remove_stickyban_alt.warn_execute()
qdel(query_remove_stickyban_alt)
@@ -165,12 +153,9 @@
SSstickyban.cache[ckey] = ban
if (SSdbcore.Connect())
- // var/datum/db_query/query_edit_stickyban = SSdbcore.NewQuery(
- // "UPDATE [format_table_name("stickyban")] SET reason = :reason WHERE ckey = :ckey",
- // list("reason" = reason, "ckey" = ckey)
- // )
- var/datum/DBQuery/query_edit_stickyban = SSdbcore.NewQuery(
- "UPDATE [format_table_name("stickyban")] SET reason = [reason] WHERE ckey = [ckey]"
+ var/datum/db_query/query_edit_stickyban = SSdbcore.NewQuery(
+ "UPDATE [format_table_name("stickyban")] SET reason = :reason WHERE ckey = :ckey",
+ list("reason" = reason, "ckey" = ckey)
)
query_edit_stickyban.warn_execute()
qdel(query_edit_stickyban)
@@ -218,12 +203,9 @@
SSstickyban.cache[ckey] = ban
if (SSdbcore.Connect())
- // var/datum/db_query/query_exempt_stickyban_alt = SSdbcore.NewQuery(
- // "UPDATE [format_table_name("stickyban_matched_ckey")] SET exempt = 1 WHERE stickyban = :ckey AND matched_ckey = :alt",
- // list("ckey" = ckey, "alt" = alt)
- // )
- var/datum/DBQuery/query_exempt_stickyban_alt = SSdbcore.NewQuery(
- "UPDATE [format_table_name("stickyban_matched_ckey")] SET exempt = 1 WHERE stickyban = [ckey] AND matched_ckey = [alt]"
+ var/datum/db_query/query_exempt_stickyban_alt = SSdbcore.NewQuery(
+ "UPDATE [format_table_name("stickyban_matched_ckey")] SET exempt = 1 WHERE stickyban = :ckey AND matched_ckey = :alt",
+ list("ckey" = ckey, "alt" = alt)
)
query_exempt_stickyban_alt.warn_execute()
qdel(query_exempt_stickyban_alt)
@@ -271,12 +253,9 @@
SSstickyban.cache[ckey] = ban
if (SSdbcore.Connect())
- // var/datum/db_query/query_unexempt_stickyban_alt = SSdbcore.NewQuery(
- // "UPDATE [format_table_name("stickyban_matched_ckey")] SET exempt = 0 WHERE stickyban = :ckey AND matched_ckey = :alt",
- // list("ckey" = ckey, "alt" = alt)
- // )
- var/datum/DBQuery/query_unexempt_stickyban_alt = SSdbcore.NewQuery(
- "UPDATE [format_table_name("stickyban_matched_ckey")] SET exempt = 0 WHERE stickyban = [ckey] AND matched_ckey = [alt]"
+ var/datum/db_query/query_unexempt_stickyban_alt = SSdbcore.NewQuery(
+ "UPDATE [format_table_name("stickyban_matched_ckey")] SET exempt = 0 WHERE stickyban = :ckey AND matched_ckey = :alt",
+ list("ckey" = ckey, "alt" = alt)
)
query_unexempt_stickyban_alt.warn_execute()
qdel(query_unexempt_stickyban_alt)
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 86ddc24ca3..cd29da186c 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -1269,8 +1269,10 @@
else if(href_list["messageedits"])
if(!check_rights(R_ADMIN))
return
- var/message_id = sanitizeSQL("[href_list["messageedits"]]")
- var/datum/DBQuery/query_get_message_edits = SSdbcore.NewQuery("SELECT edits FROM [format_table_name("messages")] WHERE id = '[message_id]'")
+ var/datum/db_query/query_get_message_edits = SSdbcore.NewQuery(
+ "SELECT edits FROM [format_table_name("messages")] WHERE id = :message_id",
+ list("message_id" = href_list["messageedits"])
+ )
if(!query_get_message_edits.warn_execute())
qdel(query_get_message_edits)
return
@@ -2499,9 +2501,6 @@
break
return
- else if(href_list["secrets"])
- Secrets_topic(href_list["secrets"],href_list)
-
else if(href_list["ac_view_wanted"]) //Admin newscaster Topic() stuff be here
if(!check_rights(R_ADMIN))
return
@@ -2970,16 +2969,19 @@
to_chat(usr, "The client chosen is an admin! Cannot mentorize.")
return
if(SSdbcore.Connect())
- var/datum/DBQuery/query_get_mentor = SSdbcore.NewQuery("SELECT id FROM [format_table_name("mentor")] WHERE ckey = '[ckey]'")
+ var/datum/db_query/query_get_mentor = SSdbcore.NewQuery(
+ "SELECT id FROM [format_table_name("mentor")] WHERE ckey = :ckey",
+ list("ckey" = ckey)
+ )
if(!query_get_mentor.warn_execute())
return
if(query_get_mentor.NextRow())
to_chat(usr, "[ckey] is already a mentor.")
return
- var/datum/DBQuery/query_add_mentor = SSdbcore.NewQuery("INSERT INTO `[format_table_name("mentor")]` (`id`, `ckey`) VALUES (null, '[ckey]')")
+ var/datum/db_query/query_add_mentor = SSdbcore.NewQuery("INSERT INTO `[format_table_name("mentor")]` (`id`, `ckey`) VALUES (null, '[ckey]')")
if(!query_add_mentor.warn_execute())
return
- var/datum/DBQuery/query_add_admin_log = SSdbcore.NewQuery("INSERT INTO `[format_table_name("admin_log")]` (`id` ,`datetime` ,`adminckey` ,`adminip` ,`log` ) VALUES (NULL , NOW( ) , '[usr.ckey]', '[usr.client.address]', 'Added new mentor [ckey]');")
+ var/datum/db_query/query_add_admin_log = SSdbcore.NewQuery("INSERT INTO `[format_table_name("admin_log")]` (`id` ,`datetime` ,`adminckey` ,`adminip` ,`log` ) VALUES (NULL , NOW( ) , '[usr.ckey]', '[usr.client.address]', 'Added new mentor [ckey]');")
if(!query_add_admin_log.warn_execute())
return
else
@@ -3003,10 +3005,10 @@
C.mentor_datum = null
GLOB.mentors -= C
if(SSdbcore.Connect())
- var/datum/DBQuery/query_remove_mentor = SSdbcore.NewQuery("DELETE FROM [format_table_name("mentor")] WHERE ckey = '[ckey]'")
+ var/datum/db_query/query_remove_mentor = SSdbcore.NewQuery("DELETE FROM [format_table_name("mentor")] WHERE ckey = '[ckey]'")
if(!query_remove_mentor.warn_execute())
return
- var/datum/DBQuery/query_add_admin_log = SSdbcore.NewQuery("INSERT INTO `[format_table_name("admin_log")]` (`id` ,`datetime` ,`adminckey` ,`adminip` ,`log` ) VALUES (NULL , NOW( ) , '[usr.ckey]', '[usr.client.address]', 'Removed mentor [ckey]');")
+ var/datum/db_query/query_add_admin_log = SSdbcore.NewQuery("INSERT INTO `[format_table_name("admin_log")]` (`id` ,`datetime` ,`adminckey` ,`adminip` ,`log` ) VALUES (NULL , NOW( ) , '[usr.ckey]', '[usr.client.address]', 'Removed mentor [ckey]');")
if(!query_add_admin_log.warn_execute())
return
else
diff --git a/code/modules/admin/verbs/adminhelp.dm b/code/modules/admin/verbs/adminhelp.dm
index 45d05c70b9..5c35450592 100644
--- a/code/modules/admin/verbs/adminhelp.dm
+++ b/code/modules/admin/verbs/adminhelp.dm
@@ -205,7 +205,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
MessageNoRecipient(msg)
//send it to irc if nobody is on and tell us how many were on
- var/admin_number_present = send2irc_adminless_only(initiator_ckey, "Ticket #[id]: [name]")
+ var/admin_number_present = send2tgs_adminless_only(initiator_ckey, "Ticket #[id]: [name]")
log_admin_private("Ticket #[id]: [key_name(initiator)]: [name] - heard by [admin_number_present] non-AFK admins who have +BAN.")
if(admin_number_present <= 0)
to_chat(C, "No active admins are online, your adminhelp was sent to the admin irc.")
@@ -222,7 +222,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
/datum/admin_help/proc/AddInteraction(formatted_message)
if(heard_by_no_admins && usr && usr.ckey != initiator_ckey)
heard_by_no_admins = FALSE
- send2irc(initiator_ckey, "Ticket #[id]: Answered by [key_name(usr)]")
+ send2adminchat(initiator_ckey, "Ticket #[id]: Answered by [key_name(usr)]")
_interactions += "[TIME_STAMP("hh:mm:ss", FALSE)]: [formatted_message]"
//Removes the ahelp verb and returns it after 2 minutes
@@ -573,7 +573,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
. = list("total" = list(), "noflags" = list(), "afk" = list(), "stealth" = list(), "present" = list())
for(var/client/X in GLOB.admins)
.["total"] += X
- if(requiredflags != 0 && !check_rights_for(X, requiredflags))
+ if(requiredflags != NONE && !check_rights_for(X, requiredflags))
.["noflags"] += X
else if(X.is_afk())
.["afk"] += X
@@ -582,7 +582,7 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
else
.["present"] += X
-/proc/send2irc_adminless_only(source, msg, requiredflags = R_BAN)
+/proc/send2tgs_adminless_only(source, msg, requiredflags = R_BAN)
var/list/adm = get_admin_counts(requiredflags)
var/list/activemins = adm["present"]
. = activemins.len
@@ -596,30 +596,52 @@ GLOBAL_DATUM_INIT(ahelp_tickets, /datum/admin_help_tickets, new)
final = "[msg] - No admins online"
else
final = "[msg] - All admins stealthed\[[english_list(stealthmins)]\], AFK\[[english_list(afkmins)]\], or lacks +BAN\[[english_list(powerlessmins)]\]! Total: [allmins.len] "
- send2irc(source,final)
+ send2adminchat(source,final)
send2otherserver(source,final)
-
-/proc/send2irc(msg,msg2)
- msg = replacetext(replacetext(msg, "\proper", ""), "\improper", "")
- msg2 = replacetext(replacetext(msg2, "\proper", ""), "\improper", "")
- world.TgsTargetedChatBroadcast("[msg] | [msg2]", TRUE)
-
-/proc/send2otherserver(source,msg,type = "Ahelp")
- var/comms_key = CONFIG_GET(string/comms_key)
- if(!comms_key)
+/**
+ * Sends a message to a set of cross-communications-enabled servers using world topic calls
+ *
+ * Arguments:
+ * * source - Who sent this message
+ * * msg - The message body
+ * * type - The type of message, becomes the topic command under the hood
+ * * target_servers - A collection of servers to send the message to, defined in config
+ * * additional_data - An (optional) associated list of extra parameters and data to send with this world topic call
+ */
+/proc/send2otherserver(source, msg, type = "Ahelp", target_servers, list/additional_data = list())
+ if(!CONFIG_GET(string/comms_key))
+ debug_world_log("Server cross-comms message not sent for lack of configured key")
return
- var/list/message = list()
- message["message_sender"] = source
- message["message"] = msg
- message["source"] = "([CONFIG_GET(string/cross_comms_name)])"
- message["key"] = comms_key
- message += type
+
+ var/our_id = CONFIG_GET(string/cross_comms_name)
+ additional_data["message_sender"] = source
+ additional_data["message"] = msg
+ additional_data["source"] = "([our_id])"
+ additional_data += type
var/list/servers = CONFIG_GET(keyed_list/cross_server)
for(var/I in servers)
- world.Export("[servers[I]]?[list2params(message)]")
+ if(I == our_id) //No sending to ourselves
+ continue
+ if(target_servers && !(I in target_servers))
+ continue
+ world.send_cross_comms(I, additional_data)
+/// Sends a message to a given cross comms server by name (by name for security).
+/world/proc/send_cross_comms(server_name, list/message, auth = TRUE)
+ set waitfor = FALSE
+ if (auth)
+ var/comms_key = CONFIG_GET(string/comms_key)
+ if(!comms_key)
+ debug_world_log("Server cross-comms message not sent for lack of configured key")
+ return
+ message["key"] = comms_key
+ var/list/servers = CONFIG_GET(keyed_list/cross_server)
+ var/server_url = servers[server_name]
+ if (!server_url)
+ CRASH("Invalid cross comms config: [server_name]")
+ world.Export("[server_url]?[list2params(message)]")
/proc/ircadminwho()
var/list/message = list("Admins: ")
diff --git a/code/modules/admin/verbs/adminpm.dm b/code/modules/admin/verbs/adminpm.dm
index ab0b0d933a..3a708ef182 100644
--- a/code/modules/admin/verbs/adminpm.dm
+++ b/code/modules/admin/verbs/adminpm.dm
@@ -165,7 +165,7 @@
to_chat(src, "PM to-Admins: [rawmsg]", confidential = TRUE)
var/datum/admin_help/AH = admin_ticket_log(src, "Reply PM from-[key_name(src, TRUE, TRUE)] to External: [keywordparsedmsg]")
ircreplyamount--
- send2irc("[AH ? "#[AH.id] " : ""]Reply: [ckey]", rawmsg)
+ send2adminchat("[AH ? "#[AH.id] " : ""]Reply: [ckey]", rawmsg)
else
var/badmin = FALSE //Lets figure out if an admin is getting bwoinked.
diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm
index 5704448053..29ee35c117 100644
--- a/code/modules/admin/verbs/debug.dm
+++ b/code/modules/admin/verbs/debug.dm
@@ -790,11 +790,12 @@
if(!check_rights(R_DEBUG))
return
- SSmedals.hub_enabled = !SSmedals.hub_enabled
+ SSachievements.achievements_enabled = !SSachievements.achievements_enabled
- message_admins("[key_name_admin(src)] [SSmedals.hub_enabled ? "disabled" : "enabled"] the medal hub lockout.")
+ message_admins("[key_name_admin(src)] [SSachievements.achievements_enabled ? "disabled" : "enabled"] the medal hub lockout.")
SSblackbox.record_feedback("tally", "admin_verb", 1, "Toggle Medal Disable") // If...
- log_admin("[key_name(src)] [SSmedals.hub_enabled ? "disabled" : "enabled"] the medal hub lockout.")
+ log_admin("[key_name(src)] [SSachievements.achievements_enabled ? "disabled" : "enabled"] the medal hub lockout.")
+
/client/proc/view_runtimes()
set category = "Debug"
diff --git a/code/modules/admin/verbs/diagnostics.dm b/code/modules/admin/verbs/diagnostics.dm
index 6f8e9e703f..358e7f1bec 100644
--- a/code/modules/admin/verbs/diagnostics.dm
+++ b/code/modules/admin/verbs/diagnostics.dm
@@ -63,3 +63,31 @@
load_admins()
SSblackbox.record_feedback("tally", "admin_verb", 1, "Reload All Admins") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
message_admins("[key_name_admin(usr)] manually reloaded admins")
+
+/client/proc/toggle_cdn()
+ set name = "Toggle CDN"
+ set category = "Server"
+ var/static/admin_disabled_cdn_transport = null
+ if (alert(usr, "Are you sure you want to toggle the CDN asset transport?", "Confirm", "Yes", "No") != "Yes")
+ return
+ var/current_transport = CONFIG_GET(string/asset_transport)
+ if (!current_transport || current_transport == "simple")
+ if (admin_disabled_cdn_transport)
+ CONFIG_SET(string/asset_transport, admin_disabled_cdn_transport)
+ admin_disabled_cdn_transport = null
+ SSassets.OnConfigLoad()
+ message_admins("[key_name_admin(usr)] re-enabled the CDN asset transport")
+ log_admin("[key_name(usr)] re-enabled the CDN asset transport")
+ else
+ to_chat(usr, "The CDN is not enabled!")
+ if (alert(usr, "The CDN asset transport is not enabled! If you having issues with assets you can also try disabling filename mutations.", "The CDN asset transport is not enabled!", "Try disabling filename mutations", "Nevermind") == "Try disabling filename mutations")
+ SSassets.transport.dont_mutate_filenames = !SSassets.transport.dont_mutate_filenames
+ message_admins("[key_name_admin(usr)] [(SSassets.transport.dont_mutate_filenames ? "disabled" : "re-enabled")] asset filename transforms")
+ log_admin("[key_name(usr)] [(SSassets.transport.dont_mutate_filenames ? "disabled" : "re-enabled")] asset filename transforms")
+ else
+ admin_disabled_cdn_transport = current_transport
+ CONFIG_SET(string/asset_transport, "simple")
+ SSassets.OnConfigLoad()
+ SSassets.transport.dont_mutate_filenames = TRUE
+ message_admins("[key_name_admin(usr)] disabled the CDN asset transport")
+ log_admin("[key_name(usr)] disabled the CDN asset transport")
diff --git a/code/modules/admin/verbs/panicbunker.dm b/code/modules/admin/verbs/panicbunker.dm
index daaf15c70f..92bc840c5a 100644
--- a/code/modules/admin/verbs/panicbunker.dm
+++ b/code/modules/admin/verbs/panicbunker.dm
@@ -13,7 +13,7 @@
if (new_pb && !SSdbcore.Connect())
message_admins("The Database is not connected! Panic bunker will not work until the connection is reestablished.")
SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Panic Bunker", "[new_pb ? "Enabled" : "Disabled"]")) //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
- send2irc("Panic Bunker", "[key_name(usr)] has toggled the Panic Bunker, it is now [new_pb ? "enabled" : "disabled"].")
+ send2adminchat("Panic Bunker", "[key_name(usr)] has toggled the Panic Bunker, it is now [new_pb ? "enabled" : "disabled"].")
/client/proc/addbunkerbypass(ckeytobypass as text)
set category = "Special Verbs"
@@ -28,7 +28,7 @@
SSpersistence.SavePanicBunker() //we can do this every time, it's okay
log_admin("[key_name(usr)] has added [ckeytobypass] to the current round's bunker bypass list.")
message_admins("[key_name_admin(usr)] has added [ckeytobypass] to the current round's bunker bypass list.")
- send2irc("Panic Bunker", "[key_name(usr)] has added [ckeytobypass] to the current round's bunker bypass list.")
+ send2adminchat("Panic Bunker", "[key_name(usr)] has added [ckeytobypass] to the current round's bunker bypass list.")
/client/proc/revokebunkerbypass(ckeytobypass as text)
set category = "Special Verbs"
@@ -42,4 +42,4 @@
SSpersistence.SavePanicBunker()
log_admin("[key_name(usr)] has removed [ckeytobypass] from the current round's bunker bypass list.")
message_admins("[key_name_admin(usr)] has removed [ckeytobypass] from the current round's bunker bypass list.")
- send2irc("Panic Bunker", "[key_name(usr)] has removed [ckeytobypass] from the current round's bunker bypass list.")
+ send2adminchat("Panic Bunker", "[key_name(usr)] has removed [ckeytobypass] from the current round's bunker bypass list.")
diff --git a/code/modules/admin/verbs/pray.dm b/code/modules/admin/verbs/pray.dm
index e532dcc2b2..873007a2a5 100644
--- a/code/modules/admin/verbs/pray.dm
+++ b/code/modules/admin/verbs/pray.dm
@@ -3,7 +3,7 @@
set name = "Pray"
if(GLOB.say_disabled) //This is here to try to identify lag problems
- to_chat(usr, "Speech is currently admin-disabled.")
+ to_chat(usr, "Speech is currently admin-disabled.", confidential = TRUE)
return
msg = copytext_char(sanitize(msg), 1, MAX_MESSAGE_LEN)
@@ -12,7 +12,7 @@
log_prayer("[src.key]/([src.name]): [msg]")
if(usr.client)
if(usr.client.prefs.muted & MUTE_PRAY)
- to_chat(usr, "You cannot pray (muted).")
+ to_chat(usr, "You cannot pray (muted).", confidential = TRUE)
return
if(src.client.handle_spam_prevention(msg,MUTE_PRAY))
return
@@ -44,34 +44,35 @@
for(var/client/C in GLOB.admins)
if(C.prefs.chat_toggles & CHAT_PRAYER)
- to_chat(C, msg)
+ to_chat(C, msg, confidential = TRUE)
if(C.prefs.toggles & SOUND_PRAYERS)
if(usr.job == "Chaplain")
SEND_SOUND(C, sound('sound/effects/pray.ogg'))
- else
- SEND_SOUND(C, sound('sound/effects/ding.ogg'))
- to_chat(usr, "You pray to the gods: \"[msg_tmp]\"")
+ to_chat(usr, "You pray to the gods: \"[msg_tmp]\"", confidential = TRUE)
SSblackbox.record_feedback("tally", "admin_verb", 1, "Prayer") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
//log_admin("HELP: [key_name(src)]: [msg]")
-/proc/CentCom_announce(text , mob/Sender)
+/// Used by communications consoles to message CentCom
+/proc/message_centcom(text, mob/sender)
var/msg = copytext_char(sanitize(text), 1, MAX_MESSAGE_LEN)
- msg = "CENTCOM:[ADMIN_FULLMONTY(Sender)] [ADMIN_CENTCOM_REPLY(Sender)]: [msg]"
- to_chat(GLOB.admins, msg)
- for(var/obj/machinery/computer/communications/C in GLOB.machines)
- C.overrideCooldown()
+ msg = "CENTCOM:[ADMIN_FULLMONTY(sender)] [ADMIN_CENTCOM_REPLY(sender)]: [msg]"
+ to_chat(GLOB.admins, msg, confidential = TRUE)
+ for(var/obj/machinery/computer/communications/console in GLOB.machines)
+ console.override_cooldown()
-/proc/Syndicate_announce(text , mob/Sender)
+/// Used by communications consoles to message the Syndicate
+/proc/message_syndicate(text, mob/sender)
var/msg = copytext_char(sanitize(text), 1, MAX_MESSAGE_LEN)
- msg = "SYNDICATE:[ADMIN_FULLMONTY(Sender)] [ADMIN_SYNDICATE_REPLY(Sender)]: [msg]"
- to_chat(GLOB.admins, msg)
- for(var/obj/machinery/computer/communications/C in GLOB.machines)
- C.overrideCooldown()
+ msg = "SYNDICATE:[ADMIN_FULLMONTY(sender)] [ADMIN_SYNDICATE_REPLY(sender)]: [msg]"
+ to_chat(GLOB.admins, msg, confidential = TRUE)
+ for(var/obj/machinery/computer/communications/console in GLOB.machines)
+ console.override_cooldown()
-/proc/Nuke_request(text , mob/Sender)
+/// Used by communications consoles to request the nuclear launch codes
+/proc/nuke_request(text, mob/sender)
var/msg = copytext_char(sanitize(text), 1, MAX_MESSAGE_LEN)
- msg = "NUKE CODE REQUEST:[ADMIN_FULLMONTY(Sender)] [ADMIN_CENTCOM_REPLY(Sender)] [ADMIN_SET_SD_CODE]: [msg]"
- to_chat(GLOB.admins, msg)
- for(var/obj/machinery/computer/communications/C in GLOB.machines)
- C.overrideCooldown()
+ msg = "NUKE CODE REQUEST:[ADMIN_FULLMONTY(sender)] [ADMIN_CENTCOM_REPLY(sender)] [ADMIN_SET_SD_CODE]: [msg]"
+ to_chat(GLOB.admins, msg, confidential = TRUE)
+ for(var/obj/machinery/computer/communications/console in GLOB.machines)
+ console.override_cooldown()
diff --git a/code/modules/admin/verbs/randomverbs.dm b/code/modules/admin/verbs/randomverbs.dm
index 84702898ec..e5390bc457 100644
--- a/code/modules/admin/verbs/randomverbs.dm
+++ b/code/modules/admin/verbs/randomverbs.dm
@@ -1547,22 +1547,17 @@ GLOBAL_LIST_EMPTY(custom_outfits) //Admin created outfits
msg += "