diff --git a/SQL/database_changelog.txt b/SQL/database_changelog.txt
index e142f20cfab..9835d792f6e 100644
--- a/SQL/database_changelog.txt
+++ b/SQL/database_changelog.txt
@@ -1,3 +1,16 @@
+16th April 2016
+
+Added ipintel table, only required if ip intel is enabled in the config
+
+CREATE TABLE `ipintel` (
+`ip` INT UNSIGNED NOT NULL ,
+`date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL ,
+`intel` REAL NOT NULL DEFAULT '0',
+PRIMARY KEY ( `ip` )
+) ENGINE = INNODB;
+
+---------------------------------------------------
+
21 September 2015, by Jordie0608
Modified table 'poll_question', adding columns 'createdby_ckey', 'createdby_ip' and 'for_trialmin' to bring it inline with the schema used by the tg servers.
diff --git a/SQL/tgstation_schema.sql b/SQL/tgstation_schema.sql
index 34d83537256..72d08af90f7 100644
--- a/SQL/tgstation_schema.sql
+++ b/SQL/tgstation_schema.sql
@@ -364,5 +364,14 @@ CREATE TABLE `notes` (
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
-
+DROP TABLE IF EXISTS `ipintel`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `ipintel` (
+`ip` INT UNSIGNED NOT NULL ,
+`date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL ,
+`intel` REAL NOT NULL DEFAULT '0',
+PRIMARY KEY ( `ip` )
+) ENGINE = INNODB;
+/*!40101 SET character_set_client = @saved_cs_client */;
-- Dump completed on 2013-03-24 18:02:35
diff --git a/SQL/tgstation_schema_prefixed.sql b/SQL/tgstation_schema_prefixed.sql
index fe40d2f72cf..ab49b337cf3 100644
--- a/SQL/tgstation_schema_prefixed.sql
+++ b/SQL/tgstation_schema_prefixed.sql
@@ -359,5 +359,14 @@ CREATE TABLE `SS13_notes` (
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
-
+DROP TABLE IF EXISTS `SS13_ipintel`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `SS13_ipintel` (
+`ip` INT UNSIGNED NOT NULL ,
+`date` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL ,
+`intel` REAL NOT NULL DEFAULT '0',
+PRIMARY KEY ( `ip` )
+) ENGINE = INNODB;
+/*!40101 SET character_set_client = @saved_cs_client */;
-- Dump completed on 2013-03-24 18:02:35
diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm
index 67e4e70422e..f8aec416e19 100644
--- a/code/controllers/configuration.dm
+++ b/code/controllers/configuration.dm
@@ -70,6 +70,13 @@
var/forbid_singulo_possession = 0
var/useircbot = 0
+ //IP Intel vars
+ var/ipintel_email
+ var/ipintel_rating_bad = 1
+ var/ipintel_save_good = 12
+ var/ipintel_save_bad = 1
+ var/ipintel_domain = "check.getipintel.net"
+
var/admin_legacy_system = 0 //Defines whether the server uses the legacy admin system with admins.txt or the SQL system. Config option in config.txt
var/ban_legacy_system = 0 //Defines whether the server uses the legacy banning system with the files in /data or the SQL system. Config option in config.txt
var/use_age_restriction_for_jobs = 0 //Do jobs use account age restrictions? --requires database
@@ -389,6 +396,17 @@
config.notify_new_player_age = text2num(value)
if("irc_first_connection_alert")
config.irc_first_connection_alert = 1
+ if("ipintel_email")
+ if (value != "ch@nge.me")
+ config.ipintel_email = value
+ if("ipintel_rating_bad")
+ config.ipintel_rating_bad = text2num(value)
+ if("ipintel_domain")
+ config.ipintel_domain = value
+ if("ipintel_save_good")
+ config.ipintel_save_good = text2num(value)
+ if("ipintel_save_bad")
+ config.ipintel_save_bad = text2num(value)
if("aggressive_changelog")
config.aggressive_changelog = 1
if("log_runtimes")
diff --git a/code/controllers/subsystem/ipintel.dm b/code/controllers/subsystem/ipintel.dm
new file mode 100644
index 00000000000..b1b99d59392
--- /dev/null
+++ b/code/controllers/subsystem/ipintel.dm
@@ -0,0 +1,19 @@
+var/datum/subsystem/ipintel/SSipintel
+
+/datum/subsystem/ipintel
+ name = "XKeyScore"
+ init_order = -10
+ flags = SS_NO_FIRE
+ var/enabled = 0 //disable at round start to avoid checking reconnects
+ var/throttle = 0
+ var/errors = 0
+
+ var/list/cache = list()
+
+/datum/subsystem/ipintel/New()
+ NEW_SS_GLOBAL(SSipintel)
+
+/datum/subsystem/ipintel/Initialize(timeofday, zlevel)
+ enabled = 1
+ . = ..()
+
diff --git a/code/game/gamemodes/clock_cult/clock_cult.dm b/code/game/gamemodes/clock_cult/clock_cult.dm
index a56ada0193f..8260ff5ff90 100644
--- a/code/game/gamemodes/clock_cult/clock_cult.dm
+++ b/code/game/gamemodes/clock_cult/clock_cult.dm
@@ -83,8 +83,14 @@ This file's folder contains:
M << "You must not involve yourself in other affairs, but... this one... you see it all. Your world glows a brilliant yellow, and all it once it comes to you. \
Ratvar, the Clockwork Justiciar, lies derelict and forgotten in an unseen realm."
var/mob/living/simple_animal/drone/D = M
- D.update_drone_hack(TRUE, TRUE)
- D.languages_spoken |= HUMAN
+ if(!is_eligible_servant(M))
+ if(!silent && !M.stat)
+ D.visible_message("[M] whirs as it resists an outside influence!")
+ M << "Corrupt data purged. Resetting repair processor to factory defaults... complete."
+ return 0
+ else
+ D.update_drone_hack(TRUE, TRUE)
+ D.languages_spoken |= HUMAN
else if(!silent)
M << "Your world glows a brilliant yellow! All at once it comes to you. Ratvar, the Clockwork Justiciar, lies in exile, derelict and forgotten in an unseen realm."
diff --git a/code/game/gamemodes/clock_cult/clock_items.dm b/code/game/gamemodes/clock_cult/clock_items.dm
index c3f7db74900..75051b889f6 100644
--- a/code/game/gamemodes/clock_cult/clock_items.dm
+++ b/code/game/gamemodes/clock_cult/clock_items.dm
@@ -132,19 +132,11 @@
return 1
/obj/item/clockwork/slab/proc/recite_scripture(mob/living/user)
- var/servants = 0
- var/unconverted_ai_exists = FALSE
- for(var/mob/living/M in living_mob_list)
- if(is_servant_of_ratvar(M))
- servants++
- for(var/mob/living/silicon/ai/ai in living_mob_list)
- if(!is_servant_of_ratvar(ai) && ai.client)
- unconverted_ai_exists = TRUE
var/list/tiers_of_scripture = list("Drivers")
- tiers_of_scripture += "Scripts[ratvar_awakens || (servants >= 5 && clockwork_caches >= 1) || no_cost ? "" : " \[LOCKED\]"]"
- tiers_of_scripture += "Applications[ratvar_awakens || (servants >= 8 && clockwork_caches >= 3 && clockwork_construction_value >= 50) || no_cost ? "" : " \[LOCKED\]"]"
- tiers_of_scripture += "Revenant[ratvar_awakens || (servants >= 10 && clockwork_construction_value >= 100) || no_cost ? "" : " \[LOCKED\]"]"
- tiers_of_scripture += "Judgement[ratvar_awakens || (servants >= 10 && clockwork_construction_value >= 100 && !unconverted_ai_exists) || no_cost ? "" : " \[LOCKED\]"]"
+ tiers_of_scripture += "Scripts[ratvar_awakens || scripture_unlock_check(SCRIPTURE_SCRIPT) || no_cost ? "" : " \[LOCKED\]"]"
+ tiers_of_scripture += "Applications[ratvar_awakens || scripture_unlock_check(SCRIPTURE_APPLICATION) || no_cost ? "" : " \[LOCKED\]"]"
+ tiers_of_scripture += "Revenant[ratvar_awakens || scripture_unlock_check(SCRIPTURE_REVENANT) || no_cost ? "" : " \[LOCKED\]"]"
+ tiers_of_scripture += "Judgement[ratvar_awakens || scripture_unlock_check(SCRIPTURE_JUDGEMENT) || no_cost ? "" : " \[LOCKED\]"]"
var/scripture_tier = input(user, "Choose a category of scripture to recite.", "[src]") as null|anything in tiers_of_scripture
if(!scripture_tier || !user.canUseTopic(src))
return 0
@@ -187,11 +179,15 @@
/obj/item/clockwork/slab/proc/show_stats(mob/living/user) //A bit barebones, but there really isn't any more needed
var/servants = 0
+ var/validservants = 0
for(var/mob/living/L in living_mob_list)
if(is_servant_of_ratvar(L))
servants++
+ if(ishuman(L) || issilicon(L))
+ validservants++
user << "State of the Enlightened"
user << "Total servants: [servants]"
+ user << "Servants valid for scripture unlock: [validservants]"
user << "Total construction value: [clockwork_construction_value]"
user << "Total tinkerer's caches: [clockwork_caches]"
user << "Total tinkerer's daemons: [clockwork_daemons] ([servants / 5 < clockwork_daemons ? "DISABLED: Too few servants (5 servants per daemon)!" : "Functioning Normally"])"
diff --git a/code/game/gamemodes/clock_cult/clock_scripture.dm b/code/game/gamemodes/clock_cult/clock_scripture.dm
index 0e302f4ae51..2621c2689ea 100644
--- a/code/game/gamemodes/clock_cult/clock_scripture.dm
+++ b/code/game/gamemodes/clock_cult/clock_scripture.dm
@@ -1162,7 +1162,7 @@ Judgement: 10 servants, 100 CV, and any existing AIs are converted or destroyed
L.fully_heal()
L.stun_absorption = TRUE
L.status_flags |= GODMODE
- animate(invoker, color = initial(invoker.color), time = 150, easing = EASE_IN)
+ animate(L, color = initial(L.color), time = 150, easing = EASE_IN)
affected_servants += L
sleep(150)
for(var/mob/living/L in affected_servants)
diff --git a/code/game/gamemodes/clock_cult/clock_unsorted.dm b/code/game/gamemodes/clock_cult/clock_unsorted.dm
index b688ac714cd..e8fc69c171b 100644
--- a/code/game/gamemodes/clock_cult/clock_unsorted.dm
+++ b/code/game/gamemodes/clock_cult/clock_unsorted.dm
@@ -20,27 +20,6 @@
qdel(F)
return 1
-/proc/generate_cache_component(specific_component_id) //generates a component in the global component cache, either random based on lowest or a specific component
- if(specific_component_id)
- clockwork_component_cache[specific_component_id]++
- else
- var/component_to_generate = get_weighted_component_id()
- clockwork_component_cache[component_to_generate]++
-
-/proc/get_weighted_component_id(obj/item/clockwork/slab/storage_slab) //returns a chosen component id based on the lowest amount of that component
- if(storage_slab)
- return pickweight(list("belligerent_eye" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*(clockwork_component_cache["belligerent_eye"] + storage_slab.stored_components["belligerent_eye"]), 1), \
- "vanguard_cogwheel" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*(clockwork_component_cache["vanguard_cogwheel"] + storage_slab.stored_components["vanguard_cogwheel"]), 1), \
- "guvax_capacitor" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*(clockwork_component_cache["guvax_capacitor"] + storage_slab.stored_components["guvax_capacitor"]), 1), \
- "replicant_alloy" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*(clockwork_component_cache["replicant_alloy"] + storage_slab.stored_components["replicant_alloy"]), 1), \
- "hierophant_ansible" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*(clockwork_component_cache["hierophant_ansible"] + storage_slab.stored_components["hierophant_ansible"]), 1)))
-
- return pickweight(list("belligerent_eye" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*clockwork_component_cache["belligerent_eye"], 1), \
- "vanguard_cogwheel" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*clockwork_component_cache["vanguard_cogwheel"], 1), \
- "guvax_capacitor" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*clockwork_component_cache["guvax_capacitor"], 1), \
- "replicant_alloy" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*clockwork_component_cache["replicant_alloy"], 1), \
- "hierophant_ansible" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*clockwork_component_cache["hierophant_ansible"], 1)))
-
//allows a mob to select a target to gate to
/atom/movable/proc/procure_gateway(mob/living/invoker, time_duration, gateway_uses, two_way)
var/list/possible_targets = list()
@@ -92,6 +71,53 @@
S2.visible_message("The air in front of [target] ripples before suddenly tearing open!")
return 1
+/proc/scripture_unlock_check(scripture_tier) //check if the selected scripture tier is unlocked
+ var/servants = 0
+ var/unconverted_ai_exists = FALSE
+ for(var/mob/living/M in living_mob_list)
+ if(is_servant_of_ratvar(M) && (ishuman(M) || issilicon(M)))
+ servants++
+ for(var/mob/living/silicon/ai/ai in living_mob_list)
+ if(!is_servant_of_ratvar(ai) && ai.client)
+ unconverted_ai_exists = TRUE
+ switch(scripture_tier)
+ if(SCRIPTURE_DRIVER)
+ return 1
+ if(SCRIPTURE_SCRIPT)
+ if(servants >= 5 && clockwork_caches)
+ return 1 //5 or more non-brain servants and any number of clockwork caches
+ if(SCRIPTURE_APPLICATION)
+ if(servants >= 8 && clockwork_caches >= 3 && clockwork_construction_value >= 50)
+ return 1 //8 or more non-brain servants, 3+ clockwork caches, and at least 50 CV
+ if(SCRIPTURE_REVENANT)
+ if(servants >= 10 && clockwork_caches >= 3 && clockwork_construction_value >= 100)
+ return 1 //10 or more non-brain servants, 3+ clockwork caches, and at least 100 CV
+ if(SCRIPTURE_JUDGEMENT)
+ if(servants >= 10 && clockwork_caches >= 3 && clockwork_construction_value >= 100 && !unconverted_ai_exists)
+ return 1 //10 or more non-brain servants, 3+ clockwork caches, at least 100 CV, and there are no living, non-servant ais
+ return 0
+
+/proc/generate_cache_component(specific_component_id) //generates a component in the global component cache, either random based on lowest or a specific component
+ if(specific_component_id)
+ clockwork_component_cache[specific_component_id]++
+ else
+ var/component_to_generate = get_weighted_component_id()
+ clockwork_component_cache[component_to_generate]++
+
+/proc/get_weighted_component_id(obj/item/clockwork/slab/storage_slab) //returns a chosen component id based on the lowest amount of that component
+ if(storage_slab)
+ return pickweight(list("belligerent_eye" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*(clockwork_component_cache["belligerent_eye"] + storage_slab.stored_components["belligerent_eye"]), 1), \
+ "vanguard_cogwheel" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*(clockwork_component_cache["vanguard_cogwheel"] + storage_slab.stored_components["vanguard_cogwheel"]), 1), \
+ "guvax_capacitor" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*(clockwork_component_cache["guvax_capacitor"] + storage_slab.stored_components["guvax_capacitor"]), 1), \
+ "replicant_alloy" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*(clockwork_component_cache["replicant_alloy"] + storage_slab.stored_components["replicant_alloy"]), 1), \
+ "hierophant_ansible" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*(clockwork_component_cache["hierophant_ansible"] + storage_slab.stored_components["hierophant_ansible"]), 1)))
+
+ return pickweight(list("belligerent_eye" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*clockwork_component_cache["belligerent_eye"], 1), \
+ "vanguard_cogwheel" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*clockwork_component_cache["vanguard_cogwheel"], 1), \
+ "guvax_capacitor" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*clockwork_component_cache["guvax_capacitor"], 1), \
+ "replicant_alloy" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*clockwork_component_cache["replicant_alloy"], 1), \
+ "hierophant_ansible" = max(MAX_COMPONENTS_BEFORE_RAND - LOWER_PROB_PER_COMPONENT*clockwork_component_cache["hierophant_ansible"], 1)))
+
/*
The Ratvarian Language
diff --git a/code/game/machinery/porta_turret/portable_turret.dm b/code/game/machinery/porta_turret/portable_turret.dm
index 0927ef42271..8036ba00401 100644
--- a/code/game/machinery/porta_turret/portable_turret.dm
+++ b/code/game/machinery/porta_turret/portable_turret.dm
@@ -706,7 +706,6 @@
pixel_x = (dir & 3)? 0 : (dir == 4 ? -24 : 24)
pixel_y = (dir & 3)? (dir ==1 ? -24 : 24) : 0
power_change() //Checks power and initial settings
- return
/obj/machinery/turretid/initialize() //map-placed turrets autolink turrets
if(control_area && istext(control_area))
@@ -779,13 +778,7 @@
return
user.set_machine(src)
- var/loc = src.loc
- if (istype(loc, /turf))
- loc = loc:loc
- if (!istype(loc, /area))
- user << text("Turret badly positioned - loc.loc is [].", loc)
- return
- var/area/area = loc
+ var/area/area = get_area(src)
var/t = ""
if(src.locked && (!(istype(user, /mob/living/silicon) || IsAdminGhost(user))))
diff --git a/code/modules/admin/IsBanned.dm b/code/modules/admin/IsBanned.dm
index 0d1c1c2370b..75eaeab65fb 100644
--- a/code/modules/admin/IsBanned.dm
+++ b/code/modules/admin/IsBanned.dm
@@ -99,7 +99,6 @@
log_access("Failed Login: [key] [computer_id] [address] - Banned [.["reason"]]")
return .
-
. = ..() //default pager ban stuff
if (.)
//byond will not trigger isbanned() for "global" host bans,
diff --git a/code/modules/admin/ipintel.dm b/code/modules/admin/ipintel.dm
new file mode 100644
index 00000000000..d176de32bb5
--- /dev/null
+++ b/code/modules/admin/ipintel.dm
@@ -0,0 +1,134 @@
+/datum/ipintel
+ var/ip
+ var/intel = 0
+ var/cache = FALSE
+ var/cacheminutesago = 0
+ var/cachedate = ""
+ var/cacherealtime = 0
+
+/datum/ipintel/New()
+ cachedate = SQLtime()
+ cacherealtime = world.realtime
+
+/datum/ipintel/proc/is_valid()
+ . = FALSE
+ if (intel < 0)
+ return
+ if (intel <= config.ipintel_rating_bad)
+ if (world.realtime < cacherealtime+(config.ipintel_save_good*60*60*10))
+ return TRUE
+ else
+ if (world.realtime < cacherealtime+(config.ipintel_save_bad*60*60*10))
+ return TRUE
+
+/proc/get_ip_intel(ip, bypasscache = FALSE, updatecache = TRUE)
+ var/datum/ipintel/res = new()
+ res.ip = ip
+ . = res
+ if (!ip || !config.ipintel_email || !SSipintel.enabled)
+ return
+ if (!bypasscache)
+ var/datum/ipintel/cachedintel = SSipintel.cache[ip]
+ if (cachedintel && cachedintel.is_valid())
+ cachedintel.cache = TRUE
+ return cachedintel
+
+ if (establish_db_connection())
+ var/DBQuery/query = dbcon.NewQuery({"
+ SELECT date, intel, TIMESTAMPDIFF(MINUTE,date,NOW())
+ FROM [format_table_name("ipintel")]
+ WHERE
+ ip = INET_ATON('[ip]')
+ AND ((
+ intel < [config.ipintel_rating_bad]
+ AND
+ date + INTERVAL [config.ipintel_save_good] HOUR > NOW()
+ ) OR (
+ intel >= [config.ipintel_rating_bad]
+ AND
+ date + INTERVAL [config.ipintel_save_bad] HOUR > NOW()
+ ))
+ "})
+ query.Execute()
+ if (query.NextRow())
+ res.cache = TRUE
+ res.cachedate = query.item[1]
+ res.intel = query.item[2]
+ res.cacheminutesago = query.item[3]
+ res.cacherealtime = world.realtime - (query.item[3]*10*60)
+ SSipintel.cache[ip] = res
+ return
+ res.intel = ip_intel_query(ip)
+ if (updatecache && res.intel >= 0)
+ SSipintel.cache[ip] = res
+ if (establish_db_connection())
+ var/DBQuery/query = dbcon.NewQuery("INSERT INTO [format_table_name("ipintel")] (ip, intel) VALUES (INET_ATON('[ip]'), [res.intel]) ON DUPLICATE KEY UPDATE intel = VALUES(intel), date = NOW()")
+ query.Execute()
+ return
+
+
+
+/proc/ip_intel_query(ip, var/retryed=0)
+ . = -1 //default
+ if (!ip)
+ return
+ if (SSipintel.throttle > world.timeofday)
+ return
+ if (!SSipintel.enabled)
+ return
+
+ var/list/http[] = world.Export("http://[config.ipintel_domain]/check.php?ip=[ip]&contact=[config.ipintel_email]&format=json&flags=f")
+
+ if (http)
+ var/status = text2num(http["STATUS"])
+
+ if (status == 200)
+ var/response = json_decode(file2text(http["CONTENT"]))
+ if (response)
+ if (response["status"] == "success")
+ var/intelnum = text2num(response["result"])
+ if (isnum(intelnum))
+ return text2num(response["result"])
+ else
+ ipintel_handle_error("Bad intel from server: [response["result"]].", ip, retryed)
+ if (!retryed)
+ sleep(25)
+ return .(ip, 1)
+ else
+ ipintel_handle_error("Bad response from server: [response["status"]].", ip, retryed)
+ if (!retryed)
+ sleep(25)
+ return .(ip, 1)
+
+ else if (status == 429)
+ ipintel_handle_error("Error #429: We have exceeded the rate limit.", ip, 1)
+ return
+ else
+ ipintel_handle_error("Unknown status code: [status].", ip, retryed)
+ if (!retryed)
+ sleep(25)
+ return .(ip, 1)
+ else
+ ipintel_handle_error("Unable to connect to API.", ip, retryed)
+ if (!retryed)
+ sleep(25)
+ return .(ip, 1)
+
+
+/proc/ipintel_handle_error(error, ip, retryed)
+ if (retryed)
+ SSipintel.errors++
+ error += " Could not check [ip]. Disabling IPINTEL for [SSipintel.errors] minute[( SSipintel.errors == 1 ? "" : "s" )]"
+ SSipintel.throttle = world.timeofday + (10 * 120 * SSipintel.errors)
+ else
+ error += " Attempting retry on [ip]."
+ log_ipintel(error)
+
+/proc/log_ipintel(text)
+ log_game("IPINTEL: [text]")
+ debug_admins("IPINTEL: [text]")
+
+
+
+
+
diff --git a/code/modules/atmospherics/machinery/portable/canister.dm b/code/modules/atmospherics/machinery/portable/canister.dm
index 426730f9851..e711fffbfde 100644
--- a/code/modules/atmospherics/machinery/portable/canister.dm
+++ b/code/modules/atmospherics/machinery/portable/canister.dm
@@ -307,9 +307,10 @@
if(!holding)
var/plasma = air_contents.gases["plasma"]
var/n2o = air_contents.gases["n2o"]
- if(n2o || plasma)
- message_admins("[key_name_admin(usr)] (?) (FLW) opened a canister that contains [n2o ? "N2O" : ""][n2o && plasma ? " & " : ""][plasma ? "Plasma" : ""]! (JMP)")
- log_admin("[key_name(usr)] opened a canister that contains [n2o ? "N2O" : ""][n2o && plasma ? " & " : ""][plasma ? "Plasma" : ""] at [x], [y], [z]")
+ var/bz = air_contents.gases["bz"]
+ if(n2o || plasma || bz)
+ message_admins("[key_name_admin(usr)] (?) (FLW) opened a canister that contains [n2o ? "N2O" : ""][n2o && plasma ? " & " : ""][plasma ? "Plasma" : ""][(n2o || plasma) && bz ? " & " : ""][bz ? "BZ" : ""]! (JMP)")
+ log_admin("[key_name(usr)] opened a canister that contains [n2o ? "N2O" : ""][n2o && plasma ? " & " : ""][plasma ? "Plasma" : ""][(n2o || plasma) && bz ? " & " : ""][bz ? "BZ" : ""] at [x], [y], [z]")
else
logmsg = "Valve was closed by [key_name(usr)], stopping the transfer into \the [holding || "air"].
"
investigate_log(logmsg, "atmos")
diff --git a/code/modules/client/client_defines.dm b/code/modules/client/client_defines.dm
index 78b19cb7198..7443a4a17ed 100644
--- a/code/modules/client/client_defines.dm
+++ b/code/modules/client/client_defines.dm
@@ -45,6 +45,7 @@
// Used by html_interface module.
var/hi_last_pos
+ var/ip_intel = "Disabled"
//datum that controls the displaying and hiding of tooltips
var/datum/tooltip/tooltips
diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm
index a888c025b7e..c9fa33716bf 100644
--- a/code/modules/client/client_procs.dm
+++ b/code/modules/client/client_procs.dm
@@ -210,6 +210,8 @@ var/next_external_rsc = 0
sync_client_with_db()
+ check_ip_intel()
+
send_resources()
if(!void)
@@ -322,6 +324,15 @@ var/next_external_rsc = 0
var/DBQuery/query_accesslog = dbcon.NewQuery("INSERT INTO `[format_table_name("connection_log")]` (`id`,`datetime`,`serverip`,`ckey`,`ip`,`computerid`) VALUES(null,Now(),'[serverip]','[sql_ckey]','[sql_ip]','[sql_computerid]');")
query_accesslog.Execute()
+/client/proc/check_ip_intel()
+ set waitfor = 0 //we sleep when getting the intel, no need to hold up the client connection while we sleep
+ if (config.ipintel_email)
+ var/datum/ipintel/res = get_ip_intel(address)
+ if (res.intel >= config.ipintel_rating_bad)
+ message_admins("Proxy Detection: [key_name_admin(src)] IP intel rated [res.intel*100]% likely to be a Proxy/VPN.")
+ ip_intel = res.intel
+
+
/client/proc/add_verbs_from_config()
if(config.see_own_notes)
verbs += /client/proc/self_notes
diff --git a/code/modules/client/preferences_toggles.dm b/code/modules/client/preferences_toggles.dm
index 965e60ddc1f..3b61fb2dedb 100644
--- a/code/modules/client/preferences_toggles.dm
+++ b/code/modules/client/preferences_toggles.dm
@@ -235,7 +235,7 @@ var/global/list/ghost_forms = list("ghost","ghostking","ghostian2","skeleghost",
"ghost_blue","ghost_yellow","ghost_green","ghost_pink", \
"ghost_cyan","ghost_dblue","ghost_dred","ghost_dgreen", \
"ghost_dcyan","ghost_grey","ghost_dyellow","ghost_dpink", "ghost_purpleswirl","ghost_funkypurp","ghost_pinksherbert","ghost_blazeit",\
- "ghost_mellow","ghost_rainbow","ghost_camo","ghost_fire")
+ "ghost_mellow","ghost_rainbow","ghost_camo","ghost_fire", "catghost")
/client/proc/pick_form()
if(!is_content_unlocked())
alert("This setting is for accounts with BYOND premium only.")
diff --git a/code/modules/hydroponics/grown/ambrosia.dm b/code/modules/hydroponics/grown/ambrosia.dm
index dc810aba585..ef576982bed 100644
--- a/code/modules/hydroponics/grown/ambrosia.dm
+++ b/code/modules/hydroponics/grown/ambrosia.dm
@@ -53,21 +53,23 @@
//Ambrosia Gaia
/obj/item/seeds/ambrosia/gaia
name = "pack of ambrosia gaia seeds"
- desc = "These seeds grow into ambrosia gaia, incredibly rare but imbued with breathtaking potential."
+ desc = "These seeds grow into ambrosia gaia, filled with infinite potential."
icon_state = "seed-ambrosia_gaia"
species = "ambrosia_gaia"
plantname = "Ambrosia Gaia"
product = /obj/item/weapon/reagent_containers/food/snacks/grown/ambrosia/gaia
mutatelist = list()
- reagents_add = list("earthsblood" = 0.4, "nutriment" = 0.2, "vitamin" = 0.1)
- rarity = 100 //These are some pretty good plants right here
+ reagents_add = list("earthsblood" = 0.05, "nutriment" = 0.06, "vitamin" = 0.05)
+ rarity = 30 //These are some pretty good plants right here
oneharvest = TRUE
+ weed_rate = 4
+ weed_chance = 100
/obj/item/weapon/reagent_containers/food/snacks/grown/ambrosia/gaia
- name = "ambrosia gaia"
- desc = "The bringer of light."
+ name = "ambrosia gaia branch"
+ desc = "Eating this makes you immortal."
icon_state = "ambrosia_gaia"
filling_color = rgb(255, 175, 0)
- origin_tech = "biotech=7"
+ origin_tech = "biotech=6;materials=5"
luminosity = 3
seed = /obj/item/seeds/ambrosia/gaia
diff --git a/code/modules/hydroponics/grown/berries.dm b/code/modules/hydroponics/grown/berries.dm
index d3eaac3ea7c..3ac1d6c11e8 100644
--- a/code/modules/hydroponics/grown/berries.dm
+++ b/code/modules/hydroponics/grown/berries.dm
@@ -136,50 +136,6 @@
filling_color = "#6495ED"
bitesize_mod = 2
-//Cherry Bombs
-/obj/item/seeds/cherry/bomb
- name = "pack of cherry bomb pits"
- desc = "They give you vibes of dread and frustration."
- icon_state = "seed-cherry_bomb"
- species = "cherry_bomb"
- plantname = "Cherry Bomb Tree"
- product = /obj/item/weapon/reagent_containers/food/snacks/grown/cherry_bomb
- mutatelist = list()
- reagents_add = list("nutriment" = 0.1, "sugar" = 0.1, "blackpowder" = 0.1)
- rarity = 25
-
-/obj/item/weapon/reagent_containers/food/snacks/grown/cherry_bomb
- name = "cherry bombs"
- desc = "You think you can hear the hissing of a tiny fuse."
- icon_state = "cherry_bomb"
- filling_color = rgb(20, 20, 20)
- seed = /obj/item/seeds/cherry/bomb
- bitesize_mod = 2
-
-/obj/item/weapon/reagent_containers/food/snacks/grown/cherry_bomb/attack_self(mob/living/user)
- var/area/A = get_area(user)
- user.visible_message("[user] plucks the stem from [src]!", "You pluck the stem from [src], which begins to hiss loudly!")
- message_admins("[user] ([user.key ? user.key : "no key"]) primed a cherry bomb for detonation at [A] ([user.x], [user.y], [user.z]) (JMP)")
- log_game("[user] ([user.key ? user.key : "no key"]) primed a cherry bomb for detonation at [A] ([user.x],[user.y],[user.z]).")
- prime()
-
-/obj/item/weapon/reagent_containers/food/snacks/grown/cherry_bomb/burn()
- prime()
- ..()
-
-/obj/item/weapon/reagent_containers/food/snacks/grown/cherry_bomb/proc/prime()
- icon_state = "cherry_bomb_lit"
- playsound(src, 'sound/effects/fuse.ogg', seed.potency, 0)
- sleep(30)
- if(!src)
- return
- var/d_strength = round(seed.potency / 100)
- var/h_strength = round(seed.potency / 50)
- var/l_strength = round(seed.potency / 20)
- var/f_strength = l_strength
- explosion(get_turf(src), d_strength, h_strength, l_strength, f_strength)
- qdel(src)
-
// Grapes
/obj/item/seeds/grape
name = "pack of grape seeds"
diff --git a/code/modules/hydroponics/grown/misc.dm b/code/modules/hydroponics/grown/misc.dm
index 34bb0d6a5d3..4504d1740ba 100644
--- a/code/modules/hydroponics/grown/misc.dm
+++ b/code/modules/hydroponics/grown/misc.dm
@@ -94,3 +94,44 @@
origin_tech = "combat=6"
trash = /obj/item/weapon/gun/projectile/revolver
bitesize_mod = 2
+
+//Cherry Bombs
+/obj/item/seeds/cherry/bomb
+ name = "pack of cherry bomb pits"
+ desc = "They give you vibes of dread and frustration."
+ icon_state = "seed-cherry_bomb"
+ species = "cherry_bomb"
+ plantname = "Cherry Bomb Tree"
+ product = /obj/item/weapon/reagent_containers/food/snacks/grown/cherry_bomb
+ mutatelist = list()
+ reagents_add = list("nutriment" = 0.1, "sugar" = 0.1, "blackpowder" = 0.7)
+ rarity = 60 //See above
+
+/obj/item/weapon/reagent_containers/food/snacks/grown/cherry_bomb
+ name = "cherry bombs"
+ desc = "You think you can hear the hissing of a tiny fuse."
+ icon_state = "cherry_bomb"
+ filling_color = rgb(20, 20, 20)
+ seed = /obj/item/seeds/cherry/bomb
+ bitesize_mod = 2
+ volume = 125 //Gives enough room for the black powder at max potency
+
+/obj/item/weapon/reagent_containers/food/snacks/grown/cherry_bomb/attack_self(mob/living/user)
+ var/area/A = get_area(user)
+ user.visible_message("[user] plucks the stem from [src]!", "You pluck the stem from [src], which begins to hiss loudly!")
+ message_admins("[user] ([user.key ? user.key : "no key"]) primed a cherry bomb for detonation at [A] ([user.x], [user.y], [user.z]) (JMP)")
+ log_game("[user] ([user.key ? user.key : "no key"]) primed a cherry bomb for detonation at [A] ([user.x],[user.y],[user.z]).")
+ prime()
+
+/obj/item/weapon/reagent_containers/food/snacks/grown/cherry_bomb/burn()
+ prime()
+ ..()
+
+/obj/item/weapon/reagent_containers/food/snacks/grown/cherry_bomb/ex_act(severity)
+ qdel(src) //Ensuring that it's deleted by its own explosion
+
+/obj/item/weapon/reagent_containers/food/snacks/grown/cherry_bomb/proc/prime()
+ icon_state = "cherry_bomb_lit"
+ playsound(src, 'sound/effects/fuse.ogg', seed.potency, 0)
+ reagents.chem_temp = 1000 //Sets off the black powder
+ reagents.handle_reactions()
diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm
index ef3645353b8..f3ede6629ea 100644
--- a/code/modules/hydroponics/hydroponics.dm
+++ b/code/modules/hydroponics/hydroponics.dm
@@ -80,7 +80,7 @@
if(istype(I, /obj/item/weapon/crowbar))
if(using_irrigation)
- user << "Unscrew the hoses first!"
+ user << "Disconnect the hoses first!"
else if(default_deconstruction_crowbar(I, 1))
return
else
@@ -168,10 +168,12 @@
if(waterlevel <= 0)
adjustHealth(-rand(0,2) / rating)
- // Sufficient water level and nutrient level = plant healthy
+ // Sufficient water level and nutrient level = plant healthy but also spawns weeds
else if(waterlevel > 10 && nutrilevel > 0)
adjustHealth(rand(1,2) / rating)
- if(prob(5)) //5 percent chance the weed population will increase
+ if(myseed && prob(myseed.weed_chance))
+ adjustWeeds(myseed.weed_rate)
+ else if(prob(5)) //5 percent chance the weed population will increase
adjustWeeds(1 / rating)
//Toxins/////////////////////////////////////////////////////////////////
@@ -316,9 +318,9 @@
if(myseed)
user << "It has [myseed.plantname] planted."
if (dead)
- user << "It's dead."
+ user << "It's dead!"
else if (harvest)
- user << "It's ready to harvest."
+ user << "It's ready to harvest."
else if (health <= (myseed.endurance / 2))
user << "It looks unhealthy."
else
@@ -328,7 +330,7 @@
user << "Water: [waterlevel]/[maxwater]"
user << "Nutrient: [nutrilevel]/[maxnutri]"
else
- user << "It doesn't require any maintenance."
+ user << "It doesn't require any water or nutrients."
if(weedlevel >= 5)
user << "[src] is filled with weeds!"
@@ -345,7 +347,7 @@
qdel(myseed)
myseed = null
else
- oldPlantName = "Empty tray"
+ oldPlantName = "empty tray"
switch(rand(1,18)) // randomly pick predominative weed
if(16 to 18)
myseed = new /obj/item/seeds/reishi(src)
@@ -370,7 +372,7 @@
weedlevel = 0 // Reset
pestlevel = 0 // Reset
update_icon()
- visible_message("[oldPlantName] overtaken by [myseed.plantname].")
+ visible_message("The [oldPlantName] is overtaken by some [myseed.plantname]!")
/obj/machinery/hydroponics/proc/mutate(lifemut = 2, endmut = 5, productmut = 1, yieldmut = 2, potmut = 25) // Mutates the current seed
@@ -404,7 +406,7 @@
sleep(5) // Wait a while
update_icon()
- visible_message("[oldPlantName] suddenly mutated into [myseed.plantname]!")
+ visible_message("[oldPlantName] suddenly mutates into [myseed.plantname]!")
/obj/machinery/hydroponics/proc/mutateweed() // If the weeds gets the mutagent instead. Mind you, this pretty much destroys the old plant
@@ -424,9 +426,9 @@
sleep(5) // Wait a while
update_icon()
- visible_message("The mutated weeds in [src] spawned a [myseed.plantname]!")
+ visible_message("The mutated weeds in [src] spawn some [myseed.plantname]!")
else
- usr << "The few weeds in [src] seem to react, but only for a moment..."
+ usr << "The few weeds in [src] seem to react, but only for a moment..."
/obj/machinery/hydroponics/proc/plantdies() // OH NOES!!!!! I put this all in one function to make things easier
@@ -440,12 +442,12 @@
/obj/machinery/hydroponics/proc/mutatepest()
if(pestlevel > 5)
- visible_message("The pests seem to behave oddly...")
+ visible_message("The pests seem to behave oddly...")
for(var/i=0, i<3, i++)
var/obj/effect/spider/spiderling/S = new(src.loc)
S.grow_as = /mob/living/simple_animal/hostile/poison/giant_spider/hunter
else
- usr << "The pests seem to behave oddly, but quickly settle down..."
+ usr << "The pests seem to behave oddly, but quickly settle down..."
/obj/machinery/hydroponics/proc/applyChemicals(datum/reagents/S)
if(myseed)
@@ -463,13 +465,13 @@
if(41 to 65)
mutate()
if(21 to 41)
- usr << "The plants don't seem to react..."
+ usr << "The plants don't seem to react..."
if(11 to 20)
mutateweed()
if(1 to 10)
mutatepest()
else
- usr << "Nothing happens..."
+ usr << "Nothing happens..."
// 2 or 1 units is enough to change the yield and other stats.// Can change the yield and other stats, but requires more than mutagen
else if(S.has_reagent("mutagen", 2) || S.has_reagent("radium", 5) || S.has_reagent("uranium", 5))
@@ -667,7 +669,7 @@
if(1 to 32)
mutatepest()
else
- usr << "Nothing happens..."
+ usr << "Nothing happens..."
/obj/machinery/hydroponics/attackby(obj/item/O, mob/user, params)
//Called when mob user "attacks" it with object O
@@ -803,7 +805,7 @@
else if(istype(O, /obj/item/weapon/wrench) && unwrenchable)
if(using_irrigation)
- user << "Unscrew the hoses first!"
+ user << "Disconnect the hoses first!"
return
if(!anchored && !isinspace())
@@ -848,7 +850,7 @@
if(myseed) //Could be that they're just using it as a de-weeder
qdel(myseed)
myseed = null
- weedlevel = 0 //Side-effect of cleaning up those nasty weeds
+ weedlevel = 0 //Has a side effect of cleaning up those nasty weeds
update_icon()
else
@@ -929,7 +931,7 @@
return // Has no lights
/obj/machinery/hydroponics/soil/attackby(obj/item/O, mob/user, params)
- if(istype(O, /obj/item/weapon/shovel) && !istype(O, /obj/item/weapon/shovel/spade))
+ if(istype(O, /obj/item/weapon/shovel) && !istype(O, /obj/item/weapon/shovel/spade)) //Doesn't include spades because of uprooting plants
user << "You clear up [src]!"
qdel(src)
else
diff --git a/code/modules/hydroponics/seeds.dm b/code/modules/hydroponics/seeds.dm
index aa7f785b152..e85418941d0 100644
--- a/code/modules/hydroponics/seeds.dm
+++ b/code/modules/hydroponics/seeds.dm
@@ -37,6 +37,9 @@
var/innate_yieldmod = 1 //modifier for yield, seperate to the one in Hydro trays, as that one is SPECIFICALLY for nutriment/chems (which means it's constantly reset)
//This is added onto the yield mod of the hydro tray, yield *= (parent.yieldmod+innate_yieldmod)
+ var/weed_rate = 1 //If the chance below passes, then this many weeds sprout during growth
+ var/weed_chance = 5 //Percentage chance per tray update to grow weeds
+
/obj/item/seeds/New(loc, nogenes = 0)
..()
pixel_x = rand(-8, 8)
diff --git a/code/modules/mining/fulton.dm b/code/modules/mining/fulton.dm
index 09c612b4f18..08da1d69739 100644
--- a/code/modules/mining/fulton.dm
+++ b/code/modules/mining/fulton.dm
@@ -58,6 +58,7 @@ var/list/total_extraction_beacons = list()
else
if(!safe_for_living_creatures && check_for_living_mobs(A))
user << "[src] is not safe for use with living creatures, they wouldn't survive the trip back!"
+ return
if(A.loc == user || A == user) // no extracting stuff you're holding in your hands/yourself
return
if(A.anchored)
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm b/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
index e32df3acb9b..5f46a35cf42 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
+++ b/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
@@ -45,6 +45,7 @@
staticOverlays = list()
hud_possible = list(DIAG_STAT_HUD, DIAG_HUD, ANTAG_HUD)
unique_name = TRUE
+ faction = list("silicon")
var/staticChoice = "static"
var/list/staticChoices = list("static", "blank", "letter", "animal")
var/picked = FALSE //Have we picked our visual appearence (+ colour if applicable)
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/say.dm b/code/modules/mob/living/simple_animal/friendly/drone/say.dm
index ce32409fff8..8d72c6eadf8 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/say.dm
+++ b/code/modules/mob/living/simple_animal/friendly/drone/say.dm
@@ -1,4 +1,3 @@
-
/////////////
//DRONE SAY//
/////////////
@@ -23,5 +22,10 @@
/mob/living/simple_animal/drone/proc/drone_chat(msg)
- var/rendered = "DRONE CHAT: [name]: [msg]"
+ var/rendered = "Drone Chat: \
+ [name]: \
+ [msg]"
alert_drones(rendered, 1)
+
+/mob/living/simple_animal/drone/binarycheck()
+ return TRUE
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/megafauna.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/megafauna.dm
index 2c2a48a2d61..bc6dd31cdd4 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/megafauna.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/megafauna.dm
@@ -12,6 +12,7 @@
atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
minbodytemp = 0
maxbodytemp = INFINITY
+ anchored = TRUE
layer = LARGE_MOB_LAYER //Looks weird with them slipping under mineral walls and cameras and shit otherwise
/mob/living/simple_animal/hostile/megafauna/death(gibbed)
diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
index 7d5fe115568..46bc5472217 100644
--- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
@@ -1003,7 +1003,7 @@ datum/reagent/medicine/syndicate_nanites/on_mob_life(mob/living/M)
..()
. = 1
-/datum/reagent/medicine/earthsblood
+/datum/reagent/medicine/earthsblood //Created by ambrosia gaia plants
name = "Earthsblood"
id = "earthsblood"
description = "Ichor from an extremely powerful plant. Great for restoring wounds, but it's a little heavy on the brain."
@@ -1015,11 +1015,11 @@ datum/reagent/medicine/syndicate_nanites/on_mob_life(mob/living/M)
M.adjustFireLoss(-3 * REM, 0)
M.adjustOxyLoss(-15 * REM, 0)
M.adjustToxLoss(-3 * REM, 0)
- M.adjustBrainLoss(0.5 * REM) //This does, after all, come from ambrosia
+ M.adjustBrainLoss(2 * REM) //This does, after all, come from ambrosia, and the most powerful ambrosia in existence, at that!
M.adjustCloneLoss(-1 * REM, 0)
M.adjustStaminaLoss(-30 * REM, 0)
M.jitteriness = min(max(0, M.jitteriness + 3), 30)
- M.druggy = min(max(0, M.druggy + 3), 15) //See above
+ M.druggy = min(max(0, M.druggy + 10), 15) //See above
..()
. = 1
diff --git a/config/config.txt b/config/config.txt
index 5cbfcd0c5b0..d04cd64cd3d 100644
--- a/config/config.txt
+++ b/config/config.txt
@@ -111,6 +111,19 @@ HOSTEDBY Yournamehere
## Uncomment this to stop people connecting to your server without a registered ckey. (i.e. guest-* are all blocked from connecting)
GUEST_BAN
+### IPINTEL:
+### This allows you to detect likely proxies by checking ips against getipintel.net
+## Rating to warn at: (0.90 is good, 1 is 100% likely to be a spammer/proxy, 0.8 is 80%, etc) anything equal to or higher then this number triggers an admin warning
+#IPINTEL_RATING_BAD 0.90
+## Contact email, (required to use the service, leaving blank or default disables IPINTEL)
+#IPINTEL_EMAIL ch@nge.me
+## How long to save good matches (ipintel rate limits to 15 per minute and 500 per day. so this shouldn't be too low, getipintel.net suggests 6 hours, time is in hours) (Your ip will get banned if you go over 500 a day too many times)
+#IPINTEL_SAVE_GOOD 12
+## How long to save bad matches (these numbers can change as ips change hands, best not to save these for too long in case somebody gets a new ip used by a spammer/proxy before.)
+#IPINTEL_SAVE_BAD 3
+## Domain name to query (leave commented out for the default, only needed if you pay getipintel.net for more querys)
+#IPINTEL_DOMAIN check.getipintel.net
+
## Uncomment to allow web client connections
#ALLOW_WEBCLIENT
diff --git a/html/changelogs/AutoChangeLog-pr-18636.yml b/html/changelogs/AutoChangeLog-pr-18636.yml
new file mode 100644
index 00000000000..63b7219dad5
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-18636.yml
@@ -0,0 +1,4 @@
+author: "coiax"
+delete-after: True
+changes:
+ - rscadd: "Drones can hear robotic talk, but cannot communicate on it. AIs and cyborgs are encouraged to share information with station repair drones."
diff --git a/html/changelogs/AutoChangeLog-pr-18669.yml b/html/changelogs/AutoChangeLog-pr-18669.yml
new file mode 100644
index 00000000000..c47fea8e108
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-18669.yml
@@ -0,0 +1,4 @@
+author: "Joan"
+delete-after: True
+changes:
+ - rscdel: "Clock cult scripture unlock now only counts humans and silicons."
diff --git a/html/changelogs/AutoChangeLog-pr-18675.yml b/html/changelogs/AutoChangeLog-pr-18675.yml
new file mode 100644
index 00000000000..a397fba7d15
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-18675.yml
@@ -0,0 +1,4 @@
+author: "coiax"
+delete-after: True
+changes:
+ - rscdel: "AI turrets no longer fire at drones."
diff --git a/icons/mob/head.dmi b/icons/mob/head.dmi
index 3d7dd56ecec..35204a7ea8f 100644
Binary files a/icons/mob/head.dmi and b/icons/mob/head.dmi differ
diff --git a/icons/mob/mob.dmi b/icons/mob/mob.dmi
index d0d78177369..16d3103afd9 100644
Binary files a/icons/mob/mob.dmi and b/icons/mob/mob.dmi differ
diff --git a/tgstation.dme b/tgstation.dme
index 94ba8781c4a..301e7c4f1bd 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -136,6 +136,7 @@
#include "code\controllers\subsystem\fastprocess.dm"
#include "code\controllers\subsystem\garbage.dm"
#include "code\controllers\subsystem\icon_smooth.dm"
+#include "code\controllers\subsystem\ipintel.dm"
#include "code\controllers\subsystem\jobs.dm"
#include "code\controllers\subsystem\lighting.dm"
#include "code\controllers\subsystem\machines.dm"
@@ -833,6 +834,7 @@
#include "code\modules\admin\create_poll.dm"
#include "code\modules\admin\create_turf.dm"
#include "code\modules\admin\holder2.dm"
+#include "code\modules\admin\ipintel.dm"
#include "code\modules\admin\IsBanned.dm"
#include "code\modules\admin\NewBan.dm"
#include "code\modules\admin\player_panel.dm"