diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3c666df08fd..5f2580a3a92 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -37,6 +37,18 @@ jobs:
with:
outputFile: output-annotations.txt
+ odlint:
+ name: Lint with OpenDream
+ runs-on: ubuntu-20.04
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup OD
+ run: |
+ bash tools/ci/setup_od.sh
+ - name: Run OD
+ run: |
+ bash tools/ci/run_od.sh
+
compile_all_maps:
name: Compile All Maps
runs-on: ubuntu-20.04
diff --git a/.gitignore b/.gitignore
index 05e36684f47..8940b2c8572 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,3 +55,5 @@ __pycache__/
# Tools some of us like to keep in the repo
dmm-tools.exe
+OpenDream
+paradise.json
diff --git a/__odlint.dm b/__odlint.dm
new file mode 100644
index 00000000000..b61ea68b291
--- /dev/null
+++ b/__odlint.dm
@@ -0,0 +1,16 @@
+// This file is included right at the start of the DME.
+// Its purpose is to enable multiple lints (pragmas) that are supported by OpenDream to better validate the codebase
+// These are essentially nitpicks the DM compiler should pick up on but doesnt
+
+#ifndef SPACEMAN_DMM
+#ifdef OPENDREAM
+/*
+ For the person who asks
+ "AA, why are these in their own file?"
+ Quite simple
+ 1. I can codeown that file
+ 2. You need to do it with an include as a hack to avoid SpacemanDMM evaluating the #pragma lines, even if its outside a block it cares about
+*/
+#include "tools/ci/lints.dm"
+#endif
+#endif
diff --git a/code/__HELPERS/type2type.dm b/code/__HELPERS/type2type.dm
index b38ac58526a..2ca7a083ea9 100644
--- a/code/__HELPERS/type2type.dm
+++ b/code/__HELPERS/type2type.dm
@@ -21,7 +21,7 @@
var/char = copytext(hex, i, i + 1)
switch(char)
if("0")
- //Apparently, switch works with empty statements, yay! If that doesn't work, blame me, though. -- Urist
+ pass() // Do nothing
if("9", "8", "7", "6", "5", "4", "3", "2", "1")
num += text2num(char) * 16 ** power
if("a", "A")
@@ -103,8 +103,6 @@
return "northwest"
if(10.0)
return "southwest"
- else
- return
//Turns text into proper directions
/proc/text2dir(direction)
@@ -125,8 +123,6 @@
return 6
if("SOUTHWEST")
return 10
- else
- return
//Converts an angle (degrees) into an ss13 direction
/proc/angle2dir(degree)
diff --git a/code/controllers/subsystem/SSacid.dm b/code/controllers/subsystem/SSacid.dm
index befc1ebfc62..469cd7d06fc 100644
--- a/code/controllers/subsystem/SSacid.dm
+++ b/code/controllers/subsystem/SSacid.dm
@@ -34,8 +34,7 @@ SUBSYSTEM_DEF(acid)
return
continue
- if(O.acid_level && O.acid_processing())
- else
+ if(!(O.acid_level && O.acid_processing()))
O.cut_overlay(GLOB.acid_overlay, TRUE)
processing -= O
diff --git a/code/game/gamemodes/revolution/revolution.dm b/code/game/gamemodes/revolution/revolution.dm
index e7b67b7a8a6..f17b9ef91f0 100644
--- a/code/game/gamemodes/revolution/revolution.dm
+++ b/code/game/gamemodes/revolution/revolution.dm
@@ -177,7 +177,7 @@
/datum/game_mode/revolution/latespawn(mob/living/carbon/human/player)
..()
var/datum/mind/player_mind = player.mind
- var/static/list/real_command_positions = GLOB.command_positions.Copy() - "Nanotrasen Representative"
+ var/list/real_command_positions = GLOB.command_positions.Copy() - "Nanotrasen Representative"
if(player_mind && (player_mind.assigned_role in real_command_positions))
for(var/datum/mind/rev_mind in head_revolutionaries)
mark_for_death(rev_mind, player_mind)
diff --git a/code/game/machinery/doors/airlock.dm b/code/game/machinery/doors/airlock.dm
index ddca345f091..3e4c50ae4d2 100644
--- a/code/game/machinery/doors/airlock.dm
+++ b/code/game/machinery/doors/airlock.dm
@@ -390,7 +390,6 @@ GLOBAL_LIST_EMPTY(airlock_emissive_underlays)
state = AIRLOCK_CLOSED
else
state = AIRLOCK_OPEN
- if(AIRLOCK_OPEN, AIRLOCK_CLOSED)
if(AIRLOCK_DENY, AIRLOCK_OPENING, AIRLOCK_CLOSING, AIRLOCK_EMAG)
icon_state = "nonexistenticonstate" //MADNESS
diff --git a/code/game/objects/effects/spawners/random_spawners.dm b/code/game/objects/effects/spawners/random_spawners.dm
index 797f9a2f9d5..c920193ccc1 100644
--- a/code/game/objects/effects/spawners/random_spawners.dm
+++ b/code/game/objects/effects/spawners/random_spawners.dm
@@ -23,6 +23,8 @@
var/thing_to_place = pickweight(result)
if(ispath(thing_to_place, /datum/nothing))
// Nothing.
+ qdel(src) // See line 13, this needs moving to /Initialize() so we can use the qdel hint already
+ return
else if(ispath(thing_to_place, /turf))
T.ChangeTurf(thing_to_place)
else
diff --git a/code/game/objects/items/blueprints.dm b/code/game/objects/items/blueprints.dm
index d6ff9c422d5..8f706558445 100644
--- a/code/game/objects/items/blueprints.dm
+++ b/code/game/objects/items/blueprints.dm
@@ -327,8 +327,6 @@
switch(check_tile_is_border(NT,dir))
if(BORDER_NONE)
pending+=NT
- if(BORDER_BETWEEN)
- //do nothing, may be later i'll add 'rejected' list as optimization
if(BORDER_2NDTILE)
found+=NT //tile included to new area, but we dont seek more
if(BORDER_SPACE)
diff --git a/code/game/objects/obj_defense.dm b/code/game/objects/obj_defense.dm
index 0208d9b2750..fbe24478e2f 100644
--- a/code/game/objects/obj_defense.dm
+++ b/code/game/objects/obj_defense.dm
@@ -23,11 +23,8 @@
/obj/proc/run_obj_armor(damage_amount, damage_type, damage_flag = 0, attack_dir, armour_penetration_flat = 0, armour_penetration_percentage = 0)
if(damage_flag == MELEE && damage_amount < damage_deflection)
return 0
- switch(damage_type)
- if(BRUTE)
- if(BURN)
- else
- return 0
+ if(damage_type != BRUTE && damage_type != BURN)
+ return 0
var/armor_protection = 0
if(damage_flag)
armor_protection = armor.getRating(damage_flag)
diff --git a/code/game/objects/structures/crates_lockers/closets/utility_closets.dm b/code/game/objects/structures/crates_lockers/closets/utility_closets.dm
index 3440d6c9d01..d407c451fcc 100644
--- a/code/game/objects/structures/crates_lockers/closets/utility_closets.dm
+++ b/code/game/objects/structures/crates_lockers/closets/utility_closets.dm
@@ -43,12 +43,10 @@
new /obj/item/tank/internals/emergency_oxygen/engi(src)
new /obj/item/clothing/mask/breath(src)
new /obj/item/storage/firstaid/o2(src)
- if("nothing")
- // doot
// teehee - Ah, tg coders...
if("delete")
- qdel(src)
+ qdel(src) // Please make this use init hints its called from Initialize() I beg
/obj/structure/closet/emcloset/legacy/populate_contents()
diff --git a/code/game/objects/structures/transit_tubes/transit_tube.dm b/code/game/objects/structures/transit_tubes/transit_tube.dm
index 9ccd1677ad6..7b09c3d567a 100644
--- a/code/game/objects/structures/transit_tubes/transit_tube.dm
+++ b/code/game/objects/structures/transit_tubes/transit_tube.dm
@@ -286,8 +286,6 @@
return 6
if("SOUTHWEST", "SW")
return 10
- else
- return 0
@@ -311,5 +309,4 @@
return "NW"
if(10)
return "SW"
- else
- return
+
diff --git a/code/game/turfs/simulated/walls.dm b/code/game/turfs/simulated/walls.dm
index ad2514b7a47..87e79f5261e 100644
--- a/code/game/turfs/simulated/walls.dm
+++ b/code/game/turfs/simulated/walls.dm
@@ -187,7 +187,6 @@
switch(severity)
if(1.0)
ChangeTurf(baseturf)
- return
if(2.0)
if(prob(50))
take_damage(rand(150, 250))
@@ -195,8 +194,6 @@
dismantle_wall(1, 1)
if(3.0)
take_damage(rand(0, 250))
- else
- return
/turf/simulated/wall/blob_act(obj/structure/blob/B)
if(prob(50))
diff --git a/code/game/turfs/turf.dm b/code/game/turfs/turf.dm
index e10badc2057..68548657cb9 100644
--- a/code/game/turfs/turf.dm
+++ b/code/game/turfs/turf.dm
@@ -34,8 +34,6 @@
var/blocks_air = FALSE
- var/datum/pathnode/PNode = null //associated PathNode in the A* algorithm
-
flags = 0
var/image/obscured //camerachunks
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index ea7d571f444..8736e5d2526 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -3227,7 +3227,6 @@
SSnightshift.can_fire = FALSE
SSnightshift.update_nightshift(FALSE, FALSE)
to_chat(usr, "Night shift forced off.")
- else
if(usr)
log_admin("[key_name(usr)] used secret [href_list["secretsadmin"]]")
if(ok)
diff --git a/code/modules/awaymissions/maploader/reader.dm b/code/modules/awaymissions/maploader/reader.dm
index c26a57f5bde..ae543a4f967 100644
--- a/code/modules/awaymissions/maploader/reader.dm
+++ b/code/modules/awaymissions/maploader/reader.dm
@@ -312,13 +312,14 @@ GLOBAL_DATUM_INIT(_preloader, /datum/dmm_suite/preloader, new())
var/turf/T = locate(x, y, z)
if(T)
+ // Turfs need special attention
if(ispath(path, /turf))
T.ChangeTurf(path, defer_change = TRUE, keep_icon = FALSE, copy_existing_baseturf = FALSE)
instance = T
- else if(ispath(path, /area))
-
else
- instance = new path(T) // first preloader pass
+ // Anything that isnt an area, init!
+ if(!ispath(path, /area))
+ instance = new path(T) // first preloader pass
if(GLOB.use_preloader && instance) // second preloader pass, for those atoms that don't ..() in New()
GLOB._preloader.load(instance)
diff --git a/code/modules/events/blob/theblob.dm b/code/modules/events/blob/theblob.dm
index 83ce5b73924..4a0081bc9cf 100644
--- a/code/modules/events/blob/theblob.dm
+++ b/code/modules/events/blob/theblob.dm
@@ -220,7 +220,6 @@ GLOBAL_LIST_EMPTY(blob_nodes)
damage_amount *= brute_resist
if(BURN)
damage_amount *= fire_resist
- if(CLONE)
else
return 0
var/armor_protection = 0
diff --git a/code/modules/events/false_alarm.dm b/code/modules/events/false_alarm.dm
index fbaefaa30f0..6d5a84f7361 100644
--- a/code/modules/events/false_alarm.dm
+++ b/code/modules/events/false_alarm.dm
@@ -6,12 +6,12 @@
var/list/event
var/list/minor_fake_events = list(
list("A solar flare has been detected on collision course with the station. Do not conduct space walks or approach windows until the flare has passed!", "Incoming Solar Flare", 'sound/AI/flare.ogg'),
- list("Ionospheric anomalies detected. Temporary telecommunication failure imminent. Please contact you*%fj00)`5vc-BZZT"),
+ list("Ionospheric anomalies detected. Temporary telecommunication failure imminent. Please contact you*%fj00`5vc-BZZT"),
list("Overload detected in [station_name()]'s powernet. Engineering, please repair shorted APCs.", "Systems Power Failure", 'sound/AI/power_overload.ogg'),
list("Localized hyper-energetic flux wave detected on long range scanners. Expected location of impact: Kitchen.", "Anomaly Alert", 'sound/AI/anomaly_flux.ogg'),
list("Overload detected in [station_name()]'s powernet. Engineering, please check all underfloor APC terminals.", "Critical Power Failure", 'sound/AI/power_overload.ogg'),
list("Hostile runtime detected in door controllers. Isolation lockdown protocols are now in effect. Please remain calm.", "Network Alert", 'sound/AI/door_runtimes.ogg'),
- list("Rampant brand intelligence has been detected aboard [station_name()], please stand-by. The origin is believed to be \a vendomat.", "Machine Learning Alert", 'sound/AI/brand_intelligence.ogg'),
+ list("Rampant brand intelligence has been detected aboard [station_name()], please stand-by. The origin is believed to be a vendomat.", "Machine Learning Alert", 'sound/AI/brand_intelligence.ogg'),
list("Massive migration of unknown biological entities has been detected near [station_name()], please stand-by.", "Lifesign Alert"),
list("An electrical storm has been detected in your area, please repair potential electronic overloads.", "Electrical Storm Alert", 'sound/AI/elec_storm.ogg'),
list("What the fuck was that?!", "General Alert"),
@@ -29,11 +29,13 @@
list("The scrubbers network is experiencing a backpressure surge. Some ejection of contents may occur.", "Atmospherics alert", 'sound/AI/scrubbers.ogg'),
list("Space-time anomalies detected on the station. There is no additional data.", "Anomaly Alert", 'sound/AI/spanomalies.ogg')
)
+
var/list/major_fake_events = list(
list("Confirmed outbreak of level 3-X biohazard aboard [station_name()]. All personnel must contain the outbreak.", "Biohazard Alert", 'sound/AI/outbreak3.ogg'),
list("Confirmed outbreak of level 3-S biohazard aboard [station_name()]. All personnel must contain the outbreak.", "Biohazard Alert", 'sound/AI/outbreak3.ogg'),
list("Confirmed outbreak of level 5 biohazard aboard [station_name()]. All personnel must contain the outbreak.", "Biohazard Alert", 'sound/AI/outbreak5.ogg')
)
+
if(prob(90))
event = pick_n_take(minor_fake_events)
GLOB.minor_announcement.Announce(event[1], listgetindex(event, 2), listgetindex(event, 3))
diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm
index 2343058123c..3f2273eedd4 100644
--- a/code/modules/mining/lavaland/necropolis_chests.dm
+++ b/code/modules/mining/lavaland/necropolis_chests.dm
@@ -510,18 +510,21 @@
var/timerid
var/list/input_list = list()
var/list/combo_strings = list()
- var/static/list/combo_list = list(
+ var/list/combo_list = list()
+
+
+/obj/item/cursed_katana/Initialize(mapload)
+ . = ..()
+ AddComponent(/datum/component/parry, _stamina_constant = 2, _stamina_coefficient = 0.5, _parryable_attack_types = NON_PROJECTILE_ATTACKS)
+ combo_list = list(
ATTACK_STRIKE = list(COMBO_STEPS = list(LEFT_SLASH, LEFT_SLASH, RIGHT_SLASH), COMBO_PROC = TYPE_PROC_REF(/obj/item/cursed_katana, strike)),
ATTACK_SLICE = list(COMBO_STEPS = list(RIGHT_SLASH, LEFT_SLASH, LEFT_SLASH), COMBO_PROC = TYPE_PROC_REF(/obj/item/cursed_katana, slice)),
ATTACK_DASH = list(COMBO_STEPS = list(LEFT_SLASH, RIGHT_SLASH, RIGHT_SLASH), COMBO_PROC = TYPE_PROC_REF(/obj/item/cursed_katana, dash)),
ATTACK_CUT = list(COMBO_STEPS = list(RIGHT_SLASH, RIGHT_SLASH, LEFT_SLASH), COMBO_PROC = TYPE_PROC_REF(/obj/item/cursed_katana, cut)),
ATTACK_HEAL = list(COMBO_STEPS = list(LEFT_SLASH, RIGHT_SLASH, LEFT_SLASH, RIGHT_SLASH), COMBO_PROC = TYPE_PROC_REF(/obj/item/cursed_katana, heal)),
ATTACK_SHATTER = list(COMBO_STEPS = list(RIGHT_SLASH, LEFT_SLASH, RIGHT_SLASH, LEFT_SLASH), COMBO_PROC = TYPE_PROC_REF(/obj/item/cursed_katana, shatter)),
- )
+ )
-/obj/item/cursed_katana/Initialize(mapload)
- . = ..()
- AddComponent(/datum/component/parry, _stamina_constant = 2, _stamina_coefficient = 0.5, _parryable_attack_types = NON_PROJECTILE_ATTACKS)
for(var/combo in combo_list)
var/list/combo_specifics = combo_list[combo]
var/step_string = english_list(combo_specifics[COMBO_STEPS])
diff --git a/code/modules/mob/living/carbon/human/human_update_icons.dm b/code/modules/mob/living/carbon/human/human_update_icons.dm
index d061ec01264..3e3f543f6c5 100644
--- a/code/modules/mob/living/carbon/human/human_update_icons.dm
+++ b/code/modules/mob/living/carbon/human/human_update_icons.dm
@@ -350,8 +350,6 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
else
overlays_standing[HEAD_ACCESSORY_LAYER] = mutable_appearance(head_accessory_standing, layer = -HEAD_ACCESSORY_LAYER)
apply_overlay(HEAD_ACCESSORY_LAYER)
- else
- //warning("Invalid ha_style for [species.name]: [ha_style]")
/**
* Generates overlays for the hair layer.
@@ -478,9 +476,6 @@ GLOBAL_LIST_EMPTY(damage_icon_parts)
else
overlays_standing[FHAIR_LAYER] = mutable_appearance(face_standing, layer = -FHAIR_LAYER)
apply_overlay(FHAIR_LAYER)
- else
- //warning("Invalid f_style for [species.name]: [f_style]")
-
/mob/living/carbon/human/update_mutations()
diff --git a/code/modules/mob/living/simple_animal/bot/bot.dm b/code/modules/mob/living/simple_animal/bot/bot.dm
index ff5b3ab9770..7b1870891cc 100644
--- a/code/modules/mob/living/simple_animal/bot/bot.dm
+++ b/code/modules/mob/living/simple_animal/bot/bot.dm
@@ -719,6 +719,7 @@ Pass a positive integer as an argument to override a bot's default speed.
if("home")
to_chat(src, "RETURN HOME!")
if("ejectpai")
+ return // Do nothing for this
else
to_chat(src, "Unidentified control sequence received: [command]")
diff --git a/code/modules/mob/mob_misc_procs.dm b/code/modules/mob/mob_misc_procs.dm
index 2756ebe9c9b..22336ec2649 100644
--- a/code/modules/mob/mob_misc_procs.dm
+++ b/code/modules/mob/mob_misc_procs.dm
@@ -358,13 +358,18 @@
while(counter>=1)
newletter=copytext(phrase,(leng-counter)+1,(leng-counter)+2)
if(newletter in list(" ", "!", "?", ".", ","))
- //do nothing
+ // Skip these
+ counter -= 1
+ continue
+
else if(lowertext(newletter) in list("a", "e", "i", "o", "u", "y"))
newletter = "ph"
+
else
newletter = "m"
- newphrase+="[newletter]"
- counter-=1
+
+ newphrase += "[newletter]"
+ counter -= 1
return newphrase
/proc/muffledspeech_all(list/message_pieces)
diff --git a/code/modules/mob/mob_say_base.dm b/code/modules/mob/mob_say_base.dm
index 58135cceab3..c570e26a116 100644
--- a/code/modules/mob/mob_say_base.dm
+++ b/code/modules/mob/mob_say_base.dm
@@ -167,7 +167,7 @@
prefixes[++prefixes.len] = list(L, i, i + length(selection))
else if(!L && i == 1)
prefixes[++prefixes.len] = list(get_default_language(), i, i)
- else
+
return prefixes
/proc/strip_prefixes(message)
diff --git a/code/modules/reagents/chemistry/reagents/drinks_reagents.dm b/code/modules/reagents/chemistry/reagents/drinks_reagents.dm
index bea7db87eaf..2b62c63e9c3 100644
--- a/code/modules/reagents/chemistry/reagents/drinks_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/drinks_reagents.dm
@@ -70,12 +70,8 @@
var/update_flags = STATUS_UPDATE_NONE
M.AdjustEyeBlurry(-2 SECONDS)
M.AdjustEyeBlind(-2 SECONDS)
- switch(current_cycle)
- if(1 to 20)
- //nothing
- if(21 to INFINITY)
- if(prob(current_cycle - 10))
- update_flags |= M.cure_nearsighted(EYE_DAMAGE, FALSE)
+ if(current_cycle > 20 && prob(current_cycle - 10))
+ update_flags |= M.cure_nearsighted(EYE_DAMAGE, FALSE)
return ..() | update_flags
/datum/reagent/consumable/drink/doctor_delight
diff --git a/code/modules/shuttle/shuttle.dm b/code/modules/shuttle/shuttle.dm
index 27fbe84058f..866b0a5be92 100644
--- a/code/modules/shuttle/shuttle.dm
+++ b/code/modules/shuttle/shuttle.dm
@@ -195,8 +195,7 @@
/obj/docking_port/stationary/transit
name = "In transit"
turf_type = /turf/space/transit
- var/area/shuttle/transit/assigned_area
- lock_shuttle_doors = 1
+ lock_shuttle_doors = TRUE
/obj/docking_port/stationary/transit/register()
if(!..())
diff --git a/code/modules/telesci/telesci_computer.dm b/code/modules/telesci/telesci_computer.dm
index 72b15358ecc..a312399914a 100644
--- a/code/modules/telesci/telesci_computer.dm
+++ b/code/modules/telesci/telesci_computer.dm
@@ -279,14 +279,12 @@
telefail()
temp_msg = "ERROR!
Elevation is less than 1 or greater than 90."
return
- if(z_co == 2 || z_co < 1 || z_co > 6)
- if(z_co == 7 & emagged)
- // This should be empty, allows for it to continue if the z-level is 7 and the machine is emagged.
- else
- telefail()
- temp_msg = "ERROR! Sector is less than 1,
greater than [src.emagged ? "7" : "6"], or equal to 2."
- return
-
+ // THIS FUCKING THING USES ZLEVEL NUMBERS WHY
+ var/cc_z = level_name_to_num(CENTCOMM)
+ if(z_co == cc_z || z_co < cc_z + 1 || cc_z > world.maxz)
+ telefail()
+ temp_msg = "ERROR! Sector must be greater than or equal to 2, and less than or equal to [world.maxz]."
+ return
var/truePower = clamp(power + power_off, 1, 1000)
var/trueRotation = rotation + rotation_off
diff --git a/paradise.dme b/paradise.dme
index 9f9252dffd7..f89f4ed74d5 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -10,6 +10,7 @@
#define DEBUG
// END_PREFERENCES
// BEGIN_INCLUDE
+#include "__odlint.dm"
#include "_maps\__MAP_DEFINES.dm"
#include "_maps\base_map.dm"
#include "_maps\map_datums.dm"
diff --git a/tools/ci/lints.dm b/tools/ci/lints.dm
new file mode 100644
index 00000000000..39c14e1bbc9
--- /dev/null
+++ b/tools/ci/lints.dm
@@ -0,0 +1,27 @@
+//1000-1999
+#pragma FileAlreadyIncluded error
+#pragma MissingIncludedFile error
+#pragma MisplacedDirective error
+#pragma UndefineMissingDirective error
+#pragma DefinedMissingParen error
+#pragma ErrorDirective error
+#pragma WarningDirective error
+#pragma MiscapitalizedDirective error
+
+//2000-2999
+#pragma SoftReservedKeyword error
+#pragma DuplicateVariable error
+#pragma DuplicateProcDefinition error
+#pragma TooManyArguments error
+#pragma PointlessParentCall error
+#pragma PointlessBuiltinCall error
+#pragma SuspiciousMatrixCall error
+#pragma MalformedRange error
+#pragma InvalidRange error
+#pragma InvalidSetStatement error
+#pragma InvalidOverride error
+#pragma DanglingVarType error
+#pragma MissingInterpolatedExpression error
+
+//3000-3999
+#pragma EmptyBlock error
diff --git a/tools/ci/run_od.sh b/tools/ci/run_od.sh
new file mode 100755
index 00000000000..1224de77ef7
--- /dev/null
+++ b/tools/ci/run_od.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+set -eo pipefail
+dotnet OpenDream/DMCompiler/bin/Release/net7.0/DMCompiler.dll --suppress-unimplemented paradise.dme
diff --git a/tools/ci/setup_od.sh b/tools/ci/setup_od.sh
new file mode 100755
index 00000000000..aad45f2bc97
--- /dev/null
+++ b/tools/ci/setup_od.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+set -eo pipefail
+
+git clone https://github.com/OpenDreamProject/OpenDream.git OpenDream
+cd OpenDream
+git submodule update --init --recursive
+dotnet restore
+dotnet build -c Release
diff --git a/tools/ci/unticked_files.py b/tools/ci/unticked_files.py
index 30e290f8141..5ab3f6d41ba 100644
--- a/tools/ci/unticked_files.py
+++ b/tools/ci/unticked_files.py
@@ -26,7 +26,9 @@ INCLUDER_FILES = [
IGNORE_FILES = {
# Included directly in the function /datum/tgs_api/v5#ApiVersion
- 'code/modules/tgs/v5/v5_interop_version.dm'
+ 'code/modules/tgs/v5/v5_interop_version.dm',
+ # Included as part of OD lints
+ 'tools/ci/lints.dm'
}
def get_unticked_files(root:Path):