diff --git a/.github/workflows/changelog_generation.yml b/.github/workflows/changelog_generation.yml index c5e27896f1c..0d5c44c5182 100644 --- a/.github/workflows/changelog_generation.yml +++ b/.github/workflows/changelog_generation.yml @@ -31,5 +31,6 @@ jobs: run: | git config --global user.email "${{ secrets.BOT_EMAIL }}" git config --global user.name "${{ secrets.BOT_NAME }}" + git diff --quiet --exit-code && echo "No changes found, abort." && exit 0 git commit -m "Automatic changelog generation" -a git push diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5163a859f2e..06875666dc1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ on: env: BYOND_MAJOR: "512" BYOND_MINOR: "1485" - SPACEMAN_DMM_VERSION: suite-1.5 + SPACEMAN_DMM_VERSION: suite-1.7 jobs: DreamChecker: @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f - name: Setup Cache - uses: actions/cache@1a9e2138d905efd099035b49d8b7a3888c653ca8 + uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 with: path: $HOME/spaceman_dmm/$SPACEMAN_DMM_VERSION key: ${{ runner.os }}-spacemandmm-${{ env.SPACEMAN_DMM_VERSION }} @@ -42,7 +42,7 @@ jobs: steps: - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f - name: Setup Cache - uses: actions/cache@1a9e2138d905efd099035b49d8b7a3888c653ca8 + uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 with: path: $HOME/BYOND-${BYOND_MAJOR}.${BYOND_MINOR} key: ${{ runner.os }}-byond-${{ env.BYOND_MAJOR }}-${{ env.BYOND_MINOR }} @@ -71,7 +71,7 @@ jobs: steps: - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f - name: Setup Cache - uses: actions/cache@1a9e2138d905efd099035b49d8b7a3888c653ca8 + uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 with: path: $HOME/BYOND-${BYOND_MAJOR}.${BYOND_MINOR} key: ${{ runner.os }}-byond-${{ env.BYOND_MAJOR }}-${{ env.BYOND_MINOR }} diff --git a/.gitignore b/.gitignore index 57fa28b45a8..51b8ab89874 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ Thumbs.db *.backup *.before data/ +dmdoc/ cfg/ build_log.txt use_map @@ -20,8 +21,10 @@ atupdate config/* sql/test_db -# vscode +# VisualStudioCode .vscode/* +!.vscode/settings.json +*.code-workspace .history # swap @@ -39,7 +42,6 @@ Session.vim # auto-generated tag files tags -baystation12.code-workspace # ignore built libs lib/*.dll diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000000..d08913c25b6 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "files.eol": "\r\n", + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.trimTrailingWhitespace": true, + "gitblame.commitUrl": "https://github.com/baystation12/baystation12/commit/${hash}" +} diff --git a/SpacemanDMM.toml b/SpacemanDMM.toml index b8274722546..085babef829 100644 --- a/SpacemanDMM.toml +++ b/SpacemanDMM.toml @@ -1,2 +1,5 @@ [langserver] dreamchecker = true + +[debugger] +engine = "auxtools" diff --git a/baystation12.dme b/baystation12.dme index 1ad954194da..fea22a8046d 100644 --- a/baystation12.dme +++ b/baystation12.dme @@ -99,6 +99,7 @@ #include "code\_helpers\atmospherics.dm" #include "code\_helpers\atom_movables.dm" #include "code\_helpers\builtin_proc_callers.dm" +#include "code\_helpers\client.dm" #include "code\_helpers\cmp.dm" #include "code\_helpers\functional.dm" #include "code\_helpers\game.dm" @@ -114,6 +115,7 @@ #include "code\_helpers\names.dm" #include "code\_helpers\sanitize_values.dm" #include "code\_helpers\spawn_sync.dm" +#include "code\_helpers\species.dm" #include "code\_helpers\storage.dm" #include "code\_helpers\text.dm" #include "code\_helpers\time.dm" @@ -173,6 +175,8 @@ #include "code\controllers\evacuation\evacuation_predicate.dm" #include "code\controllers\evacuation\evacuation_shuttle.dm" #include "code\controllers\evacuation\~evac.dm" +#include "code\controllers\subsystems\ai.dm" +#include "code\controllers\subsystems\aifast.dm" #include "code\controllers\subsystems\air.dm" #include "code\controllers\subsystems\alarm.dm" #include "code\controllers\subsystems\antags.dm" @@ -245,6 +249,7 @@ #include "code\datums\hierarchy.dm" #include "code\datums\local_network.dm" #include "code\datums\mil_ranks.dm" +#include "code\datums\mutable_appearance.dm" #include "code\datums\progressbar.dm" #include "code\datums\recipe.dm" #include "code\datums\ruins.dm" @@ -440,6 +445,7 @@ #include "code\datums\underwear\undershirt.dm" #include "code\datums\underwear\underwear.dm" #include "code\datums\uplink\ammunition.dm" +#include "code\datums\uplink\augments.dm" #include "code\datums\uplink\badassery.dm" #include "code\datums\uplink\devices and tools.dm" #include "code\datums\uplink\grenades.dm" @@ -845,6 +851,7 @@ #include "code\game\objects\items\contraband.dm" #include "code\game\objects\items\crayons.dm" #include "code\game\objects\items\cryobag.dm" +#include "code\game\objects\items\dehydrated_carp.dm" #include "code\game\objects\items\documents.dm" #include "code\game\objects\items\glassjar.dm" #include "code\game\objects\items\holosign_creator.dm" @@ -1226,6 +1233,7 @@ #include "code\modules\admin\buildmode\_click_handler.dm" #include "code\modules\admin\buildmode\_color_pool.dm" #include "code\modules\admin\buildmode\_overlay.dm" +#include "code\modules\admin\buildmode\ai.dm" #include "code\modules\admin\buildmode\areas.dm" #include "code\modules\admin\buildmode\atmosphere.dm" #include "code\modules\admin\buildmode\build.dm" @@ -1306,6 +1314,22 @@ #include "code\modules\admin\view_variables\view_variables.dm" #include "code\modules\admin\view_variables\view_variables_global.dm" #include "code\modules\admin\view_variables\vv_set_handlers.dm" +#include "code\modules\ai\_defines.dm" +#include "code\modules\ai\ai_holder.dm" +#include "code\modules\ai\ai_holder_combat.dm" +#include "code\modules\ai\ai_holder_combat_unseen.dm" +#include "code\modules\ai\ai_holder_communication.dm" +#include "code\modules\ai\ai_holder_cooperation.dm" +#include "code\modules\ai\ai_holder_debug.dm" +#include "code\modules\ai\ai_holder_disabled.dm" +#include "code\modules\ai\ai_holder_fleeing.dm" +#include "code\modules\ai\ai_holder_follow.dm" +#include "code\modules\ai\ai_holder_movement.dm" +#include "code\modules\ai\ai_holder_pathfinding.dm" +#include "code\modules\ai\ai_holder_targeting.dm" +#include "code\modules\ai\interfaces.dm" +#include "code\modules\ai\say_list.dm" +#include "code\modules\ai\aI_holder_subtypes\simple_mob_ai.dm" #include "code\modules\alarm\alarm.dm" #include "code\modules\alarm\alarm_handler.dm" #include "code\modules\alarm\atmosphere_alarm.dm" @@ -1373,15 +1397,26 @@ #include "code\modules\atmospherics\components\unary\vent_scrubber.dm" #include "code\modules\augment\active.dm" #include "code\modules\augment\augment.dm" +#include "code\modules\augment\cbm.dm" +#include "code\modules\augment\equip.dm" #include "code\modules\augment\simple.dm" +#include "code\modules\augment\active\adaptive_binoculars.dm" #include "code\modules\augment\active\armblades.dm" #include "code\modules\augment\active\circuit.dm" +#include "code\modules\augment\active\corrective_lenses.dm" +#include "code\modules\augment\active\glare_dampeners.dm" #include "code\modules\augment\active\hudimplants.dm" +#include "code\modules\augment\active\iatric_monitor.dm" +#include "code\modules\augment\active\internal_air_system.dm" +#include "code\modules\augment\active\leukocyte_breeder.dm" +#include "code\modules\augment\active\nerve_dampeners.dm" #include "code\modules\augment\active\polytool.dm" +#include "code\modules\augment\active\popout_shotgun.dm" #include "code\modules\augment\active\tool\engineering.dm" #include "code\modules\augment\active\tool\surgical.dm" #include "code\modules\augment\passive\armor.dm" #include "code\modules\augment\passive\boost.dm" +#include "code\modules\augment\passive\fluff.dm" #include "code\modules\augment\passive\nanoaura.dm" #include "code\modules\augment\passive\boost\muscle.dm" #include "code\modules\augment\passive\boost\reflex.dm" @@ -1433,6 +1468,7 @@ #include "code\modules\client\preference_setup\loadout\gear_tweaks.dm" #include "code\modules\client\preference_setup\loadout\loadout.dm" #include "code\modules\client\preference_setup\loadout\lists\accessories.dm" +#include "code\modules\client\preference_setup\loadout\lists\augments.dm" #include "code\modules\client\preference_setup\loadout\lists\clothing.dm" #include "code\modules\client\preference_setup\loadout\lists\earwear.dm" #include "code\modules\client\preference_setup\loadout\lists\eyegear.dm" @@ -1548,6 +1584,7 @@ #include "code\modules\clothing\under\accessories\stethoscope.dm" #include "code\modules\clothing\under\accessories\storage.dm" #include "code\modules\clothing\under\accessories\ties.dm" +#include "code\modules\clothing\under\accessories\wristwatches.dm" #include "code\modules\clothing\under\jobs\civilian.dm" #include "code\modules\clothing\under\jobs\engineering.dm" #include "code\modules\clothing\under\jobs\medsci.dm" @@ -1569,6 +1606,7 @@ #include "code\modules\codex\entries\_codex_entry.dm" #include "code\modules\codex\entries\ascent.dm" #include "code\modules\codex\entries\atmospherics.dm" +#include "code\modules\codex\entries\augments.dm" #include "code\modules\codex\entries\clothing.dm" #include "code\modules\codex\entries\codex.dm" #include "code\modules\codex\entries\engineering.dm" @@ -1690,7 +1728,6 @@ #include "code\modules\events\event.dm" #include "code\modules\events\event_container.dm" #include "code\modules\events\event_dynamic.dm" -#include "code\modules\events\exo_awaken.dm" #include "code\modules\events\gravity.dm" #include "code\modules\events\grid_check.dm" #include "code\modules\events\infestation.dm" @@ -1713,6 +1750,8 @@ #include "code\modules\events\toilets.dm" #include "code\modules\events\wallrot.dm" #include "code\modules\events\whale_migration.dm" +#include "code\modules\events\exo_awakening\_datums.dm" +#include "code\modules\events\exo_awakening\exo_awaken.dm" #include "code\modules\ext_scripts\irc.dm" #include "code\modules\fabrication\__fabricator_defines.dm" #include "code\modules\fabrication\_fabricator.dm" @@ -2169,6 +2208,9 @@ #include "code\modules\mob\living\silicon\robot\modules\module_security.dm" #include "code\modules\mob\living\silicon\robot\modules\module_standard.dm" #include "code\modules\mob\living\silicon\robot\modules\module_uncertified.dm" +#include "code\modules\mob\living\simple_animal\combat.dm" +#include "code\modules\mob\living\simple_animal\defense.dm" +#include "code\modules\mob\living\simple_animal\life.dm" #include "code\modules\mob\living\simple_animal\natural_weapons.dm" #include "code\modules\mob\living\simple_animal\shade.dm" #include "code\modules\mob\living\simple_animal\simple_animal.dm" @@ -2188,6 +2230,7 @@ #include "code\modules\mob\living\simple_animal\constructs\soulstone.dm" #include "code\modules\mob\living\simple_animal\crow\crow.dm" #include "code\modules\mob\living\simple_animal\familiars\familiars.dm" +#include "code\modules\mob\living\simple_animal\friendly\_friendly.dm" #include "code\modules\mob\living\simple_animal\friendly\cat.dm" #include "code\modules\mob\living\simple_animal\friendly\corgi.dm" #include "code\modules\mob\living\simple_animal\friendly\crab.dm" @@ -2208,7 +2251,6 @@ #include "code\modules\mob\living\simple_animal\hostile\drake.dm" #include "code\modules\mob\living\simple_animal\hostile\faithful_hound.dm" #include "code\modules\mob\living\simple_animal\hostile\faithless.dm" -#include "code\modules\mob\living\simple_animal\hostile\giant_spider.dm" #include "code\modules\mob\living\simple_animal\hostile\hivebot.dm" #include "code\modules\mob\living\simple_animal\hostile\hostile.dm" #include "code\modules\mob\living\simple_animal\hostile\leech.dm" @@ -2222,9 +2264,11 @@ #include "code\modules\mob\living\simple_animal\hostile\vagrant.dm" #include "code\modules\mob\living\simple_animal\hostile\voxslug.dm" #include "code\modules\mob\living\simple_animal\hostile\commanded\_command_defines.dm" -#include "code\modules\mob\living\simple_animal\hostile\commanded\bear_companion.dm" -#include "code\modules\mob\living\simple_animal\hostile\commanded\commanded.dm" -#include "code\modules\mob\living\simple_animal\hostile\commanded\nanomachines.dm" +#include "code\modules\mob\living\simple_animal\hostile\giant_spider\_giant_spider.dm" +#include "code\modules\mob\living\simple_animal\hostile\giant_spider\guard.dm" +#include "code\modules\mob\living\simple_animal\hostile\giant_spider\hunter.dm" +#include "code\modules\mob\living\simple_animal\hostile\giant_spider\nurse.dm" +#include "code\modules\mob\living\simple_animal\hostile\giant_spider\spitter.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\clown.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\drone.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\exoplanet.dm" @@ -2991,6 +3035,7 @@ #include "code\modules\tables\rack.dm" #include "code\modules\tables\tables.dm" #include "code\modules\tables\update_triggers.dm" +#include "code\modules\tension\tension.dm" #include "code\modules\turbolift\_turbolift.dm" #include "code\modules\turbolift\turbolift.dm" #include "code\modules\turbolift\turbolift_areas.dm" diff --git a/code/__defines/_planes+layers.dm b/code/__defines/_planes+layers.dm index 54bc990aef6..b16c78e2fea 100644 --- a/code/__defines/_planes+layers.dm +++ b/code/__defines/_planes+layers.dm @@ -192,8 +192,15 @@ What is the naming convention for planes or layers? //This is difference between planes used for atoms and effects #define PLANE_DIFFERENCE 3 -/atom - plane = DEFAULT_PLANE + +/atom/plane = DEFAULT_PLANE + +#define DEFAULT_APPEARANCE_FLAGS (PIXEL_SCALE) + +/atom/appearance_flags = DEFAULT_APPEARANCE_FLAGS +/image/appearance_flags = DEFAULT_APPEARANCE_FLAGS +/mutable_appearance/appearance_flags = DEFAULT_APPEARANCE_FLAGS //Inherits /image but re docs, subject to change + /image/proc/plating_decal_layerise() plane = DEFAULT_PLANE @@ -216,7 +223,7 @@ What is the naming convention for planes or layers? */ /obj/screen/plane_master - appearance_flags = PLANE_MASTER + appearance_flags = DEFAULT_APPEARANCE_FLAGS | PLANE_MASTER screen_loc = "CENTER,CENTER" globalscreen = 1 @@ -226,7 +233,7 @@ What is the naming convention for planes or layers? /obj/screen/plane_master/ghost_dummy // this avoids a bug which means plane masters which have nothing to control get angry and mess with the other plane masters out of spite alpha = 0 - appearance_flags = 0 + appearance_flags = DEFAULT_APPEARANCE_FLAGS plane = OBSERVER_PLANE GLOBAL_LIST_INIT(ghost_master, list( diff --git a/code/__defines/damage_organs.dm b/code/__defines/damage_organs.dm index b53134d9a84..778a6357836 100644 --- a/code/__defines/damage_organs.dm +++ b/code/__defines/damage_organs.dm @@ -83,8 +83,8 @@ // that dealing just enough burn damage to kill the player will cause the given // proportion of their max blood volume to be lost // (e.g. 0.6 == 60% lost if 200 burn damage is taken). -#define FLUIDLOSS_WIDE_BURN 0.6 //for burns from heat applied over a wider area, like from fire -#define FLUIDLOSS_CONC_BURN 0.4 //for concentrated burns, like from lasers +#define FLUIDLOSS_WIDE_BURN 0.15 //for burns from heat applied over a wider area, like from fire +#define FLUIDLOSS_CONC_BURN 0.1 //for concentrated burns, like from lasers // Damage above this value must be repaired with surgery. #define ROBOLIMB_SELF_REPAIR_CAP 30 @@ -103,4 +103,3 @@ #define BLOOD_VOLUME_OKAY 70 #define BLOOD_VOLUME_BAD 60 #define BLOOD_VOLUME_SURVIVE 30 - diff --git a/code/__defines/lists.dm b/code/__defines/lists.dm index b67c4d5347b..9dd0fd472d4 100644 --- a/code/__defines/lists.dm +++ b/code/__defines/lists.dm @@ -57,3 +57,43 @@ __BIN_MID = __BIN_ITEM.##COMPARE > IN.##COMPARE ? __BIN_MID : __BIN_MID + 1;\ LIST.Insert(__BIN_MID, IN);\ } + +/// Passed into BINARY_INSERT to compare keys +#define COMPARE_KEY __BIN_LIST[__BIN_MID] +/// Passed into BINARY_INSERT to compare values +#define COMPARE_VALUE __BIN_LIST[__BIN_LIST[__BIN_MID]] + +/**** + * Binary search sorted insert from TG + * INPUT: Object to be inserted + * LIST: List to insert object into + * TYPECONT: The typepath of the contents of the list + * COMPARE: The object to compare against, usualy the same as INPUT + * COMPARISON: The variable on the objects to compare + * COMPTYPE: How should the values be compared? Either COMPARE_KEY or COMPARE_VALUE. + */ +#define BINARY_INSERT_TG(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \ + do {\ + var/list/__BIN_LIST = LIST;\ + var/__BIN_CTTL = length(__BIN_LIST);\ + if(!__BIN_CTTL) {\ + __BIN_LIST += INPUT;\ + } else {\ + var/__BIN_LEFT = 1;\ + var/__BIN_RIGHT = __BIN_CTTL;\ + var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + var ##TYPECONT/__BIN_ITEM;\ + while(__BIN_LEFT < __BIN_RIGHT) {\ + __BIN_ITEM = COMPTYPE;\ + if(__BIN_ITEM.##COMPARISON <= COMPARE.##COMPARISON) {\ + __BIN_LEFT = __BIN_MID + 1;\ + } else {\ + __BIN_RIGHT = __BIN_MID;\ + };\ + __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + };\ + __BIN_ITEM = COMPTYPE;\ + __BIN_MID = __BIN_ITEM.##COMPARISON > COMPARE.##COMPARISON ? __BIN_MID : __BIN_MID + 1;\ + __BIN_LIST.Insert(__BIN_MID, INPUT);\ + };\ + } while(FALSE) diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm index b4958c8a575..c076730e675 100644 --- a/code/__defines/misc.dm +++ b/code/__defines/misc.dm @@ -5,6 +5,7 @@ #define TRANSITIONEDGE 7 // Distance from edge to move to another z-level. #define RUIN_MAP_EDGE_PAD 15 +#define LANDING_ZONE_RADIUS 15 // Used for autoplacing landmarks on exoplanets // Invisibility constants. #define INVISIBILITY_LIGHTING 20 diff --git a/code/__defines/mobs.dm b/code/__defines/mobs.dm index 235b4a56258..64f38ccab15 100644 --- a/code/__defines/mobs.dm +++ b/code/__defines/mobs.dm @@ -9,6 +9,7 @@ #define CANPARALYSE 0x4 #define CANPUSH 0x8 #define PASSEMOTES 0x10 // Mob has a cortical borer or holders inside of it that need to see emotes. +#define LEAPING 0x16 #define GODMODE 0x1000 #define FAKEDEATH 0x2000 // Replaces stuff like changeling.changeling_fakedeath. #define NO_ANTAG 0x4000 // Players are restricted from gaining antag roles when occupying this mob @@ -35,12 +36,23 @@ #define BORGXRAY 0x4 #define BORGMATERIAL 8 -#define HOSTILE_STANCE_IDLE 1 -#define HOSTILE_STANCE_ALERT 2 -#define HOSTILE_STANCE_ATTACK 3 -#define HOSTILE_STANCE_ATTACKING 4 -#define HOSTILE_STANCE_TIRED 5 -#define HOSTILE_STANCE_INSIDE 6 + +#define STANCE_SLEEP 0 // Doing (almost) nothing, to save on CPU because nobody is around to notice or the mob died. +#define STANCE_IDLE 1 // The more or less default state. Wanders around, looks for baddies, and spouts one-liners. +#define STANCE_ALERT 2 // A baddie is visible but not too close, and essentially we tell them to go away or die. +#define STANCE_APPROACH 3 // Attempting to get into range to attack them. +#define STANCE_FIGHT 4 // Actually fighting, with melee or ranged. +#define STANCE_BLINDFIGHT 5 // Fighting something that cannot be seen by the mob, from invisibility or out of sight. +#define STANCE_REPOSITION 6 // Relocating to a better position while in combat. Also used when moving away from a danger like grenades. +#define STANCE_MOVE 7 // Similar to above but for out of combat. If a baddie is seen, they'll cancel and fight them. +#define STANCE_FOLLOW 8 // Following somone, without trying to murder them. +#define STANCE_FLEE 9 // Run away from the target because they're too spooky/we're dying/some other reason. +#define STANCE_DISABLED 10 // Used when the holder is afflicted with certain status effects, such as stuns or confusion. + +#define STANCE_ATTACK 11 // Backwards compatability +#define STANCE_ATTACKING 12 // Ditto + +#define STANCES_COMBAT list(STANCE_ALERT, STANCE_APPROACH, STANCE_FIGHT, STANCE_BLINDFIGHT, STANCE_REPOSITION) #define LEFT 0x1 #define RIGHT 0x2 @@ -97,6 +109,14 @@ #define APPEARANCE_COMMON (APPEARANCE_DNA2|APPEARANCE_RACE|APPEARANCE_GENDER|APPEARANCE_SKIN|APPEARANCE_ALL_HAIR|APPEARANCE_EYES|APPEARANCE_LANG) + +// /sprite_accessory flags +#define DO_COLORATION_USER 1 //! Allow a user to set their own sprite_accessory color; tattoos, etc +#define DO_COLORATION_SKIN 2 //! Take a coloration cue from skin tone +#define DO_COLORATION_HAIR 4 //! Take a coloration cue from hair color +#define DO_COLORATION_AUTO 6 //! Use hair if available, otherwise skin + + // Click cooldown #define DEFAULT_ATTACK_COOLDOWN 8 //Default timeout for aggressive actions #define DEFAULT_QUICK_COOLDOWN 4 @@ -359,6 +379,28 @@ #define MOB_FLAG_HOLY_BAD 0x001 // If this mob is allergic to holiness +// More refined version of SA_* ""intelligence"" seperators. +// Now includes bitflags, so to target two classes you just do 'MOB_CLASS_ANIMAL|MOB_CLASS_HUMANOID' +#define MOB_CLASS_NONE 0 // Default value, and used to invert for _ALL. + +#define MOB_CLASS_PLANT 1 // Unused at the moment. +#define MOB_CLASS_ANIMAL 2 // Animals and beasts like spiders, saviks, and bears. +#define MOB_CLASS_HUMANOID 4 // Non-robotic humanoids, including /simple_mob and /carbon/humans and their alien variants. +#define MOB_CLASS_SYNTHETIC 8 // Silicons, mechanical simple mobs, FBPs, and anything else that would pass is_synthetic() +#define MOB_CLASS_SLIME 16 // Everyone's favorite xenobiology specimen (and maybe prometheans?). +#define MOB_CLASS_ABERRATION 32 // Weird shit. +#define MOB_CLASS_DEMONIC 64 // Cult stuff. +#define MOB_CLASS_BOSS 128 // Future megafauna hopefully someday. +#define MOB_CLASS_ILLUSION 256 // Fake mobs, e.g. Technomancer illusions. +#define MOB_CLASS_PHOTONIC 512 // Holographic mobs like holocarp, similar to _ILLUSION, but that make no attempt to hide their true nature. + +#define MOB_CLASS_ALL (~MOB_CLASS_NONE) + +// For slime commanding. Higher numbers allow for more actions. +#define SLIME_COMMAND_OBEY 1 // When disciplined. +#define SLIME_COMMAND_FACTION 2 // When in the same 'faction'. +#define SLIME_COMMAND_FRIEND 3 // When befriended with a slime friendship agent. + #define MARKING_TARGET_SKIN 0 // Draw a datum/sprite_accessory/marking to the mob's body, eg. tattoos #define MARKING_TARGET_HAIR 1 // Draw a datum/sprite_accessory/marking to the mob's hair, eg. color fades #define MARKING_TARGET_HEAD 2 // Draw a datum/sprite_accessory/marking to the mob's head after their hair, eg. ears, horns @@ -391,3 +433,5 @@ #define DO_MISSING_USER (-1) #define DO_MISSING_TARGET (-2) #define DO_INCAPACITATED (-3) + +#define FAKE_INVIS_ALPHA_THRESHOLD 127 // If something's alpha var is at or below this number, certain things will pretend it is invisible. diff --git a/code/__defines/spaceman_dmm.dm b/code/__defines/spaceman_dmm.dm index 247d28d359c..51b09cd036e 100644 --- a/code/__defines/spaceman_dmm.dm +++ b/code/__defines/spaceman_dmm.dm @@ -1,7 +1,8 @@ -// Interfaces for the SpacemanDMM linter, define'd to nothing when the linter -// is not in use. +/** +* SpacemanDMM dreamchecker extensions for suite 1.7 +* +*/ -// The SPACEMAN_DMM define is set by the linter and other tooling when it runs. #ifdef SPACEMAN_DMM #define RETURN_TYPE(X) set SpacemanDMM_return_type = X #define SHOULD_CALL_PARENT(X) set SpacemanDMM_should_call_parent = X @@ -11,6 +12,7 @@ #define SHOULD_BE_PURE(X) set SpacemanDMM_should_be_pure = X #define PRIVATE_PROC(X) set SpacemanDMM_private_proc = X #define PROTECTED_PROC(X) set SpacemanDMM_protected_proc = X + #define CAN_BE_REDEFINED(X) set SpacemanDMM_can_be_redefined = X #define VAR_FINAL var/SpacemanDMM_final #define VAR_PRIVATE var/SpacemanDMM_private #define VAR_PROTECTED var/SpacemanDMM_protected @@ -23,6 +25,7 @@ #define SHOULD_BE_PURE(X) #define PRIVATE_PROC(X) #define PROTECTED_PROC(X) + #define CAN_BE_REDEFINED(X) #define VAR_FINAL var #define VAR_PRIVATE var #define VAR_PROTECTED var diff --git a/code/__defines/subsystem-priority.dm b/code/__defines/subsystem-priority.dm index 66c93ec0e54..995b9713e76 100644 --- a/code/__defines/subsystem-priority.dm +++ b/code/__defines/subsystem-priority.dm @@ -24,6 +24,7 @@ #define SS_PRIORITY_RADIATION 20 // Radiation processing and cache updates. #define SS_PRIORITY_OPEN_SPACE 20 // Open turf updates. #define SS_PRIORITY_AIRFLOW 15 // Object movement from ZAS airflow. +#define SS_PRIORITY_AI 15 // Mob AI #define SS_PRIORITY_VOTE 10 // Vote management. #define SS_PRIORITY_INACTIVITY 10 // Idle kicking. #define SS_PRIORITY_SUPPLY 10 // Supply point accumulation. diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm index 964949c89d1..dd35eb2e691 100644 --- a/code/__defines/subsystems.dm +++ b/code/__defines/subsystems.dm @@ -61,6 +61,8 @@ return;\ #define SS_INIT_XENOARCH -10 #define SS_INIT_BAY_LEGACY -12 #define SS_INIT_TICKER -20 +#define SS_INIT_AI -21 +#define SS_INIT_AIFAST -22 #define SS_INIT_CHAT -90 // Should be lower to ensure chat remains smooth during init. #define SS_INIT_UNIT_TESTS -100 diff --git a/code/__defines/zmimic.dm b/code/__defines/zmimic.dm index 5f48020fb44..517d7522783 100644 --- a/code/__defines/zmimic.dm +++ b/code/__defines/zmimic.dm @@ -8,7 +8,7 @@ #define ZM_ALLOW_LIGHTING 4 // If this turf should permit passage of lighting. #define ZM_ALLOW_ATMOS 8 // If this turf permits passage of air. #define ZM_MIMIC_NO_AO 16 // If the turf shouldn't apply regular turf AO and only do Z-mimic AO. -#define ZM_FIX_BIGTURF 32 // Fix bigturf (greater than world.icon_size) rendering at the cost of breaking object layering a bit. This flag is infectious, so all Z-turfs above this one will also get this flag. Valid on non-zturfs. +#define ZM_NO_OCCLUDE 32 // Don't occlude below atoms if we're a non-mimic z-turf. // Convenience flag. #define ZM_MIMIC_DEFAULTS (ZM_MIMIC_BELOW|ZM_ALLOW_LIGHTING) @@ -20,5 +20,5 @@ var/list/mimic_defines = list( "ZM_ALLOW_LIGHTING", "ZM_ALLOW_ATMOS", "ZM_MIMIC_NO_AO", - "ZM_FIX_BIGTURF" + "ZM_NO_OCCLUDE" ) diff --git a/code/_helpers/client.dm b/code/_helpers/client.dm new file mode 100644 index 00000000000..fa038aa9e55 --- /dev/null +++ b/code/_helpers/client.dm @@ -0,0 +1,51 @@ +/* + Helpers related to /client +*/ + + +/// Duck check to see if text looks like a ckey +/proc/valid_ckey(text) + var/static/regex/matcher = new (@"^[a-z0-9]{1,30}$") + return regex_find(matcher, text) + + +/// Duck check to see if text looks like a key +/proc/valid_key(text) + var/static/regex/matcher = new (@"^[0-9A-Za-z][0-9A-Za-z_\. -]{2,29}$") + return regex_find(matcher, text) + + +/// Get the client associated with ckey text if it is currently connected +/proc/ckey2client(text) + if (valid_ckey(text)) + FOR_BLIND(client/C, GLOB.clients) + if (C.ckey == text) + return C + + +/// Get the client associated with key text if it is currently connected +/proc/key2client(text) + if (valid_key(text)) + FOR_BLIND(client/C, GLOB.clients) + if (C.key == text) + return C + + +/// Null, or a client if thing is a client, a mob with a client, a connected ckey, or null +/proc/resolve_client(client/thing) + if (!thing) + return usr + if (istype(thing)) + return thing + if (ismob(thing)) + var/mob/M = thing + return M.client + return ckey2client(thing) + + +/// Null or a client from the list of connected clients, chosen by actor if actor is valid +/proc/select_client(client/actor, message = "Connected clients:", title = "Select Client") + actor = resolve_client(actor) + if (!actor) + return + return input(actor, message, title) as null | anything in GLOB.clients diff --git a/code/_helpers/global_access.dm b/code/_helpers/global_access.dm index 4a477b5e867..83bfd8465f9 100644 --- a/code/_helpers/global_access.dm +++ b/code/_helpers/global_access.dm @@ -31,6 +31,10 @@ return global.Master; if("OOClog") return global.OOClog; + if("SSai") + return global.SSai; + if("SSaifast") + return global.SSaifast; if("SSair") return global.SSair; if("SSairflow") @@ -906,6 +910,10 @@ global.Master=newval; if("OOClog") global.OOClog=newval; + if("SSai") + global.SSai=newval; + if("SSaifast") + global.SSaifast=newval; if("SSair") global.SSair=newval; if("SSairflow") @@ -1765,6 +1773,8 @@ "LIGHTING_CORNER_DIAGONAL", "Master", "OOClog", + "SSai", + "SSaifast", "SSair", "SSairflow", "SSalarm", diff --git a/code/_helpers/lists.dm b/code/_helpers/lists.dm index 9ab46f91a2a..60f54033c30 100644 --- a/code/_helpers/lists.dm +++ b/code/_helpers/lists.dm @@ -699,3 +699,44 @@ proc/dd_sortedTextList(list/incoming) var/atom/A = key if(A.type == T) return A + +/** + * Returns a new list with only atoms that are in typecache L + * + */ +/proc/typecache_filter_list(list/atoms, list/typecache) + . = list() + for(var/thing in atoms) + var/atom/A = thing + if(typecache[A.type]) + . += A + +/** + * Like typesof() or subtypesof(), but returns a typecache instead of a list + */ +/proc/typecacheof(path, ignore_root_path, only_root_path = FALSE) + if(ispath(path)) + var/list/types = list() + if(only_root_path) + types = list(path) + else + types = ignore_root_path ? subtypesof(path) : typesof(path) + var/list/L = list() + for(var/T in types) + L[T] = TRUE + return L + else if(islist(path)) + var/list/pathlist = path + var/list/L = list() + if(ignore_root_path) + for(var/P in pathlist) + for(var/T in subtypesof(P)) + L[T] = TRUE + else + for(var/P in pathlist) + if(only_root_path) + L[P] = TRUE + else + for(var/T in typesof(P)) + L[T] = TRUE + return L diff --git a/code/_helpers/medical_scans.dm b/code/_helpers/medical_scans.dm index 1f74454de9a..2b66997ea2a 100644 --- a/code/_helpers/medical_scans.dm +++ b/code/_helpers/medical_scans.dm @@ -76,6 +76,10 @@ scan["internal_organs"] = list() for(var/obj/item/organ/internal/I in H.internal_organs) + if (istype(I, /obj/item/organ/internal/augment)) + var/obj/item/organ/internal/augment/A = I + if (!A.known) // Hidden augments don't appear on scans + continue var/list/O = list() O["name"] = I.name O["is_broken"] = I.is_broken() diff --git a/code/_helpers/species.dm b/code/_helpers/species.dm new file mode 100644 index 00000000000..260d10c8066 --- /dev/null +++ b/code/_helpers/species.dm @@ -0,0 +1,13 @@ +/** + Helpers related to /datum/species +*/ + + +/// Null, or a species if thing is a species, a species path, or a species name +/proc/resolve_species(datum/species/thing) + if (ispath(thing, /datum/species)) + thing = initial(thing.name) + if (istext(thing)) + thing = all_species[thing] + if (istype(thing)) + return thing diff --git a/code/_macros.dm b/code/_macros.dm index 1d9f86eb6ae..3633f41a282 100644 --- a/code/_macros.dm +++ b/code/_macros.dm @@ -44,7 +44,7 @@ var/const/NEGATIVE_INFINITY = -1#INF // win: -1.#INF, lin: -inf #define isclient(A) istype(A, /client) -#define iscorgi(A) istype(A, /mob/living/simple_animal/corgi) +#define iscorgi(A) istype(A, /mob/living/simple_animal/friendly/corgi) #define is_drone(A) istype(A, /mob/living/silicon/robot/drone) @@ -58,7 +58,7 @@ var/const/NEGATIVE_INFINITY = -1#INF // win: -1.#INF, lin: -inf #define isliving(A) istype(A, /mob/living) -#define ismouse(A) istype(A, /mob/living/simple_animal/mouse) +#define ismouse(A) istype(A, /mob/living/simple_animal/friendly/mouse) #define isnewplayer(A) istype(A, /mob/new_player) @@ -100,11 +100,7 @@ var/const/NEGATIVE_INFINITY = -1#INF // win: -1.#INF, lin: -inf #define isPlunger(A) istype(A, /obj/item/clothing/mask/plunger) || istype(A, /obj/item/device/plunger/robot) -/proc/isspecies(A, B) - if(!iscarbon(A)) - return FALSE - var/mob/living/carbon/C = A - return C.species?.name == B +#define isadmin(X) (check_rights(R_ADMIN, 0, (X)) != 0) #define sequential_id(key) uniqueness_repository.Generate(/datum/uniqueness_generator/id_sequential, key) diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index cf23f274410..c06c1193156 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -34,7 +34,7 @@ /turf/allow_click_through(var/atom/A, var/params, var/mob/user) return TRUE - + /* Standard mob ClickOn() Handles exceptions: middle click, modified clicks, exosuit actions diff --git a/code/_onclick/hud/screen_objects.dm b/code/_onclick/hud/screen_objects.dm index 35e050f6593..86fe8355b59 100644 --- a/code/_onclick/hud/screen_objects.dm +++ b/code/_onclick/hud/screen_objects.dm @@ -11,7 +11,7 @@ icon = 'icons/mob/screen1.dmi' plane = HUD_PLANE layer = HUD_BASE_LAYER - appearance_flags = NO_CLIENT_COLOR + appearance_flags = DEFAULT_APPEARANCE_FLAGS | NO_CLIENT_COLOR unacidable = TRUE var/obj/master = null //A reference to the object in the slot. Grabs or items, generally. var/globalscreen = FALSE //Global screens are not qdeled when the holding mob is destroyed. @@ -149,7 +149,11 @@ /obj/screen/zone_sel/proc/set_selected_zone(bodypart) var/old_selecting = selecting selecting = bodypart - if(old_selecting != selecting) + var/mob/living/carbon/human/user = usr + if (istype(user) && (old_selecting == BP_MOUTH || selecting == BP_MOUTH) && user.aiming && user.aiming.active && user.aiming.aiming_at == user) + var/obj/aiming_overlay/AO = user.aiming + AO.aim_at(user, user.aiming.aiming_with, TRUE) + if (old_selecting != selecting) update_icon() return TRUE @@ -280,7 +284,12 @@ C.set_internals(tankcheck[best], "\the [tankcheck[best]] [from] your [nicename[best]]") if(!C.internal) - to_chat(C, "You don't have \a [breathes] tank.") + // Finally, check for an internal air system. + // We use this as an absolute last resort, so we don't include it in the above logic + // There's no need to check that the gas contents are safe, because its internal logic always make sure it is + var/obj/item/organ/internal/augment/active/internal_air_system/IAS = locate() in C.internal_organs + if (!IAS?.activate()) + to_chat(C, SPAN_WARNING("You don't have \a [breathes] tank.")) if("act_intent") usr.a_intent_change("right") diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index 5b723e77352..72f3b5a7f22 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -104,3 +104,20 @@ avoid code duplication. This includes items that may sometimes act as a standard if(MUTATION_HULK in user.mutations) power *= 2 return target.hit_with_weapon(src, user, power, hit_zone) + +/** + * Used to get how fast a mob should attack, and influences click delay. + * This is just for inheritance. + */ +/mob/proc/get_attack_speed() + return DEFAULT_ATTACK_COOLDOWN + +/** + * W is the item being used in the attack, if any. modifier is if the attack should be longer or shorter than usual, for whatever reason. + */ +/mob/living/get_attack_speed(var/obj/item/W) + var/speed = base_attack_cooldown + if(istype(W)) + speed = W.attack_cooldown + + return speed diff --git a/code/_version.dm b/code/_version.dm index f944894f73f..2b08610f0ea 100644 --- a/code/_version.dm +++ b/code/_version.dm @@ -73,8 +73,8 @@ DM version compatibility macros & procs #if DM_VERSION < 514 - -/proc/rgb2num(T) //Take "#RGB" or "#RGBA" or "#RRGGBB" or "#RRGGBBAA" and turn it into list(R, G, B[, A]). Ignores color space. +/// Create the list(R, G, B[, A]) for inputs "#RGB", "#RGBA", "#RRGGBB", or "#RRGGBBAA". IGNORES color space. +/proc/rgb2num(T) var/static/regex/allowed = new(@"^#[0-9a-fA-F]{3,8}$") if (findtext(T, allowed)) switch (length(T)) @@ -86,3 +86,19 @@ DM version compatibility macros & procs #endif + + +/** + FOR_BLIND provides a common pattern for a typed for..in that SKIPS type checking. + This kind of loop provides a decent performance improvement but is best reserved + for hotspot code where the type of the members of the collection is never in doubt. + "as anything" became valid syntax in build 513.1540: + eg: + FOR_BLIND(client/C, GLOB.clients) + to_chat(C, "Hello [C].") +*/ +#if DM_BUILD < 1540 +#define FOR_BLIND(V, C) for (var/V as() in C) +#else +#define FOR_BLIND(V, C) for (var/V as anything in C) +#endif diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index dc324173ee1..65bd5647515 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -20,6 +20,7 @@ var/list/gamemode_cache = list() var/log_hrefs = 0 // logs all links clicked in-game. Could be used for debugging and tracking down exploits var/log_runtime = 0 // logs world.log to a file var/log_world_output = 0 // log world.log to game log + var/log_timers_on_bucket_reset = 0 // logs all timers in buckets on automatic bucket reset (Useful for timer debugging) var/allow_admin_ooccolor = 0 // Allows admins with relevant permissions to have their own ooc colour var/allow_vote_restart = 0 // allow votes to restart var/ert_admin_call_only = 0 @@ -322,6 +323,9 @@ var/list/gamemode_cache = list() if ("log_world_output") config.log_world_output = 1 + if("log_timers_on_bucket_reset") + config.log_timers_on_bucket_reset = 1 + if ("log_hrefs") config.log_hrefs = 1 diff --git a/code/controllers/subsystems/ai.dm b/code/controllers/subsystems/ai.dm new file mode 100644 index 00000000000..b993813abb5 --- /dev/null +++ b/code/controllers/subsystems/ai.dm @@ -0,0 +1,37 @@ +SUBSYSTEM_DEF(ai) + name = "AI" + init_order = SS_INIT_AI + priority = SS_PRIORITY_AI + wait = 2 SECONDS + //mobs can mess up unrelated tests, so we don't turn their AI on during them + #ifdef UNIT_TEST + flags = SS_NO_FIRE|SS_NO_INIT + #else + flags = SS_NO_INIT + #endif + runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME + + var/list/processing = list() + var/list/currentrun = list() + +/datum/controller/subsystem/ai/stat_entry(msg_prefix) + var/list/msg = list(msg_prefix) + msg += "P:[processing.len]" + ..(msg.Join()) + +/datum/controller/subsystem/ai/fire(resumed = 0) + if (!resumed) + src.currentrun = processing.Copy() + + //cache for sanic speed (lists are references anyways) + var/list/currentrun = src.currentrun + + while(currentrun.len) + var/datum/ai_holder/A = currentrun[currentrun.len] + --currentrun.len + if(!A || QDELETED(A) || A.busy) // Doesn't exist or won't exist soon or not doing it this tick + continue + A.handle_strategicals() + + if(MC_TICK_CHECK) + return \ No newline at end of file diff --git a/code/controllers/subsystems/aifast.dm b/code/controllers/subsystems/aifast.dm new file mode 100644 index 00000000000..490a3a475b2 --- /dev/null +++ b/code/controllers/subsystems/aifast.dm @@ -0,0 +1,38 @@ +SUBSYSTEM_DEF(aifast) + name = "AI (Fast)" + init_order = SS_INIT_AIFAST + priority = SS_PRIORITY_AI + wait = 0.25 SECONDS // Every quarter second + + #ifdef UNIT_TEST + flags = SS_NO_FIRE|SS_NO_INIT + #else + flags = SS_NO_INIT + #endif + + runlevels = RUNLEVEL_GAME | RUNLEVEL_POSTGAME + + var/list/processing = list() + var/list/currentrun = list() + +/datum/controller/subsystem/aifast/stat_entry(msg_prefix) + var/list/msg = list(msg_prefix) + msg += "P:[processing.len]" + ..(msg.Join()) + +/datum/controller/subsystem/aifast/fire(resumed = 0) + if (!resumed) + src.currentrun = processing.Copy() + + //cache for sanic speed (lists are references anyways) + var/list/currentrun = src.currentrun + + while(currentrun.len) + var/datum/ai_holder/A = currentrun[currentrun.len] + --currentrun.len + if(!A || QDELETED(A) || A.busy || !A.holder) // Doesn't exist or won't exist soon or not doing it this tick + continue + A.handle_tactics() + + if(MC_TICK_CHECK) + return diff --git a/code/controllers/subsystems/fluids.dm b/code/controllers/subsystems/fluids.dm index 046faac85ce..88973bd4065 100644 --- a/code/controllers/subsystems/fluids.dm +++ b/code/controllers/subsystems/fluids.dm @@ -36,13 +36,14 @@ SUBSYSTEM_DEF(fluids) active_fluids_copied_yet = FALSE af_index = 1 + var/dry_run = FALSE var/flooded_a_neighbor // Not used, required by FLOOD_TURF_NEIGHBORS. var/list/curr_sources = processing_sources while (curr_sources.len) var/turf/T = curr_sources[curr_sources.len] curr_sources.len-- - FLOOD_TURF_NEIGHBORS(T, FALSE) + FLOOD_TURF_NEIGHBORS(T, dry_run) if (MC_TICK_CHECK) return diff --git a/code/controllers/subsystems/skybox.dm b/code/controllers/subsystems/skybox.dm index e9836738f2c..51d48724022 100644 --- a/code/controllers/subsystems/skybox.dm +++ b/code/controllers/subsystems/skybox.dm @@ -12,6 +12,23 @@ SUBSYSTEM_DEF(skybox) var/star_path = 'icons/skybox/skybox.dmi' var/star_state = "stars" var/list/skybox_cache = list() + var/list/space_appearance_cache + +/datum/controller/subsystem/skybox/PreInit() + build_space_appearances() + +/datum/controller/subsystem/skybox/proc/build_space_appearances() + space_appearance_cache = new(26) + for (var/i in 0 to 25) + var/mutable_appearance/dust = mutable_appearance('icons/turf/space_dust.dmi', "[i]") + dust.plane = DUST_PLANE + dust.alpha = 80 + dust.blend_mode = BLEND_ADD + + var/mutable_appearance/space = new /mutable_appearance(/turf/space) + space.icon_state = "white" + space.overlays += dust + space_appearance_cache[i + 1] = space.appearance /datum/controller/subsystem/skybox/Initialize() . = ..() @@ -33,7 +50,6 @@ SUBSYSTEM_DEF(skybox) /datum/controller/subsystem/skybox/proc/generate_skybox(z) var/image/res = image(skybox_icon) - res.appearance_flags = KEEP_TOGETHER var/image/base = overlay_image(skybox_icon, background_icon, background_color) @@ -51,7 +67,7 @@ SUBSYSTEM_DEF(skybox) for(var/obj/effect/overmap/visitable/other in O.loc) if(other != O) overmap.overlays += other.get_skybox_representation() - overmap.appearance_flags = RESET_COLOR + overmap.appearance_flags |= RESET_COLOR res.overlays += overmap for(var/datum/event/E in SSevent.active_events) diff --git a/code/controllers/subsystems/ticker.dm b/code/controllers/subsystems/ticker.dm index 4854086742c..546277f0ee9 100644 --- a/code/controllers/subsystems/ticker.dm +++ b/code/controllers/subsystems/ticker.dm @@ -29,6 +29,9 @@ SUBSYSTEM_DEF(ticker) var/secret_force_mode = "secret" + ///Set to TRUE when an admin forcefully ends the round. + var/forced_end = FALSE + /datum/controller/subsystem/ticker/Initialize() to_world("Welcome to the pre-game lobby!") to_world("Please, setup your character and select ready. Game will start in [round(pregame_timeleft/10)] seconds") @@ -347,6 +350,9 @@ Helpers return 0 /datum/controller/subsystem/ticker/proc/game_finished() + if (forced_end) + return TRUE + if(mode.explosion_in_progress) return 0 if(config.continous_rounds) @@ -355,6 +361,9 @@ Helpers return mode.check_finished() || (evacuation_controller.round_over() && evacuation_controller.emergency_evacuation) || universe_has_ended /datum/controller/subsystem/ticker/proc/mode_finished() + if (forced_end) + return TRUE + if(config.continous_rounds) return mode.check_finished() else diff --git a/code/controllers/subsystems/timer.dm b/code/controllers/subsystems/timer.dm index d6318764592..cdf5dbb5e87 100644 --- a/code/controllers/subsystems/timer.dm +++ b/code/controllers/subsystems/timer.dm @@ -1,31 +1,56 @@ -#define BUCKET_LEN (round(10*(60/world.tick_lag), 1)) //how many ticks should we keep in the bucket. (1 minutes worth) +/// Controls how many buckets should be kept, each representing a tick. (1 minutes worth) +#define BUCKET_LEN (world.fps*1*60) +/// Helper for getting the correct bucket for a given timer #define BUCKET_POS(timer) (((round((timer.timeToRun - SStimer.head_offset) / world.tick_lag)+1) % BUCKET_LEN)||BUCKET_LEN) +/// Gets the maximum time at which timers will be invoked from buckets, used for deferring to secondary queue #define TIMER_MAX (world.time + TICKS2DS(min(BUCKET_LEN-(SStimer.practical_offset-DS2TICKS(world.time - SStimer.head_offset))-1, BUCKET_LEN-1))) -#define TIMER_ID_MAX (2**24) //max float with integer precision - +/// Max float with integer precision +#define TIMER_ID_MAX (2**24) + +/** + * # Timer Subsystem + * + * Handles creation, callbacks, and destruction of timed events. + * + * It is important to understand the buckets used in the timer subsystem are just a series of doubly-linked + * lists. The object at a given index in bucket_list is a /datum/timedevent, the head of a list, which has prev + * and next references for the respective elements in that bucket's list. + */ SUBSYSTEM_DEF(timer) name = "Timer" wait = 1 //SS_TICKER subsystem, so wait is in ticks priority = SS_PRIORITY_TIMER + flags = SS_NO_INIT | SS_TICKER - var/list/datum/timedevent/second_queue = list() //awe, yes, you've had first queue, but what about second queue? + /// Queue used for storing timers that do not fit into the current buckets + var/list/datum/timedevent/second_queue = list() + /// A hashlist dictionary used for storing unique timers var/list/hashes = list() - - var/head_offset = 0 //world.time of the first entry in the the bucket. - var/practical_offset = 1 //index of the first non-empty item in the bucket. - var/bucket_resolution = 0 //world.tick_lag the bucket was designed for - var/bucket_count = 0 //how many timers are in the buckets - - var/list/bucket_list = list() //list of buckets, each bucket holds every timer that has to run that byond tick. - - var/list/timer_id_dict = list() //list of all active timers assoicated to their timer id (for easy lookup) - - var/list/clienttime_timers = list() //special snowflake timers that run on fancy pansy "client time" - + /// world.time of the first entry in the bucket list, effectively the 'start time' of the current buckets + var/head_offset = 0 + /// Index of the wrap around pivot for buckets. buckets before this are later running buckets wrapped around from the end of the bucket list. + var/practical_offset = 1 + /// world.tick_lag the bucket was designed for + var/bucket_resolution = 0 + /// How many timers are in the buckets + var/bucket_count = 0 + /// List of buckets, each bucket holds every timer that has to run that byond tick + var/list/bucket_list = list() + /// List of all active timers associated to their timer ID (for easy lookup) + var/list/timer_id_dict = list() + /// Special timers that run in real-time, not BYOND time; these are more expensive to run and maintain + var/list/clienttime_timers = list() + /// Contains the last time that a timer's callback was invoked, or the last tick the SS fired if no timers are being processed var/last_invoke_tick = 0 + /// Keeps track of the next index to work on for client timers + var/next_clienttime_timer_index = 0 + /// Contains the last time that a warning was issued for not invoking callbacks var/static/last_invoke_warning = 0 + /// Boolean operator controlling if the timer SS will automatically reset buckets if it fails to invoke callbacks for an extended period of time var/static/bucket_auto_reset = TRUE + /// How many times bucket was reset + var/bucket_reset_count = 0 /datum/controller/subsystem/timer/PreInit() bucket_list.len = BUCKET_LEN @@ -33,47 +58,58 @@ SUBSYSTEM_DEF(timer) bucket_resolution = world.tick_lag /datum/controller/subsystem/timer/stat_entry(msg) - ..("B:[bucket_count] P:[length(second_queue)] H:[length(hashes)] C:[length(clienttime_timers)] S:[length(timer_id_dict)]") + ..("B:[bucket_count] P:[length(second_queue)] H:[length(hashes)] C:[length(clienttime_timers)] S:[length(timer_id_dict)] RST:[bucket_reset_count]") + +/datum/controller/subsystem/timer/proc/dump_timer_buckets(full = TRUE) + var/list/to_log = list("Timer bucket reset. world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") + if (full) + for (var/i in 1 to length(bucket_list)) + var/datum/timedevent/bucket_head = bucket_list[i] + if (!bucket_head) + continue + + to_log += "Active timers at index [i]:" + var/datum/timedevent/bucket_node = bucket_head + var/anti_loop_check = 1 + do + to_log += get_timer_debug_string(bucket_node) + bucket_node = bucket_node.next + anti_loop_check-- + while(bucket_node && bucket_node != bucket_head && anti_loop_check) + + to_log += "Active timers in the second_queue queue:" + for(var/I in second_queue) + to_log += get_timer_debug_string(I) + + // Dump all the logged data to the world log + log_ss(name, to_log.Join("\n")) /datum/controller/subsystem/timer/fire(resumed = FALSE) + // Store local references to datum vars as it is faster to access them var/lit = last_invoke_tick - var/last_check = world.time - TICKS2DS(BUCKET_LEN*1.5) var/list/bucket_list = src.bucket_list + var/last_check = world.time - TICKS2DS(BUCKET_LEN * 1.5) + // If there are no timers being tracked, then consider now to be the last invoked time if(!bucket_count) last_invoke_tick = world.time + // Check that we have invoked a callback in the last 1.5 minutes of BYOND time, + // and throw a warning and reset buckets if this is true if(lit && lit < last_check && head_offset < last_check && last_invoke_warning < last_check) last_invoke_warning = world.time - var/msg = "No regular timers processed in the last [BUCKET_LEN*1.5] ticks[bucket_auto_reset ? ", resetting buckets" : ""]!" + var/msg = "No regular timers processed in the last [BUCKET_LEN * 1.5] ticks[bucket_auto_reset ? ", resetting buckets" : ""]!" message_admins(msg) WARNING(msg) if(bucket_auto_reset) bucket_resolution = 0 + dump_timer_buckets(config.log_timers_on_bucket_reset) - log_ss(name, "Timer bucket reset. world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") - for (var/i in 1 to length(bucket_list)) - var/datum/timedevent/bucket_head = bucket_list[i] - if (!bucket_head) - continue - - log_ss(name, "Active timers at index [i]:") - - var/datum/timedevent/bucket_node = bucket_head - var/anti_loop_check = 1000 - do - log_ss(name, get_timer_debug_string(bucket_node)) - bucket_node = bucket_node.next - anti_loop_check-- - while(bucket_node && bucket_node != bucket_head && anti_loop_check) - log_ss(name, "Active timers in the second_queue queue:") - for(var/I in second_queue) - log_ss(name, get_timer_debug_string(I)) - - var/next_clienttime_timer_index = 0 - var/len = length(clienttime_timers) - - for (next_clienttime_timer_index in 1 to len) + // Process client-time timers + if (next_clienttime_timer_index) + clienttime_timers.Cut(1, next_clienttime_timer_index+1) + next_clienttime_timer_index = 0 + for (next_clienttime_timer_index in 1 to length(clienttime_timers)) if (MC_TICK_CHECK) next_clienttime_timer_index-- break @@ -84,8 +120,8 @@ SUBSYSTEM_DEF(timer) var/datum/callback/callBack = ctime_timer.callBack if (!callBack) - clienttime_timers.Cut(next_clienttime_timer_index,next_clienttime_timer_index+1) - CRASH("Invalid timer: [get_timer_debug_string(ctime_timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset], REALTIMEOFDAY: [REALTIMEOFDAY]") + CRASH("Invalid timer: [get_timer_debug_string(ctime_timer)] world.time: [world.time], \ + head_offset: [head_offset], practical_offset: [practical_offset], REALTIMEOFDAY: [REALTIMEOFDAY]") ctime_timer.spent = REALTIMEOFDAY callBack.InvokeAsync() @@ -93,135 +129,93 @@ SUBSYSTEM_DEF(timer) if(ctime_timer.flags & TIMER_LOOP) ctime_timer.spent = 0 ctime_timer.timeToRun = REALTIMEOFDAY + ctime_timer.wait - BINARY_INSERT(ctime_timer, clienttime_timers, datum/timedevent, timeToRun) + BINARY_INSERT_TG(ctime_timer, clienttime_timers, /datum/timedevent, ctime_timer, timeToRun, COMPARE_KEY) else qdel(ctime_timer) - + // Remove invoked client-time timers if (next_clienttime_timer_index) clienttime_timers.Cut(1, next_clienttime_timer_index+1) + next_clienttime_timer_index = 0 - if (MC_TICK_CHECK) - return - - var/static/list/spent = list() - var/static/datum/timedevent/timer + // Check for when we need to loop the buckets, this occurs when + // the head_offset is approaching BUCKET_LEN ticks in the past if (practical_offset > BUCKET_LEN) head_offset += TICKS2DS(BUCKET_LEN) practical_offset = 1 resumed = FALSE + // Check for when we have to reset buckets, typically from auto-reset if ((length(bucket_list) != BUCKET_LEN) || (world.tick_lag != bucket_resolution)) reset_buckets() bucket_list = src.bucket_list resumed = FALSE - if (!resumed) - timer = null - - while (practical_offset <= BUCKET_LEN && head_offset + ((practical_offset-1)*world.tick_lag) <= world.time) - var/datum/timedevent/head = bucket_list[practical_offset] - if (!timer || !head || timer == head) - head = bucket_list[practical_offset] - timer = head - while (timer) + // Iterate through each bucket starting from the practical offset + while (practical_offset <= BUCKET_LEN && head_offset + ((practical_offset - 1) * world.tick_lag) <= world.time) + var/datum/timedevent/timer + while ((timer = bucket_list[practical_offset])) var/datum/callback/callBack = timer.callBack if (!callBack) - bucket_resolution = null //force bucket recreation - CRASH("Invalid timer: [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") + stack_trace("Invalid timer: [get_timer_debug_string(timer)] world.time: [world.time], \ + head_offset: [head_offset], practical_offset: [practical_offset], bucket_joined: [timer.bucket_joined]") + if (!timer.spent) + bucket_resolution = null // force bucket recreation + return + + timer.bucketEject() //pop the timer off of the bucket list. + // Invoke callback if possible if (!timer.spent) - spent += timer timer.spent = world.time callBack.InvokeAsync() last_invoke_tick = world.time - if (MC_TICK_CHECK) - return - - timer = timer.next - if (timer == head) - break - - - bucket_list[practical_offset++] = null + if (timer.flags & TIMER_LOOP) // Prepare looping timers to re-enter the queue + timer.spent = 0 + timer.timeToRun = world.time + timer.wait + timer.bucketJoin() + else + qdel(timer) - //we freed up a bucket, lets see if anything in second_queue needs to be shifted to that bucket. - var/i = 0 - var/L = length(second_queue) - for (i in 1 to L) - timer = second_queue[i] - if (timer.timeToRun >= TIMER_MAX) - i-- + if (MC_TICK_CHECK) break - if (timer.timeToRun < head_offset) - bucket_resolution = null //force bucket recreation - crash_with("[i] Invalid timer state: Timer in long run queue with a time to run less then head_offset. [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") - - if (timer.callBack && !timer.spent) - timer.callBack.InvokeAsync() - spent += timer - bucket_count++ - else if(!QDELETED(timer)) - qdel(timer) - continue - - if (timer.timeToRun < head_offset + TICKS2DS(practical_offset-1)) - bucket_resolution = null //force bucket recreation - crash_with("[i] Invalid timer state: Timer in long run queue that would require a backtrack to transfer to short run queue. [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") - if (timer.callBack && !timer.spent) - timer.callBack.InvokeAsync() - spent += timer - bucket_count++ - else if(!QDELETED(timer)) - qdel(timer) - continue - - bucket_count++ - var/bucket_pos = max(1, BUCKET_POS(timer)) - - var/datum/timedevent/bucket_head = bucket_list[bucket_pos] - if (!bucket_head) - bucket_list[bucket_pos] = timer - timer.next = null - timer.prev = null - continue - - if (!bucket_head.prev) - bucket_head.prev = bucket_head - timer.next = bucket_head - timer.prev = bucket_head.prev - timer.next.prev = timer - timer.prev.next = timer - if (i) - second_queue.Cut(1, i+1) - - timer = null - - bucket_count -= length(spent) - - for (var/i in spent) - var/datum/timedevent/qtimer = i - if(QDELETED(qtimer)) - bucket_count++ - continue - if(!(qtimer.flags & TIMER_LOOP)) - qdel(qtimer) - else - bucket_count++ - qtimer.spent = 0 - qtimer.bucketEject() - if(qtimer.flags & TIMER_CLIENT_TIME) - qtimer.timeToRun = REALTIMEOFDAY + qtimer.wait - else - qtimer.timeToRun = world.time + qtimer.wait - qtimer.bucketJoin() - - spent.len = 0 + if (!bucket_list[practical_offset]) + // Empty the bucket, check if anything in the secondary queue should be shifted to this bucket + bucket_list[practical_offset] = null // Just in case + practical_offset++ + var/i = 0 + for (i in 1 to length(second_queue)) + timer = second_queue[i] + if (timer.timeToRun >= TIMER_MAX) + i-- + break + + // Check for timers that are scheduled to run in the past + if (timer.timeToRun < head_offset) + bucket_resolution = null // force bucket recreation + stack_trace("[i] Invalid timer state: Timer in long run queue with a time to run less then head_offset. \ + [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") + break + + // Check for timers that are not capable of being scheduled to run without rebuilding buckets + if (timer.timeToRun < head_offset + TICKS2DS(practical_offset - 1)) + bucket_resolution = null // force bucket recreation + stack_trace("[i] Invalid timer state: Timer in long run queue that would require a backtrack to transfer to \ + short run queue. [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") + break + + timer.bucketJoin() + if (i) + second_queue.Cut(1, i+1) + if (MC_TICK_CHECK) + break -//formated this way to be runtime resistant +/** + * Generates a string with details about the timed event for debugging purposes + */ /datum/controller/subsystem/timer/proc/get_timer_debug_string(datum/timedevent/TE) . = "Timer: [TE]" . += "Prev: [TE.prev ? TE.prev : "NULL"], Next: [TE.next ? TE.next : "NULL"]" @@ -232,12 +226,19 @@ SUBSYSTEM_DEF(timer) if(!TE.callBack) . += ", NO CALLBACK" +/** + * Destroys the existing buckets and creates new buckets from the existing timed events + */ /datum/controller/subsystem/timer/proc/reset_buckets() - var/list/bucket_list = src.bucket_list + log_debug("Timer buckets has been reset, this may cause timer to lag") + bucket_reset_count++ + + var/list/bucket_list = src.bucket_list // Store local reference to datum var, this is faster var/list/alltimers = list() - //collect the timers currently in the bucket + + // Get all timers currently in the buckets for (var/bucket_head in bucket_list) - if (!bucket_head) + if (!bucket_head) // if bucket is empty for this tick continue var/datum/timedevent/bucket_node = bucket_head do @@ -245,25 +246,38 @@ SUBSYSTEM_DEF(timer) bucket_node = bucket_node.next while(bucket_node && bucket_node != bucket_head) + // Empty the list by zeroing and re-assigning the length bucket_list.len = 0 bucket_list.len = BUCKET_LEN + // Reset values for the subsystem to their initial values practical_offset = 1 bucket_count = 0 head_offset = world.time bucket_resolution = world.tick_lag + // Add all timed events from the secondary queue as well alltimers += second_queue + + // If there are no timers being tracked by the subsystem, + // there is no need to do any further rebuilding if (!length(alltimers)) return - sortTim(alltimers, /proc/cmp_timer) + // Sort all timers by time to run + sortTim(alltimers, .proc/cmp_timer) + // Get the earliest timer, and if the TTR is earlier than the current world.time, + // then set the head offset appropriately to be the earliest time tracked by the + // current set of buckets var/datum/timedevent/head = alltimers[1] - if (head.timeToRun < head_offset) head_offset = head.timeToRun + // Iterate through each timed event and insert it into an appropriate bucket, + // up unto the point that we can no longer insert into buckets as the TTR + // is outside the range we are tracking, then insert the remainder into the + // secondary queue var/new_bucket_count var/i = 1 for (i in 1 to length(alltimers)) @@ -271,19 +285,24 @@ SUBSYSTEM_DEF(timer) if (!timer) continue - var/bucket_pos = BUCKET_POS(timer) + // Check that the TTR is within the range covered by buckets, when exceeded we've finished if (timer.timeToRun >= TIMER_MAX) i-- break - + // Check that timer has a valid callback and hasn't been invoked if (!timer.callBack || timer.spent) - WARNING("Invalid timer: [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") + WARNING("Invalid timer: [get_timer_debug_string(timer)] world.time: [world.time], \ + head_offset: [head_offset], practical_offset: [practical_offset]") if (timer.callBack) qdel(timer) continue + // Insert the timer into the bucket, and perform necessary doubly-linked list operations new_bucket_count++ + var/bucket_pos = BUCKET_POS(timer) + timer.bucket_pos = bucket_pos + var/datum/timedevent/bucket_head = bucket_list[bucket_pos] if (!bucket_head) bucket_list[bucket_pos] = timer @@ -291,14 +310,14 @@ SUBSYSTEM_DEF(timer) timer.prev = null continue - if (!bucket_head.prev) - bucket_head.prev = bucket_head + bucket_head.prev = timer timer.next = bucket_head - timer.prev = bucket_head.prev - timer.next.prev = timer - timer.prev.next = timer + timer.prev = null + bucket_list[bucket_pos] = timer + + // Cut the timers that are tracked by the buckets from the secondary queue if (i) - alltimers.Cut(1, i+1) + alltimers.Cut(1, i + 1) second_queue = alltimers bucket_count = new_bucket_count @@ -309,45 +328,68 @@ SUBSYSTEM_DEF(timer) timer_id_dict |= SStimer.timer_id_dict bucket_list |= SStimer.bucket_list +/** + * # Timed Event + * + * This is the actual timer, it contains the callback and necessary data to maintain + * the timer. + * + * See the documentation for the timer subsystem for an explanation of the buckets referenced + * below in next and prev + */ /datum/timedevent + /// ID used for timers when the TIMER_STOPPABLE flag is present var/id + /// The callback to invoke after the timer completes var/datum/callback/callBack + /// The time at which the callback should be invoked at var/timeToRun + /// The length of the timer var/wait + /// Unique hash generated when TIMER_UNIQUE flag is present var/hash + /// The source of the timedevent, whatever called addtimer + var/source + /// Flags associated with the timer, see _DEFINES/subsystems.dm var/list/flags - var/spent = 0 //time we ran the timer. - var/name //for easy debugging. - //cicular doublely linked list + /// Time at which the timer was invoked or destroyed + var/spent = 0 + /// An informative name generated for the timer as its representation in strings, useful for debugging + var/name + /// Next timed event in the bucket var/datum/timedevent/next + /// Previous timed event in the bucket var/datum/timedevent/prev + /// Boolean indicating if timer joined into bucket + var/bucket_joined = FALSE + /// Initial bucket position + var/bucket_pos = -1 -/datum/timedevent/New(datum/callback/callBack, wait, flags, hash) +/datum/timedevent/New(datum/callback/callBack, wait, flags, hash, source) var/static/nextid = 1 id = TIMER_ID_NULL src.callBack = callBack src.wait = wait src.flags = flags src.hash = hash + src.source = source - if (flags & TIMER_CLIENT_TIME) - timeToRun = REALTIMEOFDAY + wait - else - timeToRun = world.time + wait + // Determine time at which the timer's callback should be invoked + timeToRun = (flags & TIMER_CLIENT_TIME ? REALTIMEOFDAY : world.time) + wait + // Include the timer in the hash table if the timer is unique if (flags & TIMER_UNIQUE) SStimer.hashes[hash] = src + // Generate ID for the timer if the timer is stoppable, include in the timer id dictionary if (flags & TIMER_STOPPABLE) id = num2text(nextid, 100) if (nextid >= TIMER_ID_MAX) - nextid += min(1, 2**round(nextid/TIMER_ID_MAX)) + nextid += min(1, 2 ** round(nextid / TIMER_ID_MAX)) else nextid++ SStimer.timer_id_dict[id] = src - name = "Timer: [id] (\ref[src]), TTR: [timeToRun], Flags: [jointext(bitfield2list(flags, list("TIMER_UNIQUE", "TIMER_OVERRIDE", "TIMER_CLIENT_TIME", "TIMER_STOPPABLE", "TIMER_NO_HASH_WAIT", "TIMER_LOOP")), ", ")], callBack: \ref[callBack], callBack.object: [callBack.object]\ref[callBack.object]([getcallingtype()]), callBack.delegate:[callBack.delegate]([callBack.arguments ? callBack.arguments.Join(", ") : ""])" - if ((timeToRun < world.time || timeToRun < SStimer.head_offset) && !(flags & TIMER_CLIENT_TIME)) CRASH("Invalid timer state: Timer created that would require a backtrack to run (addtimer would never let this happen): [SStimer.get_timer_debug_string(src)]") @@ -388,64 +430,104 @@ SUBSYSTEM_DEF(timer) prev = null return QDEL_HINT_IWILLGC +/** + * Removes this timed event from any relevant buckets, or the secondary queue + */ /datum/timedevent/proc/bucketEject() - var/bucketpos = BUCKET_POS(src) + // Store local references for the bucket list and secondary queue + // This is faster than referencing them from the datum itself var/list/bucket_list = SStimer.bucket_list var/list/second_queue = SStimer.second_queue + + // Attempt to get the head of the bucket var/datum/timedevent/buckethead - if(bucketpos > 0) - buckethead = bucket_list[bucketpos] + if(bucket_pos > 0) + buckethead = bucket_list[bucket_pos] + + // Decrement the number of timers in buckets if the timed event is + // the head of the bucket, or has a TTR less than TIMER_MAX implying it fits + // into an existing bucket, or is otherwise not present in the secondary queue if(buckethead == src) - bucket_list[bucketpos] = next + bucket_list[bucket_pos] = next SStimer.bucket_count-- - else if(timeToRun < TIMER_MAX || next || prev) + else if(timeToRun < TIMER_MAX) SStimer.bucket_count-- else var/l = length(second_queue) second_queue -= src if(l == length(second_queue)) SStimer.bucket_count-- - if(prev != next) + + // Remove the timed event from the bucket, ensuring to maintain + // the integrity of the bucket's list if relevant + if (prev && prev.next == src) prev.next = next + if (next && next.prev == src) next.prev = prev - else - prev?.next = null - next?.prev = null prev = next = null - + bucket_pos = -1 + bucket_joined = FALSE + +/** + * Attempts to add this timed event to a bucket, will enter the secondary queue + * if there are no appropriate buckets at this time. + * + * Secondary queueing of timed events will occur when the timespan covered by the existing + * buckets is exceeded by the time at which this timed event is scheduled to be invoked. + * If the timed event is tracking client time, it will be added to a special bucket. + */ /datum/timedevent/proc/bucketJoin() - var/list/L + // Generate debug-friendly name for timer + var/static/list/bitfield_flags = list("TIMER_UNIQUE", "TIMER_OVERRIDE", "TIMER_CLIENT_TIME", "TIMER_STOPPABLE", "TIMER_NO_HASH_WAIT", "TIMER_LOOP") + name = "Timer: [id] (\ref[src]), TTR: [timeToRun], wait:[wait] Flags: [jointext(bitfield2list(flags, bitfield_flags), ", ")], \ + callBack: \ref[callBack], callBack.object: [callBack.object]\ref[callBack.object]([getcallingtype()]), \ + callBack.delegate:[callBack.delegate]([callBack.arguments ? callBack.arguments.Join(", ") : ""]), source: [source]" + + if (bucket_joined) + stack_trace("Bucket already joined! [name]") + // Check if this timed event should be diverted to the client time bucket, or the secondary queue + var/list/L if (flags & TIMER_CLIENT_TIME) L = SStimer.clienttime_timers else if (timeToRun >= TIMER_MAX) L = SStimer.second_queue - if(L) - BINARY_INSERT(src, L, datum/timedevent, timeToRun) + BINARY_INSERT_TG(src, L, /datum/timedevent, src, timeToRun, COMPARE_KEY) return - //get the list of buckets + // Get a local reference to the bucket list, this is faster than referencing the datum var/list/bucket_list = SStimer.bucket_list - //calculate our place in the bucket list - var/bucket_pos = BUCKET_POS(src) + // Find the correct bucket for this timed event + bucket_pos = BUCKET_POS(src) + + if (bucket_pos < SStimer.practical_offset && timeToRun < (SStimer.head_offset + TICKS2DS(BUCKET_LEN))) + WARNING("Bucket pos in past: bucket_pos = [bucket_pos] < practical_offset = [SStimer.practical_offset] \ + && timeToRun = [timeToRun] < [SStimer.head_offset + TICKS2DS(BUCKET_LEN)], Timer: [name]") + bucket_pos = SStimer.practical_offset // Recover bucket_pos to avoid timer blocking queue - //get the bucket for our tick var/datum/timedevent/bucket_head = bucket_list[bucket_pos] SStimer.bucket_count++ - //empty bucket, we will just add ourselves + + // If there is no timed event at this position, then the bucket is 'empty' + // and we can just set this event to that position if (!bucket_head) + bucket_joined = TRUE bucket_list[bucket_pos] = src return - //other wise, lets do a simplified linked list add. - if (!bucket_head.prev) - bucket_head.prev = bucket_head + + // Otherwise, we merely add this timed event into the bucket, which is a + // doubly-linked list + bucket_joined = TRUE + bucket_head.prev = src next = bucket_head - prev = bucket_head.prev - next.prev = src - prev.next = src + prev = null + bucket_list[bucket_pos] = src +/** + * Returns a string of the type of the callback for this timer + */ /datum/timedevent/proc/getcallingtype() . = "ERROR" if (callBack.object == GLOBAL_PROC) @@ -453,59 +535,72 @@ SUBSYSTEM_DEF(timer) else . = "[callBack.object.type]" +/** + * Create a new timer and insert it in the queue. + * You should not call this directly, and should instead use the addtimer macro, which includes source information. + * + * Arguments: + * * callback the callback to call on timer finish + * * wait deciseconds to run the timer for + * * flags flags for this timer, see: code\__DEFINES\subsystems.dm + */ /proc/addtimer(datum/callback/callback, wait = 0, flags = 0) if (!callback) CRASH("addtimer called without a callback") if (wait < 0) - crash_with("addtimer called with a negative wait. Converting to [world.tick_lag]") + stack_trace("addtimer called with a negative wait. Converting to [world.tick_lag]") if (callback.object != GLOBAL_PROC && QDELETED(callback.object) && !QDESTROYING(callback.object)) - crash_with("addtimer called with a callback assigned to a qdeleted object. In the future such timers will not be supported and may refuse to run or run with a 0 wait") + stack_trace("addtimer called with a callback assigned to a qdeleted object. In the future such timers will not \ + be supported and may refuse to run or run with a 0 wait") wait = max(CEILING(wait, world.tick_lag), world.tick_lag) if(wait >= INFINITY) CRASH("Attempted to create timer with INFINITY delay") + // Generate hash if relevant for timed events with the TIMER_UNIQUE flag var/hash - if (flags & TIMER_UNIQUE) - var/list/hashlist - if(flags & TIMER_NO_HASH_WAIT) - hashlist = list(callback.object, "(\ref[callback.object])", callback.delegate, flags & TIMER_CLIENT_TIME) - else - hashlist = list(callback.object, "(\ref[callback.object])", callback.delegate, wait, flags & TIMER_CLIENT_TIME) + var/list/hashlist = list(callback.object, "(\ref[callback.object])", callback.delegate, flags & TIMER_CLIENT_TIME) + if(!(flags & TIMER_NO_HASH_WAIT)) + hashlist += wait hashlist += callback.arguments hash = hashlist.Join("|||||||") var/datum/timedevent/hash_timer = SStimer.hashes[hash] if(hash_timer) - if (hash_timer.spent) //it's pending deletion, pretend it doesn't exist. - hash_timer.hash = null //but keep it from accidentally deleting us + if (hash_timer.spent) // it's pending deletion, pretend it doesn't exist. + hash_timer.hash = null // but keep it from accidentally deleting us else if (flags & TIMER_OVERRIDE) - hash_timer.hash = null //no need having it delete it's hash if we are going to replace it + hash_timer.hash = null // no need having it delete it's hash if we are going to replace it qdel(hash_timer) else if (hash_timer.flags & TIMER_STOPPABLE) . = hash_timer.id return else if(flags & TIMER_OVERRIDE) - crash_with("TIMER_OVERRIDE used without TIMER_UNIQUE") + stack_trace("TIMER_OVERRIDE used without TIMER_UNIQUE") var/datum/timedevent/timer = new(callback, wait, flags, hash) return timer.id +/** + * Delete a timer + * + * Arguments: + * * id a timerid or a /datum/timedevent + */ /proc/deltimer(id) if (!id) return FALSE if (id == TIMER_ID_NULL) CRASH("Tried to delete a null timerid. Use TIMER_STOPPABLE flag") - if (!istext(id)) - if (istype(id, /datum/timedevent)) - qdel(id) - return TRUE + if (istype(id, /datum/timedevent)) + qdel(id) + return TRUE //id is string var/datum/timedevent/timer = SStimer.timer_id_dict[id] if (timer && !timer.spent) diff --git a/code/controllers/subsystems/vote.dm b/code/controllers/subsystems/vote.dm index d366dc80ee4..1c8aa50fe87 100644 --- a/code/controllers/subsystems/vote.dm +++ b/code/controllers/subsystems/vote.dm @@ -67,11 +67,11 @@ SUBSYSTEM_DEF(vote) if(!automatic && (!istype(creator) || !creator.client)) return FALSE - if(last_started_time != null && !(is_admin(creator) || automatic)) + if(last_started_time != null && !(isadmin(creator) || automatic)) var/next_allowed_time = (last_started_time + config.vote_delay) if(next_allowed_time > world.time) return FALSE - + var/datum/vote/new_vote = new vote_type if(!new_vote.setup(creator, automatic)) return FALSE @@ -83,7 +83,7 @@ SUBSYSTEM_DEF(vote) /datum/controller/subsystem/vote/proc/interface(client/C) if(!C) return - var/admin = is_admin(C) + var/admin = isadmin(C) voting |= C . = list() @@ -126,7 +126,7 @@ SUBSYSTEM_DEF(vote) voting -= user.client /datum/controller/subsystem/vote/proc/cancel_vote(mob/user) - if(!is_admin(user)) + if(!isadmin(user)) return active_vote.report_result() // Will not make announcement, but do any override failure reporting tasks. QDEL_NULL(active_vote) @@ -180,7 +180,7 @@ SUBSYSTEM_DEF(vote) return 0 if(automatic) return (SSticker.mode.addantag_allowed & ADDANTAG_AUTO) && !antag_added - if(is_admin(creator)) + if(isadmin(creator)) return SSticker.mode.addantag_allowed & (ADDANTAG_ADMIN|ADDANTAG_PLAYER) else return (SSticker.mode.addantag_allowed & ADDANTAG_PLAYER) && !antag_added diff --git a/code/controllers/subsystems/zcopy.dm b/code/controllers/subsystems/zcopy.dm index 9a8d0e2d521..e581fe8ad1a 100644 --- a/code/controllers/subsystems/zcopy.dm +++ b/code/controllers/subsystems/zcopy.dm @@ -18,7 +18,10 @@ SUBSYSTEM_DEF(zcopy) var/openspace_overlays = 0 var/openspace_turfs = 0 - // Highest Z level in a given Z-group for absolute layering used by FIX_BIGTURF. + var/multiqueue_skips_turf = 0 + var/multiqueue_skips_object = 0 + + // Highest Z level in a given Z-group for absolute layering. // zstm[zlev] = group_max var/list/zlev_maximums = list() @@ -38,7 +41,7 @@ SUBSYSTEM_DEF(zcopy) T.update_mimic() num_upd += 1 - else if (istype(A, /atom/movable/openspace/overlay)) + else if (istype(A, /atom/movable/openspace/mimic)) var/turf/Tloc = A.loc if (TURF_IS_MIMICING(Tloc)) Tloc.update_mimic() @@ -68,7 +71,7 @@ SUBSYSTEM_DEF(zcopy) T.update_mimic() num_turfs += 1 - else if (istype(A, /atom/movable/openspace/overlay)) + else if (istype(A, /atom/movable/openspace/mimic)) qdel(A) num_deleted += 1 @@ -79,13 +82,12 @@ SUBSYSTEM_DEF(zcopy) enable() /datum/controller/subsystem/zcopy/stat_entry() - ..("Q:{T:[queued_turfs.len - (qt_idex - 1)]|O:[queued_overlays.len - (qo_idex - 1)]} T:{T:[openspace_turfs]|O:[openspace_overlays]}") + ..("Mx:[json_encode(zlev_maximums)]\n\tQ:{T:[queued_turfs.len - (qt_idex - 1)]|O:[queued_overlays.len - (qo_idex - 1)]}\n\tT:{T:[openspace_turfs]|O:[openspace_overlays]}\n\tSk:{T:[multiqueue_skips_turf]|O:[multiqueue_skips_object]}") /datum/controller/subsystem/zcopy/Initialize(timeofday) calculate_zstack_limits() // Flush the queue. fire(FALSE, TRUE) - return ..() // If you add a new Zlevel or change Z-connections, call this. /datum/controller/subsystem/zcopy/proc/calculate_zstack_limits() @@ -98,7 +100,7 @@ SUBSYSTEM_DEF(zcopy) if (z - start_zlev > OPENTURF_MAX_DEPTH) log_ss("zcopy", "WARNING: Z-levels [start_zlev] through [z] exceed maximum depth of [OPENTURF_MAX_DEPTH]; layering may behave strangely in this Z-stack.") else if (z - start_zlev > 1) - log_ss("zcopy", "Found Z-Stack: [start_zlev] -> [z] = [z - start_zlev] zl") + log_ss("zcopy", "Found Z-Stack: [start_zlev] -> [z] = [z - start_zlev + 1] zl") start_zlev = z + 1 log_ss("zcopy", "Z-Level maximums: [json_encode(zlev_maximums)]") @@ -126,7 +128,7 @@ SUBSYSTEM_DEF(zcopy) curr_turfs[qt_idex] = null qt_idex += 1 - if (!isturf(T) || !T.below || !(T.z_flags & ZM_MIMIC_BELOW) || !T.z_queued) + if (!isturf(T) || !(T.z_flags & ZM_MIMIC_BELOW) || !T.z_queued) if (no_mc_tick) CHECK_TICK else if (MC_TICK_CHECK) @@ -136,16 +138,43 @@ SUBSYSTEM_DEF(zcopy) // If we're not at our most recent queue position, don't bother -- we're updating again later anyways. if (T.z_queued > 1) T.z_queued -= 1 + multiqueue_skips_turf += 1 + if (no_mc_tick) + CHECK_TICK + else if (MC_TICK_CHECK) + break + continue + + // Z-Turf on the bottom-most level, just fake-copy space. + // If this is ever true, that turf should always pass this condition, so don't bother cleaning up beyond the Destroy() hook. + if (!T.below) // Z-turf on the bottom-most level, just fake-copy space. + if (T.z_flags & ZM_MIMIC_OVERWRITE) + T.appearance = SSskybox.space_appearance_cache[(((T.x + T.y) ^ ~(T.x * T.y) + T.z) % 25) + 1] + T.name = initial(T.name) + T.desc = initial(T.desc) + T.gender = initial(T.gender) + else + // Some openturfs have icons, so we can't overwrite their appearance. + if (!T.mimic_underlay) + T.mimic_underlay = new(T) + var/atom/movable/openspace/turf_proxy/TO = T.mimic_underlay + TO.appearance = SSskybox.space_appearance_cache[(((T.x + T.y) ^ ~(T.x * T.y) + T.z) % 25) + 1] + TO.name = T.name + TO.gender = T.gender // Need to grab this too so PLURAL works properly in examine. + TO.mouse_opacity = initial(TO.mouse_opacity) + if (no_mc_tick) CHECK_TICK else if (MC_TICK_CHECK) - return + break continue if (!T.shadower) // If we don't have a shadower yet, something has gone horribly wrong. WARNING("Turf [T] at [T.x],[T.y],[T.z] was queued, but had no shadower.") continue + T.z_generation += 1 + // Get the bottom-most turf, the one we want to mimic. var/turf/Td = T while (Td.below) @@ -162,13 +191,10 @@ SUBSYSTEM_DEF(zcopy) T.z_eventually_space = TRUE t_target = SPACE_PLANE - if (T.below.z_flags & ZM_FIX_BIGTURF) - T.z_flags |= ZM_FIX_BIGTURF // this flag is infectious - if (T.z_flags & ZM_MIMIC_OVERWRITE) // This openturf doesn't care about its icon, so we can just overwrite it. - if (T.below.bound_overlay) - QDEL_NULL(T.below.bound_overlay) + if (T.below.mimic_proxy) + QDEL_NULL(T.below.mimic_proxy) T.appearance = T.below T.name = initial(T.name) T.desc = initial(T.desc) @@ -177,9 +203,9 @@ SUBSYSTEM_DEF(zcopy) T.plane = t_target else // Some openturfs have icons, so we can't overwrite their appearance. - if (!T.below.bound_overlay) - T.below.bound_overlay = new(T) - var/atom/movable/openspace/turf_delegate/TO = T.below.bound_overlay + if (!T.below.mimic_proxy) + T.below.mimic_proxy = new(T) + var/atom/movable/openspace/turf_proxy/TO = T.below.mimic_proxy TO.appearance = Td TO.name = T.name TO.gender = T.gender // Need to grab this too so PLURAL works properly in examine. @@ -193,15 +219,15 @@ SUBSYSTEM_DEF(zcopy) // I think it's possible to get this to work without discrete delegate copy objects, but I'd rather this just work. if ((T.below.z_flags & (ZM_MIMIC_BELOW|ZM_MIMIC_OVERWRITE)) == ZM_MIMIC_BELOW) // Below is a delegate, gotta explicitly copy it for recursive copy. - if (!T.below.z_delegate) - T.below.z_delegate = new(T) - var/atom/movable/openspace/delegate_copy/DC = T.below.z_delegate + if (!T.below.mimic_above_copy) + T.below.mimic_above_copy = new(T) + var/atom/movable/openspace/turf_mimic/DC = T.below.mimic_above_copy DC.appearance = T.below DC.mouse_opacity = initial(DC.mouse_opacity) DC.plane = OPENTURF_MAX_PLANE - else if (T.below.z_delegate) - QDEL_NULL(T.below.z_delegate) + else if (T.below.mimic_above_copy) + QDEL_NULL(T.below.mimic_above_copy) // Handle below atoms. @@ -214,49 +240,56 @@ SUBSYSTEM_DEF(zcopy) // Special case: these are merged into the shadower to reduce memory usage. if (object.type == /atom/movable/lighting_overlay) - // T.shadower.copy_lighting(object) - else - if (!object.bound_overlay) // Generate a new overlay if the atom doesn't already have one. - object.bound_overlay = new(T) - object.bound_overlay.associated_atom = object - - var/override_depth - var/original_type = object.type - var/original_z = object.z - switch (object.type) - if (/atom/movable/openspace/overlay) - var/atom/movable/openspace/overlay/OOO = object - original_type = OOO.mimiced_type - override_depth = OOO.override_depth - original_z = OOO.original_z - - if (/atom/movable/openspace/turf_delegate, /atom/movable/openspace/delegate_copy) - // If we're a turf overlay (the mimic for a non-OVERWRITE turf), we need to make sure copies of us respect space parallax too - if (T.z_eventually_space) - // Yes, this is an awful hack; I don't want to add yet another override_* var. - override_depth = OPENTURF_MAX_PLANE - SPACE_PLANE - - if ((T.z_flags & ZM_FIX_BIGTURF) && original_type == /atom/movable/openspace/multiplier) - override_depth = turf_depth - - var/atom/movable/openspace/overlay/OO = object.bound_overlay - - // If the OO was queued for destruction but was claimed by another OT, stop the destruction timer. - if (OO.destruction_timer) - deltimer(OO.destruction_timer) - OO.destruction_timer = null - - OO.depth = override_depth || min(T.z - original_z, OPENTURF_MAX_DEPTH) - OO.mimiced_type = original_type - OO.override_depth = override_depth - OO.original_z = original_z - - if (!OO.queued) - OO.queued = TRUE - queued_overlays += OO + //T.shadower.copy_lighting(object) + continue + + if (!object.bound_overlay) // Generate a new overlay if the atom doesn't already have one. + object.bound_overlay = new(T) + object.bound_overlay.associated_atom = object + + var/override_depth + var/original_type = object.type + var/original_z = object.z + switch (object.type) + if (/atom/movable/openspace/mimic) + var/atom/movable/openspace/mimic/OOO = object + original_type = OOO.mimiced_type + override_depth = OOO.override_depth + original_z = OOO.original_z + + if (/atom/movable/openspace/turf_proxy, /atom/movable/openspace/turf_mimic) + // If we're a turf overlay (the mimic for a non-OVERWRITE turf), we need to make sure copies of us respect space parallax too + if (T.z_eventually_space) + // Yes, this is an awful hack; I don't want to add yet another override_* var. + override_depth = OPENTURF_MAX_PLANE - SPACE_PLANE + + var/atom/movable/openspace/mimic/OO = object.bound_overlay + + // If the OO was queued for destruction but was claimed by another OT, stop the destruction timer. + if (OO.destruction_timer) + deltimer(OO.destruction_timer) + OO.destruction_timer = null + + OO.depth = override_depth || min(zlev_maximums[T.z] - original_z, OPENTURF_MAX_DEPTH) + + // These types need to be pushed a layer down for bigturfs to function correctly. + switch (original_type) + if (/atom/movable/openspace/multiplier, /atom/movable/openspace/turf_mimic, /atom/movable/openspace/turf_proxy) + if (OO.depth < OPENTURF_MAX_DEPTH) + OO.depth += 1 + + OO.mimiced_type = original_type + OO.override_depth = override_depth + OO.original_z = original_z + + // Multi-queue to maintain ordering of updates to these + // queueing it multiple times will result in only the most recent + // actually processing. + OO.queued += 1 + queued_overlays += OO T.z_queued -= 1 - if (!no_mc_tick && T.above) + if (T.above) T.above.update_mimic() if (no_mc_tick) @@ -272,11 +305,11 @@ SUBSYSTEM_DEF(zcopy) MC_SPLIT_TICK while (qo_idex <= curr_ov.len) - var/atom/movable/openspace/overlay/OO = curr_ov[qo_idex] + var/atom/movable/openspace/mimic/OO = curr_ov[qo_idex] curr_ov[qo_idex] = null qo_idex += 1 - if (QDELETED(OO)) + if (QDELETED(OO) || !OO.queued) if (no_mc_tick) CHECK_TICK else if (MC_TICK_CHECK) @@ -292,6 +325,16 @@ SUBSYSTEM_DEF(zcopy) break continue + // Don't update unless we're at the most recent queue occurrence. + if (OO.queued > 1) + OO.queued -= 1 + multiqueue_skips_object += 1 + if (no_mc_tick) + CHECK_TICK + else if (MC_TICK_CHECK) + break + continue + // Actually update the overlay. if (OO.dir != OO.associated_atom.dir) OO.set_dir(OO.associated_atom.dir) @@ -300,7 +343,7 @@ SUBSYSTEM_DEF(zcopy) OO.plane = OPENTURF_MAX_PLANE - OO.depth OO.opacity = FALSE - OO.queued = FALSE + OO.queued = 0 if (OO.bound_overlay) // If we have a bound overlay, queue it too. OO.update_above() @@ -316,6 +359,9 @@ SUBSYSTEM_DEF(zcopy) #define FMT_DEPTH(X) (X == null ? "(null)" : X) +// This is a dummy object used so overlays can be shown in the analyzer. +/atom/movable/openspace/debug + /client/proc/analyze_openturf(turf/T) set name = "Analyze Openturf" set desc = "Show the layering of an openturf and everything it's mimicking." @@ -324,15 +370,20 @@ SUBSYSTEM_DEF(zcopy) if (!check_rights(R_DEBUG)) return + var/is_above_space = T.is_above_space() var/list/out = list( "", "

Analysis of [T] at [T.x],[T.y],[T.z]

", "Queue occurrences: [T.z_queued]", - "Above space: Apparent [T.z_eventually_space ? "Yes" : "No"], Actual [T.is_above_space() ? "Yes" : "No"]", + "Above space: Apparent [T.z_eventually_space ? "Yes" : "No"], Actual [is_above_space ? "Yes" : "No"] - [T.z_eventually_space == is_above_space ? "OK" : "MISMATCH"]", "Z Flags: [english_list(bitfield2list(T.z_flags, global.mimic_defines), "(none)")]", "Has Shadower: [T.shadower ? "Yes" : "No"]", + "Has turf proxy: [T.mimic_proxy ? "Yes" : "No"]", + "Has above copy: [T.mimic_above_copy ? "Yes" : "No"]", + "Has mimic underlay: [T.mimic_underlay ? "Yes" : "No"]", "Below: [!T.below ? "(nothing)" : "[T.below] at [T.below.x],[T.below.y],[T.below.z]"]", "Depth: [FMT_DEPTH(T.z_depth)] [T.z_depth == OPENTURF_MAX_DEPTH ? "(max)" : ""]", + "Generation: [T.z_generation]", "