diff --git a/.editorconfig b/.editorconfig index 7fc74c92066..ba0519edb24 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,28 +1,12 @@ +# http://editorconfig.org root = true [*] -indent_style = tab -indent_size = 4 charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true end_of_line = lf - -[*.yml] indent_style = space -indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true -[*.py] -indent_style = space - -[*.md] -indent_style = space -indent_size = 2 -trim_trailing_whitespace = false - -[*.sql] -indent_style = space -indent_size = 2 - -[Dockerfile] -indent_style = space +[*.{dm,json}] +indent_style = tab diff --git a/.gitconfig b/.gitconfig deleted file mode 100644 index cd4cf17954b..00000000000 --- a/.gitconfig +++ /dev/null @@ -1,4 +0,0 @@ -[merge "merge-dmm"] - name = mapmerge driver - driver = ./mapmerge.sh %O %A %B - recursive = text diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index c5e1ec3ec73..00000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Bug report -about: Create a report to help reproduce and fix the issue ---- - -## Round ID: - - - -## Testmerges: - - - -## Reproduction: - - - - diff --git a/.github/ISSUE_TEMPLATE/bug_report_form.yml b/.github/ISSUE_TEMPLATE/bug_report_form.yml new file mode 100644 index 00000000000..0a86431e977 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_form.yml @@ -0,0 +1,65 @@ +name: Bug Report +description: Create a report to help reproduce and fix the issue. +body: + - type: markdown + attributes: + value: | + # **Please read the following guidelines. Follow all instructions or else your issue is subject to closure.** + ## If you use the "Report Issue" button in the top-right corner of the game, it will automatically fill in some of the information below. + + If you are reporting an issue found in another branch or codebase, you _must_ link the branch or codebase repo in your issue report or it will be closed. + For branches, If you have not pushed your code up, please either reproduce it on master or push your code up before making an issue report. + For other codebases, if you do not have a public code repository you will be refused help unless you can completely reproduce the issue on our master branch. + - type: input + id: reporting-version + attributes: + label: "Client Version:" + description: | + The BYOND version you are using to report this issue. You can find this information in the bottom left corner of the "About BYOND" window in the BYOND client. + It is strongly recommended that you include this, especially for concerns on the visual aspects of the game. + placeholder: "xxx.xxxx" + validations: + required: false + - type: textarea + id: issue-summary + attributes: + label: "Issue Summary:" + description: | + Briefly explain your issue in a few plain sentences. You may copy and paste the issue title here if it is suitable. + placeholder: | + "When I do X, Y happens instead of Z." + "X on Y map has Z issue." + validations: + required: true + - type: input + id: round-id + attributes: + label: "Round ID:" + description: | + If you discovered this issue from playing tgstation hosted servers, the Round ID can be found in the Status panel or retrieved from https://statbus.space/ + The Round ID lets us look up valuable information and logs for the round the bug happened. Leave this blank if there is no round ID. + placeholder: "XXXXXX" + validations: + required: false + - type: textarea + id: test-merges + attributes: + label: "Test Merge Information:" + description: | + If you're certain the issue is to be caused by a test merge [OOC Tab -> Show Server Revision], report it in the pull request's comment section rather than on the tracker. + If you're unsure you can refer to the issue number by prefixing said number with #. The issue number can be found beside the title after submission of this form. + validations: + required: false + - type: textarea + id: reproduction + attributes: + label: "Reproduction Steps:" + description: | + Describe the steps to reproduce the issue in detail. Include any relevant information, such as the map, round type, and any other factors that may be relevant. + If it is a runtime-related error, please include the runtime here as that is pertient information. Issues are not for oddities introduced by admin varedits, ensure these occur in normal circumstances. + placeholder: | + 1. Go to the X location + 2. Do Y action + 3. Observe Z result + validations: + required: true diff --git a/.github/actions/restore_or_install_byond/action.yml b/.github/actions/restore_or_install_byond/action.yml new file mode 100644 index 00000000000..87cb17968cc --- /dev/null +++ b/.github/actions/restore_or_install_byond/action.yml @@ -0,0 +1,51 @@ +# This action attempts to restore BYOND from a cache, or to install it otherwise. +name: Restore or Install BYOND +description: Attempts to restore a specified BYOND version from cache; if it can't, it installs it. + +inputs: + major: + description: "The major BYOND version to install. Defaults to the BYOND_MAJOR specified in `dependencies.sh`." + required: false + type: string + minor: + description: "The minor BYOND version to install. Defaults to the BYOND_MINOR specified in `dependencies.sh`." + required: false + type: string + +runs: + using: composite + steps: + - name: Configure BYOND version from inputs + if: ${{ inputs.major }} + shell: bash + run: | + echo "BYOND_MAJOR=${{ inputs.major }}" >> $GITHUB_ENV + echo "BYOND_MINOR=${{ inputs.minor }}" >> $GITHUB_ENV + - name: Configure BYOND version from dependencies.sh + if: ${{ !inputs.major }} + shell: bash + run: | + source dependencies.sh + echo "BYOND_MAJOR=$BYOND_MAJOR" >> $GITHUB_ENV + echo "BYOND_MINOR=$BYOND_MINOR" >> $GITHUB_ENV + + # The use of `actions/cache/restore` and `actions/cache/save` here is deliberate, as we want to + # save the BYOND install to a cache as early as possible. If we used just `actions/cache`, it + # would only attempt to save the cache at the end of a job. This ensures that if a workflow run + # is cancelled, we already have a cache to restore from. + - name: Restore BYOND cache + id: restore_byond_cache + uses: actions/cache/restore@v4 + with: + path: ~/BYOND + key: ${{ runner.os }}-byond-${{ env.BYOND_MAJOR }}-${{ env.BYOND_MINOR }} + - name: Install BYOND + if: ${{ !steps.restore_byond_cache.outputs.cache-hit }} + shell: bash + run: bash tools/ci/install/install_byond.sh + - name: Save BYOND cache + if: ${{ !steps.restore_byond_cache.outputs.cache-hit }} + uses: actions/cache/save@v4 + with: + path: ~/BYOND + key: ${{ steps.restore_byond_cache.outputs.cache-primary-key }} diff --git a/.github/max_required_byond_client.txt b/.github/max_required_byond_client.txt new file mode 100644 index 00000000000..06a6d36fdca --- /dev/null +++ b/.github/max_required_byond_client.txt @@ -0,0 +1,8 @@ +# Highest byond client version allowed to be required by the byond world. Set to 9999 to disable the check flat out. +# If the compiled world requires clients use a version higher than this, ci will fail. +# for instance: if this is set to 514, and a pr uses a 515 client feature, an alert will trigger +# If you have to update this number for your pr, you should make it VERY CLEAR in the pr body that you did so. +# (Requiring clients update to connect to the game server is not something we like to spring on them with no notice, +# especially for beta builds where the pager/updater won't let them update without additional configuration.) + +516 diff --git a/.github/workflows/auto_changelog.yml b/.github/workflows/auto_changelog.yml new file mode 100644 index 00000000000..61ed47c74cc --- /dev/null +++ b/.github/workflows/auto_changelog.yml @@ -0,0 +1,25 @@ +# Creates an entry in html/changelogs automatically, to eventually be compiled by compile_changelogs +name: Auto Changelog +on: + pull_request_target: + types: + - closed + branches: + - master +permissions: + contents: write +jobs: + auto_changelog: + runs-on: ubuntu-24.04 + if: github.event.pull_request.merged == true + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Run auto changelog + uses: actions/github-script@v7 + with: + script: | + const { processAutoChangelog } = await import('${{ github.workspace }}/tools/pull_request_hooks/autoChangelog.js') + await processAutoChangelog({ github, context }) + github-token: ${{ secrets.SILICONS_BOT_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/auto_changelong.yml b/.github/workflows/auto_changelong.yml deleted file mode 100644 index 8fa502cd70d..00000000000 --- a/.github/workflows/auto_changelong.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Creates an entry in html/changelogs automatically, to eventually be compiled by compile_changelogs -name: Auto Changelog -on: - pull_request_target: - types: - - closed - branches: - - master -permissions: - contents: write -jobs: - auto_changelog: - runs-on: ubuntu-latest - if: github.event.pull_request.merged == true - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Run auto changelog - uses: actions/github-script@v6 - with: - script: | - const { processAutoChangelog } = await import('${{ github.workspace }}/tools/pull_request_hooks/autoChangelog.js') - await processAutoChangelog({ github, context }) - github-token: ${{ secrets.SILICONS_BOT_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci_suite.yml b/.github/workflows/ci_suite.yml index e926dee91f0..b682de6687c 100644 --- a/.github/workflows/ci_suite.yml +++ b/.github/workflows/ci_suite.yml @@ -1,18 +1,13 @@ name: CI Suite + on: - push: - branches: - - master - - 'project/**' - - 'gh-readonly-queue/master/**' - - 'gh-readonly-queue/project/**' pull_request: branches: - - master - - 'project/**' + - master + - "project/**" merge_group: branches: - - master + - master concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -22,7 +17,7 @@ jobs: start_gate: if: ( !contains(github.event.head_commit.message, '[ci skip]') ) name: Start Gate - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Mandatory Empty Step run: exit 0 @@ -30,182 +25,48 @@ jobs: run_linters: name: Run Linters needs: start_gate - runs-on: ubuntu-24.04 - timeout-minutes: 10 + uses: ./.github/workflows/run_linters.yml - steps: - - uses: actions/checkout@v4 - - name: Restore SpacemanDMM cache - uses: actions/cache@v4 - with: - path: ~/SpacemanDMM - key: ${{ runner.os }}-spacemandmm-${{ hashFiles('dependencies.sh') }} - restore-keys: | - ${{ runner.os }}-spacemandmm- - - name: Restore Yarn cache - uses: actions/cache@v4 - with: - path: tgui/.yarn/cache - key: ${{ runner.os }}-yarn-${{ hashFiles('tgui/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - name: Restore Node cache - uses: actions/cache@v4 - with: - path: ~/.nvm - key: ${{ runner.os }}-node-${{ hashFiles('dependencies.sh') }} - restore-keys: | - ${{ runner.os }}-node- - - name: Restore Bootstrap cache - uses: actions/cache@v4 - with: - path: tools/bootstrap/.cache - key: ${{ runner.os }}-bootstrap-${{ hashFiles('tools/requirements.txt') }} - restore-keys: | - ${{ runner.os }}-bootstrap- - - name: Restore Rust cache - uses: actions/cache@v4 - with: - path: ~/.cargo - key: ${{ runner.os }}-rust-${{ hashFiles('tools/ci/ci_dependencies.sh')}} - restore-keys: | - ${{ runner.os }}-rust- - - name: Restore Cutter cache - uses: actions/cache@v4 - with: - path: tools/icon_cutter/cache - key: ${{ runner.os }}-cutter-${{ hashFiles('dependencies.sh') }} - - name: Install Tools - run: | - bash tools/ci/install_node.sh - bash tools/ci/install/install_spaceman_dmm.sh dreamchecker - bash tools/ci/install_ripgrep.sh - sudo apt install -y python3-pip - tools/bootstrap/python -c '' - - name: Give Linters A Go - id: linter-setup - run: ':' - - name: Run Grep Checks - if: steps.linter-setup.conclusion == 'success' && !cancelled() - run: bash tools/ci/check_grep.sh - - name: Run DreamChecker - if: steps.linter-setup.conclusion == 'success' && !cancelled() - shell: bash - run: ~/dreamchecker 2>&1 | bash tools/ci/annotate_dm.sh - - name: Run Map Checks - if: steps.linter-setup.conclusion == 'success' && !cancelled() - run: | - tools/bootstrap/python -m mapmerge2.dmm_test - - name: Run DMI Tests - if: steps.linter-setup.conclusion == 'success' && !cancelled() - run: tools/bootstrap/python -m dmi.test - - name: Check File Directories - if: steps.linter-setup.conclusion == 'success' && !cancelled() - run: bash tools/ci/check_filedirs.sh citadel.dme - - name: Check Changelogs - if: steps.linter-setup.conclusion == 'success' && !cancelled() - run: bash tools/ci/check_changelogs.sh - - name: Check Miscellaneous Files - if: steps.linter-setup.conclusion == 'success' && !cancelled() - run: bash tools/ci/check_misc.sh - - name: Run TGUI Checks - if: steps.linter-setup.conclusion == 'success' && !cancelled() - run: tools/build/build --ci lint tgui-test + collect_data: + name: Collect data and setup caches for other tasks + needs: start_gate + uses: ./.github/workflows/collect_data.yml compile_all_maps: name: Compile Maps - needs: start_gate - runs-on: ubuntu-22.04 - timeout-minutes: 10 + needs: collect_data + uses: ./.github/workflows/compile_all_maps.yml + with: + max_required_byond_client: ${{ needs.collect_data.outputs.max_required_byond_client }} - steps: - - uses: actions/checkout@v4 - - name: Restore BYOND cache - uses: actions/cache@v4 - with: - path: ~/BYOND - key: ${{ runner.os }}-byond-${{ hashFiles('dependencies.sh') }} - - name: Compile "All" Maps - run: | - bash tools/ci/install/install_byond.sh - source $HOME/BYOND/byond/bin/byondsetup - tools/build/build --ci dm -DCIBUILDING -DCITESTING -DALL_MAPS + setup_build_artifacts: + name: Setup build artifacts + needs: collect_data + uses: ./.github/workflows/setup_build_artifacts.yml + with: + build_versions: ${{ needs.collect_data.outputs.required_build_versions }} - run_integration_tests: + run_all_tests: name: Integration Tests - needs: start_gate - runs-on: ubuntu-latest - timeout-minutes: 15 - services: - mysql: - image: mysql:latest - env: - MYSQL_ROOT_PASSWORD: root - ports: - - 3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - steps: - - uses: actions/checkout@v4 - - name: Restore Flyway - uses: actions/cache@v4 - with: - path: ~/flyway - key: ${{ runner.os }}-flyway-${{ hashFiles('dependencies.sh') }} - - name: Restore BYOND cache - uses: actions/cache@v4 - with: - path: ~/BYOND - key: ${{ runner.os }}-byond-${{ hashFiles('dependencies.sh') }} - - name: Install flyway - run: | - bash tools/ci/install/install_flyway.sh - - name: Setup database - run: | - sudo systemctl start mysql - mysql -u root -proot -e 'CREATE DATABASE ss13;' - source dependencies.sh - ~/flyway/flyway-$FLYWAY_VERSION/flyway -user=root -password=root -url=jdbc:mariadb://localhost:3306/ss13 -locations="filesystem:sql/migrations" migrate - - name: Install rust-g - run: | - bash tools/ci/install/install_rust_g.sh - - name: Compile Tests - id: compile_tests - run: | - bash tools/ci/install/install_byond.sh - source $HOME/BYOND/byond/bin/byondsetup - tools/build/build --ci dm -DCIBUILDING -DANSICOLORS -Werror -ITG0001 -I"loop_checks" - - name: Run Tests - run: | - source $HOME/BYOND/byond/bin/byondsetup - bash tools/ci/run_server.sh - - test_windows: - name: Windows Build - needs: start_gate - runs-on: windows-latest - timeout-minutes: 5 - - steps: - - uses: actions/checkout@v4 - - name: Restore Yarn cache - uses: actions/cache@v4 - with: - path: tgui/.yarn/cache - key: ${{ runner.os }}-yarn-${{ hashFiles('tgui/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - name: Compile - run: pwsh tools/ci/build.ps1 - env: - DM_EXE: "C:\\byond\\bin\\dm.exe" + needs: [collect_data, setup_build_artifacts] + uses: ./.github/workflows/perform_regular_version_tests.yml + with: + max_required_byond_client: ${{ needs.collect_data.outputs.max_required_byond_client }} completion_gate: # Serves as a non-moving target for branch rulesets if: always() && !cancelled() name: Completion Gate - needs: [ test_windows, run_integration_tests, compile_all_maps, run_linters ] - runs-on: ubuntu-latest + needs: [ + # compare_screenshots, + compile_all_maps, + run_all_tests, + # run_alternate_tests, + run_linters, + ] + runs-on: ubuntu-24.04 steps: - name: Decide whether the needed jobs succeeded or failed uses: re-actors/alls-green@release/v1 with: jobs: ${{ toJSON(needs) }} + # allowed-skips: compare_screenshots diff --git a/.github/workflows/codeowner_reviews.yml b/.github/workflows/codeowner_reviews.yml index ed06f9b8a99..f1cb174de37 100644 --- a/.github/workflows/codeowner_reviews.yml +++ b/.github/workflows/codeowner_reviews.yml @@ -7,12 +7,12 @@ on: jobs: assign-users: - - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + timeout-minutes: 5 steps: # Checks-out your repository under $GITHUB_WORKSPACE, so the job can access it - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 #Parse the Codeowner file on non draft PRs - name: CodeOwnersParser @@ -25,5 +25,5 @@ jobs: if: steps.CodeOwnersParser.outputs.owners != '' uses: tgstation/RequestReviewFromUser@v1 with: - separator: ' ' + separator: " " users: ${{ steps.CodeOwnersParser.outputs.owners }} diff --git a/.github/workflows/collect_data.yml b/.github/workflows/collect_data.yml new file mode 100644 index 00000000000..2218f620c29 --- /dev/null +++ b/.github/workflows/collect_data.yml @@ -0,0 +1,40 @@ +name: Collect Data + +on: + workflow_call: + outputs: + max_required_byond_client: + description: "The max required byond client version" + value: ${{ jobs.collect_data.outputs.max_required_byond_client }} + required_build_versions: + description: "Build versions that need to be precompiled" + value: ${{ jobs.collect_data.outputs.required_build_versions }} + +jobs: + collect_data: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + max_required_byond_client: ${{ steps.max_required_byond_client.outputs.max_required_byond_client }} + required_build_versions: ${{ steps.setup_required_build_versions.outputs.required_build_versions }} + + steps: + - uses: actions/checkout@v6 + - name: Collect byond client version configuration + id: max_required_byond_client + #the regex here does not filter out non-numbers because error messages about no input are less helpful then error messages about bad input (which includes the bad input) + run: | + echo "max_required_byond_client=$(grep -Ev '^[[:blank:]]{0,}#{1,}|^[[:blank:]]{0,}$' .github/max_required_byond_client.txt | tail -n1)" >> $GITHUB_OUTPUT + - name: Set up BYOND cache + uses: ./.github/actions/restore_or_install_byond + - name: Set up required build versions + id: setup_required_build_versions + run: | + DEFAULT_VERSION='[{"major": "${{ env.BYOND_MAJOR }}","minor": "${{ env.BYOND_MINOR }}"}]' + + REQUIRED_BUILD_VERSIONS=$(jq -nc \ + --argjson alternate "$DEFAULT_VERSION" \ + --argjson default "$DEFAULT_VERSION" ' + ($alternate + $default) | map({major, minor}) | unique + ') + echo "required_build_versions=$REQUIRED_BUILD_VERSIONS" >> $GITHUB_OUTPUT diff --git a/.github/workflows/compile_all_maps.yml b/.github/workflows/compile_all_maps.yml new file mode 100644 index 00000000000..565129582e8 --- /dev/null +++ b/.github/workflows/compile_all_maps.yml @@ -0,0 +1,45 @@ +name: Compile All Maps + +on: + workflow_call: + inputs: + max_required_byond_client: + required: true + type: string + +jobs: + compile_all_maps: + name: Compile All Maps + runs-on: ubuntu-24.04 + timeout-minutes: 5 + + steps: + - uses: actions/checkout@v6 + - name: Restore BYOND from Cache + uses: ./.github/actions/restore_or_install_byond + - name: Compile All Maps + run: | + bash tools/ci/install/install_byond.sh + source $HOME/BYOND/byond/bin/byondsetup + tools/build/build --ci dm -DCIBUILDING -DCITESTING -DALL_MAPS + - name: Check client Compatibility + uses: tgstation/byond-client-compatibility-check@v3 + with: + dmb-location: citadel.dmb + max-required-client-version: ${{inputs.max_required_byond_client}} + + # compile_all_templates: + # name: Compile All Templates + # runs-on: ubuntu-24.04 + # timeout-minutes: 5 + + # steps: + # - uses: actions/checkout@v6 + # - name: Setup Bun + # uses: ./.github/actions/setup_bun + # - name: Restore BYOND from Cache + # uses: ./.github/actions/restore_or_install_byond + # - name: Compile All Maps + # run: | + # source $HOME/BYOND/byond/bin/byondsetup + # tools/build/build.sh --ci dm -DCIBUILDING -DCITESTING -DALL_TEMPLATES diff --git a/.github/workflows/labeling.yml b/.github/workflows/labeling.yml new file mode 100644 index 00000000000..7ea148d6e64 --- /dev/null +++ b/.github/workflows/labeling.yml @@ -0,0 +1,36 @@ +name: Label +on: + pull_request_target: + types: [closed, opened, synchronize] + +jobs: + # labeler must run before gbp because gbp calculates itself based on labels + labeler: + runs-on: ubuntu-24.04 + if: github.event.action == 'opened' || github.event.action == 'synchronize' + permissions: + pull-requests: write # to apply labels + issues: write # to apply labels + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Run Auto Labeler + uses: actions/github-script@v7 + with: + script: | + const { get_updated_label_set } = await import('${{ github.workspace }}/tools/pull_request_hooks/autoLabel.js'); + const new_labels = await get_updated_label_set({ github, context }); + github.rest.issues.setLabels({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + labels: new_labels, + }); + size-label: + runs-on: ubuntu-24.04 + steps: + - name: size-label + uses: pascalgn/size-label-action@v0.5.4 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + IGNORED: "**/*.bundle.*\n**/*.chunk.*" # **/*.dmm\n diff --git a/.github/workflows/perform_regular_version_tests.yml b/.github/workflows/perform_regular_version_tests.yml new file mode 100644 index 00000000000..2cf76b18728 --- /dev/null +++ b/.github/workflows/perform_regular_version_tests.yml @@ -0,0 +1,21 @@ +name: Run Regular BYOND Version Tests + +on: + workflow_call: + inputs: + max_required_byond_client: + required: true + type: string + +jobs: + run: + uses: ./.github/workflows/run_integration_tests.yml + + # strategy: + # fail-fast: false + # matrix: + # map: ${{ fromJSON(inputs.maps).paths }} + + with: + # map: ${{ matrix.map }} + max_required_byond_client: ${{ inputs.max_required_byond_client }} diff --git a/.github/workflows/remove_guide_comments.yml b/.github/workflows/remove_guide_comments.yml new file mode 100644 index 00000000000..e5e168df7f5 --- /dev/null +++ b/.github/workflows/remove_guide_comments.yml @@ -0,0 +1,18 @@ +# Removes guide comments from PRs when opened, so that when we merge them +# and reuse the pull request description, the clutter is not left behind +name: Remove guide comments +on: + pull_request_target: + types: [opened] +jobs: + remove_guide_comments: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Remove guide comments + uses: actions/github-script@v7 + with: + script: | + const { removeGuideComments } = await import('${{ github.workspace }}/tools/pull_request_hooks/removeGuideComments.js') + await removeGuideComments({ github, context }) diff --git a/.github/workflows/round_id_linker.yml b/.github/workflows/round_id_linker.yml index 5aede3503d1..9b3ddfe2d36 100644 --- a/.github/workflows/round_id_linker.yml +++ b/.github/workflows/round_id_linker.yml @@ -5,8 +5,8 @@ on: jobs: link_rounds: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 steps: - - uses: tgstation/round_linker@master - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + - uses: tgstation/round_linker@master + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/run_integration_tests.yml b/.github/workflows/run_integration_tests.yml new file mode 100644 index 00000000000..192f258a692 --- /dev/null +++ b/.github/workflows/run_integration_tests.yml @@ -0,0 +1,106 @@ +# This is a reusable workflow to run integration tests on a single map. +# This is run for every single map in ci_suite.yml. You might want to edit that instead. +name: Run Integration Tests + +on: + workflow_call: + inputs: + major: + required: false + type: string + minor: + required: false + type: string + max_required_byond_client: + required: true + type: string + +jobs: + run_integration_tests: + # If `inputs.major` is specified, this will output `Run Tests (major.minor; map; max)`. + # For example, `Run Tests (515.1627; runtimestation; 515)`. + # + # Otherwise, it will output `Run Tests (map; max)`. + # For example, `Run Tests (runtimestation; 515)`. + name: Run Tests (${{ inputs.major && format('{0}.{1}; ', inputs.major, inputs.minor) || '' }}; ${{ inputs.max_required_byond_client }}) + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - name: Restore BYOND from Cache + uses: ./.github/actions/restore_or_install_byond + with: + major: ${{ inputs.major }} + minor: ${{ inputs.minor }} + - name: Download build outputs + uses: actions/download-artifact@v7 + with: + name: build-artifact-${{ inputs.major || env.BYOND_MAJOR }}-${{ inputs.minor || env.BYOND_MINOR}} + path: ./ + - name: Restore Flyway + id: restore_flyway_cache + uses: actions/cache@v4 + with: + path: ~/flyway + key: ${{ runner.os }}-flyway-${{ hashFiles('dependencies.sh') }} + - name: Install flyway + if: ${{ !steps.restore_flyway_cache.outputs.cache-hit }} + shell: bash + run: bash tools/ci/install/install_flyway.sh + - name: Setup database + # env: + # MYSQL_CONFIG_FILE: tools/ci/mysql_config.cnf + # mysql --defaults-extra-file=${{ env.MYSQL_CONFIG_FILE }} -e 'CREATE DATABASE tg_ci;' + # mysql --defaults-extra-file=${{ env.MYSQL_CONFIG_FILE }} tg_ci < SQL/tgstation_schema.sql + # mysql --defaults-extra-file=${{ env.MYSQL_CONFIG_FILE }} -e 'CREATE DATABASE tg_ci_prefixed;' + # mysql --defaults-extra-file=${{ env.MYSQL_CONFIG_FILE }} tg_ci_prefixed < SQL/tgstation_schema_prefixed.sql + run: | + sudo systemctl start mysql + mysql -u root -proot -e 'CREATE DATABASE ss13;' + source dependencies.sh + ~/flyway/flyway-$FLYWAY_VERSION/flyway -user=root -password=root -url=jdbc:mariadb://localhost:3306/ss13 -locations="filesystem:sql/migrations" migrate + echo "Sucessful MySQL Database Setup" + - name: Install rust-g + run: | + bash tools/ci/install/install_rust_g.sh + - name: Run Tests + id: run_tests + run: | + source $HOME/BYOND/byond/bin/byondsetup + bash tools/ci/run_server.sh + # - name: Upload screenshot tests + # if: always() + # uses: actions/upload-artifact@v6 + # with: + # name: test_artifacts_${{ inputs.major }}_${{ inputs.minor }} + # path: data/screenshots_new/ + # retention-days: 1 + - name: On test fail, write a step summary + if: always() && steps.run_tests.outcome == 'failure' + run: | + # Get a JSON array of failed unit tests + FAILED_UNIT_TESTS=$(jq 'to_entries | map(.value | select(.status == 1))' data/unit_tests.json) + + FAIL_COUNT=$(echo $FAILED_UNIT_TESTS | jq 'length') + + echo "# Test failures" >> $GITHUB_STEP_SUMMARY + echo "$FAIL_COUNT tests failed." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + for i in $( seq $FAIL_COUNT ); do + CURRENT_FAIL=$(echo $FAILED_UNIT_TESTS | jq --arg i $i '.[($i | tonumber) - 1]') + + TEST=$(echo $CURRENT_FAIL | jq --raw-output '.name') + + echo "### $TEST" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo $CURRENT_FAIL | jq --raw-output '.message' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + done + - name: Check client Compatibility + if: always() && steps.compile_tests.outcome == 'success' + uses: tgstation/byond-client-compatibility-check@v3 + with: + dmb-location: tgstation.dmb + max-required-client-version: ${{inputs.max_required_byond_client}} diff --git a/.github/workflows/run_linters.yml b/.github/workflows/run_linters.yml new file mode 100644 index 00000000000..5615f4917da --- /dev/null +++ b/.github/workflows/run_linters.yml @@ -0,0 +1,114 @@ +name: Run Linters + +on: + workflow_call: + +jobs: + linters: + runs-on: ubuntu-24.04 + timeout-minutes: 5 + + steps: + - uses: actions/checkout@v6 + - name: Restore SpacemanDMM cache + uses: actions/cache@v5 + with: + path: ~/SpacemanDMM + key: ${{ runner.os }}-spacemandmm-${{ hashFiles('dependencies.sh') }} + restore-keys: | + ${{ runner.os }}-spacemandmm- + - name: Restore Yarn cache + uses: actions/cache@v5 + with: + path: tgui/.yarn/cache + key: ${{ runner.os }}-yarn-${{ hashFiles('tgui/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + - name: Restore Node cache + uses: actions/cache@v5 + with: + path: ~/.nvm + key: ${{ runner.os }}-node-${{ hashFiles('dependencies.sh') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Restore Bootstrap cache + uses: actions/cache@v5 + with: + path: tools/bootstrap/.cache + key: ${{ runner.os }}-bootstrap-${{ hashFiles('/tools/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-bootstrap- + - name: Restore Rust cache + uses: actions/cache@v5 + with: + path: ~/.cargo + key: ${{ runner.os }}-rust-${{ hashFiles('/tools/ci/ci_dependencies.sh')}} + restore-keys: | + ${{ runner.os }}-rust- + - name: Restore Cutter cache + uses: actions/cache@v5 + with: + path: tools/icon_cutter/cache + key: ${{ runner.os }}-cutter-${{ hashFiles('dependencies.sh') }} + - name: Setup .NET SDK + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 9.x + - name: Install OpenDream + uses: robinraju/release-downloader@v1.12 + with: + repository: "OpenDreamProject/OpenDream" + tag: "latest" + fileName: "DMCompiler_linux-x64.tar.gz" + extract: true + - name: Install Tools + run: | + bash tools/ci/install_node.sh + pip3 install setuptools + bash tools/ci/install/install_spaceman_dmm.sh dreamchecker + bash tools/ci/install/install_ripgrep.sh + tools/bootstrap/python -c '' + - name: Give Linters A Go + id: linter-setup + run: ":" + - name: Run Grep Checks + if: steps.linter-setup.conclusion == 'success' && !cancelled() + run: bash tools/ci/check_grep.sh + # - name: Ticked File Enforcement + # if: steps.linter-setup.conclusion == 'success' && !cancelled() + # run: | + # tools/bootstrap/python tools/ticked_file_enforcement/ticked_file_enforcement.py < tools/ticked_file_enforcement/schemas/unit_tests.json + # tools/bootstrap/python tools/ticked_file_enforcement/ticked_file_enforcement.py < tools/ticked_file_enforcement/schemas/citadel_dme.json + # - name: Check Define Sanity + # if: steps.linter-setup.conclusion == 'success' && !cancelled() + # run: tools/bootstrap/python -m define_sanity.check + - name: Run DreamChecker + if: steps.linter-setup.conclusion == 'success' && !cancelled() + shell: bash + run: ~/dreamchecker 2>&1 | bash tools/ci/annotate_dm.sh + - name: Run OpenDream + if: steps.linter-setup.conclusion == 'success' && !cancelled() + run: ./DMCompiler_linux-x64/DMCompiler citadel.dme --suppress-unimplemented --define=CIBUILDING | bash tools/ci/annotate_od.sh + - name: Run Map Checks + if: steps.linter-setup.conclusion == 'success' && !cancelled() + run: | + tools/bootstrap/python -m mapmerge2.dmm_test + tools/bootstrap/python -m tools.maplint.source + - name: Check Cutter + if: steps.linter-setup.conclusion == 'success' && !cancelled() + run: tools/bootstrap/python -m tools.icon_cutter.check + - name: Run DMI Tests + if: steps.linter-setup.conclusion == 'success' && !cancelled() + run: tools/bootstrap/python -m dmi.test + - name: Check File Directories + if: steps.linter-setup.conclusion == 'success' && !cancelled() + run: bash tools/ci/check_filedirs.sh citadel.dme + - name: Check Changelogs + if: steps.linter-setup.conclusion == 'success' && !cancelled() + run: bash tools/ci/check_changelogs.sh + - name: Check Miscellaneous Files + if: steps.linter-setup.conclusion == 'success' && !cancelled() + run: bash tools/ci/check_misc.sh + - name: Run TGUI Checks + if: steps.linter-setup.conclusion == 'success' && !cancelled() + run: tools/build/build --ci lint tgui-test diff --git a/.github/workflows/setup_build_artifact.yml b/.github/workflows/setup_build_artifact.yml new file mode 100644 index 00000000000..3afc29cfa03 --- /dev/null +++ b/.github/workflows/setup_build_artifact.yml @@ -0,0 +1,40 @@ +on: + workflow_call: + inputs: + major: + required: true + type: string + minor: + required: true + type: string + +jobs: + setup_build_artifact: + name: Setup build artifact (${{ format('{0}.{1}', inputs.major, inputs.minor) }}) + runs-on: ubuntu-24.04 + timeout-minutes: 5 + steps: + - uses: actions/checkout@v6 + - name: Restore BYOND from Cache + uses: ./.github/actions/restore_or_install_byond + with: + major: ${{ inputs.major }} + minor: ${{ inputs.minor }} + - name: Compile + run: | + source $HOME/BYOND/byond/bin/byondsetup + tools/build/build --ci dm -DCIBUILDING -DANSICOLORS -Werror -ITG0001 -I"loop_checks" + - name: Setup variable for output path + run: | + OUTPUT_DIR="target" + echo "OUTPUT_DIR=$OUTPUT_DIR" >> $GITHUB_ENV + - name: Copy output files + run: | + bash tools/ci/copy_build_output.sh $OUTPUT_DIR + - name: Upload artifact + if: success() + uses: actions/upload-artifact@v6 + with: + name: build-artifact-${{ inputs.major }}-${{ inputs.minor }} + path: ${{ env.OUTPUT_DIR }} + retention-days: 1 diff --git a/.github/workflows/setup_build_artifacts.yml b/.github/workflows/setup_build_artifacts.yml new file mode 100644 index 00000000000..43d151d3d88 --- /dev/null +++ b/.github/workflows/setup_build_artifacts.yml @@ -0,0 +1,21 @@ +name: Setup build artifacts + +on: + workflow_call: + inputs: + build_versions: + required: true + type: string + +jobs: + run: + uses: ./.github/workflows/setup_build_artifact.yml + + strategy: + fail-fast: true + matrix: + setup: ${{ fromJSON(inputs.build_versions) }} + + with: + major: ${{ matrix.setup.major }} + minor: ${{ matrix.setup.minor }} diff --git a/.github/workflows/size_labeling.yml b/.github/workflows/size_labeling.yml deleted file mode 100644 index 47d3ee779c4..00000000000 --- a/.github/workflows/size_labeling.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: Size-Based Labeling -on: pull_request_target -jobs: - size-label: - runs-on: ubuntu-latest - steps: - - name: size-label - uses: pascalgn/size-label-action@v0.5.4 - env: - GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - IGNORED: "**/*.bundle.*\n**/*.chunk.*" # **/*.dmm\n diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index e420bfd8f74..5c2362534fc 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -2,32 +2,35 @@ name: Mark stale issues and pull requests on: schedule: - - cron: "0 0 * * *" + - cron: "0 0 * * *" permissions: contents: read jobs: stale: - permissions: - issues: write # for actions/stale to close stale issues - pull-requests: write # for actions/stale to close stale PRs - runs-on: ubuntu-20.04 + issues: write # for actions/stale to close stale issues + pull-requests: write # for actions/stale to close stale PRs + runs-on: ubuntu-24.04 steps: - - uses: actions/stale@v4 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-pr-message: "This PR has been inactive for long enough to be automatically marked as stale. This means it is at risk of being auto closed in ~ two week, please address any outstanding review items and ensure your PR is finished, if these are all true and you are auto-staled anyway, you need to actively ask maintainers if your PR will be merged. Once you have done any of the previous actions then you should request a maintainer remove the stale label on your PR, to reset the stale timer. If you feel no maintainer will respond in that time, you may wish to close this PR youself, while you seek maintainer comment, as you will then be able to reopen the PR yourself" - days-before-stale: 14 - days-before-close: 14 - stale-pr-label: 'Stale' - close-pr-label: 'Closed By Stale' - close-pr-message: 'This PR was closed due to inactivity, if you feel this was done in error, please request a maintainer remove the stale label on your PR, to reset the stale timer.' - days-before-issue-stale: -1 - stale-issue-label: 'Cleanup Flagged' - remove-issue-stale-when-updated: false - exempt-draft-pr: true - exempt-pr-labels: 'Good First PR,Work in Progress,Do Not Merge,Stale Exempt' - operations-per-run: 300 + - uses: actions/stale@v10 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-pr-message: "This PR has been inactive for long enough to be automatically marked as stale. This means it is at risk of being auto closed in ~ two week, please address any outstanding review items and ensure your PR is finished, if these are all true and you are auto-staled anyway, you need to actively ask maintainers if your PR will be merged. Once you have done any of the previous actions then you should request a maintainer remove the stale label on your PR, to reset the stale timer. If you feel no maintainer will respond in that time, you may wish to close this PR youself, while you seek maintainer comment, as you will then be able to reopen the PR yourself" + stale-issue-message: "This issue has been marked for cleanup and will be automatically closed in ~ 14 days. If there is evidence that the issue still occurs, leave a comment with it attached and contact a maintainer to have the label removed." + days-before-stale: 14 + days-before-close: 14 + days-before-issue-stale: 14 + days-before-issue-close: 14 + stale-pr-label: "Stale" + stale-issue-label: "Stale" + any-of-issue-labels: "Cleanup Flagged,🤖 Flaky Test Report" + remove-issue-stale-when-updated: false + exempt-issue-labels: "RED LABEL" + exempt-pr-labels: "RED LABEL,Good First PR,Work in Progress,Do Not Merge,Stale Exempt" + close-pr-label: "Closed By Stale" + close-pr-message: "This PR was closed due to inactivity, if you feel this was done in error, please request a maintainer remove the stale label on your PR, to reset the stale timer." + exempt-draft-pr: true + operations-per-run: 300 diff --git a/.github/workflows/tgs_test.yml b/.github/workflows/tgs_test.yml new file mode 100644 index 00000000000..fefe765bb75 --- /dev/null +++ b/.github/workflows/tgs_test.yml @@ -0,0 +1,70 @@ +name: TGS Test Suite +on: + push: + branches: + - master + - "project/**" + - "gh-readonly-queue/master/**" + - "gh-readonly-queue/project/**" + paths: + - ".tgs.yml" + - ".github/workflows/tgs_test.yml" + - "dependencies.sh" + - "code/__DEFINES/tgs.config.dm" + - "code/__DEFINES/tgs.dm" + - "code/game/world.dm" + - "code/modules/tgs/**" + - "tools/bootstrap/**" + - "tools/tgs_scripts/**" + - "tools/tgs_test/**" + pull_request: + branches: + - master + - "project/**" + paths: + - ".tgs.yml" + - ".github/workflows/tgs_test.yml" + - "dependencies.sh" + - "code/__DEFINES/tgs.config.dm" + - "code/__DEFINES/tgs.dm" + - "code/game/world.dm" + - "code/modules/tgs/**" + - "tools/bootstrap/**" + - "tools/tgs_scripts/**" + - "tools/tgs_test/**" + merge_group: + branches: + - master +env: + TGS_API_PORT: 5000 + PR_NUMBER: ${{ github.event.number }} +jobs: + test_tgs_docker: + if: ( !contains(github.event.head_commit.message, '[ci skip]') ) + name: Test TGS Docker + runs-on: ubuntu-24.04 + concurrency: + group: test_tgs_docker-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + services: + tgs: + image: tgstation/server + env: + Database__DatabaseType: Sqlite + Database__ConnectionString: Data Source=TGS_TGTest.sqlite3;Mode=ReadWriteCreate + General__ConfigVersion: 5.0.0 + General__ApiPort: ${{ env.TGS_API_PORT }} + General__SetupWizardMode: Never + ports: + - 5000:5000 #Can't use env here for some reason + steps: + - name: Setup dotnet + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 8.0.x + + - name: Checkout Repository + uses: actions/checkout@v6 + + - name: Test TGS Integration + run: dotnet run -c Release --project tools/tgs_test ${{ github.repository }} /tgs_instances/tgstation ${{ env.TGS_API_PORT }} ${{ github.event.pull_request.head.sha || github.sha }} ${{ secrets.GITHUB_TOKEN }} ${{ env.PR_NUMBER }} diff --git a/.github/workflows/update_tgs_dmapi.yml b/.github/workflows/update_tgs_dmapi.yml index e5a3f7002f6..a510b984dda 100644 --- a/.github/workflows/update_tgs_dmapi.yml +++ b/.github/workflows/update_tgs_dmapi.yml @@ -7,43 +7,46 @@ on: jobs: update-dmapi: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 if: github.repository == 'Citadel-Station-13/Citadel-Station-13-RP' # Don't run on forks! name: Update the TGS DMAPI + permissions: + contents: write + pull-requests: write steps: - - name: Clone - uses: actions/checkout@v4 + - name: Clone + uses: actions/checkout@v6 - - name: Branch - run: | - git branch -f tgs-dmapi-update - git checkout tgs-dmapi-update - git reset --hard master + - name: Branch + run: | + git branch -f tgs-dmapi-update + git checkout tgs-dmapi-update + git reset --hard master - - name: Apply DMAPI update - uses: tgstation/tgs-dmapi-updater@v2 - id: dmapi-update - with: - header-path: 'code/__DEFINES/tgs.dm' - library-path: 'code/modules/tgs' + - name: Apply DMAPI update + uses: tgstation/tgs-dmapi-updater@v2 + id: dmapi-update + with: + header-path: "code/__DEFINES/tgs.dm" + library-path: "code/modules/tgs" - - name: Commit and Push - continue-on-error: true - run: | - git config user.name "tgstation-server-ci[bot]" - git config user.email "161980869+tgstation-server-ci[bot]@users.noreply.github.com" - git add . - git commit -m 'Update TGS DMAPI' - git push -f -u origin tgs-dmapi-update + - name: Commit and Push + continue-on-error: true + run: | + git config user.name "tgstation-ci[bot]" + git config user.email "179393467+tgstation-ci[bot]@users.noreply.github.com" + git add . + git commit -m 'Update TGS DMAPI' + git push -f -u origin tgs-dmapi-update - - name: Create Pull Request - uses: repo-sync/pull-request@v2 - if: ${{ success() }} - with: - source_branch: "tgs-dmapi-update" - destination_branch: "master" - pr_title: "Automatic TGS DMAPI Update" - pr_body: "This pull request updates the TGS DMAPI to the latest version. Please note any changes that may be breaking or unimplemented in your codebase by checking what changes are in the definitions file: code/__DEFINES/tgs.dm before merging.\n\n${{ steps.dmapi-update.outputs.release-notes }}" - pr_label: "Tools" - pr_allow_empty: false - github_token: ${{ secrets.SILICONS_BOT_TOKEN }} + - name: Create Pull Request + uses: repo-sync/pull-request@v2 + if: ${{ success() }} + with: + source_branch: "tgs-dmapi-update" + destination_branch: "master" + pr_title: "Automatic TGS DMAPI Update" + pr_body: "This pull request updates the TGS DMAPI to the latest version. Please note any changes that may be breaking or unimplemented in your codebase by checking what changes are in the definitions file: code/__DEFINES/tgs.dm before merging.\n\n${{ steps.dmapi-update.outputs.release-notes }}" + pr_label: "Tools" + pr_allow_empty: false + github_token: ${{ secrets.SILICONS_BOT_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index c0f3ce12263..21c34db4352 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ -### Files and folders specified here will never be tracked. +###Files and folders specified here will never be tracked. # Ignore everything in datafolder and subdirectories /data/**/* /tmp/**/* +/cache/**/* # Ignore byond config folder. /cfg/**/* @@ -31,10 +32,6 @@ *.lk *.int *.backup - -# Opendream compile result -citadel.json - ### https://raw.github.com/github/gitignore/cc542de017c606138a87ee4880e5f06b3a306def/Global/Linux.gitignore *~ @@ -188,12 +185,12 @@ Temporary Items *.kdev4* *.kate-swp -# Extra map stuff +# extra map stuff /maps/**/backup/ /maps/templates.dm -# Extra DMI stuff -/icons/*.dme +#dmm palette file. You really should be using StrongDMM instead. +*.dmm-pal #dmdoc default folder /dmdoc @@ -207,25 +204,37 @@ Temporary Items !/config/title_screens/images/exclude #Linux docker -/tools/LinuxOneShot/SetupProgram/obj/* -/tools/LinuxOneShot/SetupProgram/bin/* -/tools/LinuxOneShot/SetupProgram/.vs -/tools/LinuxOneShot/Database -/tools/LinuxOneShot/TGS_Config -/tools/LinuxOneShot/TGS_Instances -/tools/LinuxOneShot/TGS_Logs +/tools/tgs_test/.vs/* +/tools/tgs_test/bin/* +/tools/tgs_test/obj/* # Built auxtools libraries and intermediate files aux*.dll libaux*.so -aux*.pdb +*.pdb # byond-tracy, we intentionally do not ship this and do not want to maintain it +# https://github.com/mafemergency/byond-tracy/ prof.dll libprof.so +# Tracy can read source files when it is in the root folder, even without absolute paths. +# If you're interested, run this hack: +# https://gist.github.com/Mothblocks/db5462aa84d7d6b1d1b1276b820f62da +Tracy.exe + # JavaScript tools **/node_modules # Screenshot tests /artifacts + +# named byond versions config +/tools/build/dm_versions.json + +# From /tools/define_sanity/check.py - potential output file that we load onto the user's machine that we don't want to have committed. +define_sanity_output.txt + +# Running OpenDream locally +citadel.json +rust_g64.dll diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000000..43eea47231b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,14 @@ +# Directories +changelogs +data +font-awesome +jquery +juke +**/build +**/dist +**/node_modules + +# File names / types +*.min.* +*.pnp.* +*.bundle.* diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 00000000000..987ee8534a6 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1 @@ +# This file is okay being empty as it will tell prettier to run in the repo diff --git a/.tgs.yml b/.tgs.yml new file mode 100644 index 00000000000..a8cd14e45b6 --- /dev/null +++ b/.tgs.yml @@ -0,0 +1,25 @@ +# This file is used by TGS (https://github.com/tgstation/tgstation-server) clients to quickly initialize a server instance for the codebase +# The format isn't documented anywhere but hopefully we never have to change it. If there are questions, contact the TGS maintainer Cyberboss/@Dominion#0444 +version: 1 +# The BYOND version to use (kept in sync with dependencies.sh by the "TGS Test Suite" CI job) +# Must be interpreted as a string, keep quoted +byond: "516.1659" +# Folders to create in "/Configuration/GameStaticFiles/" +static_files: + # Config directory should be static + - name: config + # This implies the folder should be pre-populated with contents from the repo + populate: true + # Data directory must be static + - name: data +# String dictionary. The value is the location of the file in the repo to upload to TGS. The key is the name of the file to upload to "/Configuration/EventScripts/" +# This one is for Linux hosted servers +linux_scripts: + PreCompile.sh: tools/tgs_scripts/PreCompile.sh + WatchdogLaunch.sh: tools/tgs_scripts/WatchdogLaunch.sh + InstallDeps.sh: tools/tgs_scripts/InstallDeps.sh +# Same as above for Windows hosted servers +windows_scripts: + PreCompile.bat: tools/tgs_scripts/PreCompile.bat +# The security level the game should be run at +security: Trusted diff --git a/.tgs4.yml b/.tgs4.yml deleted file mode 100644 index d137f1dbc8a..00000000000 --- a/.tgs4.yml +++ /dev/null @@ -1,9 +0,0 @@ -static_files: - - name: config - populate: true - - name: data - - name: tmp -linux_scripts: - PreCompile.sh: tools/tgs4_scripts/PreCompile.sh -windows_scripts: - PreCompile.bat: tools/tgs4_scripts/PreCompile.bat diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 44df201905e..78213cb99f7 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,13 +1,13 @@ { "recommendations": [ "platymuus.dm-langclient", - "editorconfig.editorconfig", + "EditorConfig.EditorConfig", "arcanis.vscode-zipfs", - "dbaeumer.vscode-eslint", "stylemistake.auto-comment-blocks", - "donkie.vscode-tgstation-test-adapter", + "Donkie.vscode-tgstation-test-adapter", "anturk.dmi-editor", - "aaron-bond.better-comments", + "esbenp.prettier-vscode", + "biomejs.biome", "ss13.opendream", "tamasfe.even-better-toml" ] diff --git a/.vscode/launch.json b/.vscode/launch.json index 6eb531cfc71..e137e741ef4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,48 @@ { "version": "0.2.0", "configurations": [ + { + "type": "byond", + "request": "launch", + "name": "Launch DreamSeeker", + "preLaunchTask": "Build All", + "dmb": "${workspaceFolder}/${command:CurrentDMB}" + }, + { + "type": "byond", + "request": "launch", + "name": "Launch DreamSeeker (low memory mode)", + "preLaunchTask": "Build All (low memory mode)", + "dmb": "${workspaceFolder}/${command:CurrentDMB}" + }, + { + "type": "byond", + "request": "launch", + "name": "Launch DreamSeeker (absolute minimum)", + "preLaunchTask": "Build All (absolute minimum)", + "dmb": "${workspaceFolder}/${command:CurrentDMB}" + }, + { + "type": "byond", + "request": "launch", + "name": "Launch DreamSeeker (testing)", + "preLaunchTask": "Build All (testing)", + "dmb": "${workspaceFolder}/${command:CurrentDMB}" + }, + { + "type": "byond", + "request": "launch", + "name": "Launch DreamSeeker (testing + low memory mode)", + "preLaunchTask": "Build All (testing + low memory mode)", + "dmb": "${workspaceFolder}/${command:CurrentDMB}" + }, + { + "type": "byond", + "request": "launch", + "name": "Launch DreamSeeker (map testing)", + "preLaunchTask": "Build All (map testing)", + "dmb": "${workspaceFolder}/${command:CurrentDMB}" + }, { "type": "byond", "request": "launch", @@ -12,15 +54,65 @@ { "type": "byond", "request": "launch", - "name": "Launch DreamSeeker", - "preLaunchTask": "Build All", + "name": "Launch DreamDaemon (low memory mode)", + "preLaunchTask": "Build All (low memory mode)", "dmb": "${workspaceFolder}/${command:CurrentDMB}", - "dreamDaemon": false + "dreamDaemon": true + }, + { + "type": "byond", + "request": "launch", + "name": "Launch DreamDaemon (absolute minimum)", + "preLaunchTask": "Build All (absolute minimum)", + "dmb": "${workspaceFolder}/${command:CurrentDMB}", + "dreamDaemon": true + }, + { + "type": "byond", + "request": "launch", + "name": "Launch DreamDaemon (testing)", + "preLaunchTask": "Build All (testing)", + "dmb": "${workspaceFolder}/${command:CurrentDMB}", + "dreamDaemon": true + }, + { + "type": "byond", + "request": "launch", + "name": "Launch DreamDaemon (testing + low memory mode)", + "preLaunchTask": "Build All (testing + low memory mode)", + "dmb": "${workspaceFolder}/${command:CurrentDMB}", + "dreamDaemon": true + }, + { + "type": "byond", + "request": "launch", + "name": "Launch DreamDaemon (map testing)", + "preLaunchTask": "Build All (map testing)", + "dmb": "${workspaceFolder}/${command:CurrentDMB}", + "dreamDaemon": true + }, + { + "name": "Debug External Libraries", + "type": "cppvsdbg", + "request": "launch", + "program": "${command:dreammaker.returnDreamDaemonPath}", + "cwd": "${workspaceRoot}", + "args": ["${command:dreammaker.getFilenameDmb}", "-trusted"], + "preLaunchTask": "Build All" + }, + { + "name": "Debug External Libraries (low memory mode)", + "type": "cppvsdbg", + "request": "launch", + "program": "${command:dreammaker.returnDreamDaemonPath}", + "cwd": "${workspaceRoot}", + "args": ["${command:dreammaker.getFilenameDmb}", "-trusted"], + "preLaunchTask": "Build All (low memory mode)" }, { "type": "opendream", "request": "launch", - "name": "OpenDream", + "name": "Launch OpenDream (requires extension, 64 bit rustg, and an SS14 account)", "preLaunchTask": "OpenDream: compile ${command:CurrentDME}", "json_path": "${workspaceFolder}/${command:CurrentJson}" } diff --git a/.vscode/settings.json b/.vscode/settings.json index f6f66b04326..2995cea1566 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,16 +1,6 @@ { - "eslint.nodePath": "./tgui/.yarn/sdks", - "eslint.workingDirectories": ["./tgui"], - "prettier.prettierPath": "./tgui/.yarn/sdks/prettier/index.cjs", - "typescript.tsdk": "./tgui/.yarn/sdks/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true, - "typescript.tsserver.experimental.enableProjectDiagnostics": true, - "search.exclude": { - "**/.yarn": true, - "**/.pnp.*": true - }, "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" + "source.fixAll.biome": "explicit" }, "files.eol": "\n", "files.insertFinalNewline": true, @@ -19,16 +9,20 @@ "git.branchProtection": ["master"], "gitlens.advanced.blame.customArguments": ["-w"], "tgstationTestExplorer.project.resultsType": "json", - "[html][scss][css][json][jsonc][markdown][yaml]": { - "editor.rulers": [80], + "[javascript][typescript][javascriptreact][typescriptreact][css][json][jsonc]": { + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, + "editor.rulers": [80] + }, + "[yaml][markdown][html][scss]": { "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true + "editor.formatOnSave": true, + "editor.rulers": [80] }, "workbench.editorAssociations": { "*.dmi": "dmiEditor.dmiEditor" }, - "[javascript][typescript][typescriptreact][javascriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "editor.formatOnSave": true + "search.exclude": { + "**/node_modules": true + } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0cec6b15c64..c6ecb4281eb 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,11 +12,7 @@ "DM_EXE": "${config:dreammaker.byondPath}" } }, - "problemMatcher": [ - "$dreammaker", - "$tsc", - "$eslint-stylish" - ], + "problemMatcher": ["$dreammaker", "$tsc", "$eslint-stylish"], "group": { "kind": "build", "isDefault": true @@ -24,12 +20,132 @@ "dependsOn": "dm: reparse", "label": "Build All" }, + { + "type": "process", + "command": "tools/build/build", + "args": ["-DLOWMEMORYMODE"], + "windows": { + "command": ".\\tools\\build\\build.bat", + "args": ["-DLOWMEMORYMODE"] + }, + "options": { + "env": { + "DM_EXE": "${config:dreammaker.byondPath}" + } + }, + "problemMatcher": ["$dreammaker", "$tsc", "$eslint-stylish"], + "group": { + "kind": "build" + }, + "dependsOn": "dm: reparse", + "label": "Build All (low memory mode)" + }, + { + "type": "process", + "command": "tools/build/build", + "args": ["-DLOWMEMORYMODE", "-DABSOLUTE_MINIMUM"], + "windows": { + "command": ".\\tools\\build\\build.bat", + "args": ["-DLOWMEMORYMODE", "-DABSOLUTE_MINIMUM"] + }, + "options": { + "env": { + "DM_EXE": "${config:dreammaker.byondPath}" + } + }, + "problemMatcher": ["$dreammaker", "$tsc", "$eslint-stylish"], + "group": { + "kind": "build" + }, + "dependsOn": "dm: reparse", + "label": "Build All (absolute minimum)" + }, + { + "type": "process", + "command": "tools/build/build", + "args": [ + "-DTESTING", + "-DREAGENTS_TESTING", + "-DTIMER_DEBUG", + "-DREFERENCE_DOING_IT_LIVE" + ], + "windows": { + "command": ".\\tools\\build\\build.bat", + "args": [ + "-DTESTING", + "-DREAGENTS_TESTING", + "-DTIMER_DEBUG", + "-DREFERENCE_DOING_IT_LIVE" + ] + }, + "options": { + "env": { + "DM_EXE": "${config:dreammaker.byondPath}" + } + }, + "problemMatcher": ["$dreammaker", "$tsc", "$eslint-stylish"], + "group": { + "kind": "build" + }, + "dependsOn": "dm: reparse", + "label": "Build All (testing)" + }, + { + "type": "process", + "command": "tools/build/build", + "args": [ + "-DLOWMEMORYMODE", + "-DTESTING", + "-DREAGENTS_TESTING", + "-DTIMER_DEBUG", + "-DREFERENCE_DOING_IT_LIVE" + ], + "windows": { + "command": ".\\tools\\build\\build.bat", + "args": [ + "-DLOWMEMORYMODE", + "-DTESTING", + "-DREAGENTS_TESTING", + "-DTIMER_DEBUG", + "-DREFERENCE_DOING_IT_LIVE" + ] + }, + "options": { + "env": { + "DM_EXE": "${config:dreammaker.byondPath}" + } + }, + "problemMatcher": ["$dreammaker", "$tsc", "$eslint-stylish"], + "group": { + "kind": "build" + }, + "dependsOn": "dm: reparse", + "label": "Build All (testing + low memory mode)" + }, + { + "type": "process", + "command": "tools/build/build", + "args": ["-DTESTING", "-DMAP_TEST"], + "windows": { + "command": ".\\tools\\build\\build.bat", + "args": ["-DTESTING", "-DMAP_TEST"] + }, + "options": { + "env": { + "DM_EXE": "${config:dreammaker.byondPath}" + } + }, + "problemMatcher": ["$dreammaker", "$tsc", "$eslint-stylish"], + "group": { + "kind": "build" + }, + "dependsOn": "dm: reparse", + "label": "Build All (map testing)" + }, { "type": "dreammaker", "dme": "citadel.dme", - "problemMatcher": [ - "$dreammaker" - ], + "problemMatcher": ["$dreammaker"], "group": "build", "label": "dm: build - citadel.dme" }, @@ -44,10 +160,7 @@ "windows": { "command": ".\\bin\\tgui-build.cmd" }, - "problemMatcher": [ - "$tsc", - "$eslint-stylish" - ], + "problemMatcher": ["$tsc", "$eslint-stylish"], "group": "build", "label": "tgui: build" }, @@ -57,10 +170,7 @@ "windows": { "command": ".\\bin\\tgui-dev.cmd" }, - "problemMatcher": [ - "$tsc", - "$eslint-stylish" - ], + "problemMatcher": ["$tsc", "$eslint-stylish"], "group": "build", "label": "tgui: dev server" }, @@ -70,10 +180,7 @@ "windows": { "command": ".\\bin\\tgui-bench.cmd" }, - "problemMatcher": [ - "$tsc", - "$eslint-stylish" - ], + "problemMatcher": ["$tsc", "$eslint-stylish"], "group": "build", "label": "tgui: bench" }, @@ -83,12 +190,19 @@ "windows": { "command": ".\\bin\\tgui-sonar.cmd" }, - "problemMatcher": [ - "$tsc", - "$eslint-stylish" - ], + "problemMatcher": ["$tsc", "$eslint-stylish"], "group": "build", "label": "tgui: sonar" + }, + { + "type": "shell", + "command": "bin/tgfont", + "windows": { + "command": ".\\bin\\tgfont.cmd" + }, + "problemMatcher": ["$tsc", "$eslint-stylish"], + "group": "build", + "label": "tgui: rebuild tgfont" } ] } diff --git a/__odlint.dm b/__odlint.dm new file mode 100644 index 00000000000..b7c120514a1 --- /dev/null +++ b/__odlint.dm @@ -0,0 +1,10 @@ +// 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 + +#if !defined(SPACEMAN_DMM) && defined(OPENDREAM) +// This is in a separate file as a hack to avoid SpacemanDMM +// evaluating the #pragma lines, even if its outside a block it cares about +// (Also so people can code-own it. Shoutout to AA) +#include "tools/ci/od_lints.dm" +#endif diff --git a/citadel.dme b/citadel.dme index c7f6be454fa..0e3c160bca8 100644 --- a/citadel.dme +++ b/citadel.dme @@ -13,6 +13,7 @@ // END_PREFERENCES // BEGIN_INCLUDE +#include "__odlint.dm" #include "_mapload\_basemap.dm" #include "code\___compile_options.dm" #include "code\__byond_version_compat.dm" diff --git a/code/__DEFINES/admin/verbs.dm b/code/__DEFINES/admin/verbs.dm index 90996325d69..60190bc37f3 100644 --- a/code/__DEFINES/admin/verbs.dm +++ b/code/__DEFINES/admin/verbs.dm @@ -45,7 +45,7 @@ do { \ metric_increment_nested_numerical(/datum/metric/nested_numerical/admin_verb_invocation, #PATH_SUFFIX, 1); \ log_admin("[key_name(usr)] invoked admin verb '[#PATH_SUFFIX]'"); \ - }; \ + } \ while(FALSE); \ call(usr.client, /datum/admin_verb_abstraction::verb__invoke_##PATH_SUFFIX())(arglist(list(usr.client) + args)); \ }; \ diff --git a/code/__DEFINES/unit_tests.dm b/code/__DEFINES/unit_tests.dm index ed7cd8490ec..4bf0bcf7137 100644 --- a/code/__DEFINES/unit_tests.dm +++ b/code/__DEFINES/unit_tests.dm @@ -1,16 +1,14 @@ -/** - * Are tests enabled with no focus? - * Use this when performing test assertions outside of a unit test, - * since a focused test means that you're trying to run a test quickly. - * If a parameter is provided, will check if the focus is on that test name. - * For example, PERFORM_ALL_TESTS(log_mapping) will only run if either - * no test is focused, or the focus is log_mapping. - */ +/// Are tests enabled with no focus? +/// Use this when performing test assertions outside of a unit test, +/// since a focused test means that you're trying to run a test quickly. +/// If a parameter is provided, will check if the focus is on that test name. +/// For example, PERFORM_ALL_TESTS(log_mapping) will only run if either +/// no test is focused, or the focus is log_mapping. #ifdef UNIT_TESTS -/// Bit of a trick here, if focus isn't passed in then it'll check for /datum/unit_test/, which is never the case. -#define PERFORM_ALL_TESTS(focus...) (isnull(GLOB.focused_test) || GLOB.focused_test == /datum/unit_test/##focus) +// Bit of a trick here, if focus isn't passed in then it'll check for /datum/unit_test/, which is never the case. +#define PERFORM_ALL_TESTS(focus...) (isnull(GLOB.focused_tests) || (/datum/unit_test/##focus in GLOB.focused_tests)) #else -/// UNLINT necessary here so that if (PERFORM_ALL_TESTS()) works +// UNLINT necessary here so that if (PERFORM_ALL_TESTS()) works #define PERFORM_ALL_TESTS(...) UNLINT(FALSE) #endif @@ -20,3 +18,33 @@ #else #define TEST_ONLY_ASSERT(test, explanation) #endif + +/** + * Used for registering typepaths of item to be tracked as a "required map item" + * This is used to ensure that that all station maps have certain items mapped in that they should have + * Or that people aren't mapping in an excess of items that they shouldn't be + * (For example, all map should only ever have 1 Pun Pun) + * + * Min is inclusive, Max is inclusive (so 1, 1 means min of 1, max of 1, or only 1 allowed) + * + * This should only be used in Initialize(). And don't forget to update the unit test with the type itself! + */ +#ifdef UNIT_TESTS +#define REGISTER_REQUIRED_MAP_ITEM(min, max) \ + do { \ + if(mapload) { \ + var/turf/spawn_turf = get_turf(src); \ + if(spawn_turf?.z && SSmapping.level_has_trait(spawn_turf.z, ZTRAIT_STATION)) { \ + var/datum/required_item/existing_value = GLOB.required_map_items[type]; \ + if(isnull(existing_value)) { \ + var/datum/required_item/new_value = new(type, min, max); \ + GLOB.required_map_items[type] = new_value; \ + } else { \ + existing_value.total_amount += 1; \ + }; \ + }; \ + }; \ + } while (FALSE) +#else +#define REGISTER_REQUIRED_MAP_ITEM(min, max) +#endif diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm index 8d35db3d057..51cbf2abce2 100644 --- a/code/__HELPERS/_logging.dm +++ b/code/__HELPERS/_logging.dm @@ -50,7 +50,7 @@ GLOBAL_LIST_INIT(testing_global_profiler, list("_PROFILE_NAME" = "Global")) #define testing_profile_local_output(NAME) testing_profile_output(NAME, _timer_system) #define testing_profile_local_output_all testing_profile_output_all(_timer_system) -#if defined(UNIT_TESTS) || defined(SPACEMAN_DMM) || defined(INCLUDE_UNIT_TESTS) +#if defined(UNIT_TESTS) || defined(SPACEMAN_DMM) /proc/log_test(text) WRITE_LOG(GLOB.test_log, text) SEND_TEXT(world.log, text) diff --git a/code/__HELPERS/vfx/emissives.dm b/code/__HELPERS/vfx/emissives.dm index d4291e49b6e..c4e716db1c7 100644 --- a/code/__HELPERS/vfx/emissives.dm +++ b/code/__HELPERS/vfx/emissives.dm @@ -2,8 +2,14 @@ /proc/emissive_appearance(icon, icon_state = "", layer = FLOAT_LAYER, alpha = 255, appearance_flags = NONE) var/mutable_appearance/appearance = mutable_appearance(icon, icon_state, layer, EMISSIVE_PLANE, alpha, appearance_flags) appearance.color = GLOB.emissive_color + + //Test to make sure emissives with broken or missing icon states are created + if(PERFORM_ALL_TESTS(focus_only/invalid_emissives)) + if(icon_state && !icon_exists(icon, icon_state)) + stack_trace("An emissive appearance was added with non-existant icon_state \"[icon_state]\" in [icon]!") + return appearance - + /// Produces a mutable appearance glued to the [EMISSIVE_PLANE] dyed to be the [EMISSIVE_BLOCKER_COLOR]. /proc/emissive_blocking_appearance(icon, icon_state = "", layer = FLOAT_LAYER, alpha = 255, appearance_flags = NONE) var/mutable_appearance/appearance = mutable_appearance(icon, icon_state, layer, EMISSIVE_PLANE, alpha, appearance_flags) diff --git a/code/___compile_options.dm b/code/___compile_options.dm index a5217220852..6cc60a2b195 100644 --- a/code/___compile_options.dm +++ b/code/___compile_options.dm @@ -16,14 +16,16 @@ #define DEBUG_SHUTTLES #endif +#if defined(OPENDREAM) && !defined(SPACEMAN_DMM) && !defined(CIBUILDING) +// The code is being compiled for OpenDream, and not just for the CI linting. +#define OPENDREAM_REAL +#endif + /** * By using the testing("message") proc you can create debug-feedback for people with this uncommented, * but not visible in the release version. */ -// #define TESTING #ifdef TESTING - #warn compiling in TESTING mode. testing() debug messages will be visible. - /// Used to find the sources of harddels, quite laggy, don't be surpised if it freezes your client for a good while. // #define REFERENCE_TRACKING #ifdef REFERENCE_TRACKING @@ -34,13 +36,14 @@ // #define REFERENCE_TRACKING_DEBUG - /// Run a lookup on things hard deleting by default. - // #define GC_FAILURE_HARD_LOOKUP + ///Run a lookup on things hard deleting by default. + //#define GC_FAILURE_HARD_LOOKUP + #ifdef GC_FAILURE_HARD_LOOKUP + ///Don't stop when searching, go till you're totally done + #define FIND_REF_NO_CHECK_TICK + #endif //ifdef GC_FAILURE_HARD_LOOKUP - /// Don't stop when searching, go till you're totally done. - #define FIND_REF_NO_CHECK_TICK - - #endif + #endif //ifdef REFERENCE_TRACKING /** @@ -56,52 +59,53 @@ /// Highlights atmos active turfs in green. #define VISUALIZE_ACTIVE_TURFS -#endif +#endif //ifdef TESTING +/// If this is uncommented, we set up the ref tracker to be used in a live environment +/// And to log events to [log_dir]/harddels.log +//#define REFERENCE_DOING_IT_LIVE +#ifdef REFERENCE_DOING_IT_LIVE +// compile the backend +#define REFERENCE_TRACKING +// actually look for refs +#define GC_FAILURE_HARD_LOOKUP +// Log references in their own file +#define REFERENCE_TRACKING_LOG_APART +#endif // REFERENCE_DOING_IT_LIVE -/** - * If this is uncommented, we do a single run though of the game setup and tear down process with unit tests in between. - */ -// #define UNIT_TESTS +/// Sets up the reftracker to be used locally, to hunt for hard deletions +/// Errors are logged to [log_dir]/harddels.log +//#define REFERENCE_TRACKING_STANDARD +#ifdef REFERENCE_TRACKING_STANDARD +// compile the backend +#define REFERENCE_TRACKING +// actually look for refs +#define GC_FAILURE_HARD_LOOKUP +// spend ALL our time searching, not just part of it +#define FIND_REF_NO_CHECK_TICK +// Log references in their own file +#define REFERENCE_TRACKING_LOG_APART +#endif // REFERENCE_TRACKING_STANDARD -/** - * If this is uncommented, we will compile in the unit test code without actually running them. - */ -// #define INCLUDE_UNIT_TESTS +// If this is uncommented, we do a single run though of the game setup and tear down process with unit tests in between +//#define UNIT_TESTS -#ifdef INCLUDE_UNIT_TESTS - #warn Unit tests are compiled in manually. This shouldn't be on in live. -#endif - -/** - * If this is uncommented, will attempt to load and initialize prof.dll/libprof.so. - * We do not ship byond-tracy. Build it yourself here: https://github.com/mafemergency/byond-tracy/ - */ +// If this is uncommented, will attempt to load and initialize prof.dll/libprof.so by default. +// Even if it's not defined, you can pass "tracy" via -params in order to try to load it. +// We do not ship byond-tracy. Build it yourself here: https://github.com/mafemergency/byond-tracy, +// or the fork which writes profiling data to a file: https://github.com/ParadiseSS13/byond-tracy // #define USE_BYOND_TRACY -/** - * If this is uncommented, will profile mapload atom initializations. - */ +// If defined, we will compile with FULL timer debug info, rather then a limited scope +// Be warned, this increases timer creation cost by 5x +// #define TIMER_DEBUG + +// If defined, we will NOT defer asset generation till later in the game, and will instead do it all at once, during initiialize +//#define DO_NOT_DEFER_ASSETS + +/// If this is uncommented, will profile mapload atom initializations // #define PROFILE_MAPLOAD_INIT_ATOM -/** - * If this is uncommented, force our verb processing into just the 2% of a tick. - * We normally reserve for it. - *! NEVER run this on live, it's for simulating highpop only. - */ -// #define VERB_STRESS_TEST -#ifdef VERB_STRESS_TEST - #warn Hey brother, you're running in LAG MODE. - #warn IF YOU PUT THIS ON LIVE I WILL FIND YOU AND MAKE YOU WISH YOU WERE NEVE- - - /** - * Uncomment this to force all verbs to run into overtime all of the time. - * Essentially negating the reserve 2%. - */ - // #define FORCE_VERB_OVERTIME -#endif - - #ifndef PRELOAD_RSC /** * Set to: @@ -112,7 +116,21 @@ #define PRELOAD_RSC 2 #endif -// ## CBT BUILD DEFINES +#ifdef LOWMEMORYMODE +// TODO only load centcom or something +// #ifndef ABSOLUTE_MINIMUM +// #define FORCE_MAP "runtimestation" +// #else +// #define FORCE_MAP "runtimestation_minimal" +// #endif +// #define FORCE_MAP_DIRECTORY "_maps" +#endif + +//Additional code for the above flags. +#ifdef TESTING + #warn compiling in TESTING mode. testing() debug messages will be visible. +#endif + #if defined(CIBUILDING) && !defined(OPENDREAM) #define UNIT_TESTS #endif @@ -122,17 +140,17 @@ #endif #if defined(UNIT_TESTS) - //Hard del testing defines - #define REFERENCE_TRACKING - #define REFERENCE_TRACKING_DEBUG - // #define FIND_REF_NO_CHECK_TICK - // #define GC_FAILURE_HARD_LOOKUP - // Test init. - #define CF_ATOM_TRACE_INIT_EARLY_QDEL - //Ensures all early assets can actually load early - #define DO_NOT_DEFER_ASSETS - //Test at full capacity, the extra cost doesn't matter - #define TIMER_DEBUG +//Hard del testing defines +#define REFERENCE_TRACKING +#define REFERENCE_TRACKING_DEBUG +#define FIND_REF_NO_CHECK_TICK +#define GC_FAILURE_HARD_LOOKUP +//Ensures all early assets can actually load early +#define DO_NOT_DEFER_ASSETS +//Test at full capacity, the extra cost doesn't matter +#define TIMER_DEBUG +// cit specific +#define CF_ATOM_TRACE_INIT_EARLY_QDEL #endif #ifdef TGS diff --git a/code/__byond_version_compat.dm b/code/__byond_version_compat.dm index d7f50e6216c..4e0e2b0d752 100644 --- a/code/__byond_version_compat.dm +++ b/code/__byond_version_compat.dm @@ -1,18 +1,12 @@ // This file contains defines allowing targeting byond versions newer than the supported //Update this whenever you need to take advantage of more recent byond features -#define MIN_COMPILER_VERSION 514 -#define MIN_COMPILER_BUILD 1556 -#if (DM_VERSION < MIN_COMPILER_VERSION || DM_BUILD < MIN_COMPILER_BUILD) && !defined(SPACEMAN_DMM) +#define MIN_COMPILER_VERSION 516 +#define MIN_COMPILER_BUILD 1659 +#if (DM_VERSION < MIN_COMPILER_VERSION || DM_BUILD < MIN_COMPILER_BUILD) && !defined(SPACEMAN_DMM) && !defined(OPENDREAM) //Don't forget to update this part #error Your version of BYOND is too out-of-date to compile this project. Go to https://secure.byond.com/download and update. -#error You need version 514.1556 or higher -#endif - -#if (DM_VERSION == 514 && DM_BUILD > 1575 && DM_BUILD <= 1577) -#error Your version of BYOND currently has a crashing issue that will prevent you from running Dream Daemon test servers. -#error We require developers to test their content, so an inability to test means we cannot allow the compile. -#error Please consider downgrading to 514.1575 or lower. +#error You need version 516.1659 or higher #endif // 516.1660 broke (x in vars), which breaks a lot of things. @@ -21,26 +15,12 @@ #endif // Keep savefile compatibilty at minimum supported level -#if DM_VERSION >= 515 /savefile/byond_version = MIN_COMPILER_VERSION -#endif -// 515 split call for external libraries into call_ext -#if DM_VERSION < 515 -#define LIBCALL call -#else -#define LIBCALL call_ext -#endif +// So we want to have compile time guarantees these methods exist on local type +// We use wrappers for this in case some part of the api ever changes, and to make their function more clear +// For the record: GLOBAL_VERB_REF would be useless as verbs can't be global. -// So we want to have compile time guarantees these procs exist on local type, unfortunately 515 killed the .proc/procname syntax so we have to use nameof() -#if DM_VERSION < 515 -/// Call by name proc reference, checks if the proc exists on this type or as a global proc -#define PROC_REF(X) (.proc/##X) -/// Call by name proc reference, checks if the proc exists on given type or as a global proc -#define TYPE_PROC_REF(TYPE, X) (##TYPE.proc/##X) -/// Call by name proc reference, checks if the proc is existing global proc -#define GLOBAL_PROC_REF(X) (/proc/##X) -#else /// Call by name proc references, checks if the proc exists on either this type () (AND ONLY THIS TYPE) or as a global proc. #define PROC_REF(X) (nameof(.proc/##X)) /// Call by name verb references, checks if the verb exists on either this type or as a global verb. @@ -51,6 +31,5 @@ /// Call by name verb reference, checks if the verb exists on either the given type or as a global verb #define TYPE_VERB_REF(TYPE, X) (nameof(##TYPE.verb/##X)) -/// Call by name proc reference, checks if the proc is existing global proc +/// Call by name proc reference, checks if the proc is an existing global proc #define GLOBAL_PROC_REF(X) (/proc/##X) -#endif diff --git a/code/controllers/legacy-hooks.dm b/code/controllers/legacy-hooks.dm index bf64dfdef1e..95c2db7fd24 100644 --- a/code/controllers/legacy-hooks.dm +++ b/code/controllers/legacy-hooks.dm @@ -23,7 +23,7 @@ * @param hook Identifier of the hook to call. * @returns 1 if all hooked code runs successfully, 0 otherwise. */ -/proc/callHook(hook, list/args=null) +/proc/callHook(hook, list/params=null) var/hook_path = text2path("/legacy_hook/[hook]") if(!hook_path) log_world("Invalid hook '/legacy_hook/[hook]' called.") @@ -32,7 +32,7 @@ var/delegate = new hook_path var/status = 1 for(var/P in typesof("[hook_path]/proc")) - if(!call(delegate, P)(arglist(args))) + if(!call(delegate, P)(arglist(params))) log_world("Hook '[P]' failed or runtimed.") status = 0 diff --git a/code/controllers/subsystem/overlays.dm b/code/controllers/subsystem/overlays.dm index 5ed931866d9..bf50a7e360b 100644 --- a/code/controllers/subsystem/overlays.dm +++ b/code/controllers/subsystem/overlays.dm @@ -185,9 +185,18 @@ SUBSYSTEM_DEF(overlays) listclearnulls(new_overlays) for (var/i in 1 to length(new_overlays)) var/image/cached_overlay = new_overlays[i] + if (PERFORM_ALL_TESTS(focus_only/invalid_overlays) && istext(cached_overlay) && cached_overlay) + if(!icon_exists(icon, cached_overlay)) + var/icon_file = "[icon]" || "Unknown Generated Icon" + stack_trace("Invalid overlay: Icon object '[icon_file]' [REF(icon)] used in '[src]' [type] is missing icon state [cached_overlay].") + continue APPEARANCEIFY(cached_overlay, new_overlays[i]) return new_overlays else + if (PERFORM_ALL_TESTS(focus_only/invalid_overlays) && istext(new_overlays) && new_overlays) + if(!icon_exists(icon, new_overlays)) + var/icon_file = "[icon]" || "Unknown Generated Icon" + stack_trace("Invalid overlay: Icon object '[icon_file]' [REF(icon)] used in '[src]' [type] is missing icon state [new_overlays].") APPEARANCEIFY(new_overlays, .) // The same as the above, but with ZM_AUTOMANGLE. diff --git a/code/controllers/world/world_debug_enabler.dm b/code/controllers/world/world_debug_enabler.dm index 79b20ecf4cd..e008dacd569 100644 --- a/code/controllers/world/world_debug_enabler.dm +++ b/code/controllers/world/world_debug_enabler.dm @@ -1,7 +1,7 @@ /datum/world_debug_enabler/New() var/debug_server = world.GetConfig("env", "AUXTOOLS_DEBUG_DLL") if (debug_server) - LIBCALL(debug_server, "auxtools_init")() + call_ext(debug_server, "auxtools_init")() enable_debugging() debug_loop() diff --git a/code/datums/elements/_element.dm b/code/datums/elements/_element.dm index 81430e2ecf2..fedfbc7234b 100644 --- a/code/datums/elements/_element.dm +++ b/code/datums/elements/_element.dm @@ -9,22 +9,33 @@ var/element_flags = NONE /** * The index of the first attach argument to consider for duplicate elements - * Is only used when flags contains ELEMENT_BESPOKE + * + * All arguments from this index onwards (1 based, until `argument_hash_end_idx` is reached, if set) + * are hashed into the key to determine if this is a new unique element or one already exists + * + * Is only used when flags contains [ELEMENT_BESPOKE] + * * This is infinity so you must explicitly set this */ var/id_arg_index = INFINITY /// Activates the functionality defined by the element on the given target datum /datum/element/proc/Attach(datum/target) - SHOULD_CALL_PARENT(1) + SHOULD_CALL_PARENT(TRUE) if(type == /datum/element) return ELEMENT_INCOMPATIBLE if(element_flags & ELEMENT_DETACH) - RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(Detach), override = TRUE) + RegisterSignal(target, COMSIG_PARENT_QDELETING, PROC_REF(OnTargetDelete), override = TRUE) + +/datum/element/proc/OnTargetDelete(datum/source) + SIGNAL_HANDLER + Detach(source) /// Deactivates the functionality defines by the element on the given datum -/datum/element/proc/Detach(datum/source, force) - SHOULD_CALL_PARENT(1) +/datum/element/proc/Detach(datum/source, ...) + SIGNAL_HANDLER + SHOULD_CALL_PARENT(TRUE) + UnregisterSignal(source, COMSIG_PARENT_QDELETING) /datum/element/Destroy(force) diff --git a/code/datums/uplink/announcements.dm b/code/datums/uplink/announcements.dm index b339c6b4818..e26f22adef3 100644 --- a/code/datums/uplink/announcements.dm +++ b/code/datums/uplink/announcements.dm @@ -24,18 +24,18 @@ return return list("title" = title, "message" = message) -/datum/uplink_item/abstract/announcements/fake_centcom/get_goods(obj/item/uplink/U, loc, mob/user, list/args) +/datum/uplink_item/abstract/announcements/fake_centcom/get_goods(obj/item/uplink/U, loc, mob/user, list/extra_args) for (var/obj/machinery/computer/communications/C in GLOB.machines) if(! (C.machine_stat & (BROKEN|NOPOWER)) ) var/obj/item/paper/P = new /obj/item/paper( C.loc ) P.name = "'[command_name()] Update.'" - P.info = replacetext(args["message"], "\n", "
") + P.info = replacetext(extra_args["message"], "\n", "
") P.update_space(P.info) P.update_icon() - C.messagetitle.Add(args["title"]) + C.messagetitle.Add(extra_args["title"]) C.messagetext.Add(P.info) - command_announcement.Announce(args["message"], args["title"]) + command_announcement.Announce(extra_args["message"], extra_args["title"]) return 1 /datum/uplink_item/abstract/announcements/fake_crew_arrival @@ -43,7 +43,7 @@ desc = "Creates a fake crew arrival announcement as well as fake crew records, using your current appearance (including held items!) and worn id card. Trigger with care!" item_cost = 30 -/datum/uplink_item/abstract/announcements/fake_crew_arrival/get_goods(var/obj/item/uplink/U, var/loc, var/mob/user, var/list/args) +/datum/uplink_item/abstract/announcements/fake_crew_arrival/get_goods(obj/item/uplink/U, loc, mob/user, list/extra_args) if(!user) return 0 diff --git a/code/game/antagonist/antagonist_panel.dm b/code/game/antagonist/antagonist_panel.dm index 3f397d29c38..65de5cc7018 100644 --- a/code/game/antagonist/antagonist_panel.dm +++ b/code/game/antagonist/antagonist_panel.dm @@ -17,7 +17,7 @@ /datum/antagonist/proc/get_extra_panel_options() return -/datum/antagonist/proc/get_check_antag_output(var/datum/admins/caller) +/datum/antagonist/proc/get_check_antag_output(datum/admins/admin_caller) if(!current_antagonists || !current_antagonists.len) return "" @@ -31,7 +31,7 @@ if(!M.client) dat += " (logged out)" if(M.stat == DEAD) dat += " (DEAD)" dat += "" - dat += "\[PP]\[PM\]\[TP\]" + dat += "\[PP]\[PM\]\[TP\]" else dat += "[player.ckey] Mob not found!" dat += "" @@ -57,5 +57,5 @@ return dat //Overridden elsewhere. -/datum/antagonist/proc/get_additional_check_antag_output(var/datum/admins/caller) +/datum/antagonist/proc/get_additional_check_antag_output(datum/admins/admin_caller) return "" diff --git a/code/game/atoms/atoms_initializing_EXPENSIVE.dm b/code/game/atoms/atoms_initializing_EXPENSIVE.dm index 583d27b340c..3fee2a5f32c 100644 --- a/code/game/atoms/atoms_initializing_EXPENSIVE.dm +++ b/code/game/atoms/atoms_initializing_EXPENSIVE.dm @@ -20,7 +20,6 @@ #ifdef UNIT_TESTS if(start_tick != world.time) BadInitializeCalls[the_type] |= BAD_INIT_SLEPT - stack_trace("[A] ([A.type]) slept during init.") #endif var/qdeleted = FALSE diff --git a/code/game/click/click.dm b/code/game/click/click.dm index 380826756b0..914e4d27868 100644 --- a/code/game/click/click.dm +++ b/code/game/click/click.dm @@ -243,7 +243,7 @@ * pretty much just for hooks that happen before standard handling and I'm too lazy to rewrite. */ /mob/proc/legacy_click_on(atom/target, location, control, params) - if(client.buildmode) + if(client?.buildmode) build_click(src, client.buildmode, params, target) return TRUE return FALSE diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm index de1a5fb20eb..0c494b2dd66 100644 --- a/code/game/machinery/computer/communications.dm +++ b/code/game/machinery/computer/communications.dm @@ -40,6 +40,9 @@ /obj/machinery/computer/communications/Initialize(mapload) . = ..() + // All maps should have at least 1 comms console + REGISTER_REQUIRED_MAP_ITEM(1, INFINITY) + ATC = SSlegacy_atc crew_announcement.newscast = 1 diff --git a/code/game/machinery/pipe/pipe_dispenser.dm b/code/game/machinery/pipe/pipe_dispenser.dm index 5cbf48dda5c..fb4d299c7e9 100644 --- a/code/game/machinery/pipe/pipe_dispenser.dm +++ b/code/game/machinery/pipe/pipe_dispenser.dm @@ -148,17 +148,17 @@ disposals = TRUE //Allow you to drag-drop disposal pipes into it -/obj/machinery/pipedispenser/disposal/MouseDroppedOnLegacy(var/obj/structure/disposalconstruct/pipe as obj, mob/usr as mob) - if(!CHECK_MOBILITY(usr, MOBILITY_CAN_UI)) +/obj/machinery/pipedispenser/disposal/MouseDroppedOnLegacy(obj/structure/disposalconstruct/pipe as obj, mob/user as mob) + if(!CHECK_MOBILITY(user, MOBILITY_CAN_UI)) return - if (!istype(pipe) || get_dist(usr, src) > 1 || get_dist(src,pipe) > 1 ) + if (!istype(pipe) || get_dist(user, src) > 1 || get_dist(src,pipe) > 1 ) return if (pipe.anchored) return - to_chat(usr, SPAN_NOTICE("You shove [pipe] back in [src].")) + to_chat(user, SPAN_NOTICE("You shove [pipe] back in [src].")) qdel(pipe) //Adding a pipe dispensers that spawn unhooked from the ground diff --git a/code/game/machinery/point_redemption_vendor/point_redemption_item.dm b/code/game/machinery/point_redemption_vendor/point_redemption_item.dm index 9888e2e1c1e..0df7de15945 100644 --- a/code/game/machinery/point_redemption_vendor/point_redemption_item.dm +++ b/code/game/machinery/point_redemption_vendor/point_redemption_item.dm @@ -30,7 +30,7 @@ if(!ispath(src.path, /atom/movable)) CRASH("invalid path '[src.path]'.") - if(desc == initial(desc)) + if(desc == initial(src.desc)) var/atom/movable/casted = src.path desc = initial(casted.desc) diff --git a/code/game/objects/items/weapons/mop.dm b/code/game/objects/items/weapons/mop.dm index 506fe582bc7..f3bbe35917d 100644 --- a/code/game/objects/items/weapons/mop.dm +++ b/code/game/objects/items/weapons/mop.dm @@ -91,9 +91,9 @@ GLOBAL_LIST_BOILERPLATE(all_mops, /obj/item/mop) animate(mopimage, alpha = 50, time = sweep_time*1.2) var/sweep_step = (sweep_time - 1) * 0.5 spawn(1) - mopimage.forceMove(target, sweep_step) + mopimage.forceMove(target) sleep(sweep_step) - mopimage.forceMove(end, sweep_step) + mopimage.forceMove(end) spawn(sweep_time+1) qdel(mopimage) diff --git a/code/game/rendering/legacy/alert.dm b/code/game/rendering/legacy/alert.dm index 1e4b3af2ea9..52db2241df6 100644 --- a/code/game/rendering/legacy/alert.dm +++ b/code/game/rendering/legacy/alert.dm @@ -107,6 +107,8 @@ /atom/movable/screen/alert/Initialize(mapload) . = ..() + if(PERFORM_ALL_TESTS(focus_only/screen_alert_overlay) && background_state && !icon_exists(background_icon, background_state)) + stack_trace("background_state: \"[background_state || "null"]\" that couldn't be found background_icon: \"[background_icon || "null"]\"") update_icon() /atom/movable/screen/alert/update_icon(updates) diff --git a/code/game/world.dm b/code/game/world.dm index 4a9991cbc4f..f5a9f222a93 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -331,6 +331,23 @@ GLOBAL_LIST(topic_status_cache) sleep(0) //yes, 0, this'll let Reboot finish and prevent byond memes qdel(src) //shut it down +/// Returns TRUE if the world should do a TGS hard reboot. +/world/proc/check_hard_reboot() + if(!TgsAvailable()) + return FALSE + var/ruhr = CONFIG_GET(number/rounds_until_hard_restart) + switch(ruhr) + if(-1) + return FALSE + if(0) + return TRUE + else + if(GLOB.restart_counter >= ruhr) + return TRUE + else + text2file("[++GLOB.restart_counter]", RESTART_COUNTER_PATH) + return FALSE + /** * byond reboot proc * @@ -358,38 +375,26 @@ GLOBAL_LIST(topic_status_cache) return #endif - if(TgsAvailable()) - var/do_hard_reboot - // check the hard reboot counter - var/ruhr = CONFIG_GET(number/rounds_until_hard_restart) - switch(ruhr) - if(-1) - do_hard_reboot = FALSE - if(0) - do_hard_reboot = TRUE - else - if(GLOB.restart_counter >= ruhr) - do_hard_reboot = TRUE - else - text2file("[++GLOB.restart_counter]", RESTART_COUNTER_PATH) - do_hard_reboot = FALSE - - if(do_hard_reboot) - log_world("World hard rebooted at [time_stamp()]") - shutdown_logging() // See comment below. - TgsEndProcess() + if(check_hard_reboot()) + log_world("World hard rebooted at [time_stamp()]") + shutdown_logging() // See comment below. + TgsEndProcess() + return ..() log_world("World rebooted at [time_stamp()]") - TgsReboot() shutdown_logging() // Past this point, no logging procs can be used, at risk of data loss. + TgsReboot() // TGS can decide to kill us right here, so it's important to do it last + + //! Shutdown Auxtools // AUXTOOLS_SHUTDOWN(AUXTOOLS_YAML) //! Finale // hmmm let's sleep for one (1) second incase rust_g threads are running for whatever reason - sleep(1 SECONDS) + // sleep(1 SECONDS) no, rustg should cease beyond this point (no logging) + ..() /world/Del() diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm index dfa61cd2601..211f50feec8 100644 --- a/code/modules/admin/verbs/debug.dm +++ b/code/modules/admin/verbs/debug.dm @@ -296,8 +296,6 @@ dellog += "
  • Sleeps: [I.slept_destroy]
  • " if (I.no_respect_force) dellog += "
  • Ignored force: [I.no_respect_force]
  • " - if (I.no_hint) - dellog += "
  • No hint: [I.no_hint]
  • " dellog += "" dellog += "" diff --git a/code/modules/asset_cache/assets/vending.dm b/code/modules/asset_cache/assets/vending.dm index b21a46090fe..3489f555f45 100644 --- a/code/modules/asset_cache/assets/vending.dm +++ b/code/modules/asset_cache/assets/vending.dm @@ -2,17 +2,38 @@ name = "vending" /datum/asset_pack/spritesheet/vending/generate() - for (var/k in GLOB.vending_products) - var/atom/item = k + for (var/atom/item as anything in GLOB.vending_products) if (!ispath(item, /atom)) continue + var/icon_state = initial(item.icon_state) + var/has_gags = initial(item.greyscale_config) && initial(item.greyscale_colors) + var/has_color = initial(item.color) && icon_state + + // GAGS (as SSgreyscale) and colored icons must be pregenerated + // Otherwise we can rely on DMIcon, so skip it to save init time + if(!has_gags && !has_color) + continue + + if (PERFORM_ALL_TESTS(focus_only/invalid_vending_machine_icon_states)) + if (!has_gags && !icon_exists(initial(item.icon), icon_state)) + var/icon_file = initial(item.icon) + var/icon_states_string + for (var/an_icon_state in icon_states(icon_file)) + if (!icon_states_string) + icon_states_string = "[json_encode(an_icon_state)]([text_ref(an_icon_state)])" + else + icon_states_string += ", [json_encode(an_icon_state)]([text_ref(an_icon_state)])" + + stack_trace("[item] does not have a valid icon state, icon=[icon_file], icon_state=[json_encode(icon_state)]([text_ref(icon_state)]), icon_states=[icon_states_string]") + continue + + // pretend this is get_display_icon_for() var/icon_file if (initial(item.greyscale_colors) && initial(item.greyscale_config)) icon_file = SSgreyscale.GetColoredIconByType(initial(item.greyscale_config), initial(item.greyscale_colors)) else icon_file = initial(item.icon) - var/icon_state = initial(item.icon_state) var/icon/I var/icon_states_list = icon_states(icon_file) @@ -22,15 +43,7 @@ if (!isnull(c) && c != "#FFFFFF") I.Blend(c, ICON_MULTIPLY) else - var/icon_states_string - for (var/an_icon_state in icon_states_list) - if (!icon_states_string) - icon_states_string = "[json_encode(an_icon_state)](\ref[an_icon_state])" - else - icon_states_string += ", [json_encode(an_icon_state)](\ref[an_icon_state])" - stack_trace("[item] does not have a valid icon state, icon=[icon_file], icon_state=[json_encode(icon_state)](\ref[icon_state]), icon_states=[icon_states_string]") I = icon('icons/turf/floors.dmi', "", SOUTH) var/imgid = replacetext(replacetext("[item]", "/obj/item/", ""), "/", "-") - Insert(imgid, I) diff --git a/code/modules/clothing/under/casual/bluespace.dm b/code/modules/clothing/under/casual/bluespace.dm index 50a6a8a6c9d..98c6c4c888a 100644 --- a/code/modules/clothing/under/casual/bluespace.dm +++ b/code/modules/clothing/under/casual/bluespace.dm @@ -42,11 +42,11 @@ set src in usr bluespace_size(usr) -/obj/item/clothing/under/bluespace/proc/bluespace_size(mob/usr as mob) - if (!ishuman(usr)) +/obj/item/clothing/under/bluespace/proc/bluespace_size(mob/M as mob) + if (!ishuman(M)) return - var/mob/living/carbon/human/H = usr + var/mob/living/carbon/human/H = M if (H.stat || H.restrained()) return diff --git a/code/modules/food/food/condiment.dm b/code/modules/food/food/condiment.dm index 241fa59d5fb..85d04ddf296 100644 --- a/code/modules/food/food/condiment.dm +++ b/code/modules/food/food/condiment.dm @@ -204,7 +204,7 @@ /obj/item/reagent_containers/food/condiment/small/packet icon_state = "packet_small" w_class = WEIGHT_CLASS_TINY - possible_transfer_amounts = "1;5;10" + possible_transfer_amounts = list(1, 5, 10) amount_per_transfer_from_this = 1 volume = 5 diff --git a/code/modules/food/food/drinks.dm b/code/modules/food/food/drinks.dm index 72a729fcc1f..eff09b5ddf0 100644 --- a/code/modules/food/food/drinks.dm +++ b/code/modules/food/food/drinks.dm @@ -236,7 +236,7 @@ name = "coffee cup" desc = "A heat-formed plastic coffee cup. Can theoretically be used for other hot drinks, if you're feeling adventurous." icon_state = "coffee_cup_e" - possible_transfer_amounts = list(10) + possible_transfer_amounts = list(5, 10) volume = 30 atom_flags = OPENCONTAINER drop_sound = 'sound/items/drop/papercup.ogg' diff --git a/code/modules/food/food/snacks.dm b/code/modules/food/food/snacks.dm index 6dec4888639..bb3bea0cc41 100644 --- a/code/modules/food/food/snacks.dm +++ b/code/modules/food/food/snacks.dm @@ -5734,11 +5734,6 @@ END CITADEL CHANGE */ nutriment_desc = list("rice" = 5, "fish" = 5) nutriment_amt = 20 -/obj/item/reagent_containers/food/snacks/sliceable/sushi/crab/Initialize(mapload) - ..() - reagents.add_reagent("protein", 15) - bitesize = 5 - /obj/item/reagent_containers/food/snacks/slice/sushi/crab/filled name = "piece of sushi (crab)" desc = "A slice of a larger sushi roll, ready to devour." @@ -5758,11 +5753,6 @@ END CITADEL CHANGE */ nutriment_desc = list("rice" = 5, "fish" = 5) nutriment_amt = 20 -/obj/item/reagent_containers/food/snacks/sliceable/sushi/horse/Initialize(mapload) - ..() - reagents.add_reagent("protein", 15) - bitesize = 5 - /obj/item/reagent_containers/food/snacks/slice/sushi/horse/filled name = "piece of sushi (horse)" desc = "A slice of a larger sushi roll, ready to devour." @@ -5782,11 +5772,6 @@ END CITADEL CHANGE */ nutriment_desc = list("rice" = 5, "fish" = 5) nutriment_amt = 20 -/obj/item/reagent_containers/food/snacks/sliceable/sushi/mystery/Initialize(mapload) - ..() - reagents.add_reagent("protein", 15) - bitesize = 5 - /obj/item/reagent_containers/food/snacks/slice/sushi/mystery/filled name = "piece of sushi (???)" desc = "A slice of a larger sushi roll, ready to devour." diff --git a/code/modules/gateway/stargate/stargate-station.dm b/code/modules/gateway/stargate/stargate-station.dm index 942e2214d33..cbb38f0dfe6 100644 --- a/code/modules/gateway/stargate/stargate-station.dm +++ b/code/modules/gateway/stargate/stargate-station.dm @@ -126,12 +126,12 @@ to_chat(user, "The gate is already calibrated, there is no work for you to do here.") return -/obj/machinery/gateway/centerstation/proc/admin_setup(/mob/usr) +/obj/machinery/gateway/centerstation/proc/admin_setup(mob/M) detect() awaygate = locate(/obj/machinery/gateway/centeraway) if(!awaygate) // We still can't find the damn thing because there is no destination. - to_chat(usr, "Unable to locate awaygate (type: /obj/machinery/gateway/centeraway)") + to_chat(M, "Unable to locate awaygate (type: /obj/machinery/gateway/centeraway)") return awaygate.stationgate = src @@ -139,8 +139,8 @@ wait = 0 - toggleon(usr) - awaygate.toggleon(usr) + toggleon(M) + awaygate.toggleon(M) /obj/machinery/gateway/centerstation/vv_get_dropdown() . = ..() diff --git a/code/modules/holodeck/HolodeckObjects.dm b/code/modules/holodeck/HolodeckObjects.dm index db588825d80..386b2b31aab 100644 --- a/code/modules/holodeck/HolodeckObjects.dm +++ b/code/modules/holodeck/HolodeckObjects.dm @@ -109,11 +109,6 @@ base_icon = 'icons/turf/flooring/asteroid.dmi' initial_flooring = null -/turf/simulated/floor/holofloor/desert/Initialize(mapload) - . = ..() - if(prob(10)) - add_overlay("asteroid[rand(0,9)]") - /obj/structure/holostool name = "stool" desc = "Apply butt." diff --git a/code/modules/mob/living/carbon/human/dummy.dm b/code/modules/mob/living/carbon/human/dummy.dm index 6ae7557e14a..24d3d533aa5 100644 --- a/code/modules/mob/living/carbon/human/dummy.dm +++ b/code/modules/mob/living/carbon/human/dummy.dm @@ -89,3 +89,24 @@ GLOBAL_LIST_EMPTY(dummy_mob_list) if(istype(dummy)) GLOB.dummy_mob_list -= dummy qdel(dummy) + +/// Provides a dummy for unit_tests that functions like a normal human, but with a standardized appearance +/// Copies the stock dna setup from the dummy/consistent type +/mob/living/carbon/human/consistent + ssd_visible = FALSE + +// make it "consistent" enough +/mob/living/carbon/human/consistent/Initialize(mapload, datum/species/specieslike) + . = ..() + set_species(/datum/species/human, force = TRUE, regen_icons = FALSE) + real_name = "John Doe" + name = "John Doe" + + nutrition = 400 + hydration = 400 + + // retrigger dna creation since its name based + if(dna) + dna.ready_dna(src) + dna.real_name = real_name + sync_organ_dna() diff --git a/code/modules/reagents/reagent_containers.dm b/code/modules/reagents/reagent_containers.dm index 918b734744e..b64f3b774a9 100644 --- a/code/modules/reagents/reagent_containers.dm +++ b/code/modules/reagents/reagent_containers.dm @@ -29,7 +29,7 @@ // todo: shouldn't be settable for drinking / feeding var/amount_per_transfer_from_this = 5 // todo: typelist? - var/possible_transfer_amounts = list(5,10,15,25,30) + var/list/possible_transfer_amounts = list(5,10,15,25,30) // At what point we apply the different icon states var/list/fill_icon_thresholds = null diff --git a/code/modules/roles/jobs/job_types/station/civillian/chaplain.dm b/code/modules/roles/jobs/job_types/station/civillian/chaplain.dm index b30c1f27067..0a669903cbd 100644 --- a/code/modules/roles/jobs/job_types/station/civillian/chaplain.dm +++ b/code/modules/roles/jobs/job_types/station/civillian/chaplain.dm @@ -32,14 +32,12 @@ /datum/prototype/struct/alt_title/chaplain/counselor title = "Counselor" -/datum/prototype/role/job/station/chaplain/equip(mob/living/carbon/human/H, src) - . = ..() - if(H.mind) - H.mind.isholy = TRUE /datum/prototype/role/job/station/chaplain/equip(var/mob/living/carbon/human/H, var/alt_title, var/ask_questions = TRUE) . = ..() if(!.) return + if(H.mind) + H.mind.isholy = TRUE if(!ask_questions) return var/obj/item/storage/bible/B = locate(/obj/item/storage/bible) in H diff --git a/code/modules/species/protean/protean_blob.dm b/code/modules/species/protean/protean_blob.dm index cce408fe8e0..569f4f5a73c 100644 --- a/code/modules/species/protean/protean_blob.dm +++ b/code/modules/species/protean/protean_blob.dm @@ -400,8 +400,8 @@ return blob //For some reason, there's no way to force drop all the mobs grabbed. This ought to fix that. And be moved elsewhere. Call with caution, doesn't handle cycles. -/proc/remove_micros(var/src, var/mob/root) - for(var/obj/item/I in src) +/proc/remove_micros(this, mob/root) + for(var/obj/item/I in this) remove_micros(I, root) //Recursion. I'm honestly depending on there being no containment loop, but at the cost of performance that can be fixed too. if(istype(I, /obj/item/holder)) I.forceMove(root.drop_location()) diff --git a/code/modules/species/species.dm b/code/modules/species/species.dm index a76d2bdf9a1..49aa5f838fc 100644 --- a/code/modules/species/species.dm +++ b/code/modules/species/species.dm @@ -960,7 +960,7 @@ GLOBAL_LIST_INIT(species_oxygen_tank_by_gas, list( return TRUE // Used to find a special target for falling on, such as pouncing on someone from above. -/datum/species/proc/find_fall_target_special(src, landing) +/datum/species/proc/find_fall_target_special(mob/living/L, landing) return FALSE // Used to override normal fall behaviour. Use only when the species does fall down a level. diff --git a/code/modules/vore/trycatch_vr.dm b/code/modules/vore/trycatch_vr.dm index 167e7e686f3..0d5f16c2998 100644 --- a/code/modules/vore/trycatch_vr.dm +++ b/code/modules/vore/trycatch_vr.dm @@ -5,12 +5,12 @@ It will also log when we mess up our code rather than making it vague. Call it at the top of a stock proc with... -if(attempt_vr(object,proc to call,args)) return +if(attempt_vr(object,proc to call,params)) return ...if you are replacing an entire proc. The proc you're attemping should return nonzero values on success. */ -/proc/attempt_vr(callon, procname, list/args=null) - return call(callon,procname)(arglist(args)) +/proc/attempt_vr(callon, procname, list/params=null) + return call(callon,procname)(arglist(params)) diff --git a/code/unit_tests/README.md b/code/unit_tests/README.md index dd2f5f2592b..03d7b56ab2f 100644 --- a/code/unit_tests/README.md +++ b/code/unit_tests/README.md @@ -6,67 +6,78 @@ Unit tests are automated code to verify that parts of the game work exactly as t On their most basic level, when `UNIT_TESTS` is defined, all subtypes of `/datum/unit_test` will have their `Run` proc executed. From here, if `Fail` is called at any point, then the tests will report as failed. -## How do I write one +## How do I write one? 1. Find a relevant file. - All unit test related code is in `code/modules/unit_tests`. If you are adding a new test for a surgery, for example, then you'd open `surgeries.dm`. If a relevant file does not exist, simply create one in this folder, then `#include` it in `_unit_tests.dm`. +All unit test related code is in `code/modules/unit_tests`. If you are adding a new test for a surgery, for example, then you'd open `surgeries.dm`. If a relevant file does not exist, simply create one in this folder, then `#include` it in `_unit_tests.dm`. 2. Create the unit test. - To make a new unit test, you simply need to define a `/datum/unit_test`. +To make a new unit test, you simply need to define a `/datum/unit_test`. - For example, let's suppose that we are creating a test to make sure a proc `square` correctly raises inputs to the power of two. We'd start with first: +For example, let's suppose that we are creating a test to make sure a proc `square` correctly raises inputs to the power of two. We'd start with first: - ```dm - /datum/unit_test/square/Run() - ``` +``` +/datum/unit_test/square/Run() +``` - This defines our new unit test, `/datum/unit_test/square`. Inside this function, we're then going to run through whatever we want to check. Tests provide a few assertion functions to make this easy. For now, we're going to use `TEST_ASSERT_EQUAL`. +This defines our new unit test, `/datum/unit_test/square`. Inside this function, we're then going to run through whatever we want to check. Tests provide a few assertion functions to make this easy. For now, we're going to use `TEST_ASSERT_EQUAL`. - ```dm - /datum/unit_test/square/Run() - TEST_ASSERT_EQUAL(square(3), 9, "square(3) did not return 9") - TEST_ASSERT_EQUAL(square(4), 16, "square(4) did not return 16") - ``` +``` +/datum/unit_test/square/Run() + TEST_ASSERT_EQUAL(square(3), 9, "square(3) did not return 9") + TEST_ASSERT_EQUAL(square(4), 16, "square(4) did not return 16") +``` - As you can hopefully tell, we're simply checking if the output of `square` matches the output we are expecting. If the test fails, it'll report the error message given as well as whatever the actual output was. +As you can hopefully tell, we're simply checking if the output of `square` matches the output we are expecting. If the test fails, it'll report the error message given as well as whatever the actual output was. 3. Run the unit test - Open `code/_compile_options.dm` and uncomment the following line. +Open `code/_compile_options.dm` and uncomment the following line. - ```dm - ///If this is uncommented, we do a single run though of the game setup and tear down process with unit tests in between - //#define UNIT_TESTS - ``` +``` +//#define UNIT_TESTS //If this is uncommented, we do a single run though of the game setup and tear down process with unit tests in between +``` - Then, run citadel.dmb in Dream Daemon. Don't bother trying to connect, you won't need to. You'll be able to see the outputs of all the tests. You'll get to see which tests failed and for what reason. If they all pass, you're set! +There are 3 ways to run unit tests + +- Run citadel.dmb in Dream Daemon. Don't bother trying to connect, you won't need to. You'll be able to see the outputs of all the tests. You'll get to see which tests failed and for what reason. If they all pass, you're set! + +- Launch game from VS Code. Launch the game as normal & you will see the output of your unit tests in your fancy chat window. This is preferred as you can use the debugger to step through each line of your unit test & can use the games inbuilt debugging tools to further aid in testing + +- Use VS Code Tgstation Test Explorer Extension. This allows you to run tests without launching the game & can also run focused tests(either a single or a selected group) ## How to think about tests -Unit tests exist to prevent bugs that would happen in a real game. Thus, they should attempt to emulate the game world wherever possible. For example, the [quick swap sanity test](https://github.com/tgstation/tgstation/blob/e416283f162b86345a8623125ab866839b1ac40d/code/modules/unit_tests/quick_swap_sanity.dm) emulates a *real* scenario of the bug it fixed occurring by creating a character and giving it real items. The unrecommended alternative would be to create special test-only items. This isn't a hard rule, the [reagent method exposure tests](https://github.com/tgstation/tgstation/blob/e416283f162b86345a8623125ab866839b1ac40d/code/modules/unit_tests/reagent_mod_expose.dm) create a test-only reagent for example, but do keep it in mind. +Unit tests exist to prevent bugs that would happen in a real game. Thus, they should attempt to emulate the game world wherever possible. For example, the [quick swap sanity test](https://github.com/tgstation/tgstation/blob/e416283f162b86345a8623125ab866839b1ac40d/code/modules/unit_tests/quick_swap_sanity.dm) emulates a _real_ scenario of the bug it fixed occurring by creating a character and giving it real items. The unrecommended alternative would be to create special test-only items. This isn't a hard rule, the [reagent method exposure tests](https://github.com/tgstation/tgstation/blob/e416283f162b86345a8623125ab866839b1ac40d/code/modules/unit_tests/reagent_mod_expose.dm) create a test-only reagent for example, but do keep it in mind. -Unit tests should also be just that--testing *units* of code. For example, instead of having one massive test for reagents, there are instead several smaller tests for testing exposure, metabolization, etc. +Unit tests should also be just that--testing _units_ of code. For example, instead of having one massive test for reagents, there are instead several smaller tests for testing exposure, metabolization, etc. ## The unit testing API You can find more information about all of these from their respective doc comments, but for a brief overview: -`/datum/unit_test` - The base for all tests to be ran. Subtypes must override `Run()`. `New()` and `Destroy()` can be used for setup and teardown. To fail, use `Fail(reason)`. +`/datum/unit_test` - The base for all tests to be ran. Subtypes must override `Run()`. `New()` and `Destroy()` can be used for setup and teardown. To fail, use `TEST_FAIL(reason)`. -`/datum/unit_test/proc/allocate(type, ...)` - Allocates an instance of the provided type with the given arguments. Is automatically destroyed when the test is over. Commonly seen in the form of `var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)`. +`/datum/unit_test/proc/allocate(type, ...)` - Allocates an instance of the provided type with the given arguments. Is automatically destroyed when the test is over. Commonly seen in the form of `var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human/consistent)`. + +`TEST_FAIL(reason)` - Marks a failure at this location, but does not stop the test. `TEST_ASSERT(assertion, reason)` - Stops the unit test and fails if the assertion is not met. For example: `TEST_ASSERT(powered(), "Machine is not powered")`. +`TEST_ASSERT_NOTNULL(a, message)` - Same as `TEST_ASSERT`, but checks if `!isnull(a)`. For example: `TEST_ASSERT_NOTNULL(myatom, "My atom was never set!")`. + +`TEST_ASSERT_NULL(a, message)` - Same as `TEST_ASSERT`, but checks if `isnull(a)`. If not, gives a helpful message showing what `a` was. For example: `TEST_ASSERT_NULL(delme, "Delme was never cleaned up!")`. + `TEST_ASSERT_EQUAL(a, b, message)` - Same as `TEST_ASSERT`, but checks if `a == b`. If not, gives a helpful message showing what both `a` and `b` were. For example: `TEST_ASSERT_EQUAL(2 + 2, 4, "The universe is falling apart before our eyes!")`. `TEST_ASSERT_NOTEQUAL(a, b, message)` - Same as `TEST_ASSERT_EQUAL`, but reversed. -`TEST_FOCUS(test_path)` - *Only* run the test provided within the parameters. Useful for reducing noise. For example, if we only want to run our example square test, we can add `TEST_FOCUS(/datum/unit_test/square)`. Should *never* be pushed in a pull request--you will be laughed at. +`TEST_FOCUS(test_path)` - _Only_ run the test provided within the parameters. Useful for reducing noise. For example, if we only want to run our example square test, we can add `TEST_FOCUS(/datum/unit_test/square)`. Should _never_ be pushed in a pull request--you will be laughed at. ## Final Notes -- Writing tests before you attempt to fix the bug can actually speed up development a lot! It means you don't have to go in game and folllow the same exact steps manually every time. This process is known as "TDD" (test driven development). Write the test first, make sure it fails, *then* start work on the fix/feature, and you'll know you're done when your tests pass. If you do try this, do make sure to confirm in a non-testing environment just to double check. +- Writing tests before you attempt to fix the bug can actually speed up development a lot! It means you don't have to go in game and folllow the same exact steps manually every time. This process is known as "TDD" (test driven development). Write the test first, make sure it fails, _then_ start work on the fix/feature, and you'll know you're done when your tests pass. If you do try this, do make sure to confirm in a non-testing environment just to double check. - Make sure that your tests don't accidentally call RNG functions like `prob`. Since RNG is seeded during tests, you may not realize you have until someone else makes a PR and the tests fail! - Do your best not to change the behavior of non-testing code during tests. While it may sometimes be necessary in the case of situations such as the above, it is still a slippery slope that can lead to the code you're testing being too different from the production environment to be useful. diff --git a/code/unit_tests/_unit_tests.dm b/code/unit_tests/_unit_tests.dm index 0d9a234ac91..1faf540426b 100644 --- a/code/unit_tests/_unit_tests.dm +++ b/code/unit_tests/_unit_tests.dm @@ -1,7 +1,7 @@ //include unit test files in this module in this ifdef //Keep this sorted alphabetically -#if defined(UNIT_TESTS) || defined(SPACEMAN_DMM) || defined(INCLUDE_UNIT_TESTS) +#if defined(UNIT_TESTS) || defined(SPACEMAN_DMM) /// For advanced cases, fail unconditionally but don't return (so a test can return multiple results) #define TEST_FAIL(reason) (Fail(reason || "No reason", __FILE__, __LINE__)) @@ -55,8 +55,13 @@ #define TEST_DEFAULT 1 /// After most test steps, used for tests that run long so shorter issues can be noticed faster #define TEST_LONGER 10 -/// This must be the last test to run due to the inherent nature of the test iterating every single tangible atom in the game and qdeleting all of them (while taking long sleeps to make sure the garbage collector fires properly) taking a large amount of time. -#define TEST_CREATE_AND_DESTROY INFINITY +/// This must be the one of last tests to run due to the inherent nature of the test iterating every single tangible atom in the game and qdeleting all of them (while taking long sleeps to make sure the garbage collector fires properly) taking a large amount of time. +#define TEST_CREATE_AND_DESTROY 9001 +/** + * For tests that rely on create and destroy having iterated through every (tangible) atom so they don't have to do something similar. + * Keep in mind tho that create and destroy will absolutely break the test platform, anything that relies on its shape cannot come after it. + */ +#define TEST_AFTER_CREATE_AND_DESTROY INFINITY /// Change color to red on ANSI terminal output, if enabled with -DANSICOLORS. #ifdef ANSICOLORS @@ -70,10 +75,18 @@ #else #define TEST_OUTPUT_GREEN(text) (text) #endif - +/// Change color to yellow on ANSI terminal output, if enabled with -DANSICOLORS. +#ifdef ANSICOLORS +#define TEST_OUTPUT_YELLOW(text) "\x1B\x5B1;33m[text]\x1B\x5B0m" +#else +#define TEST_OUTPUT_YELLOW(text) (text) +#endif /// A trait source when adding traits through unit tests #define TRAIT_SOURCE_UNIT_TESTS "unit_tests" +/// Helper to allocate a new object with the implied type (the type of the variable it's assigned to) in the corner of the test room +#define EASY_ALLOCATE(arguments...) allocate(__IMPLIED_TYPE__, run_loc_floor_bottom_left, ##arguments) +// BEGIN_INCLUDE #include "atmospherics/_atmospherics.dm" #include "core/_core.dm" #include "datum/_datum.dm" @@ -83,44 +96,39 @@ #include "mob/_mob.dm" #include "reagents/_reagents.dm" -// #include "anchored_mobs.dm" +#include "anchored_mobs.dm" #include "bad_alcohol_reagents.dm" #include "bespoke_id.dm" -// #include "card_mismatch.dm" // #include "chain_pull_through_space.dm" -// #include "character_saving.dm" -// #include "combat.dm" #include "component_tests.dm" -// #include "confusion.dm" -// #include "emoting.dm" +#include "create_and_destroy.dm" #include "focus_only_tests.dm" #include "font_awesome_icons.dm" -// #include "heretic_knowledge.dm" -// #include "holidays.dm" +#include "get_turf_pixel.dm" #include "initialize_sanity.dm" -// #include "keybinding_init.dm" -// #include "machine_disassembly.dm" +// #include "keybinding_init.dm" // this works #include "map_template_paths.dm" -// #include "merge_type.dm" -// #include "metabolizing.dm" -// #include "outfit_sanity.dm" +// #include "missing_icons.dm" // there is a lot // #include "pills.dm" // #include "plantgrowth_tests.dm" // #include "projectiles.dm" #include "prototypes.dm" +#include "range_return.dm" +#include "required_map_items.dm" #include "resist.dm" // #include "say.dm" // #include "serving_tray.dm" -// #include "siunit.dm" +#include "siunit.dm" #include "spawn_humans.dm" // #include "species_whitelists.dm" // #include "stomach.dm" #include "subsystem_init.dm" // #include "surgeries.dm" // #include "teleporters.dm" +#include "tgui_create_message.dm" #include "timer_sanity.dm" #include "unit_test.dm" - +#include "verify_emoji_names.dm" // END_INCLUDE #ifdef REFERENCE_TRACKING_DEBUG //Don't try and parse this file if ref tracking isn't turned on. IE: don't parse ref tracking please mr linter #include "find_reference_sanity.dm" @@ -129,5 +137,5 @@ #undef TEST_ASSERT #undef TEST_ASSERT_EQUAL #undef TEST_ASSERT_NOTEQUAL -#undef TEST_FOCUS +//#undef TEST_FOCUS - This define is used by vscode unit test extension to pick specific unit tests to run and appended later so needs to be used out of scope here #endif diff --git a/code/unit_tests/anchored_mobs.dm b/code/unit_tests/anchored_mobs.dm index 103b97e7a99..fe01fdb230f 100644 --- a/code/unit_tests/anchored_mobs.dm +++ b/code/unit_tests/anchored_mobs.dm @@ -1,9 +1,8 @@ /datum/unit_test/anchored_mobs/Run() var/list/L = list() - for(var/i in typesof(/mob)) + var/list/magic_mobs = list(/mob/dview, /mob/observer/dead, /mob/living/bot/mulebot, /mob/living/silicon/decoy, /mob/living/silicon/ai, /mob/living/silicon/ai/announcer, /mob/living/simple_mob/animal/space/space_worm, /mob/living/simple_mob/horror/Master, /mob/new_player) + for(var/i in typesof(/mob) - magic_mobs) var/mob/M = i if(initial(M.anchored)) L += "[i]" - if(!L.len) - return //passed! - Fail("The following mobs are defined as anchored. This is incompatible with the new move force/resist system and needs to be revised.: [L.Join(" ")]") + TEST_ASSERT(!L.len, "The following mobs are defined as anchored. This is incompatible with the new move force/resist system and needs to be revised.: [L.Join(" ")]") diff --git a/code/unit_tests/card_mismatch.dm b/code/unit_tests/card_mismatch.dm deleted file mode 100644 index 506e88f19c3..00000000000 --- a/code/unit_tests/card_mismatch.dm +++ /dev/null @@ -1,7 +0,0 @@ -/datum/unit_test/card_mismatch - -/datum/unit_test/card_mismatch/Run() - var/message = checkCardpacks(SStrading_card_game.card_packs) - message += checkCardDatums() - if(message) - Fail(message) diff --git a/code/unit_tests/character_saving.dm b/code/unit_tests/character_saving.dm deleted file mode 100644 index 8d978a630d8..00000000000 --- a/code/unit_tests/character_saving.dm +++ /dev/null @@ -1,18 +0,0 @@ -/datum/unit_test/character_saving/Run() - try - var/datum/preferences/P = new - P.load_path("test") - P.features["flavor_text"] = "Foo" - P.features["ooc_notes"] = "Bar" - P.save_character() - P.load_character() - if(P.features["flavor_text"] != "Foo") - Fail("Flavor text is failing to save.") - if(P.features["ooc_notes"] != "Bar") - Fail("OOC text is failing to save.") - P.save_character() - P.load_character() - if(P.features["flavor_text"] != "Foo") - Fail("Repeated saving and loading possibly causing save deletion.") - catch(var/exception/e) - Fail("Failed to save and load character due to exception [e.file]:[e.line], [e.name]") diff --git a/code/unit_tests/combat.dm b/code/unit_tests/combat.dm deleted file mode 100644 index 96085c65f9e..00000000000 --- a/code/unit_tests/combat.dm +++ /dev/null @@ -1,101 +0,0 @@ -/datum/unit_test/harm_punch/Run() - var/mob/living/carbon/human/puncher = allocate(/mob/living/carbon/human/consistent) - var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human/consistent) - - // Avoid all randomness in tests - ADD_TRAIT(puncher, TRAIT_PERFECT_ATTACKER, INNATE_TRAIT) - - puncher.set_combat_mode(TRUE) - victim.attack_hand(puncher, list(RIGHT_CLICK = FALSE)) - - TEST_ASSERT(victim.getBruteLoss() > 0, "Victim took no brute damage after being punched") - -/datum/unit_test/harm_melee/Run() - var/mob/living/carbon/human/tider = allocate(/mob/living/carbon/human/consistent) - var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human/consistent) - var/obj/item/storage/toolbox/toolbox = allocate(/obj/item/storage/toolbox) - - tider.put_in_active_hand(toolbox, forced = TRUE) - tider.set_combat_mode(TRUE) - victim.attackby(toolbox, tider) - - TEST_ASSERT(victim.getBruteLoss() > 0, "Victim took no brute damage after being hit by a toolbox") - -/datum/unit_test/harm_different_damage/Run() - var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human/consistent) - var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human/consistent) - var/obj/item/weldingtool/welding_tool = allocate(/obj/item/weldingtool) - - attacker.put_in_active_hand(welding_tool, forced = TRUE) - attacker.set_combat_mode(TRUE) - - welding_tool.attack_self(attacker) // Turn it on - victim.attackby(welding_tool, attacker) - - TEST_ASSERT_EQUAL(victim.getBruteLoss(), 0, "Victim took brute damage from a lit welding tool") - TEST_ASSERT(victim.getFireLoss() > 0, "Victim took no burn damage after being hit by a lit welding tool") - -/datum/unit_test/attack_chain - var/attack_hit - var/post_attack_hit - var/pre_attack_hit - -/datum/unit_test/attack_chain/proc/attack_hit() - SIGNAL_HANDLER - attack_hit = TRUE - -/datum/unit_test/attack_chain/proc/post_attack_hit() - SIGNAL_HANDLER - post_attack_hit = TRUE - -/datum/unit_test/attack_chain/proc/pre_attack_hit() - SIGNAL_HANDLER - pre_attack_hit = TRUE - -/datum/unit_test/attack_chain/Run() - var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human/consistent) - var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human/consistent) - var/obj/item/storage/toolbox/toolbox = allocate(/obj/item/storage/toolbox) - - RegisterSignal(toolbox, COMSIG_ITEM_PRE_ATTACK, PROC_REF(pre_attack_hit)) - RegisterSignal(toolbox, COMSIG_ITEM_ATTACK, PROC_REF(attack_hit)) - RegisterSignal(toolbox, COMSIG_ITEM_AFTERATTACK, PROC_REF(post_attack_hit)) - - attacker.put_in_active_hand(toolbox, forced = TRUE) - attacker.set_combat_mode(TRUE) - toolbox.lazy_melee_interaction_chain(attacker, victim) - - TEST_ASSERT(pre_attack_hit, "Pre-attack signal was not fired") - TEST_ASSERT(attack_hit, "Attack signal was not fired") - TEST_ASSERT(post_attack_hit, "Post-attack signal was not fired") - -/datum/unit_test/disarm/Run() - var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human/consistent) - var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human/consistent) - var/obj/item/storage/toolbox/toolbox = allocate(/obj/item/storage/toolbox) - - victim.put_in_active_hand(toolbox, forced = TRUE) - - var/obj/structure/barricade/dense_object = allocate(/obj/structure/barricade) - - // Attacker --> Victim --> Empty space --> Wall - attacker.forceMove(run_loc_floor_bottom_left) - victim.forceMove(locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z)) - dense_object.forceMove(locate(run_loc_floor_bottom_left.x + 3, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z)) - - // First disarm, world should now look like: - // Attacker --> Empty space --> Victim --> Wall - victim.attack_hand(attacker, list(RIGHT_CLICK = TRUE)) - - TEST_ASSERT_EQUAL(victim.loc.x, run_loc_floor_bottom_left.x + 2, "Victim wasn't moved back after being pushed") - TEST_ASSERT(!victim.has_status_effect(/datum/status_effect/incapacitating/knockdown), "Victim was knocked down despite not being against a wall") - TEST_ASSERT_EQUAL(victim.get_active_held_item(), toolbox, "Victim dropped toolbox despite not being against a wall") - - attacker.forceMove(get_step(attacker, EAST)) - - // Second disarm, victim was against wall and should be down - victim.attack_hand(attacker, list(RIGHT_CLICK = TRUE)) - - TEST_ASSERT_EQUAL(victim.loc.x, run_loc_floor_bottom_left.x + 2, "Victim was moved after being pushed against a wall") - TEST_ASSERT(victim.has_status_effect(/datum/status_effect/incapacitating/knockdown), "Victim was not knocked down after being pushed against a wall") - TEST_ASSERT_EQUAL(victim.get_active_held_item(), null, "Victim didn't drop toolbox after being pushed against a wall") diff --git a/code/unit_tests/confusion.dm b/code/unit_tests/confusion.dm deleted file mode 100644 index 8282493c962..00000000000 --- a/code/unit_tests/confusion.dm +++ /dev/null @@ -1,16 +0,0 @@ -// Checks that the confusion symptom correctly gives, and removes, confusion -/datum/unit_test/confusion_symptom/Run() - var/mob/living/carbon/human/H = allocate(/mob/living/carbon/human) - var/datum/disease/advance/confusion/disease = allocate(/datum/disease/advance/confusion) - var/datum/symptom/confusion/confusion = disease.symptoms[1] - disease.processing = TRUE - disease.update_stage(5) - disease.infect(H, make_copy = FALSE) - confusion.Activate(disease) - TEST_ASSERT(H.get_confusion() > 0, "Human is not confused after getting symptom.") - disease.cure() - TEST_ASSERT_EQUAL(H.get_confusion(), 0, "Human is still confused after curing confusion.") - -/datum/disease/advance/confusion/New() - symptoms += new /datum/symptom/confusion - Refresh() diff --git a/code/unit_tests/create_and_destroy.dm b/code/unit_tests/create_and_destroy.dm new file mode 100644 index 00000000000..9e365c8eeb7 --- /dev/null +++ b/code/unit_tests/create_and_destroy.dm @@ -0,0 +1,121 @@ +//! HEY! not the full edition of create & destroy, this one only logs out BadInitializeCalls +///Delete one of every type, sleep a while, then check to see if anything has gone fucky +/datum/unit_test/create_and_destroy + //You absolutely must run after (almost) everything else + priority = TEST_CREATE_AND_DESTROY + +// GLOBAL_VAR_INIT(running_create_and_destroy, FALSE) +/datum/unit_test/create_and_destroy/Run() + //We'll spawn everything here + // var/turf/spawn_at = run_loc_floor_bottom_left + + // var/list/cached_contents = spawn_at.contents.Copy() + // var/original_turf_type = spawn_at.type + // var/original_baseturfs = islist(spawn_at.baseturfs) ? spawn_at.baseturfs.Copy() : spawn_at.baseturfs + // var/original_baseturf_count = length(original_baseturfs) + + // GLOB.running_create_and_destroy = TRUE + // for(var/type_path in typesof(/atom/movable, /turf) - uncreatables) //No areas please + // if(ispath(type_path, /turf)) + // spawn_at.ChangeTurf(type_path) + // //We change it back to prevent baseturfs stacking and hitting the limit + // spawn_at.ChangeTurf(original_turf_type, original_baseturfs) + // if(original_baseturf_count != length(spawn_at.baseturfs)) + // TEST_FAIL("[type_path] changed the amount of baseturfs from [original_baseturf_count] to [length(spawn_at.baseturfs)]; [english_list(original_baseturfs)] to [islist(spawn_at.baseturfs) ? english_list(spawn_at.baseturfs) : spawn_at.baseturfs]") + // //Warn if it changes again + // original_baseturfs = islist(spawn_at.baseturfs) ? spawn_at.baseturfs.Copy() : spawn_at.baseturfs + // original_baseturf_count = length(original_baseturfs) + // else + // var/atom/creation = new type_path(spawn_at) + // if(QDELETED(creation)) + // continue + // //Go all in + // qdel(creation, force = TRUE) + // //This will hold a ref to the last thing we process unless we set it to null + // //Yes byond is fucking sinful + // creation = null + + // //There's a lot of stuff that either spawns stuff in on create, or removes stuff on destroy. Let's cut it all out so things are easier to deal with + // var/list/to_del = spawn_at.contents - cached_contents + // if(length(to_del)) + // for(var/atom/to_kill in to_del) + // qdel(to_kill) + + // GLOB.running_create_and_destroy = FALSE + /* + // Drastically lower the amount of time it takes to GC, since we don't have clients that can hold it up. + SSgarbage.collection_timeout[GC_QUEUE_CHECK] = 10 SECONDS + //Clear it, just in case + cached_contents.Cut() + + var/list/queues_we_care_about = list() + // All of em, I want hard deletes too, since we rely on the debug info from them + for(var/i in 1 to GC_QUEUE_HARDDELETE) + queues_we_care_about += i + + //Now that we've qdel'd everything, let's sleep until the gc has processed all the shit we care about + // + 2 seconds to ensure that everything gets in the queue. + var/time_needed = 2 SECONDS + for(var/index in queues_we_care_about) + time_needed += SSgarbage.collection_timeout[index] + + var/start_time = world.time + var/real_start_time = REALTIMEOFDAY + var/garbage_queue_processed = FALSE + + sleep(time_needed) + while(!garbage_queue_processed) + var/oldest_packet_creation = INFINITY + for(var/index in queues_we_care_about) + var/list/queue_to_check = SSgarbage.queues[index] + if(!length(queue_to_check)) + continue + + var/list/oldest_packet = queue_to_check[1] + //Pull out the time we inserted at + var/qdeld_at = oldest_packet[GC_QUEUE_ITEM_GCD_DESTROYED] + + oldest_packet_creation = min(qdeld_at, oldest_packet_creation) + + //If we've found a packet that got del'd later then we finished, then all our shit has been processed + //That said, if there are any pending hard deletes you may NOT sleep, we gotta handle that shit + if(oldest_packet_creation > start_time && !length(SSgarbage.queues[GC_QUEUE_HARDDELETE])) + garbage_queue_processed = TRUE + break + + if(REALTIMEOFDAY > real_start_time + time_needed + 50 MINUTES) //If this gets us gitbanned I'm going to laugh so hard + TEST_FAIL("Something has gone horribly wrong, the garbage queue has been processing for well over 30 minutes. What the hell did you do") + break + + //Immediately fire the gc right after + SSgarbage.next_fire = 1 + //Unless you've seriously fucked up, queue processing shouldn't take "that" long. Let her run for a bit, see if anything's changed + sleep(20 SECONDS) + + //Alright, time to see if anything messed up + var/list/cache_for_sonic_speed = SSgarbage.items + for(var/path in cache_for_sonic_speed) + var/datum/qdel_item/item = cache_for_sonic_speed[path] + if(item.failures) + TEST_FAIL("[item.name] hard deleted [item.failures] times out of a total del count of [item.qdels]") + if(item.no_respect_force) + TEST_FAIL("[item.name] failed to respect force deletion [item.no_respect_force] times out of a total del count of [item.qdels]") + if(item.no_hint) + TEST_FAIL("[item.name] failed to return a qdel hint [item.no_hint] times out of a total del count of [item.qdels]") + if(LAZYLEN(item.extra_details)) + var/details = item.extra_details.Join("\n") + TEST_FAIL("[item.name] failed with extra info: \n[details]") + */ + + var/list/cache_for_sonic_speed = SSatoms.BadInitializeCalls + for(var/path in cache_for_sonic_speed) + var/fails = cache_for_sonic_speed[path] + if(fails & BAD_INIT_NO_HINT) + TEST_FAIL("[path] didn't return an Initialize hint") + if(fails & BAD_INIT_QDEL_BEFORE) + TEST_FAIL("[path] qdel'd before we could call Initialize()") + if(fails & BAD_INIT_SLEPT) + TEST_FAIL("[path] slept during Initialize()") + + //This shouldn't be needed, but let's be polite + SSgarbage.collection_timeout[GC_QUEUE_CHECK] = GC_CHECK_QUEUE diff --git a/code/unit_tests/elements/_elements.dm b/code/unit_tests/elements/_elements.dm index 62f4ac083bb..d452469eb07 100644 --- a/code/unit_tests/elements/_elements.dm +++ b/code/unit_tests/elements/_elements.dm @@ -1 +1,2 @@ #include "connect_loc.dm" +#include "dcs_get_id_from_elements.dm" diff --git a/code/unit_tests/elements/dcs_get_id_from_elements.dm b/code/unit_tests/elements/dcs_get_id_from_elements.dm new file mode 100644 index 00000000000..04bd4dfee69 --- /dev/null +++ b/code/unit_tests/elements/dcs_get_id_from_elements.dm @@ -0,0 +1,46 @@ +/// Tests that DCS' GetIdFromArguments works as expected with standard and odd cases +/datum/unit_test/dcs_get_id_from_arguments + +/datum/unit_test/dcs_get_id_from_arguments/Run() + assert_equal(list(1), list(1)) + assert_equal(list(1, 2), list(1, 2)) + assert_equal(list(src), list(src)) + + assert_equal( + list(a = "x", b = "y", c = "z"), + list(b = "y", a = "x", c = "z"), + list(c = "z", a = "x", b = "y"), + ) + + TEST_ASSERT_NOTEQUAL(get_id_from_arguments(list(1, 2)), get_id_from_arguments(list(2, 1)), "Swapped arguments should not return the same id") + TEST_ASSERT_NOTEQUAL(get_id_from_arguments(list(1, a = "x")), get_id_from_arguments(list(1)), "Named arguments were ignored when creating ids") + TEST_ASSERT_NOTEQUAL(get_id_from_arguments(list(1, a = "x")), get_id_from_arguments(list(a = "x")), "Unnamed arguments were ignored when creating ids") + TEST_ASSERT_NOTEQUAL(get_id_from_arguments(list(src)), get_id_from_arguments(list(world)), "References to different datums should not return the same id") + + TEST_ASSERT_NOTEQUAL(get_id_from_arguments(list()), SSdcs.GetIdFromArguments(list(/datum/element/dcs_get_id_from_arguments_mock_element2)), "Different elements should not match the same id") + +/datum/unit_test/dcs_get_id_from_arguments/proc/assert_equal(reference, ...) + var/result = get_id_from_arguments(reference) + + // Start at 1 so the 2nd argument is 2 + var/index = 1 + + for (var/other_case in args) + index += 1 + + var/other_result = get_id_from_arguments(other_case) + + if (other_result == result) + continue + + TEST_FAIL("Case #[index] produces a different GetIdFromArguments result from the first. [other_result] != [result]") + +/datum/unit_test/dcs_get_id_from_arguments/proc/get_id_from_arguments(list/arguments) + return SSdcs.GetIdFromArguments(list(/datum/element/dcs_get_id_from_arguments_mock_element) + arguments) + +// Necessary because GetIdFromArguments uses argument_hash_start_idx from an element type +/datum/element/dcs_get_id_from_arguments_mock_element + id_arg_index = 2 + +/datum/element/dcs_get_id_from_arguments_mock_element2 + id_arg_index = 2 diff --git a/code/unit_tests/emoting.dm b/code/unit_tests/emoting.dm deleted file mode 100644 index 7111107b709..00000000000 --- a/code/unit_tests/emoting.dm +++ /dev/null @@ -1,25 +0,0 @@ -/datum/unit_test/emoting - var/emotes_used = 0 - -/datum/unit_test/emoting/Run() - var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human) - RegisterSignal(human, COMSIG_MOB_EMOTE, PROC_REF(on_emote_used)) - - human.say("*shrug") - TEST_ASSERT_EQUAL(emotes_used, 1, "Human did not shrug") - - human.say("*beep") - TEST_ASSERT_EQUAL(emotes_used, 1, "Human beeped, when that should be restricted to silicons") - - human.setOxyLoss(140) - - TEST_ASSERT(human.stat != CONSCIOUS, "Human is somehow conscious after receiving suffocation damage") - - human.say("*shrug") - TEST_ASSERT_EQUAL(emotes_used, 1, "Human shrugged while unconscious") - - human.say("*deathgasp") - TEST_ASSERT_EQUAL(emotes_used, 2, "Human could not deathgasp while unconscious") - -/datum/unit_test/emoting/proc/on_emote_used() - emotes_used += 1 diff --git a/code/unit_tests/focus_only_tests.dm b/code/unit_tests/focus_only_tests.dm index e7a44844255..09d42e021d8 100644 --- a/code/unit_tests/focus_only_tests.dm +++ b/code/unit_tests/focus_only_tests.dm @@ -1,21 +1,22 @@ -/** - * These tests perform no behavior of their own, and have their tests offloaded onto other procs. - * This is useful in cases like in build_appearance_list where we want to know if any fail, - * but is not useful to right a test for. - * This file exists so that you can change any of these to TEST_FOCUS and only check for that test. - * For example, change /datum/unit_test/focus_only/invalid_overlays to TEST_FOCUS(/datum/unit_test/focus_only/invalid_overlays), - * and you will only test the check for invalid overlays in appearance building. - */ +/// These tests perform no behavior of their own, and have their tests offloaded onto other procs. +/// This is useful in cases like in build_appearance_list where we want to know if any fail, +/// but is not useful to right a test for. +/// This file exists so that you can change any of these to TEST_FOCUS and only check for that test. +/// For example, change /datum/unit_test/focus_only/invalid_overlays to TEST_FOCUS(/datum/unit_test/focus_only/invalid_overlays), +/// and you will only test the check for invalid overlays in appearance building. /datum/unit_test/focus_only -/// Checks that every overlay passed into build_appearance_list exists in the icon. +/// Checks that every created emissive has a valid icon_state +/datum/unit_test/focus_only/invalid_emissives + +/// Checks that every overlay passed into build_appearance_list exists in the icon /datum/unit_test/focus_only/invalid_overlays -/// Checks that every icon sent to the research_designs spritesheet is valid. -// /datum/unit_test/focus_only/invalid_research_designs +/// Checks that every screen alert with an overlay icon state is valid +/datum/unit_test/focus_only/screen_alert_overlay -/// Checks that every icon sent to vending machines is valid. -// /datum/unit_test/focus_only/invalid_vending_machine_icon_states +/// Checks that every icon sent to vending machines is valid +/datum/unit_test/focus_only/invalid_vending_machine_icon_states -/// Checks that smoothing_groups and canSmoothWith are properly sorted in /atom/Initialize. +/// Checks that smoothing_groups and canSmoothWith are properly sorted in /atom/Initialize /datum/unit_test/focus_only/sorted_smoothing_groups diff --git a/code/unit_tests/get_turf_pixel.dm b/code/unit_tests/get_turf_pixel.dm new file mode 100644 index 00000000000..2ae1d955749 --- /dev/null +++ b/code/unit_tests/get_turf_pixel.dm @@ -0,0 +1,12 @@ +///ensures that get_turf_pixel() returns turfs within the bounds of the map, +///even when called on a movable with its sprite out of bounds +/datum/unit_test/maptest_get_turf_pixel + +/datum/unit_test/maptest_get_turf_pixel/Run() + //we need long larry to peek over the top edge of the earth + var/turf/north = locate(1, world.maxy, run_loc_floor_bottom_left.z) + + //hes really long, so hes really good at peaking over the edge of the map + // we dont have colossus simplemob for some reason + var/mob/living/simple_mob/vore/aggressive/deathclaw/long_larry = allocate(/mob/living/simple_mob/vore/aggressive/deathclaw, north) + TEST_ASSERT(istype(get_turf_pixel(long_larry), /turf), "get_turf_pixel() isnt clamping a mob whos sprite is above the bounds of the world inside of the map.") diff --git a/code/unit_tests/heretic_knowledge.dm b/code/unit_tests/heretic_knowledge.dm deleted file mode 100644 index a433bce1ec9..00000000000 --- a/code/unit_tests/heretic_knowledge.dm +++ /dev/null @@ -1,21 +0,0 @@ -/// This test checks all heretic knowledge nodes - excluding the ones which are unreachable on purpose - and ensures players can reach them in game. -/// If it finds a node that is unreachable, it throws an error. -/datum/unit_test/heretic_knowledge/Run() - ///List of all knowledge excluding the unreachable base types. - var/list/blacklist = list(/datum/eldritch_knowledge/spell,/datum/eldritch_knowledge/curse,/datum/eldritch_knowledge/final,/datum/eldritch_knowledge/summon) - var/list/all_possible_knowledge = subtypesof(/datum/eldritch_knowledge) - blacklist - - var/list/list_to_check = GLOB.heretic_start_knowledge.Copy() - var/i = 0 - while(i < length(list_to_check)) - var/datum/eldritch_knowledge/eldritch_knowledge = allocate(list_to_check[++i]) - for(var/next_knowledge in eldritch_knowledge.next_knowledge) - if(next_knowledge in list_to_check) - continue - list_to_check += next_knowledge - - if(length(all_possible_knowledge) != length(all_possible_knowledge & list_to_check)) - var/list/unreachables = all_possible_knowledge - list_to_check - for(var/X in unreachables) - var/datum/eldritch_knowledge/eldritch_knowledge = X - Fail("[initial(eldritch_knowledge.name)] is unreachable by players! Add it to the blacklist in /code/modules/unit_tests/heretic_knowledge.dm if it is purposeful!") diff --git a/code/unit_tests/holidays.dm b/code/unit_tests/holidays.dm deleted file mode 100644 index 4df5443e2ee..00000000000 --- a/code/unit_tests/holidays.dm +++ /dev/null @@ -1,33 +0,0 @@ -// test Jewish holiday -/datum/unit_test/hanukkah_2123/Run() - var/datum/holiday/hebrew/hanukkah/hanukkah = new - TEST_ASSERT(hanukkah.shouldCelebrate(14, DECEMBER, 2123, 2, TUESDAY), "December 14, 2123 was not Hanukkah.") - -// test Islamic holiday -/datum/unit_test/ramadan_2165/Run() - var/datum/holiday/islamic/ramadan/ramadan = new - TEST_ASSERT(ramadan.shouldCelebrate(6, NOVEMBER, 2165, 1, WEDNESDAY), "November 6, 2165 was not Ramadan.") - -// nth day of week -/datum/unit_test/thanksgiving_2020/Run() - var/datum/holiday/nth_week/thanksgiving/thanksgiving = new - TEST_ASSERT(thanksgiving.shouldCelebrate(26, NOVEMBER, 2020, 4, THURSDAY), "November 26, 2020 was not Thanksgiving.") - -// another nth day of week -/datum/unit_test/indigenous_3683/Run() - var/datum/holiday/nth_week/indigenous/indigenous = new - TEST_ASSERT(indigenous.shouldCelebrate(11, OCTOBER, 3683, 2, MONDAY), "October 11, 3683 was not Indigenous Peoples' Day.") - -// plain old simple holiday -/datum/unit_test/hello_2020/Run() - var/datum/holiday/hello/hello = new - TEST_ASSERT(hello.shouldCelebrate(21, NOVEMBER, 2020, 3, SATURDAY), "November 21, 2020 was not Hello day.") - -// holiday which goes across months -/datum/unit_test/new_year_1983/Run() - var/datum/holiday/new_year/new_year = new - TEST_ASSERT(new_year.shouldCelebrate(2, JANUARY, 1983, 1, SUNDAY), "January 2, 1983 was not New Year.") - -/datum/unit_test/moth_week_2020/Run() - var/datum/holiday/moth/moth = new - TEST_ASSERT(moth.shouldCelebrate(19, JULY, 2020, 3, SATURDAY), "July 19, 2020 was not Moth Week.") diff --git a/code/unit_tests/keybinding_init.dm b/code/unit_tests/keybinding_init.dm index 2bd2fdee1e2..c9d17f688af 100644 --- a/code/unit_tests/keybinding_init.dm +++ b/code/unit_tests/keybinding_init.dm @@ -3,4 +3,4 @@ var/datum/keybinding/KB = i if(initial(KB.keybind_signal) || !initial(KB.name)) continue - Fail("[KB.name] does not have a keybind signal defined.") + TEST_FAIL("[KB.name] does not have a keybind signal defined.") diff --git a/code/unit_tests/machine_disassembly.dm b/code/unit_tests/machine_disassembly.dm deleted file mode 100644 index 59edb4ae9db..00000000000 --- a/code/unit_tests/machine_disassembly.dm +++ /dev/null @@ -1,12 +0,0 @@ -/// Ensures that when disassembling a machine, all the parts are given back -/datum/unit_test/machine_disassembly/Run() - var/obj/machinery/freezer = allocate(/obj/machinery/atmospherics/components/unary/thermomachine/freezer) - - var/turf/freezer_location = freezer.loc - freezer.deconstruct() - - // Check that the components are created - TEST_ASSERT(locate(/obj/item/stock_parts/micro_laser) in freezer_location, "Couldn't find micro-laser when disassembling freezer") - - // Check that the circuit board itself is created - TEST_ASSERT(locate(/obj/item/circuitboard/machine/thermomachine) in freezer_location, "Couldn't find the circuit board when disassembling freezer") diff --git a/code/unit_tests/merge_type.dm b/code/unit_tests/merge_type.dm deleted file mode 100644 index ba3cfcf492b..00000000000 --- a/code/unit_tests/merge_type.dm +++ /dev/null @@ -1,15 +0,0 @@ -/datum/unit_test/merge_type/Run() - var/list/blacklist = list(/obj/item/stack/sheet, - /obj/item/stack/sheet/mineral, - /obj/item/stack/ore, - /obj/item/stack/spacecash, - // /obj/item/stack/license_plates, - /obj/item/stack/tile/mineral, - /obj/item/stack/tile) - - var/list/paths = subtypesof(/obj/item/stack) - blacklist - - for(var/stackpath in paths) - var/obj/item/stack/stack = stackpath - if(!initial(stack.merge_type)) - Fail("([stack]) lacks set merge_type variable!") diff --git a/code/unit_tests/metabolizing.dm b/code/unit_tests/metabolizing.dm deleted file mode 100644 index 9b4968dfea5..00000000000 --- a/code/unit_tests/metabolizing.dm +++ /dev/null @@ -1,38 +0,0 @@ -/datum/unit_test/metabolization/Run() - // Pause natural mob life so it can be handled entirely by the test - SSmobs.pause() - - var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human) - var/mob/living/carbon/monkey/monkey = allocate(/mob/living/carbon/monkey) - - for (var/reagent_type in subtypesof(/datum/reagent)) - test_reagent(human, reagent_type) - test_reagent(monkey, reagent_type) - -/datum/unit_test/metabolization/proc/test_reagent(mob/living/carbon/C, reagent_type) - C.reagents.add_reagent(reagent_type, 10) - C.reagents.metabolize(C, can_overdose = TRUE) - C.reagents.clear_reagents() - -/datum/unit_test/metabolization/Destroy() - SSmobs.ignite() - return ..() - -/datum/unit_test/on_mob_end_metabolize/Run() - var/mob/living/carbon/human/user = allocate(/mob/living/carbon/human) - var/obj/item/reagent_containers/pill/pill = allocate(/obj/item/reagent_containers/pill) - var/datum/reagent/drug/methamphetamine/meth = /datum/reagent/drug/methamphetamine - - // Give them enough meth to be consumed in 2 metabolizations - pill.reagents.add_reagent(meth, initial(meth.metabolization_rate) * 1.9) - pill.lazy_melee_interaction_chain(user, user) - - user.Life() - - TEST_ASSERT(user.reagents.has_reagent(meth), "User does not have meth in their system after consuming it") - TEST_ASSERT(user.has_movespeed_modifier(/datum/movespeed_modifier/reagent/methamphetamine), "User consumed meth, but did not gain movespeed modifier") - - user.Life() - - TEST_ASSERT(!user.reagents.has_reagent(meth), "User still has meth in their system when it should've finished metabolizing") - TEST_ASSERT(!user.has_movespeed_modifier(/datum/movespeed_modifier/reagent/methamphetamine), "User still has movespeed modifier despite not containing any more meth") diff --git a/code/unit_tests/missing_icons.dm b/code/unit_tests/missing_icons.dm new file mode 100644 index 00000000000..fb240cfd9cb --- /dev/null +++ b/code/unit_tests/missing_icons.dm @@ -0,0 +1,54 @@ +/// Makes sure objects actually have icons that exist! +/datum/unit_test/missing_icons + var/static/list/possible_icon_states = list() + /// additional_icon_location is for downstream modularity support. + /// Make sure this location is also present in tools/deploy.sh + /// If you need additional paths ontop of this second one, you can add another generate_possible_icon_states_list("your/folder/path/") below the if(additional_icon_location) block in Run(), and make sure to add that path to tools/deploy.sh as well. + var/additional_icon_location = null + +/datum/unit_test/missing_icons/proc/generate_possible_icon_states_list(directory_path) + if(!directory_path) + directory_path = "icons/obj/" + for(var/file_path in flist(directory_path)) + if(findtext(file_path, ".dmi")) + for(var/sprite_icon in icon_states("[directory_path][file_path]", 1)) //2nd arg = 1 enables 64x64+ icon support, otherwise you'll end up with "sword0_1" instead of "sword" + possible_icon_states[sprite_icon] += list("[directory_path][file_path]") + else + possible_icon_states += generate_possible_icon_states_list("[directory_path][file_path]") + +/datum/unit_test/missing_icons/Run() + generate_possible_icon_states_list() + generate_possible_icon_states_list("icons/effects/") + if(additional_icon_location) + generate_possible_icon_states_list(additional_icon_location) + + //Add EVEN MORE paths if needed here! + //generate_possible_icon_states_list("your/folder/path/") + var/list/bad_list = list() + for(var/obj/obj_path as anything in subtypesof(/obj)) + if(ispath(obj_path, /obj/item)) + var/obj/item/item_path = obj_path + if(initial(item_path.item_flags) & ITEM_ABSTRACT) + continue + + var/icon = initial(obj_path.icon) + if(isnull(icon)) + continue + var/icon_state = initial(obj_path.icon_state) + if(isnull(icon_state)) + continue + + if(length(bad_list) && (icon_state in bad_list[icon])) + continue + + if(icon_exists(icon, icon_state)) + continue + + bad_list[icon] += list(icon_state) + + var/match_message + if(icon_state in possible_icon_states) + for(var/file_place in possible_icon_states[icon_state]) + match_message += (match_message ? " & '[file_place]'" : " - Matching sprite found in: '[file_place]'") + + TEST_FAIL("Missing icon_state for [obj_path] in '[icon]'.\n\ticon_state = \"[icon_state]\"[match_message]") diff --git a/code/unit_tests/outfit_sanity.dm b/code/unit_tests/outfit_sanity.dm deleted file mode 100644 index fa2ba5c2710..00000000000 --- a/code/unit_tests/outfit_sanity.dm +++ /dev/null @@ -1,50 +0,0 @@ -#define CHECK_OUTFIT_SLOT(outfit_key, slot_id) if (outfit.##outfit_key) { \ - H.equip_to_slot_or_del(new outfit.##outfit_key(H), ##slot_id, TRUE); \ - /* We don't check the result of equip_to_slot_or_del because it returns false for random jumpsuits, as they delete themselves on init */ \ - if (!H.get_item_by_slot(##slot_id)) { \ - Fail("[outfit.name]'s [#outfit_key] is invalid!"); \ - } \ -} - -/datum/unit_test/outfit_sanity/Run() - var/mob/living/carbon/human/H = allocate(/mob/living/carbon/human) - - for (var/outfit_type in subtypesof(/datum/outfit)) - // Only make one human and keep undressing it because it's much faster - for (var/obj/item/I in H.get_equipped_items(include_pockets = TRUE)) - qdel(I) - - var/datum/outfit/outfit = new outfit_type - outfit.pre_equip(H, TRUE) - - CHECK_OUTFIT_SLOT(uniform, ITEM_SLOT_ICLOTHING) - CHECK_OUTFIT_SLOT(suit, ITEM_SLOT_OCLOTHING) - CHECK_OUTFIT_SLOT(back, ITEM_SLOT_BACK) - CHECK_OUTFIT_SLOT(belt, ITEM_SLOT_BELT) - CHECK_OUTFIT_SLOT(gloves, ITEM_SLOT_GLOVES) - CHECK_OUTFIT_SLOT(shoes, ITEM_SLOT_FEET) - CHECK_OUTFIT_SLOT(head, ITEM_SLOT_HEAD) - CHECK_OUTFIT_SLOT(mask, ITEM_SLOT_MASK) - CHECK_OUTFIT_SLOT(neck, ITEM_SLOT_NECK) - CHECK_OUTFIT_SLOT(ears, ITEM_SLOT_EARS) - CHECK_OUTFIT_SLOT(glasses, ITEM_SLOT_EYES) - CHECK_OUTFIT_SLOT(id, ITEM_SLOT_ID) - CHECK_OUTFIT_SLOT(suit_store, ITEM_SLOT_SUITSTORE) - CHECK_OUTFIT_SLOT(l_pocket, ITEM_SLOT_LPOCKET) - CHECK_OUTFIT_SLOT(r_pocket, ITEM_SLOT_RPOCKET) - - if (outfit.backpack_contents || outfit.box) - var/list/backpack_contents = outfit.backpack_contents?.Copy() - if (outfit.box) - if (!backpack_contents) - backpack_contents = list() - backpack_contents.Insert(1, outfit.box) - backpack_contents[outfit.box] = 1 - - for (var/path in backpack_contents) - var/number = backpack_contents[path] || 1 - for (var/_ in 1 to number) - if (!H.equip_to_slot_or_del(new path(H), ITEM_SLOT_BACKPACK, TRUE)) - Fail("[outfit.name]'s backpack_contents are invalid! Couldn't add [path] to backpack.") - -#undef CHECK_OUTFIT_SLOT diff --git a/code/unit_tests/range_return.dm b/code/unit_tests/range_return.dm new file mode 100644 index 00000000000..8d1d251255e --- /dev/null +++ b/code/unit_tests/range_return.dm @@ -0,0 +1,20 @@ +/// This tests for an unspecified bit of behavior we rely on in energy_ball.dm code +/// Essentially, as of the current byond version, range and view will return turfs in what looks "roughly" like a circle +/// So we can be guarenteed that if we find a turf, it will be the closest turf of that sort, or at least one of them +/// This code tests for that. If this ever fails, remove the logic fron energy_ball.dm, and test if spiral_turfs would be faster +/datum/unit_test/range_return + +/datum/unit_test/range_return/Run() + var/x = (run_loc_floor_top_right.x - run_loc_floor_bottom_left.x) / 2 + var/y = (run_loc_floor_top_right.y - run_loc_floor_bottom_left.y) / 2 + // We take the turf equidistant from the two corners + var/turf/center = locate(x + run_loc_floor_bottom_left.x, y + run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z) + // Now, we'll iterate over all the turfs in range, and insure we don't see one with a higher dist then a previously seen instance + var/least_distance = 0 + for(var/turf/lad in orange(center, min(x, y))) + // get_dist is essentially max(dist deltas) + // So this is valid even if the corners aren't visited first + var/dist = get_dist(center, lad) + TEST_ASSERT(dist >= least_distance, "Range returned a turf of greater distance BEFORE a turf of lower distance. \ + Behavior has changed, remove all code that relies on this behavior") + least_distance = dist diff --git a/code/unit_tests/reagents/_reagents.dm b/code/unit_tests/reagents/_reagents.dm index 60a3545c399..c6b9b3d0b5d 100644 --- a/code/unit_tests/reagents/_reagents.dm +++ b/code/unit_tests/reagents/_reagents.dm @@ -1,2 +1,5 @@ #include "chemical_reaction.dm" +#include "container_sanity.dm" +#include "dropper.dm" #include "reagent.dm" +#include "reagent_container_defaults.dm" diff --git a/code/unit_tests/reagents/container_sanity.dm b/code/unit_tests/reagents/container_sanity.dm new file mode 100644 index 00000000000..78dfe7b6253 --- /dev/null +++ b/code/unit_tests/reagents/container_sanity.dm @@ -0,0 +1,21 @@ +/// Test to ensure that all possible reagent containers have enough space to hold any reagents they spawn in with. +/// A drink can with only 30 units of space should not be able to hold 50 units of drink, as an example. +/datum/unit_test/reagent_container_sanity + +/datum/unit_test/reagent_container_sanity/Run() + for(var/entry in subtypesof(/obj/item/reagent_containers)) + if (typesof(entry, /obj/item/reagent_containers/borghypo)) + continue // skip this + var/obj/item/reagent_containers/container = allocate(entry) + var/initialized_volume = 0 + if(!length(container.start_with_reagents)) + continue + + // Get the volume of the reagents in the container that we initialize with, must tally up all of the values in the associated list because checking it through + // the reagents datum will only ever return the maximum volume of the container when "overfull" (adding 120 units to a 100 unit beaker means you only get 100 units of stuff contained). + for(var/reagent in container.start_with_reagents) + initialized_volume += container.start_with_reagents[reagent] + + if(initialized_volume > container.volume) + // include the path as well here since there's up to like five "hypospray" or "beaker" or "soda water" types that aren't distinct enough to be differentiated by name alone. + TEST_FAIL("[container] ([container.type]) has [initialized_volume] units of reagents, but only [container.volume] units of space.") diff --git a/code/unit_tests/reagents/dropper.dm b/code/unit_tests/reagents/dropper.dm new file mode 100644 index 00000000000..120919fc3d6 --- /dev/null +++ b/code/unit_tests/reagents/dropper.dm @@ -0,0 +1,21 @@ +/// Tests the droppper picks up and dispenses reagents correctly. +/datum/unit_test/dropper_use + +/datum/unit_test/dropper_use/Run() + var/mob/living/carbon/human/consistent/chemist = EASY_ALLOCATE() + var/obj/item/reagent_containers/dropper/dropper = EASY_ALLOCATE() + var/obj/item/reagent_containers/glass/beaker/noreact/beaker = EASY_ALLOCATE() + + var/starting_volume = 50 + beaker.reagents.add_reagent(/datum/reagent/water, starting_volume) + + chemist.put_in_active_hand(dropper, INV_OP_FORCE) + click_wrapper(chemist, beaker) + + TEST_ASSERT_EQUAL(dropper.reagents.total_volume, 5, "Dropper should have taken 5 units of reagents from the beaker.") + TEST_ASSERT_EQUAL(beaker.reagents.total_volume, starting_volume - 5, "Beaker should have transferred reagents to the dropper.") + + click_wrapper(chemist, beaker) + + TEST_ASSERT_EQUAL(dropper.reagents.total_volume, 0, "Dropper should have emptied itself into the beaker.") + TEST_ASSERT_EQUAL(beaker.reagents.total_volume, starting_volume, "Beaker should have received reagents from the dropper.") diff --git a/code/unit_tests/reagents/reagent_container_defaults.dm b/code/unit_tests/reagents/reagent_container_defaults.dm new file mode 100644 index 00000000000..855574f9278 --- /dev/null +++ b/code/unit_tests/reagents/reagent_container_defaults.dm @@ -0,0 +1,14 @@ +/// Checks if reagent container transfer amount defaults match with actual possible values +/datum/unit_test/reagent_container_defaults + +/datum/unit_test/reagent_container_defaults/Run() + for(var/container_type in subtypesof(/obj/item/reagent_containers)) + if (typesof(container_type, /obj/item/reagent_containers/borghypo)) + continue // skip this + var/obj/item/reagent_containers/container = allocate(container_type) + if(!container.possible_transfer_amounts || !length(container.possible_transfer_amounts)) // we treat 0 len as no transfer + continue + var/initial_value = initial(container.amount_per_transfer_from_this) + var/index_of_initial_value = container.possible_transfer_amounts.Find(initial_value) + if(index_of_initial_value == 0) + TEST_FAIL("Reagent container [container_type]: initial value of amount_per_transfer_from_this value ([initial_value]) not found in possible_transfer_amounts list") diff --git a/code/unit_tests/required_map_items.dm b/code/unit_tests/required_map_items.dm new file mode 100644 index 00000000000..b2f92b53f35 --- /dev/null +++ b/code/unit_tests/required_map_items.dm @@ -0,0 +1,65 @@ +/** + * Tests that all expected items are mapped in roundstart. + * + * How to add an item to this test: + * - Add the typepath(s) to setup_expected_types + * - In the type's initialize, REGISTER_REQUIRED_MAP_ITEM() a minimum and maximum + */ +/datum/unit_test/maptest_required_map_items + /// A list of all typepaths that we expect to be in the required items list + var/list/expected_types = list() + +/// Used to fill the expected types list with all the types we look for on the map. +/// This list will just be full of typepaths that we expect. +/// More detailed information about each item (mainly, how much of each should exist) is set on a per item basis +/datum/unit_test/maptest_required_map_items/proc/setup_expected_types() + // expected_types += subtypesof(/obj/item/stamp/head) + // expected_types += subtypesof(/obj/machinery/modular_computer/preset/cargochat) + + // expected_types += /mob/living/basic/parrot/poly + // expected_types += /mob/living/basic/pet/dog/corgi/ian + // expected_types += /mob/living/carbon/human/species/monkey/punpun + expected_types += /obj/machinery/computer/communications + // expected_types += /obj/machinery/drone_dispenser + // expected_types += /obj/item/piggy_bank/vault + +/datum/unit_test/maptest_required_map_items/Run() + setup_expected_types() + + var/list/required_map_items = GLOB.required_map_items.Copy() + for(var/got_type in expected_types) + var/datum/required_item/item = required_map_items[got_type] + var/items_found = item?.total_amount || 0 + required_map_items -= got_type + if(items_found <= 0) + TEST_FAIL("Item [got_type] was not found, but is expected to be mapped in on mapload!") + continue + + if(items_found < item.minimum_amount) + TEST_FAIL("Item [got_type] should have at least [item.minimum_amount] mapped in but only had [items_found] on mapload!") + continue + + if(items_found > item.maximum_amount) + TEST_FAIL("Item [got_type] should have at most [item.maximum_amount] mapped in but had [items_found] on mapload!") + continue + + // This primarily serves as a reminder to include the typepath in the expected types list above. + // However we can easily delete this line in the future if it runs into false positives. + TEST_ASSERT(length(required_map_items) == 0, "The following paths were found in required map items, but weren't checked: [english_list(required_map_items)]") + +/// Datum for tracking required map items +/datum/required_item + /// Type (exact) being tracked + var/tracked_type + /// How many exist in the world + var/total_amount = 0 + /// Min. amount of this type that should exist roundstart (inclusive) + var/minimum_amount = 1 + /// Max. amount of this type that should exist roundstart (inclusive) + var/maximum_amount = 1 + +/datum/required_item/New(tracked_type, minimum_amount = 1, maximum_amount = 1) + src.tracked_type = tracked_type + src.minimum_amount = minimum_amount + src.maximum_amount = maximum_amount + total_amount += 1 diff --git a/code/unit_tests/siunit.dm b/code/unit_tests/siunit.dm index 3a7a25a98d3..7b98db497c8 100644 --- a/code/unit_tests/siunit.dm +++ b/code/unit_tests/siunit.dm @@ -12,4 +12,4 @@ TEST_ASSERT_EQUAL(siunit_pressure(999.9e3), "999.9 MPa" , "") TEST_ASSERT_EQUAL(siunit_pressure(999.9e3, 0), "1 GPa", "") TEST_ASSERT_EQUAL(siunit_pressure(1e6), "1 GPa", "") - TEST_ASSERT_EQUAL(siunit_pressure(3e17), "300000 PPa", "") + TEST_ASSERT_EQUAL(siunit_pressure(3e32), "300000 QPa", "") diff --git a/code/unit_tests/spawn_humans.dm b/code/unit_tests/spawn_humans.dm index c4922ba64f2..a75f1fad3af 100644 --- a/code/unit_tests/spawn_humans.dm +++ b/code/unit_tests/spawn_humans.dm @@ -2,8 +2,6 @@ var/locs = block(run_loc_floor_bottom_left, run_loc_floor_top_right) for(var/I in 1 to 5) - new /mob/living/carbon/human(pick(locs)) - // TODO-ZANDARIO: Do a full pass of Unit Test upgrades. - // new /mob/living/carbon/human/consistent(pick(locs)) + allocate(/mob/living/carbon/human/consistent, pick(locs)) sleep(5 SECONDS) diff --git a/code/unit_tests/subsystem_init.dm b/code/unit_tests/subsystem_init.dm index 5acf86c05dd..d8b0a1614ab 100644 --- a/code/unit_tests/subsystem_init.dm +++ b/code/unit_tests/subsystem_init.dm @@ -5,10 +5,18 @@ for(var/datum/controller/subsystem/subsystem as anything in Master.subsystems) if(subsystem.subsystem_flags & SS_NO_INIT) continue - if(!subsystem.initialized) - var/message = "[subsystem] ([subsystem.type]) is a subsystem meant to initialize but doesn't get set as initialized." + if(subsystem.initialized) + continue - if (subsystem.subsystem_flags & SS_OK_TO_FAIL_INIT) - TEST_NOTICE(src, "[message]\nThis subsystem is marked as SS_OK_TO_FAIL_INIT. This is still a bug, but it is non-blocking.") - else - TEST_FAIL(message) + var/should_fail = !(subsystem.subsystem_flags & SS_OK_TO_FAIL_INIT) + var/list/message_strings = list("[subsystem] ([subsystem.type]) is a subsystem meant to initialize but could not get initialized.") + + // if(!isnull(subsystem.initialization_failure_message)) + // message_strings += "The subsystem reported the following: [subsystem.initialization_failure_message]" + + if(should_fail) + TEST_FAIL(jointext(message_strings, "\n")) + continue + + message_strings += "This subsystem is marked as SS_OK_TO_FAIL_INIT. This is still a bug, but it is non-blocking." + TEST_NOTICE(src, jointext(message_strings, "\n")) diff --git a/code/unit_tests/tgui_create_message.dm b/code/unit_tests/tgui_create_message.dm new file mode 100644 index 00000000000..4d5a4bc0a02 --- /dev/null +++ b/code/unit_tests/tgui_create_message.dm @@ -0,0 +1,28 @@ +/// Test that `TGUI_CREATE_MESSAGE` is correctly implemented +/datum/unit_test/tgui_create_message + +/datum/unit_test/tgui_create_message/Run() + var/type = "something/here" + var/list/payload = list( + "name" = "Terry McTider", + "heads_caved" = 100, + "accomplishments" = list( + "nothing", + "literally nothing", + list( + "something" = "just kidding", + ), + ), + ) + + var/message = TGUI_CREATE_MESSAGE(type, payload) + + // Ensure consistent output to compare by performing a round-trip. + var/output = json_encode(json_decode(url_decode(message))) + + var/expected = json_encode(list( + "type" = type, + "payload" = payload, + )) + + TEST_ASSERT_EQUAL(expected, output, "TGUI_CREATE_MESSAGE didn't round trip properly") diff --git a/code/unit_tests/unit_test.dm b/code/unit_tests/unit_test.dm index a790a04483c..40eb195fc54 100644 --- a/code/unit_tests/unit_test.dm +++ b/code/unit_tests/unit_test.dm @@ -16,19 +16,25 @@ GLOBAL_VAR_INIT(failed_any_test, FALSE) GLOBAL_VAR(test_log) /// When unit testing, all logs sent to log_mapping are stored here and retrieved in log_mapping unit test. GLOBAL_LIST_EMPTY(unit_test_mapping_logs) +/// Global assoc list of required mapping items, [item typepath] to [required item datum]. +GLOBAL_LIST_EMPTY(required_map_items) -/// The name of the test that is currently focused. +GLOBAL_LIST_EMPTY(test_run_times) + +/// A list of every test that is currently focused. /// Use the PERFORM_ALL_TESTS macro instead. -GLOBAL_VAR_INIT(focused_test, focused_test()) +GLOBAL_VAR_INIT(focused_tests, focused_tests()) -/proc/focused_test() +/proc/focused_tests() + var/list/focused_tests = list() for (var/datum/unit_test/unit_test as anything in subtypesof(/datum/unit_test)) if (initial(unit_test.focus)) - return unit_test - return null + focused_tests += unit_test + + return focused_tests.len > 0 ? focused_tests : null /datum/unit_test - /// Abstract type of the test + /// Do not instantiate if type matches this abstract_type = /datum/unit_test //Bit of metadata for the future maybe @@ -47,9 +53,10 @@ GLOBAL_VAR_INIT(focused_test, focused_test()) var/list/allocated var/list/fail_reasons - var/static/datum/map_level/reservation + /// List of atoms that we don't want to ever initialize in an agnostic context, like for Create and Destroy. Stored on the base datum for usability in other relevant tests that need this data. + var/static/list/uncreatables = null - // todo: per-test reservations. some tests like atmos really should be isolated. + var/static/datum/map_level/reservation /proc/cmp_unit_test_priority(datum/unit_test/a, datum/unit_test/b) return initial(a.priority) - initial(b.priority) @@ -60,17 +67,25 @@ GLOBAL_VAR_INIT(focused_test, focused_test()) SSmapping.load_level(reserved) reservation = reserved + if (isnull(uncreatables)) + uncreatables = build_list_of_uncreatables() + allocated = new + run_loc_floor_bottom_left = get_turf(locate(/obj/landmark/unit_test_bottom_left) in GLOB.landmarks_list) run_loc_floor_top_right = get_turf(locate(/obj/landmark/unit_test_top_right) in GLOB.landmarks_list) + if(priority > TEST_CREATE_AND_DESTROY) //the create and destroy test WILL wreck havok in the unit test room. You CANNOT stop the inevitable. + return + + //Make sure that the top and bottom locations in the diagonal are floors. Anything else may get in the way of several tests. TEST_ASSERT(isfloorturf(run_loc_floor_bottom_left), "run_loc_floor_bottom_left was not a floor ([run_loc_floor_bottom_left])") TEST_ASSERT(isfloorturf(run_loc_floor_top_right), "run_loc_floor_top_right was not a floor ([run_loc_floor_top_right])") /datum/unit_test/Destroy() QDEL_LIST(allocated) // clear the test area - for (var/turf/turf in block(locate(1, 1, run_loc_floor_bottom_left.z), locate(world.maxx, world.maxy, run_loc_floor_bottom_left.z))) + for (var/turf/turf in Z_TURFS(run_loc_floor_bottom_left.z)) for (var/content in turf.contents) if (istype(content, /obj/landmark)) continue @@ -78,7 +93,7 @@ GLOBAL_VAR_INIT(focused_test, focused_test()) return ..() /datum/unit_test/proc/Run() - TEST_FAIL("Run() called parent or not implemented") + TEST_FAIL("[type]/Run() called parent or not implemented") /datum/unit_test/proc/Fail(reason = "No reason", file = "OUTDATED_TEST", line = 1) succeeded = FALSE @@ -91,6 +106,9 @@ GLOBAL_VAR_INIT(focused_test, focused_test()) /// Allocates an instance of the provided type, and places it somewhere in an available loc /// Instances allocated through this proc will be destroyed when the test is over /datum/unit_test/proc/allocate(type, ...) + if(priority > TEST_CREATE_AND_DESTROY) //I'm not using TEST_ASSERT here since these are just numbers that tell nothing useful about the problem. + TEST_FAIL("allocate() was called for a unit test after 'create_and_destroy' has finished. The unit test room is no longer a reliable testing ground for atoms.") + return null //you deserve runtime errors for it var/list/arguments = args.Copy(2) if(ispath(type, /atom)) if (!arguments.len) @@ -106,6 +124,17 @@ GLOBAL_VAR_INIT(focused_test, focused_test()) allocated += instance return instance +/// Resets the air of our testing room to its default +// TODO implement this on ZAS +// /datum/unit_test/proc/restore_atmos() +// var/area/working_area = run_loc_floor_bottom_left.loc +// var/list/turf/to_restore = working_area.get_turfs_from_all_zlevels() +// for(var/turf/open/restore in to_restore) +// var/datum/gas_mixture/GM = SSair.parse_gas_string(restore.initial_gas_mix, /datum/gas_mixture/turf) +// restore.copy_air(GM) +// restore.temperature = initial(restore.temperature) +// restore.air_update_turf(update = FALSE, remove = FALSE) + /datum/unit_test/proc/test_screenshot(name, icon/icon) if (!istype(icon)) TEST_FAIL("[icon] is not an icon.") @@ -120,15 +149,28 @@ GLOBAL_VAR_INIT(focused_test, focused_test()) var/data_filename = "data/screenshots/[path_prefix]_[name].png" fcopy(icon, data_filename) log_test("\t[path_prefix]_[name] was found, putting in data/screenshots") - else if (fexists("code")) - // We are probably running in a local build - fcopy(icon, filename) - TEST_FAIL("Screenshot for [name] did not exist. One has been created.") else - // We are probably running in real CI, so just pretend it worked and move on +#ifdef CIBUILDING + // We are runing in real CI, so just pretend it worked and move on fcopy(icon, "data/screenshots_new/[path_prefix]_[name].png") log_test("\t[path_prefix]_[name] was put in data/screenshots_new") +#else + // We are probably running in a local build + fcopy(icon, filename) + TEST_FAIL("Screenshot for [name] did not exist. One has been created.") +#endif + + +/// Helper for screenshot tests to take an image of an atom from all directions and insert it into one icon +/datum/unit_test/proc/get_flat_icon_for_all_directions(atom/thing, no_anim = TRUE) + var/icon/output = icon('icons/effects/effects.dmi', "nothing") + + for (var/direction in GLOB.cardinal) + var/icon/partial = get_flat_icon(thing, dir = direction, no_anim = no_anim) + output.Insert(partial, dir = direction) + + return output /// Logs a test message. Will use GitHub action syntax found at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions /datum/unit_test/proc/log_for_test(text, priority, file, line) @@ -140,17 +182,38 @@ GLOBAL_VAR_INIT(focused_test, focused_test()) log_world("::[priority] file=[file],line=[line],title=[map_name]: [type]::[annotation_text]") -/proc/RunUnitTest(test_path, list/test_results) - if (ispath(test_path, /datum/unit_test/focus_only)) +/** + * Helper to perform a click + * + * * clicker: The mob that will be clicking + * * clicked_on: The atom that will be clicked + * * passed_params: A list of parameters to pass to the click + */ +/datum/unit_test/proc/click_wrapper(mob/living/clicker, atom/clicked_on, list/passed_params = list("left" = 1, "button" = "left")) + clicker.next_click = -1 + clicker.next_move = -1 + usr = clicker // bypass check + clicker.click_on(clicked_on, raw_params = list2params(passed_params)) + +/proc/RunUnitTest(datum/unit_test/test_path, list/test_results) + if(ispath(test_path, /datum/unit_test/focus_only)) + return + + if(initial(test_path.abstract_type) == test_path) return var/datum/unit_test/test = new test_path GLOB.current_test = test var/duration = REALTIMEOFDAY + var/test_output_desc = "[test_path]" + var/message = "" log_world("::group::[test_path]") + test.Run() + // if(test.priority < TEST_CREATE_AND_DESTROY) //We shouldn't care about restoring atmos after create_and_destroy. + // test.restore_atmos() duration = REALTIMEOFDAY - duration GLOB.current_test = null @@ -169,10 +232,13 @@ GLOBAL_VAR_INIT(focused_test, focused_test()) // Normal log message log_entry += "\tFAILURE #[reasonID]: [text] at [file]:[line]" - var/message = log_entry.Join("\n") - log_test(message) + if(length(log_entry)) + message = log_entry.Join("\n") + log_test(message) - var/test_output_desc = "[test_path] [duration / 10]s" + test_output_desc += " [duration / 10]s" + if(duration > 10) + GLOB.test_run_times[test_path] = duration if (test.succeeded) log_world("[TEST_OUTPUT_GREEN("PASS")] [test_output_desc]") @@ -181,10 +247,48 @@ GLOBAL_VAR_INIT(focused_test, focused_test()) if (!test.succeeded) log_world("::error::[TEST_OUTPUT_RED("FAIL")] [test_output_desc]") - test_results[test_path] = list("status" = test.succeeded ? UNIT_TEST_PASSED : UNIT_TEST_FAILED, "message" = message, "name" = test_path) + var/final_status = (test.succeeded ? UNIT_TEST_PASSED : UNIT_TEST_FAILED) + test_results[test_path] = list("status" = final_status, "message" = message, "name" = test_path) qdel(test) +/// Builds (and returns) a list of atoms that we shouldn't initialize in generic testing, like Create and Destroy. +/// It is appreciated to add the reason why the atom shouldn't be initialized if you add it to this list. +/datum/unit_test/proc/build_list_of_uncreatables() + RETURN_TYPE(/list) + var/list/returnable_list = list() + // The following are just generic, singular types. + returnable_list = list( + //Never meant to be created, errors out the ass for mobcode reasons + /mob/living/carbon, + //Template type + /obj/machinery/power/turbine, + //Singleton + /mob/dview, + //This is meant to fail extremely loud every single time it occurs in any environment in any context, and it falsely alarms when this unit test iterates it. Let's not spawn it in. + /obj/merge_conflict_marker, + // requires a borg + /obj/item/reagent_containers/borghypo, + ) + + // Everything that follows is a typesof() check. + + //Say it with me now, type template + returnable_list += typesof(/obj/map_helper) + //This turf existing is an error in and of itself + returnable_list += typesof(/turf/baseturf_skipover) + returnable_list += typesof(/turf/baseturf_bottom) + //We have a baseturf limit of 10, adding more than 10 baseturf helpers will kill CI, so here's a future edge case to fix. + returnable_list += typesof(/obj/effect/baseturf_helper) + //Sparks can ignite a number of things, causing a fire to burn the floor away. Only you can prevent CI fires + returnable_list += typesof(/obj/effect/particle_effect/sparks) + //these can explode and cause the turf to be destroyed at unexpected moments + returnable_list += typesof(/obj/effect/mine) + //Asks for a shuttle that may not exist, let's leave it alone + returnable_list += typesof(/obj/item/pinpointer/shuttle) + + return returnable_list + /proc/RunUnitTests() CHECK_TICK @@ -192,30 +296,35 @@ GLOBAL_VAR_INIT(focused_test, focused_test()) var/list/focused_tests = list() for (var/_test_to_run in tests_to_run) var/datum/unit_test/test_to_run = _test_to_run - if(initial(test_to_run.abstract_type) == test_to_run) - tests_to_run -= test_to_run - continue if (initial(test_to_run.focus)) focused_tests += test_to_run if(length(focused_tests)) tests_to_run = focused_tests - tests_to_run = tim_sort(tests_to_run, GLOBAL_PROC_REF(cmp_unit_test_priority)) + tim_sort(tests_to_run, GLOBAL_PROC_REF(cmp_unit_test_priority)) var/list/test_results = list() + //Hell code, we're bound to end the round somehow so let's stop if from ending while we work + SSticker.delay_end = TRUE for(var/unit_path in tests_to_run) CHECK_TICK //We check tick first because the unit test we run last may be so expensive that checking tick will lock up this loop forever RunUnitTest(unit_path, test_results) + SSticker.delay_end = FALSE + + log_world("::group::Expensive Unit Test Times") + tim_sort(GLOB.test_run_times, cmp = GLOBAL_PROC_REF(cmp_numeric_dsc), associative = TRUE) + for(var/type, duration in GLOB.test_run_times) + log_world("[type] took [duration/10]s") + log_world("::endgroup::") var/file_name = "data/unit_tests.json" fdel(file_name) file(file_name) << json_encode(test_results) SSticker.force_ending = TRUE - sleep(5 SECONDS) //We have to call this manually because del_text can preceed us, and SSticker doesn't fire in the post game - SSticker.standard_reboot() + SSticker.declare_completion() /datum/map_level/unit_tests id = "__UnitTestLevel" diff --git a/code/unit_tests/verify_emoji_names.dm b/code/unit_tests/verify_emoji_names.dm new file mode 100644 index 00000000000..85b4a5cd96c --- /dev/null +++ b/code/unit_tests/verify_emoji_names.dm @@ -0,0 +1,10 @@ +/// Apparently, spritesheets (or maybe how the CSS backend works) do not respond well to icon_state names that are just pure numbers (which was a behavior in emoji.dmi). +/// In case we add more emoji, let's just make sure that we don't have any pure numbers in the emoji.dmi file if we ever add more. +/datum/unit_test/verify_emoji_names + +/datum/unit_test/verify_emoji_names/Run() + var/static/list/emoji_list = icon_states(icon(EMOJI_SET)) | icon_states(icon(EMOJI32_SET)) + for(var/checkable in emoji_list) + if(isnum(text2num(checkable))) + TEST_FAIL("Emoji name [checkable] in [EMOJI_SET] is a pure number. This will cause issues with the CSS backend via Spritesheets. Please rename it to something else.") + continue diff --git a/dependencies.sh b/dependencies.sh index a659f750fb3..2609b881a86 100755 --- a/dependencies.sh +++ b/dependencies.sh @@ -3,9 +3,6 @@ # Project depdendencies file # Final authority on what's used to build. -#Project dependencies file -#Final authority on what's required to fully build the project - # byond version export BYOND_MAJOR=516 export BYOND_MINOR=1659 @@ -32,4 +29,4 @@ export FLYWAY_VERSION=11.0.1 export CUTTER_REPO=spacestation13/hypnagogic #hypnagogic git tag -export CUTTER_VERSION=v4.0.0 +export CUTTER_VERSION=v5.0.0 diff --git a/icon_cutter_templates/bitmask/cardinal_32x32.toml b/icon_cutter_templates/bitmask/cardinal_32x32.toml index 9d3d4097e78..f06fb826477 100644 --- a/icon_cutter_templates/bitmask/cardinal_32x32.toml +++ b/icon_cutter_templates/bitmask/cardinal_32x32.toml @@ -1,16 +1,25 @@ mode = "BitmaskSlice" -# Don't try and put directions in our icon states -produce_dirs = false +# Dictates what sort of directions this dmi will have. +# There are currently 4 possible values: +# Standard (Default, used if none is specified) -> only 1 direction per frame +# Cardinals -> 4 directions per frame, arranged as duplicates of the full input set out to the right +# ... in order SOUTH, NORTH, EAST, WEST +# All -> 8 directions per frame, arranged in the same pattern as cardinals +# ... in order SOUTH, NORTH, EAST, WEST, SOUTHEAST, SOUTHWEST, NORTHEAST, NORTHWEST +# CardinalsRotated -> 1 direction per frame, will be expanded in the dmi to be Cardinals, with each +# ... direction being that base junction, rotated in whatever way. Exists mostly so client.dir supporting states +# ... can be created. Creates a lot of duplicate blocks otherwise. +directional_strategy = "Standard" # We smooth only with our cardinal neighbors, not the ones on the diagonal -smooth_diagonally = false +output_type = "Cardinal" # Take as input a set of 32x32 blocks [icon_size] x = 32 y = 32 -# Output our stuff at the same level as it's input +# Output our stuff at the same level as its input [output_icon_pos] x = 0 y = 0 diff --git a/icon_cutter_templates/bitmask/diagonal_32x32.toml b/icon_cutter_templates/bitmask/diagonal_32x32.toml index 1e80e3627e9..0da4d2a10c4 100644 --- a/icon_cutter_templates/bitmask/diagonal_32x32.toml +++ b/icon_cutter_templates/bitmask/diagonal_32x32.toml @@ -1,7 +1,7 @@ template = "bitmask/cardinal_32x32" # We're diagonal -smooth_diagonally = true +output_type = "StandardDiagonal" # And because of that we need a state for all directions [positions] diff --git a/icon_cutter_templates/bitmask/diagonal_corner_32x32.toml b/icon_cutter_templates/bitmask/diagonal_corner_32x32.toml new file mode 100644 index 00000000000..a895df5b0b2 --- /dev/null +++ b/icon_cutter_templates/bitmask/diagonal_corner_32x32.toml @@ -0,0 +1,20 @@ +template = "bitmask/diagonal_32x32" + +# We're a corner diagonal +output_type = "CornerDiagonal" + +# And because of that we need a state for all directions and all our asshole corner bits +[positions] +convex = 0 +vertical = 1 +horizontal = 2 +concave = 3 +flat = 4 +bottom_right_inner = 5 +bottom_left_inner = 6 +top_right_inner = 7 +top_left_inner = 8 +bottom_right_outer = 9 +bottom_left_outer = 10 +top_right_outer = 11 +top_left_outer = 12 diff --git a/icon_cutter_templates/bitmask/restore.toml b/icon_cutter_templates/bitmask/restore.toml new file mode 100644 index 00000000000..066354eedc6 --- /dev/null +++ b/icon_cutter_templates/bitmask/restore.toml @@ -0,0 +1,20 @@ +# Bitmask restoration! +# Allows for easy mass extraction of template pngs and their configs from a dmi +# Use this if you have a dmi and you want a cutter config you can edit easily +# Of note, while it tries its best it is nowhere near perfect. We don't parity check against the existing dmi +# And we also do not account for overrided states very well +# Always double check (and be aware that dmi is weird so you may get diffs of 1 rgb value when doin this) +mode = "BitmaskSliceReconstruct" +# List of icon states to pull out (by default) +extract = ["0", "3", "12", "15", "255"] + +# Map of name -> state that will be encoded into a positions list later +# Lets you extract particular states and use them to fill in for states later +# Useful to carry over odd snowflake states +#[bespoke] + +# Map of key -> value to set on the created config +# Lets you set arbitrary values on the created config, mostly useful for batch processing +# IMPORTANT NOTE: We sort of assume you'll setup a default template here (since this is for batch processing), +# so if things work odd that's likely why +#[set] diff --git a/icon_cutter_templates/bitmask/restore_corners.toml b/icon_cutter_templates/bitmask/restore_corners.toml new file mode 100644 index 00000000000..fbbcdc08628 --- /dev/null +++ b/icon_cutter_templates/bitmask/restore_corners.toml @@ -0,0 +1,25 @@ +# Bitmask restoration! (Corners) +# Allows for easy mass extraction of template pngs and their configs from a dmi +# Use this if you have a dmi and you want a cutter config you can edit easily +# Of note, while it tries its best it is nowhere near perfect. We don't parity check against the existing dmi +# And we also do not account for overrided states very well +# Always double check (and be aware that dmi is weird so you may get diffs of 1 rgb value when doin this) +mode = "BitmaskSliceReconstruct" +# List of icon states to pull out, expanded to get the standard ordering for diagonals +extract = [ + "0", "3", "12", "15", "255", + "6-diagonal", "10-diagonal", "5-diagonal", "9-diagonal", + "38-diagonal", "74-diagonal", "21-diagonal", "137-diagonal" +] + +# Map of name -> state that will be encoded into a positions list later +# Lets you extract particular states and use them to fill in for states later +# Useful to carry over odd snowflake states +#[bespoke] + +# Map of key -> value to set on the created config +# Lets you set arbitrary values on the created config, mostly useful for batch processing +# IMPORTANT NOTE: We sort of assume you'll setup a default template here (since this is for batch processing), +# so if things work odd that's likely why +#[set] +#template = "\"bitmask/diagonal_corner_32x32\"" diff --git a/icon_cutter_templates/bitmask/wall_32x32.toml b/icon_cutter_templates/bitmask/wall_32x32.toml deleted file mode 100644 index f3e65b50de7..00000000000 --- a/icon_cutter_templates/bitmask/wall_32x32.toml +++ /dev/null @@ -1,5 +0,0 @@ -template = "bitmask/diagonal_32x32" -# output_name = "wall" - -[cut_pos] -y = 5 diff --git a/icon_cutter_templates/impl/wall/rt_medieval.toml b/icon_cutter_templates/impl/wall/rt_medieval.toml index 669c61969f5..9bb76220bd1 100644 --- a/icon_cutter_templates/impl/wall/rt_medieval.toml +++ b/icon_cutter_templates/impl/wall/rt_medieval.toml @@ -1,7 +1,7 @@ template = "impl/wall_default.toml" output_name = "wall" -smooth_diagonally = false +output_type = "Cardinal" [positions] convex = 0 diff --git a/icons/UI_Icons/emoji/emoji.dmi b/icons/UI_Icons/emoji/emoji.dmi index 147f38be72c..b993735bcd6 100644 Binary files a/icons/UI_Icons/emoji/emoji.dmi and b/icons/UI_Icons/emoji/emoji.dmi differ diff --git a/icons/turf/walls/rt_medieval/boss.png.toml b/icons/turf/walls/rt_medieval/boss.png.toml index 77f5cc801bb..1264d5314a1 100644 --- a/icons/turf/walls/rt_medieval/boss.png.toml +++ b/icons/turf/walls/rt_medieval/boss.png.toml @@ -1,10 +1,2 @@ -template = "impl/wall/rt_medieval.toml" - -smooth_diagonally = true - -[positions] -convex = 0 -vertical = 1 -horizontal = 2 -concave = 3 -flat = 4 +output_name = "wall" +template = "bitmask/diagonal_32x32.toml" diff --git a/icons/turf/walls/rt_medieval/stone.png b/icons/turf/walls/rt_medieval/stone.png index bcbac709202..9b0fa103f9e 100644 Binary files a/icons/turf/walls/rt_medieval/stone.png and b/icons/turf/walls/rt_medieval/stone.png differ diff --git a/icons/turf/walls/rt_medieval/stone.png.toml b/icons/turf/walls/rt_medieval/stone.png.toml index 9e454ee970c..1264d5314a1 100644 --- a/icons/turf/walls/rt_medieval/stone.png.toml +++ b/icons/turf/walls/rt_medieval/stone.png.toml @@ -1,10 +1,2 @@ -template = "impl/wall/rt_medieval.toml" - -smooth_diagonally = true - -[positions] -convex = 0 -vertical = 1 -horizontal = 2 -flat = 3 -concave = 4 +output_name = "wall" +template = "bitmask/diagonal_32x32.toml" diff --git a/icons/turf/walls/rt_medieval/stone/brick.png b/icons/turf/walls/rt_medieval/stone/brick.png index aa1b380045f..0e84b76089d 100644 Binary files a/icons/turf/walls/rt_medieval/stone/brick.png and b/icons/turf/walls/rt_medieval/stone/brick.png differ diff --git a/icons/turf/walls/rt_medieval/stone/brick.png.toml b/icons/turf/walls/rt_medieval/stone/brick.png.toml index a1916f9133e..1264d5314a1 100644 --- a/icons/turf/walls/rt_medieval/stone/brick.png.toml +++ b/icons/turf/walls/rt_medieval/stone/brick.png.toml @@ -1,10 +1,2 @@ -template = "impl/wall/rt_medieval.toml" - -smooth_diagonally = true - -[positions] -convex = 1 -vertical = 4 -horizontal = 0 -concave = 3 -flat = 2 +output_name = "wall" +template = "bitmask/diagonal_32x32.toml" diff --git a/icons/turf/walls/rt_medieval/stone/craft.png.toml b/icons/turf/walls/rt_medieval/stone/craft.png.toml index 77f5cc801bb..1264d5314a1 100644 --- a/icons/turf/walls/rt_medieval/stone/craft.png.toml +++ b/icons/turf/walls/rt_medieval/stone/craft.png.toml @@ -1,10 +1,2 @@ -template = "impl/wall/rt_medieval.toml" - -smooth_diagonally = true - -[positions] -convex = 0 -vertical = 1 -horizontal = 2 -concave = 3 -flat = 4 +output_name = "wall" +template = "bitmask/diagonal_32x32.toml" diff --git a/icons/turf/walls/rt_medieval/stone/moss_blue.png.toml b/icons/turf/walls/rt_medieval/stone/moss_blue.png.toml index 77f5cc801bb..1264d5314a1 100644 --- a/icons/turf/walls/rt_medieval/stone/moss_blue.png.toml +++ b/icons/turf/walls/rt_medieval/stone/moss_blue.png.toml @@ -1,10 +1,2 @@ -template = "impl/wall/rt_medieval.toml" - -smooth_diagonally = true - -[positions] -convex = 0 -vertical = 1 -horizontal = 2 -concave = 3 -flat = 4 +output_name = "wall" +template = "bitmask/diagonal_32x32.toml" diff --git a/icons/turf/walls/rt_medieval/stone/moss_green.png.toml b/icons/turf/walls/rt_medieval/stone/moss_green.png.toml index 77f5cc801bb..1264d5314a1 100644 --- a/icons/turf/walls/rt_medieval/stone/moss_green.png.toml +++ b/icons/turf/walls/rt_medieval/stone/moss_green.png.toml @@ -1,10 +1,2 @@ -template = "impl/wall/rt_medieval.toml" - -smooth_diagonally = true - -[positions] -convex = 0 -vertical = 1 -horizontal = 2 -concave = 3 -flat = 4 +output_name = "wall" +template = "bitmask/diagonal_32x32.toml" diff --git a/icons/turf/walls/rt_medieval/stone/moss_red.png.toml b/icons/turf/walls/rt_medieval/stone/moss_red.png.toml index 77f5cc801bb..1264d5314a1 100644 --- a/icons/turf/walls/rt_medieval/stone/moss_red.png.toml +++ b/icons/turf/walls/rt_medieval/stone/moss_red.png.toml @@ -1,10 +1,2 @@ -template = "impl/wall/rt_medieval.toml" - -smooth_diagonally = true - -[positions] -convex = 0 -vertical = 1 -horizontal = 2 -concave = 3 -flat = 4 +output_name = "wall" +template = "bitmask/diagonal_32x32.toml" diff --git a/icons/turf/walls/rt_medieval/wood.png.toml b/icons/turf/walls/rt_medieval/wood.png.toml index 77f5cc801bb..1264d5314a1 100644 --- a/icons/turf/walls/rt_medieval/wood.png.toml +++ b/icons/turf/walls/rt_medieval/wood.png.toml @@ -1,10 +1,2 @@ -template = "impl/wall/rt_medieval.toml" - -smooth_diagonally = true - -[positions] -convex = 0 -vertical = 1 -horizontal = 2 -concave = 3 -flat = 4 +output_name = "wall" +template = "bitmask/diagonal_32x32.toml" diff --git a/interface/interface.dm b/interface/interface.dm index a01ba32c036..bdafbdbe4ca 100644 --- a/interface/interface.dm +++ b/interface/interface.dm @@ -71,23 +71,46 @@ set name = "report-issue" set desc = "Report an issue" set hidden = 1 + var/githuburl = CONFIG_GET(string/githuburl) - if(githuburl) - var/message = "This will open the Github issue reporter in your browser. Are you sure?" - if(GLOB.revdata.testmerge.len) - message += "
    The following experimental changes are active and are probably the cause of any new or sudden issues you may experience. If possible, please try to find a specific thread for your issue instead of posting to the general issue tracker:
    " - message += GLOB.revdata.GetTestMergeInfo(FALSE) - if(tgalert(src, message, "Report Issue","Yes","No")!="Yes") - return - var/issue_template = file2text(".github/ISSUE_TEMPLATE.md") - var/servername = "Citadel Station 13 RP" // CONFIG_GET(string/servername) - var/url_params = "Reporting client version: [byond_version].[byond_build]\n\n[issue_template]" - if(GLOB.round_id || servername) - url_params = "Issue reported from [GLOB.round_id ? " Round ID: [GLOB.round_id][servername ? " ([servername])" : ""]" : servername]\n\n[url_params]" - DIRECT_OUTPUT(src, link("[githuburl]/issues/new?body=[url_encode(url_params)]")) - else - to_chat(src, "The Github URL is not set in the server configuration.") - return + if(!githuburl) + to_chat(src, SPAN_DANGER("The Github URL is not set in the server configuration.")) + return + + var/testmerge_data = GLOB.revdata.testmerge + var/has_testmerge_data = (length(testmerge_data) != 0) + + var/message = "This will open the Github issue reporter in your browser. Are you sure?" + if(has_testmerge_data) + message += "
    The following experimental changes are active and are probably the cause of any new or sudden issues you may experience. If possible, please try to find a specific thread for your issue instead of posting to the general issue tracker:
    " + message += GLOB.revdata.GetTestMergeInfo(FALSE) + + // We still use tg_alert here because some people were concerned that if someone wanted to report that tgui wasn't working + // then the report issue button being tgui-based would be problematic. + if(tgalert(src, message, "Report Issue", "Yes", "No") != "Yes") + return + + var/base_link = githuburl + "/issues/new?template=bug_report_form.yml" + var/list/concatable = list(base_link) + + var/client_version = "[byond_version].[byond_build]" + concatable += ("&reporting-version=" + client_version) + + // the way it works is that we use the ID's that are baked into the template YML and replace them with values that we can collect in game. + if(GLOB.round_id) + concatable += ("&round-id=" + GLOB.round_id) + + // Insert testmerges + if(has_testmerge_data) + var/list/all_tms = list() + for(var/entry in testmerge_data) + var/datum/tgs_revision_information/test_merge/tm = entry + all_tms += "- \[[tm.title]\]([githuburl]/pull/[tm.number])" + var/all_tms_joined = jointext(all_tms, "\n") + + concatable += ("&test-merges=" + url_encode(all_tms_joined)) + + DIRECT_OUTPUT(src, link(jointext(concatable, ""))) /client/proc/changelog_async() set waitfor = FALSE diff --git a/maps/away_missions/archive/example.dmm b/maps/away_missions/archive/example.dmm index c09ef86193e..9e2d2e94ef5 100644 --- a/maps/away_missions/archive/example.dmm +++ b/maps/away_missions/archive/example.dmm @@ -400,8 +400,7 @@ /area/awaymission/example) "bG" = ( /obj/machinery/door/airlock{ - name = "Unisex Restrooms"; - + name = "Unisex Restrooms" }, /turf/simulated/floor/tiled/white, /area/awaymission/example) diff --git a/maps/away_missions/archive/wildwest.dmm b/maps/away_missions/archive/wildwest.dmm index 7b33b7c9a94..4db97a3cec0 100644 --- a/maps/away_missions/archive/wildwest.dmm +++ b/maps/away_missions/archive/wildwest.dmm @@ -1078,8 +1078,7 @@ "fm" = ( /turf/simulated/mineral{ icon_state = "sandstone0"; - mineral = "sandstone"; - + mineral = "sandstone" }, /area/awaymission/wwmines) "fo" = ( diff --git a/maps/stations/rift/map_files/rift-05-surface2.dmm b/maps/stations/rift/map_files/rift-05-surface2.dmm index e689c9d99fd..fd10e2e2eca 100644 --- a/maps/stations/rift/map_files/rift-05-surface2.dmm +++ b/maps/stations/rift/map_files/rift-05-surface2.dmm @@ -2402,11 +2402,6 @@ /obj/random/maintenance/research, /turf/simulated/floor/lythios43c/indoors, /area/rift/surfacebase/outside/outside2) -"bnN" = ( -/obj/structure/grille, -/obj/structure/grille, -/turf/simulated/mineral/icerock/lythios43c, -/area/rift/surfacebase/outside/outside2) "bog" = ( /obj/effect/overlay/snow/floor, /obj/effect/floor_decal/industrial/warning{ @@ -49739,7 +49734,7 @@ wGI lac lac lac -bnN +wGI oLJ oLJ pBj diff --git a/maps/submaps/level_specific/class_h/AuxiliaryResearchFacility.dmm b/maps/submaps/level_specific/class_h/AuxiliaryResearchFacility.dmm index 4f74e8240fa..eba7790d3cc 100644 --- a/maps/submaps/level_specific/class_h/AuxiliaryResearchFacility.dmm +++ b/maps/submaps/level_specific/class_h/AuxiliaryResearchFacility.dmm @@ -726,10 +726,6 @@ /obj/effect/debris/cleanable/blood/gibs/robot/down, /turf/simulated/floor/tiled/dark, /area/class_h/POIs/AuxiliaryResearchFacility) -"pR" = ( -/obj/structure/flora/pottedplant/large, -/turf/simulated/floor/wood, -/area/class_h/POIs/AuxiliaryResearchFacility) "qb" = ( /obj/effect/debris/cleanable/dirt, /obj/effect/debris/cleanable/dirt, @@ -743,10 +739,6 @@ }, /turf/simulated/floor/carpet/oracarpet, /area/class_h/POIs/AuxiliaryResearchFacility) -"qA" = ( -/obj/structure/simple_door/wood, -/turf/simulated/floor/wood, -/area/class_h/POIs/AuxiliaryResearchFacility) "qD" = ( /obj/effect/debris/cleanable/dirt, /turf/simulated/floor/tiled/techmaint, @@ -1793,13 +1785,8 @@ "On" = ( /turf/template_noop, /area/template_noop) -"Oo" = ( -/obj/effect/debris/cleanable/dirt, -/turf/simulated/floor/wood, -/area/class_h/POIs/AuxiliaryResearchFacility) "Ow" = ( /obj/structure/grille, -/obj/structure/grille, /obj/structure/window/reinforced/full, /obj/machinery/door/blast/regular/open{ dir = 4 @@ -2023,12 +2010,6 @@ /obj/structure/anomaly_container, /turf/simulated/floor/tiled/white, /area/class_h/POIs/AuxiliaryResearchFacility) -"UI" = ( -/obj/machinery/light{ - dir = 1 - }, -/turf/simulated/floor/wood, -/area/class_h/POIs/AuxiliaryResearchFacility) "Vp" = ( /obj/effect/debris/cleanable/dirt, /obj/effect/debris/cleanable/dirt, @@ -2253,11 +2234,6 @@ }, /turf/simulated/floor/wood, /area/class_h/POIs/AuxiliaryResearchFacility) -"YS" = ( -/obj/effect/debris/cleanable/dirt, -/obj/effect/debris/cleanable/dirt, -/turf/simulated/floor/wood, -/area/class_h/POIs/AuxiliaryResearchFacility) "YY" = ( /turf/simulated/floor/wood, /area/class_h/POIs/AuxiliaryResearchFacility) @@ -2275,9 +2251,6 @@ /obj/structure/table/glass, /turf/simulated/floor/wood, /area/class_h/POIs/AuxiliaryResearchFacility) -"ZC" = ( -/turf/simulated/floor/wood, -/area/class_h/POIs/AuxiliaryResearchFacility) "ZD" = ( /obj/structure/railing{ dir = 4 @@ -2534,7 +2507,7 @@ Zl Zl XD rG -ZC +YY Zc Zw TK @@ -2639,10 +2612,10 @@ Tu Zl Zl Zw -ZC -ZC +YY +YY Gb -ZC +YY TK VC Te @@ -2691,7 +2664,7 @@ Bk Tu Zl Zl -ZC +YY pv LV Zc @@ -2744,11 +2717,11 @@ Bk Tu Zl Zl -pR -ZC -ZC -ZC -pR +jw +YY +YY +YY +jw Wj tr YM @@ -2797,11 +2770,11 @@ Bk Tu Zl nX -ZC -ZC +YY +YY LV LV -ZC +YY Lr VC YM @@ -2850,11 +2823,11 @@ Bk Tu Zl Zl -ZC -ZC +YY +YY ja LV -ZC +YY TK zc ZH @@ -2903,12 +2876,12 @@ Bk Tu Zl Zl -ZC +YY LV LV LV -ZC -qA +YY +gp VC ZH ZH @@ -2956,12 +2929,12 @@ Bk Tu Zl Zl -ZC -ZC -ZC +YY +YY +YY LV sg -qA +gp JT Te ZH @@ -3010,8 +2983,8 @@ Tu Zl Zl AW -ZC -ZC +YY +YY sg LV TK @@ -3435,7 +3408,7 @@ Zl Zl YP ZP -Oo +LV YY jw Zl @@ -3499,7 +3472,7 @@ Zl IY BA rx -Oo +LV Zl vd vd @@ -3603,7 +3576,7 @@ YM YM Zl hr -Oo +LV YY hr Zl @@ -3675,8 +3648,8 @@ aI aI aI aI -qA -qA +gp +gp aI aI aI @@ -3762,7 +3735,7 @@ YM DM Zl up -Oo +LV VM RQ Zl @@ -3777,14 +3750,14 @@ Bk XG XG aI -Oo -YS -Oo +LV +sg +LV YY YY Jy -Oo -Oo +LV +LV YY YY aI @@ -3817,7 +3790,7 @@ Zl du tE IB -Oo +LV Zl fo VO @@ -3831,7 +3804,7 @@ XG XG aI YY -Oo +LV YY aI aI @@ -3861,13 +3834,13 @@ fI Rm xl Cn -Oo +LV Zl VC VC VC Zl -YS +sg CV hn qb @@ -3883,7 +3856,7 @@ Pr XG XG aI -UI +AW YY Ht aI @@ -3914,15 +3887,15 @@ YY HA qf ED -Oo +LV Zl VC VC VC Zl -YS +sg VH -Oo +LV VH Zl fo @@ -3938,7 +3911,7 @@ TM aI YY YY -Oo +LV YY YY YY @@ -3973,8 +3946,8 @@ VC VC VC gp -YS -Oo +sg +LV ov aC Zl @@ -3991,7 +3964,7 @@ XG aI YY YY -Oo +LV ZP BA rx @@ -4049,8 +4022,8 @@ ZP HA Fl ZP -Oo -Oo +LV +LV YY aI XG @@ -4102,8 +4075,8 @@ ZP YY YY ZP -Oo -YS +LV +sg YY aI XG @@ -4156,7 +4129,7 @@ hr YY YY YY -Oo +LV YY aI XG @@ -4208,7 +4181,7 @@ VM tE NX ZP -Oo +LV YY YY aI @@ -4364,8 +4337,8 @@ aI aI aI aI -qA -qA +gp +gp aI aI aI @@ -4416,9 +4389,9 @@ aI of YY yo -Oo -Oo -Oo +LV +LV +LV YY yG YY @@ -4469,8 +4442,8 @@ aI of YY Bx -Oo -Oo +LV +LV OG YY YY @@ -4521,12 +4494,12 @@ XG aI of YY -Oo -YS -YS -YS +LV +sg +sg +sg YY -Oo +LV YY of aI @@ -4574,10 +4547,10 @@ XG aI YY YY -Oo -Oo +LV +LV bF -Oo +LV YY Bx YY @@ -4632,8 +4605,8 @@ YY YY YY YY -Oo -Oo +LV +LV Jv aI XG @@ -4663,7 +4636,7 @@ Vt Wv VS Dp -ZC +YY MN cF VS @@ -4768,8 +4741,8 @@ Ym Sv Ym VS -ZC -ZC +YY +YY Mh sg Gt @@ -4824,7 +4797,7 @@ VS CL LV LV -ZC +YY gU ZH Mb diff --git a/maps/submaps/mountains/deadBeacon.dmm b/maps/submaps/mountains/deadBeacon.dmm index 95e39a03930..267fbe9bbeb 100644 --- a/maps/submaps/mountains/deadBeacon.dmm +++ b/maps/submaps/mountains/deadBeacon.dmm @@ -57,7 +57,7 @@ /obj/item/circuitboard/comm_server, /obj/machinery/light{ dir = 8; - status = LIGHT_EMPTY + status = 1 }, /turf/simulated/floor/tiled/asteroid_steel, /area/submap/cave/deadBeacon) diff --git a/maps/submaps/plains/Oldhouse.dmm b/maps/submaps/plains/Oldhouse.dmm index 69e1185a1b7..7a809b4cf2b 100644 --- a/maps/submaps/plains/Oldhouse.dmm +++ b/maps/submaps/plains/Oldhouse.dmm @@ -188,7 +188,7 @@ legacy_melee_damage_lower = 1; legacy_melee_damage_upper = 3; melee_miss_chance = 30; - movement_base_speed = 10 / 8; + movement_base_speed = 1.25; name = "Mr. Tuddly" }, /turf/simulated/floor/carpet/turcarpet, diff --git a/maps/submaps/plains/Oldhouse_vr.dmm b/maps/submaps/plains/Oldhouse_vr.dmm index 748fb6f27d8..ba2a3817988 100644 --- a/maps/submaps/plains/Oldhouse_vr.dmm +++ b/maps/submaps/plains/Oldhouse_vr.dmm @@ -189,7 +189,7 @@ legacy_melee_damage_lower = 1; legacy_melee_damage_upper = 3; melee_miss_chance = 30; - movement_base_speed = 10 / 8; + movement_base_speed = 1.25; name = "Mr. Tuddly" }, /turf/simulated/floor/carpet/turcarpet, diff --git a/maps/templates/shuttles/overmaps/generic/bearcat.dmm b/maps/templates/shuttles/overmaps/generic/bearcat.dmm index caf4f1f4669..ad076063e6e 100644 --- a/maps/templates/shuttles/overmaps/generic/bearcat.dmm +++ b/maps/templates/shuttles/overmaps/generic/bearcat.dmm @@ -327,9 +327,7 @@ /obj/structure/cable{ icon_state = "0-8" }, -/obj/machinery/power/apc/alarms_hidden/east_mount{ - - }, +/obj/machinery/power/apc/alarms_hidden/east_mount, /turf/simulated/floor/wood, /area/shuttle/bearcat/command_captain) "aP" = ( diff --git a/maps/templates/shuttles/overmaps/generic/shelter_6.dmm b/maps/templates/shuttles/overmaps/generic/shelter_6.dmm index 64d83ca0efb..ce8bcdde150 100644 --- a/maps/templates/shuttles/overmaps/generic/shelter_6.dmm +++ b/maps/templates/shuttles/overmaps/generic/shelter_6.dmm @@ -1235,7 +1235,6 @@ /obj/item/point_redemption_voucher/preloaded/survey/c300, /obj/item/point_redemption_voucher/preloaded/survey/c300, /obj/item/point_redemption_voucher/preloaded/survey/c300, - /obj/item/survivalcapsule/military, /obj/item/survivalcapsule/military, /obj/item/survivalcapsule, diff --git a/tools/HubMigrator/HubMigrator.dm b/tools/HubMigrator/HubMigrator.dm deleted file mode 100644 index cd7f7e6020e..00000000000 --- a/tools/HubMigrator/HubMigrator.dm +++ /dev/null @@ -1,166 +0,0 @@ -//Misc Medal hub IDs -#define MEDAL_METEOR "Your Life Before Your Eyes" -#define MEDAL_PULSE "Jackpot" -#define MEDAL_TIMEWASTE "Overextended The Joke" -#define MEDAL_RODSUPLEX "Feat of Strength" -#define MEDAL_CLOWNCARKING "Round and Full" -#define MEDAL_THANKSALOT "The Best Driver" -#define MEDAL_HELBITALJANKEN "Hel-bent on Winning" -#define MEDAL_MATERIALCRAFT "Getting an Upgrade" - - -//Boss medals - -// Medal hub IDs for boss medals (Pre-fixes) -#define BOSS_MEDAL_ANY "Boss Killer" -#define BOSS_MEDAL_MINER "Blood-drunk Miner Killer" -#define BOSS_MEDAL_BUBBLEGUM "Bubblegum Killer" -#define BOSS_MEDAL_COLOSSUS "Colossus Killer" -#define BOSS_MEDAL_DRAKE "Drake Killer" -#define BOSS_MEDAL_HIEROPHANT "Hierophant Killer" -#define BOSS_MEDAL_LEGION "Legion Killer" -#define BOSS_MEDAL_TENDRIL "Tendril Exterminator" -#define BOSS_MEDAL_SWARMERS "Swarmer Beacon Killer" - -#define BOSS_MEDAL_MINER_CRUSHER "Blood-drunk Miner Crusher" -#define BOSS_MEDAL_BUBBLEGUM_CRUSHER "Bubblegum Crusher" -#define BOSS_MEDAL_COLOSSUS_CRUSHER "Colossus Crusher" -#define BOSS_MEDAL_DRAKE_CRUSHER "Drake Crusher" -#define BOSS_MEDAL_HIEROPHANT_CRUSHER "Hierophant Crusher" -#define BOSS_MEDAL_LEGION_CRUSHER "Legion Crusher" -#define BOSS_MEDAL_SWARMERS_CRUSHER "Swarmer Beacon Crusher" - -// Medal hub IDs for boss-kill scores -#define BOSS_SCORE "Bosses Killed" -#define MINER_SCORE "BDMs Killed" -#define BUBBLEGUM_SCORE "Bubblegum Killed" -#define COLOSSUS_SCORE "Colossus Killed" -#define DRAKE_SCORE "Drakes Killed" -#define HIEROPHANT_SCORE "Hierophants Killed" -#define LEGION_SCORE "Legion Killed" -#define SWARMER_BEACON_SCORE "Swarmer Beacs Killed" -#define TENDRIL_CLEAR_SCORE "Tendrils Killed" - - - -//Migration script generation -//Replace hub information and fire to generate hub_migration.sql script to use. -/mob/verb/generate_migration_script() - set name = "Generate Hub Migration Script" - - var/hub_address = "REPLACEME" - var/hub_password = "REPLACEME" - - var/list/valid_medals = list( - MEDAL_METEOR, - MEDAL_PULSE, - MEDAL_TIMEWASTE, - MEDAL_RODSUPLEX, - MEDAL_CLOWNCARKING, - MEDAL_THANKSALOT, - MEDAL_HELBITALJANKEN, - MEDAL_MATERIALCRAFT, - BOSS_MEDAL_ANY, - BOSS_MEDAL_MINER, - BOSS_MEDAL_BUBBLEGUM, - BOSS_MEDAL_COLOSSUS, - BOSS_MEDAL_DRAKE, - BOSS_MEDAL_HIEROPHANT, - BOSS_MEDAL_LEGION, - BOSS_MEDAL_TENDRIL, - BOSS_MEDAL_SWARMERS, - BOSS_MEDAL_MINER_CRUSHER, - BOSS_MEDAL_BUBBLEGUM_CRUSHER, - BOSS_MEDAL_COLOSSUS_CRUSHER, - BOSS_MEDAL_DRAKE_CRUSHER, - BOSS_MEDAL_HIEROPHANT_CRUSHER, - BOSS_MEDAL_LEGION_CRUSHER, - BOSS_MEDAL_SWARMERS_CRUSHER) - - var/list/valid_scores = list( - BOSS_SCORE, - MINER_SCORE, - BUBBLEGUM_SCORE, - COLOSSUS_SCORE, - DRAKE_SCORE, - HIEROPHANT_SCORE, - LEGION_SCORE, - SWARMER_BEACON_SCORE, - TENDRIL_CLEAR_SCORE) - - var/ach = "achievements" //IMPORTANT : ADD PREFIX HERE IF YOU'RE USING PREFIXED SCHEMA - - var/outfile = file("hub_migration.sql") - fdel(outfile) - outfile << "BEGIN;" - - var/perpage = 100 - var/requested_page = 1 - var/hub_url = replacetext(hub_address,".","/") - var/list/medal_data = list() - var/regex/datepart_regex = regex(@"[/\s]") - while(1) - world << "Fetching page [requested_page]" - var/list/result = world.Export("http://www.byond.com/games/[hub_url]?format=text&command=view_medals&per_page=[perpage]&page=[requested_page]") - if(!result) - return - var/data = file2text(result["CONTENT"]) - var/regex/page_info = regex(@"page = (\d*)") - page_info.Find(data) - var/recieved_page = text2num(page_info.group[1]) - if(recieved_page != requested_page) //out of entries - break - else - requested_page++ - var/regex/R = regex(@'medal/\d+[\s\n]*key = "(.*)"[\s\n]*name = "(.*)"[\s\n]*desc = ".*"[\s\n]*icon = ".*"[\s\n]*earned = "(.*)"',"gm") - while(R.Find(data)) - var/key = ckey(R.group[1]) - var/medal = R.group[2] - var/list/dateparts = splittext(R.group[3],datepart_regex) - var/list/out_date = list(dateparts[3],dateparts[1],dateparts[2]) // YYYY/MM/DD - if(!valid_medals.Find(medal)) - continue - if(!medal_data[key]) - medal_data[key] = list() - medal_data[key][medal] = out_date.Join("/") - - var/list/giant_list_of_ckeys = params2list(world.GetScores(null,null,hub_address,hub_password)) - world << "Found [giant_list_of_ckeys.len] as upper scores count." - - var/list/scores_data = list() - for(var/score in valid_scores) - var/recieved_count = 0 - while(1) - world << "Fetching [score] scores, offset :[recieved_count] of [score]" - var/list/batch = params2list(world.GetScores(giant_list_of_ckeys.len,recieved_count,score,hub_address,hub_password)) - world << "Fetched [batch.len] scores for [score]." - recieved_count += batch.len - if(!batch.len) - break - for(var/value in batch) - var/key = ckey(value) - if(!scores_data[key]) - scores_data[key] = list() - if(isnum(batch[value])) - world << "NUMBER" - return - scores_data[key][score] = batch[value] - if(batch.len < 1000) //Out of scores anyway - break - - var/i = 1 - for(var/key in giant_list_of_ckeys) - world << "Generating entries for [key] [i]/[giant_list_of_ckeys.len]" - var/keyv = ckey(key) //Checkinf if you don't have any manually entered drop tables; juniors on your hub is good idea. - var/list/values = list() - for(var/cheevo in medal_data[keyv]) - values += "('[keyv]','[cheevo]',1, '[medal_data[keyv][cheevo]]')" - for(var/score in scores_data[keyv]) - values += "('[keyv]','[score]',[scores_data[keyv][score]],now())" - if(values.len) - var/list/keyline = list("INSERT INTO [ach](ckey,achievement_key,value,last_updated) VALUES") - keyline += values.Join(",") - keyline += ";" - outfile << keyline.Join() - i++ - outfile << "END" diff --git a/tools/HubMigrator/HubMigrator.dme b/tools/HubMigrator/HubMigrator.dme deleted file mode 100644 index 37cfada68c1..00000000000 --- a/tools/HubMigrator/HubMigrator.dme +++ /dev/null @@ -1,19 +0,0 @@ -// DM Environment file for HubMigrator.dme. -// All manual changes should be made outside the BEGIN_ and END_ blocks. -// New source code should be placed in .dm files: choose File/New --> Code File. - -// BEGIN_INTERNALS -// END_INTERNALS - -// BEGIN_FILE_DIR -#define FILE_DIR . -// END_FILE_DIR - -// BEGIN_PREFERENCES -#define DEBUG -// END_PREFERENCES - -// BEGIN_INCLUDE -#include "HubMigrator.dm" -// END_INCLUDE - diff --git a/tools/Runtime Condenser/Main.cpp b/tools/Runtime Condenser/Main.cpp index bc3158a407c..6985008366f 100644 --- a/tools/Runtime Condenser/Main.cpp +++ b/tools/Runtime Condenser/Main.cpp @@ -5,7 +5,7 @@ * also be caught and displayed (if any) above the list of runtimes. * * How to use: - * 1) Copy and paste your list of runtimes from Dream Daemon into input.exe + * 1) Copy and paste your list of runtimes from Dream Daemon into Input.txt * 2) Run RuntimeCondenser.exe * 3) Open output.txt for a condensed report of the runtimes * @@ -34,8 +34,8 @@ #define PROGRESS_FPS 10 #define PROGRESS_BAR_INNER_WIDTH 50 -///32KiB -#define LINEBUFFER (32*1024) +#define LINEBUFFER (32*1024) //32KiB + using namespace std; struct runtime { @@ -131,6 +131,10 @@ inline void forward_progress(FILE * inputFile) { else if (nextLine->length() >= 26 && ((*nextLine)[0] == '[' && (*nextLine)[5] == '-' && (*nextLine)[14] == ':' && (*nextLine)[20] == '.' && (*nextLine)[24] == ']')) nextLine->erase(0, 26); } + //strip out log cats + if (nextLine->length() >= 9 && safe_substr(nextLine, 0, 9) == "RUNTIME: ") { + nextLine->erase(0, 9); + } } while (!endofbuffer && nextLine->length() < 1); } diff --git a/tools/UpdatePaths/__main__.py b/tools/UpdatePaths/__main__.py index a8bf8263e9b..b930b940c68 100644 --- a/tools/UpdatePaths/__main__.py +++ b/tools/UpdatePaths/__main__.py @@ -13,6 +13,7 @@ Replacement syntax example: /turf/open/floor/iron/warningline : /obj/effect/turf_decal {@OLD} , /obj/thing {icon_state = @OLD:name; name = "meme"} /turf/open/floor/iron/warningline{dir=2} : /obj/thing /obj/effect/landmark/start/virologist : @DELETE + /mob/living{resize = @ANY} : /mob/living{@OLD; resize = @SKIP} Syntax for subtypes also exist, to update a path's type but maintain subtypes: /obj/structure/closet/crate/@SUBTYPES : /obj/structure/new_box/@SUBTYPES {@OLD} New paths properties: @@ -25,6 +26,7 @@ New paths properties: Old paths properties: Will be used as a filter. property = @UNSET - will apply the rule only if the property is not mapedited + property = @ANY - will apply the rule when the property is mapedited, regardless of its value. """ default_map_directory = "../../maps" @@ -91,7 +93,9 @@ def update_path(dmm_data, replacement_string, verbose=False): else: return [match.group(0)] else: - if old_props[filter_prop] != old_path_props[filter_prop] or old_path_props[filter_prop] == "@UNSET": + if old_path_props[filter_prop] == "@ANY": + continue + elif old_props[filter_prop] != old_path_props[filter_prop] or old_path_props[filter_prop] == "@UNSET": return [match.group(0)] #does not match current filter, skip the change. if verbose: print("Found match : {0}".format(match.group(0))) @@ -110,7 +114,8 @@ def update_path(dmm_data, replacement_string, verbose=False): out = new_path out_props = dict() - for prop_name, prop_value in new_props.items(): + for prop_name, prop_text in new_props.items(): + prop_value = str(prop_text) if prop_name == "@OLD": out_props = dict(old_props) continue @@ -119,8 +124,7 @@ def update_path(dmm_data, replacement_string, verbose=False): continue if prop_value.startswith("@OLD"): params = prop_value.split(":") - if prop_name in old_props: - out_props[prop_name] = old_props[params[1]] if len(params) > 1 else old_props[prop_name] + out_props[prop_name] = old_props[params[1]] if len(params) > 1 else old_props[prop_name] continue out_props[prop_name] = prop_value if out_props: @@ -173,9 +177,12 @@ def main(args): print("Using replacement:", args.update_source) updates = [args.update_source] else: - with open(args.update_source) as f: - updates = [line for line in f if line and not line.startswith("#") and not line.isspace()] - print(f"Using {len(updates)} replacements from file:", args.update_source) + updates = [] + for source in args.update_source: + with open(source) as f: + updates_from_file = [line for line in f if line and not line.startswith("#") and not line.isspace()] + print(f"Using {len(updates_from_file)} replacements from file:", source) + updates.extend(updates_from_file) if args.map: update_map(args.map, updates, verbose=args.verbose) @@ -187,12 +194,12 @@ def main(args): if __name__ == "__main__": prog = __spec__.name.replace('.__main__', '') if os.name == 'nt' and len(sys.argv) <= 1: - print("usage: drag-and-drop a path script .txt onto `Update Paths.bat`\n or") + print("usage: drag-and-drop one or more .txt path script onto `Update Paths.bat`\n or") parser = argparse.ArgumentParser(prog=prog, description=desc, formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument("update_source", help="update file path / line of update notation") + parser.add_argument("update_source", nargs="+", help="update file path(s) / line of update notation") parser.add_argument("--map", "-m", help="path to update, defaults to all maps in maps directory") - parser.add_argument("--directory", "-d", help="path to maps directory, defaults to maps/") + parser.add_argument("--directory", "-d", help="path to maps directory, defaults to _maps/") parser.add_argument("--inline", "-i", help="treat update source as update string instead of path", action="store_true") parser.add_argument("--verbose", "-v", help="toggle detailed update information", action="store_true") main(parser.parse_args()) diff --git a/tools/UpdatePaths/readme.md b/tools/UpdatePaths/readme.md index 7789efa99e9..23f2ce6976d 100644 --- a/tools/UpdatePaths/readme.md +++ b/tools/UpdatePaths/readme.md @@ -1,12 +1,12 @@ # UpdatePaths -## How To Use +## How To Use: Drag one of the scripts in the “Scripts” folder onto the .bat file “Update Paths” to open it with the `.bat` file (or use the Python script directly depending on your operating system). Let the script run to completion. Use this tool before using MapMerge2 or opening the map in an map editor. This is because the map editor may discard any unknown paths not found in the /tg/station environment (or what it builds after parsing `tgstation.dme`). -## Scriptmaking +## Scriptmaking: This tool updates paths in the game to new paths. For instance: @@ -89,7 +89,6 @@ On this example map key: You will then result the following: - ```dm "a" = ( /obj/structure/door/airlock/science/closed/rd, @@ -141,6 +140,7 @@ UpdatePaths has the powerful ability to output multiple paths from a single inpu ```txt /turf/open/floor/iron/i_like_spawning_mobs : /obj/mob_spawner, /turf/open/floor/iron ``` + So, now when you have the following example map keys: ```dm @@ -157,6 +157,7 @@ Running the script will mutate this into: /turf/open/floor/iron, /area/station/kitchen), ``` + Remember that this is a kind of silly example, but this is one of the things that UpdatePaths was built to do- help coders fix shitty code without having to bug out over how maps don't compile. ### Subtype Handling @@ -199,7 +200,6 @@ Running the script will update this into: Note how since you kept in `{@OLD}`, it was able to retain the re-named variables of the subtypes. - ### Old Path Variable Filtering Alright, there's a few subsections here. This is how you are able to filter out old paths to ensure you target something precise. Let's just go through them one by one. @@ -299,7 +299,7 @@ You would then get the following output: As you would have wished, only the `pixel_x` variable copied through. This is pretty constraining and might not match up to certain needs of the repository (or other repositories), so recommend using the [first example](#method-open-mind-to-all-possibilities) when possible. -#### Method: Keep All The Soul +#### Method: Keep All The Soul! Okay, let's say that you want to change all instances of `/obj/structure/sink` that have `dir=2` to `dir=1` for a laugh. However, there's an issue. You see, 2 is SOUTH in DM directions, (1 is NORTH), and code-side, `/obj/structure/sink` has `dir = 2` by default and doesn't show up in the map editor. You would have to do something like this: @@ -343,6 +343,60 @@ You would then get the following output: Note how we keep the "Money Hole" intact, while still managing to extrapolate the `dir` variable to 1 on the sink that had absolutely no variables set on it. This is useful for when you want to change a variable that is not shown in the map editor, but you want to keep the rest of the variables intact. +#### Methods: Any Value Fits All and Naming Conventions + +But what if you just want to rename the variable `maxHealth` to `good_boy_points` for all instances of `/mob/living/github_user`? Using the `@ANY` parameter after a variable name, you can capture any instance that has it edited in a map. While, to set the value of the newly named `good_boy_points` to that of the old `maxHealth`, we can use `@OLD:maxHealth`, put after the name of the new variable to achieve that. The result'll be something like this: + +```txt +/mob/living/github_user{maxHealth=@ANY} : /mob/living/github_user{good_boy_points=@OLD:maxHealth} +``` + +Though, If you read about the previous methods, you'd know that without the `@OLD` parameter (the one without colon), every other variable edit will also be discarded, so it's important to add that BEFORE any other parament, as well as `maxHealth=@SKIP` following that since we're renaming that variable. So, take two: + +```txt +/mob/living/github_user{maxHealth=@ANY} : /mob/living/github_user{@OLD; maxHealth=@SKIP; good_boy_points=@OLD:maxHealth} +``` + +Perfect, so now let's assume the following map: + +```dm +"a" = ( +/mob/living/basic/mouse{ + maxHealth = 15 + }, +/turf/open/floor/iron, +/area/github), +"b" = ( +/mob/living/github_user{ + name = "ShizCalev"; + desc= "Has more good boy points than a megafauna has health."; + maxHealth = 2083 + }, +/turf/open/floor/iron, +/area/github), +``` + +You would then get the following output: + +```dm +"a" = ( +/mob/living/basic/mouse{ + maxHealth = 15 + }, +/turf/open/floor/iron, +/area/github), +"b" = ( +/mob/living/github_user{ + name = "ShizCalev"; + desc= "Has more good boy points than a megafauna has health."; + good_boy_points = 2083 + }, +/turf/open/floor/iron, +/area/github), +``` + +As an addendum, you don't have to use both `@ANY` and `@OLD:prop_name` together. I'm merely providing a single example for the both of them and their most practical usage. + ### Blend it all together All of the examples provided within are not mutually exclusive! They can be mixed-and-matched in several ways (old scripts might have a few good examples of these), and the only limit here is your imagination. You can do some very powerful things with UpdatePaths, with your scripts lasting for years to come. diff --git a/tools/UpdatePaths/scripts/old/apc_pixel_offsets.txt b/tools/UpdatePaths/scripts/old/apc_pixel_offsets.txt deleted file mode 100644 index 30b5ba2d5d9..00000000000 --- a/tools/UpdatePaths/scripts/old/apc_pixel_offsets.txt +++ /dev/null @@ -1,4 +0,0 @@ -/obj/machinery/power/apc/@SUBTYPES{dir = 1} : @OLD{@OLD; pixel_y = 23} -/obj/machinery/power/apc/@SUBTYPES{dir = 2} : @OLD{@OLD; pixel_y = -23} -/obj/machinery/power/apc/@SUBTYPES{dir = 4} : @OLD{@OLD; pixel_x = 24} -/obj/machinery/power/apc/@SUBTYPES{dir = 8} : @OLD{@OLD; pixel_x = -25} diff --git a/tools/UpdatePaths/scripts/old/clothingunderrepath.txt b/tools/UpdatePaths/scripts/old/clothingunderrepath.txt deleted file mode 100644 index e69062586bc..00000000000 --- a/tools/UpdatePaths/scripts/old/clothingunderrepath.txt +++ /dev/null @@ -1,197 +0,0 @@ -/obj/item/clothing/under/rank/vice : /obj/item/clothing/under/misc/vice_officer -/obj/item/clothing/under/durathread : /obj/item/cloning/under/misc/durathread -/obj/item/clothing/under/burial : /obj/item/clothing/under/misc/burial -/obj/item/clothing/under/overalls : /obj/item/clothing/under/misc/overalls -/obj/item/clothing/under/assistantformal : /obj/item/clothing/under/misc/assistantformal -/obj/item/clothing/under/pj/red : /obj/item/clothing/under/misc/pj -/obj/item/clothing/under/pj/blue : /obj/item/clothing/under/misc/pj/blue -/obj/item/clothing/under/patriotsuit : /obj/item/clothing/under/misc/patriotsuit -/obj/item/clothing/under/rank/mailman : /obj/item/clothing/under/misc/mailman -/obj/item/clothing/under/rank/psyche : /obj/item/clothing/under/misc/psyche -/obj/item/clothing/under/acj : /obj/item/clothing/under/misc/adminsuit - -/obj/item/clothing/under/scratch : /obj/item/clothing/under/suit/white_on_white -/obj/item/clothing/under/scratch/skirt : /obj/item/clothing/under/suit/white/skirt -/obj/item/clothing/under/sl_suit : /obj/item/clothing/under/suit/sl -/obj/item/clothing/under/waiter : /obj/item/clothing/under/suit/waiter -/obj/item/clothing/under/suit_jacket : /obj/item/clothing/under/suit/black -/obj/item/clothing/under/suit_jacket/female : /obj/item/clothing/under/suit/black/skirt -/obj/item/clothing/under/suit_jacket/really_black : /obj/item/clothing/under/suit/black_really -/obj/item/clothing/under/suit_jacket/green : /obj/item/clothing/under/suit/green -/obj/item/clothing/under/suit_jacket/red : /obj/item/clothing/under/suit/red -/obj/item/clothing/under/suit_jacket/charcoal : /obj/item/clothing/under/suit/charcoal -/obj/item/clothing/under/suit_jacket/navy : /obj/item/clothing/under/suit/navy -/obj/item/clothing/under/suit_jacket/burgundy : /obj/item/clothing/under/suit/burgundy -/obj/item/clothing/under/suit_jacket/checkered : /obj/item/clothing/under/suit/checkered -/obj/item/clothing/under/suit_jacket/tan : /obj/item/clothing/under/suit/tan -/obj/item/clothing/under/suit_jacket/white : /obj/item/clothing/under/suit/white - -/obj/item/clothing/under/skirt/black : /obj/item/clothing/under/dress/skirt -/obj/item/clothing/under/skirt/blue : /obj/item/clothing/under/dress/skirt/blue -/obj/item/clothing/under/skirt/red : /obj/item/clothing/under/dress/skirt/red -/obj/item/clothing/under/skirt/purple : /obj/item/clothing/under/dress/skirt/purple -/obj/item/clothing/under/sundress : /obj/item/clothing/under/dress/sundress -/obj/item/clothing/under/blacktango : /obj/item/clothing/under/dress/blacktango -/obj/item/clothing/under/stripeddress : /obj/item/clothing/under/dress/striped -/obj/item/clothing/under/sailordress : /obj/item/clothing/under/dress/sailor -/obj/item/clothing/under/redeveninggown : /obj/item/clothing/under/dress/redeveninggown -/obj/item/clothing/under/plaid_skirt : /obj/item/clothing/under/dress/skirt/plaid -/obj/item/clothing/under/plaid_skirt/blue : /obj/item/clothing/under/dress/skirt/plaid/blue -/obj/item/clothing/under/plaid_skirt/purple : /obj/item/clothing/under/dress/skirt/plaid/purple -/obj/item/clothing/under/plaid_skirt/green : /obj/item/clothing/under/dress/skirt/plaid/green - -/obj/item/clothing/under/roman : /obj/item/clothing/under/costume/roman -/obj/item/clothing/under/jabroni : /obj/item/clothing/under/costume/jabroni -/obj/item/clothing/under/owl : /obj/item/clothing/under/costume/owl -/obj/item/clothing/under/griffin : /obj/item/clothing/under/costume/griffin -/obj/item/clothing/under/schoolgirl : /obj/item/clothing/under/costume/schoolgirl -/obj/item/clothing/under/schoolgirl/red : /obj/item/clothing/under/costume/schoolgirl/red -/obj/item/clothing/under/schoolgirl/green : /obj/item/clothing/under/costume/schoolgirl/green -/obj/item/clothing/under/schoolgirl/orange : /obj/item/clothing/under/costume/schoolgirl/orange -/obj/item/clothing/under/pirate : /obj/item/clothing/under/costume/pirate -/obj/item/clothing/under/soviet : /obj/item/clothing/under/costume/soviet -/obj/item/clothing/under/redcoat : /obj/item/clothing/under/costume/redcoat -/obj/item/clothing/under/kilt : /obj/item/clothing/under/costume/kilt -/obj/item/clothing/under/kilt/highlander : /obj/item/clothing/under/costume/kilt/highlander -/obj/item/clothing/under/gladiator : /obj/item/clothing/under/costume/gladiator -/obj/item/clothing/under/gladiator/ash_walker : /obj/item/clothing/under/costume/gladiator/ash_walker -/obj/item/clothing/under/maid : /obj/item/clothing/under/costume/maid -/obj/item/clothing/under/singery : /obj/item/clothing/under/costume/singer/yellow -/obj/item/clothing/under/singerb : /obj/item/clothing/under/costume/singer/blue -/obj/item/clothing/under/geisha : /obj/item/clothing/under/costume/geisha -/obj/item/clothing/under/villain : /obj/item/clothing/under/costume/villain -/obj/item/clothing/under/sailor : /obj/item/clothing/under/costume/sailor -/obj/item/clothing/under/rank/security/navyblue/russian : /obj/item/clothing/under/costume/russian_officer -/obj/item/clothing/under/mummy : /obj/item/clothing/under/costume/mummy -/obj/item/clothing/under/scarecrow : /obj/item/clothing/under/costume/scarecrow -/obj/item/clothing/under/draculass : /obj/item/clothing/under/costume/draculass -/obj/item/clothing/under/drfreeze : /obj/item/clothing/under/costume/drfreeze -/obj/item/clothing/under/lobster : /obj/item/clothing/under/costume/lobster -/obj/item/clothing/under/skeleton : /obj/item/clothing/under/costume/skeleton -/obj/item/clothing/under/mech_suit : /obj/item/clothing/under/costume/mech_suit -/obj/item/clothing/under/mech_suit/white : /obj/item/clothing/under/costume/mech_suit/white -/obj/item/clothing/under/mech_suit/blue : /obj/item/clothing/under/costume/mech_suit/blue -/obj/item/clothing/under/gondola : /obj/item/clothing/under/costume/gondola - -/obj/item/clothing/under/rank/bartender : /obj/item/clothing/under/rank/civilian/bartender -/obj/item/clothing/under/rank/bartender/purple : /obj/item/clothing/under/rank/civilian/bartender/purple -/obj/item/clothing/under/rank/bartender/skirt : /obj/item/clothing/under/rank/civilian/bartender/skirt -/obj/item/clothing/under/rank/chaplain : /obj/item/clothing/under/rank/civilian/chaplain -/obj/item/clothing/under/rank/chaplain/skirt : /obj/item/clothing/under/rank/civilian/chaplain/skirt -/obj/item/clothing/under/rank/chef : /obj/item/clothing/under/rank/civilian/chef -/obj/item/clothing/under/rank/chef/skirt : /obj/item/clothing/under/rank/civilian/chef/skirt -/obj/item/clothing/under/rank/curator : /obj/item/clothing/under/rank/civilian/curator -/obj/item/clothing/under/rank/curator/skirt: /obj/item/clothing/under/rank/civilian/curator/skirt -/obj/item/clothing/under/rank/curator/treasure_hunter : /obj/item/clothing/under/rank/civilian/curator/treasure_hunter -/obj/item/clothing/under/space : /obj/item/clothing/under/rank/civilian/curator/nasa -/obj/item/clothing/under/rank/hydroponics : /obj/item/clothing/under/rank/civilian/hydroponics -/obj/item/clothing/under/rank/hydroponics/skirt : /obj/item/clothing/under/rank/civilian/hydroponics/skirt -/obj/item/clothing/under/rank/janitor : /obj/item/clothing/under/rank/civilian/janitor -/obj/item/clothing/under/rank/janitor/skirt : /obj/item/clothing/under/rank/civilian/janitor/skirt -/obj/item/clothing/under/janimaid : /obj/item/clothing/under/rank/civilian/janitor/maid -/obj/item/clothing/under/rank/mime : /obj/item/clothing/under/rank/civilian/mime -/obj/item/clothing/under/rank/mime/skirt : /obj/item/clothing/under/rank/civilian/mime/skirt -/obj/item/clothing/under/sexymime : /obj/item/clothing/under/rank/civilian/mime/sexy -/obj/item/clothing/under/rank/clown : /obj/item/clothing/under/rank/civilian/clown -/obj/item/clothing/under/rank/blueclown : /obj/item/clothing/under/rank/civilian/clown/blue -/obj/item/clothing/under/rank/greenclown : /obj/item/clothing/under/rank/civilian/clown/green -/obj/item/clothing/under/rank/yellowclown : /obj/item/clothing/under/rank/civilian/clown/yellow -/obj/item/clothing/under/rank/purpleclown : /obj/item/clothing/under/rank/civilian/clown/purple -/obj/item/clothing/under/rank/orangeclown : /obj/item/clothing/under/rank/civilian/clown/orange -/obj/item/clothing/under/rank/rainbowclown : /obj/item/clothing/under/rank/civilian/clown/rainbow -/obj/item/clothing/under/jester : /obj/item/clothing/under/rank/civilian/clown/jester -/obj/item/clothing/under/jester/alt : /obj/item/clothing/under/rank/civilian/clown/jester/alt -/obj/item/clothing/under/rank/clown/sexy : /obj/item/clothing/under/rank/civilian/clown/sexy -/obj/item/clothing/under/lawyer : /obj/item/clothing/under/rank/civilian/lawyer -/obj/item/clothing/under/lawyer/black : /obj/item/clothing/under/rank/civilian/lawyer/black -/obj/item/clothing/under/lawyer/black/skirt : /obj/item/clothing/under/rank/civilian/lawyer/black/skirt -/obj/item/clothing/under/lawyer/female : /obj/item/clothing/under/rank/civilian/lawyer/female -/obj/item/clothing/under/lawyer/female/skirt : /obj/item/clothing/under/rank/civilian/lawyer/female/skirt -/obj/item/clothing/under/lawyer/red : /obj/item/clothing/under/rank/civilian/lawyer/red -/obj/item/clothing/under/lawyer/red/skirt : /obj/item/clothing/under/rank/civilian/lawyer/red/skirt -/obj/item/clothing/under/lawyer/blue : /obj/item/clothing/under/rank/civilian/lawyer/blue -/obj/item/clothing/under/lawyer/blue/skirt : /obj/item/clothing/under/rank/civilian/lawyer/blue/skirt -/obj/item/clothing/under/lawyer/bluesuit : /obj/item/clothing/under/rank/civilian/lawyer/bluesuit -/obj/item/clothing/under/lawyer/bluesuit/skirt : /obj/item/clothing/under/rank/civilian/lawyer/bluesuit/skirt -/obj/item/clothing/under/lawyer/purpsuit : /obj/item/clothing/under/rank/civilian/lawyer/purpsuit -/obj/item/clothing/under/lawyer/purpsuit/skirt : /obj/item/clothing/under/rank/civilian/lawyer/purpsuit/skirt -/obj/item/clothing/under/lawyer/blacksuit : /obj/item/clothing/under/suit/black -/obj/item/clothing/under/lawyer/blacksuit/skirt : /obj/item/clothing/under/suit/black/skirt -/obj/item/clothing/under/lawyer/really_black : /obj/item/clothing/under/suit/black_really -/obj/item/clothing/under/lawyer/really_black/skirt : /obj/item/clothing/under/suit/black_really/skirt -/obj/item/clothing/under/rank/head_of_personnel : /obj/item/clothing/under/rank/civilian/head_of_personnel -/obj/item/clothing/under/rank/head_of_personnel/skirt : /obj/item/clothing/under/rank/civilian/head_of_personnel/skirt -/obj/item/clothing/under/gimmick/rank/head_of_personnel/suit : /obj/item/clothing/under/rank/civilian/head_of_personnel/suit -/obj/item/clothing/under/gimmick/rank/head_of_personnel/suit/skirt : /obj/item/clothing/under/rank/civilian/head_of_personnel/suit/skirt - -/obj/item/clothing/under/rank/cargo : /obj/item/clothing/under/rank/cargo/qm -/obj/item/clothing/under/rank/cargo/skirt : /obj/item/clothing/under/rank/cargo/qm/skirt -/obj/item/clothing/under/rank/cargotech : /obj/item/clothing/under/rank/cargo/tech -/obj/item/clothing/under/rank/cargotech/skirt : /obj/item/clothing/under/rank/cargo/tech/skirt -/obj/item/clothing/under/rank/miner : /obj/item/clothing/under/rank/cargo/miner -/obj/item/clothing/under/rank/miner/lavaland : /obj/item/clothing/under/rank/cargo/miner/lavaland - -/obj/item/clothing/under/rank/research_director : /obj/item/clothing/under/rank/rnd/research_director -/obj/item/clothing/under/rank/research_director/skirt : /obj/item/clothing/under/rank/rnd/research_director/skirt -/obj/item/clothing/under/rank/research_director/alt : /obj/item/clothing/under/rank/rnd/research_director/alt -/obj/item/clothing/under/rank/research_director/alt/skirt : /obj/item/clothing/under/rank/rnd/research_director/alt/skirt -/obj/item/clothing/under/rank/research_director/turtleneck : /obj/item/clothing/under/rank/rnd/research_director/turtleneck -/obj/item/clothing/under/rank/research_director/turtleneck/skirt : /obj/item/clothing/under/rank/rnd/research_director/turtleneck/skirt -/obj/item/clothing/under/rank/scientist : /obj/item/clothing/under/rank/rnd/scientist -/obj/item/clothing/under/rank/scientist/skirt : /obj/item/clothing/under/rank/rnd/scientist/skirt -/obj/item/clothing/under/rank/roboticist : /obj/item/clothing/under/rank/rnd/roboticist -/obj/item/clothing/under/rank/roboticist/skirt : /obj/item/clothing/under/rank/rnd/roboticist/skirt - -/obj/item/clothing/under/rank/chief_medical_officer : /obj/item/clothing/under/rank/medical/chief_medical_officer -/obj/item/clothing/under/rank/chief_medical_officer/skirt : /obj/item/clothing/under/rank/medical/chief_medical_officer/skirt -/obj/item/clothing/under/rank/medical : /obj/item/clothing/under/rank/medical/doctor -/obj/item/clothing/under/rank/medical/blue : /obj/item/clothing/under/rank/medical/doctor/blue -/obj/item/clothing/under/rank/medical/green : /obj/item/clothing/under/rank/medical/doctor/green -/obj/item/clothing/under/rank/medical/purple : /obj/item/clothing/under/rank/medical/doctor/purple -/obj/item/clothing/under/rank/medical/skirt : /obj/item/clothing/under/rank/medical/doctor/skirt -/obj/item/clothing/under/rank/nursesuit : /obj/item/clothing/under/rank/medical/doctor/nurse -/obj/item/clothing/under/rank/geneticist : /obj/item/clothing/under/rank/rnd/geneticist -/obj/item/clothing/under/rank/geneticist/skirt : /obj/item/clothing/under/rank/rnd/geneticist/skirt -/obj/item/clothing/under/rank/virologist : /obj/item/clothing/under/rank/medical/virologist -/obj/item/clothing/under/rank/virologist/skirt : /obj/item/clothing/under/rank/medical/virologist/skirt -/obj/item/clothing/under/rank/chemist : /obj/item/clothing/under/rank/medical/chemist -/obj/item/clothing/under/rank/chemist/skirt : /obj/item/clothing/under/rank/medical/chemist/skirt - -/obj/item/clothing/under/rank/chief_engineer : /obj/item/clothing/under/rank/engineering/chief_engineer -/obj/item/clothing/under/rank/chief_engineer/skirt : /obj/item/clothing/under/rank/engineering/chief_engineer/skirt -/obj/item/clothing/under/rank/atmospheric_technician : /obj/item/clothing/under/rank/engineering/atmospheric_technician -/obj/item/clothing/under/rank/atmospheric_technician/skirt : /obj/item/clothing/under/rank/engineering/atmospheric_technician/skirt -/obj/item/clothing/under/rank/engineer : /obj/item/clothing/under/rank/engineering/engineer -/obj/item/clothing/under/rank/engineer/hazard : /obj/item/clothing/under/rank/engineering/engineer/hazard -/obj/item/clothing/under/rank/engineer/skirt : /obj/item/clothing/under/rank/engineering/engineer/skirt - -/obj/item/clothing/under/rank/centcom_officer : /obj/item/clothing/under/rank/centcom/officer -/obj/item/clothing/under/rank/centcom_commander : /obj/item/clothing/under/rank/centcom/commander - -/obj/item/clothing/under/gimmick/rank/captain/suit : /obj/item/clothing/under/rank/captain/suit -/obj/item/clothing/under/gimmick/rank/captain/suit/skirt : /obj/item/clothing/under/rank/captain/suit/skirt -/obj/item/clothing/under/captainparade : /obj/item/clothing/under/rank/captain/parade - -/obj/item/clothing/under/rank/security : /obj/item/clothing/under/rank/security/officer -/obj/item/clothing/under/rank/security/grey : /obj/item/clothing/under/rank/security/officer/grey -/obj/item/clothing/under/rank/security/skirt : /obj/item/clothing/under/rank/security/officer/skirt -/obj/item/clothing/under/rank/security/blueshirt : /obj/item/clothing/under/rank/security/officer/blueshirt -/obj/item/clothing/under/rank/security/navyblue : /obj/item/clothing/under/rank/security/officer/formal -/obj/item/clothing/under/rank/warden : /obj/item/clothing/under/rank/security/warden -/obj/item/clothing/under/rank/warden/grey : /obj/item/clothing/under/rank/security/warden/grey -/obj/item/clothing/under/rank/warden/skirt : /obj/item/clothing/under/rank/security/warden/skirt -/obj/item/clothing/under/rank/warden/navyblue : /obj/item/clothing/under/rank/security/warden/formal -/obj/item/clothing/under/rank/det : /obj/item/clothing/under/rank/security/detective -/obj/item/clothing/under/rank/det/skirt : /obj/item/clothing/under/rank/security/detective/skirt -/obj/item/clothing/under/rank/det/grey : /obj/item/clothing/under/rank/security/detective/grey -/obj/item/clothing/under/rank/det/grey/skirt : /obj/item/clothing/under/rank/security/detective/grey/skirt -/obj/item/clothing/under/rank/head_of_security : /obj/item/clothing/under/rank/security/head_of_security -/obj/item/clothing/under/rank/head_of_security/skirt : /obj/item/clothing/under/rank/security/head_of_security/skirt -/obj/item/clothing/under/rank/head_of_security/grey : /obj/item/clothing/under/rank/security/head_of_security/grey -/obj/item/clothing/under/rank/head_of_security/alt : /obj/item/clothing/under/rank/security/head_of_security/alt -/obj/item/clothing/under/rank/head_of_security/alt/skirt : /obj/item/clothing/under/rank/security/head_of_security/alt/skirt -/obj/item/clothing/under/rank/head_of_security/navyblue : /obj/item/clothing/under/rank/security/head_of_security/formal -/obj/item/clothing/under/hosparademale : /obj/item/clothing/under/rank/security/head_of_security/parade -/obj/item/clothing/under/hosparadefem : /obj/item/clothing/under/rank/security/head_of_security/parade/female -/obj/item/clothing/under/rank/security/spacepol : /obj/item/clothing/under/rank/security/officer/spacepol - diff --git a/tools/UpdatePaths/scripts/old/cornersfix.txt b/tools/UpdatePaths/scripts/old/cornersfix.txt deleted file mode 100644 index d9ff8a20232..00000000000 --- a/tools/UpdatePaths/scripts/old/cornersfix.txt +++ /dev/null @@ -1,30 +0,0 @@ -/obj/effect/turf_decal/stripes/corner {dir=1} : /obj/item/bear_armor {@OLD;dir=8} -/obj/effect/turf_decal/stripes/corner {dir=2} : /obj/effect/turf_decal/stripes/corner {@OLD;dir=@SKIP} -/obj/effect/turf_decal/stripes/corner {dir=4} : /obj/effect/turf_decal/stripes/corner {@OLD;dir=1} -/obj/effect/turf_decal/stripes/corner {dir=8} : /obj/effect/turf_decal/stripes/corner {@OLD;dir=4} -/obj/item/bear_armor {dir=8} : /obj/effect/turf_decal/stripes/corner {@OLD} -/obj/effect/turf_decal/stripes/red/corner {dir=1} : /obj/item/bear_armor {@OLD;dir=8} -/obj/effect/turf_decal/stripes/red/corner {dir=2} : /obj/effect/turf_decal/stripes/red/corner {@OLD;dir=@SKIP} -/obj/effect/turf_decal/stripes/red/corner {dir=4} : /obj/effect/turf_decal/stripes/red/corner {@OLD;dir=1} -/obj/effect/turf_decal/stripes/red/corner {dir=8} : /obj/effect/turf_decal/stripes/red/corner {@OLD;dir=4} -/obj/item/bear_armor {dir=8} : /obj/effect/turf_decal/stripes/red/corner {@OLD} -/obj/effect/turf_decal/stripes/white/corner {dir=1} : /obj/item/bear_armor {@OLD;dir=8} -/obj/effect/turf_decal/stripes/white/corner {dir=2} : /obj/effect/turf_decal/stripes/white/corner {@OLD;dir=@SKIP} -/obj/effect/turf_decal/stripes/white/corner {dir=4} : /obj/effect/turf_decal/stripes/white/corner {@OLD;dir=1} -/obj/effect/turf_decal/stripes/white/corner {dir=8} : /obj/effect/turf_decal/stripes/white/corner {@OLD;dir=4} -/obj/item/bear_armor {dir=8} : /obj/effect/turf_decal/stripes/white/corner {@OLD} -/obj/effect/turf_decal/box/corners {dir=1} : /obj/item/bear_armor {@OLD;dir=8} -/obj/effect/turf_decal/box/corners {dir=2} : /obj/effect/turf_decal/box/corners {@OLD;dir=@SKIP} -/obj/effect/turf_decal/box/corners {dir=4} : /obj/effect/turf_decal/box/corners {@OLD;dir=1} -/obj/effect/turf_decal/box/corners {dir=8} : /obj/effect/turf_decal/box/corners {@OLD;dir=4} -/obj/item/bear_armor {dir=8} : /obj/effect/turf_decal/box/corners {@OLD} -/obj/effect/turf_decal/box/red/corners {dir=1} : /obj/item/bear_armor {@OLD;dir=8} -/obj/effect/turf_decal/box/red/corners {dir=2} : /obj/effect/turf_decal/box/red/corners {@OLD;dir=@SKIP} -/obj/effect/turf_decal/box/red/corners {dir=4} : /obj/effect/turf_decal/box/red/corners {@OLD;dir=1} -/obj/effect/turf_decal/box/red/corners {dir=8} : /obj/effect/turf_decal/box/red/corners {@OLD;dir=4} -/obj/item/bear_armor {dir=8} : /obj/effect/turf_decal/box/red/corners {@OLD} -/obj/effect/turf_decal/box/white/corners {dir=1} : /obj/item/bear_armor {@OLD;dir=8} -/obj/effect/turf_decal/box/white/corners {dir=2} : /obj/effect/turf_decal/box/white/corners {@OLD;dir=@SKIP} -/obj/effect/turf_decal/box/white/corners {dir=4} : /obj/effect/turf_decal/box/white/corners {@OLD;dir=1} -/obj/effect/turf_decal/box/white/corners {dir=8} : /obj/effect/turf_decal/box/white/corners {@OLD;dir=4} -/obj/item/bear_armor {dir=8} : /obj/effect/turf_decal/box/white/corners {@OLD} \ No newline at end of file diff --git a/tools/UpdatePaths/scripts/old/turfs2decals.txt b/tools/UpdatePaths/scripts/old/turfs2decals.txt deleted file mode 100644 index 76b968b60eb..00000000000 --- a/tools/UpdatePaths/scripts/old/turfs2decals.txt +++ /dev/null @@ -1,467 +0,0 @@ -/turf/open/floor/circuit/killroom : /turf/open/floor/circuit/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/airless/asteroid : /obj/effect/turf_decal/sand , /turf/open/floor/plasteel/airless {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/airless/bar : /obj/effect/turf_decal/tile/bar , /obj/effect/turf_decal/tile/bar {dir=1} , /turf/open/floor/plasteel/airless {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/arrival {dir=1} : /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/white/corner {@OLD;dir=1} -/turf/open/floor/plasteel/arrival {dir=@UNSET} : /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/arrival {dir=2} : /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/arrival {dir=4} : /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/white/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/arrival {dir=5} : /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/cafeteria {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/arrival {dir=6} : /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/arrival {dir=8} : /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white/corner {@OLD;dir=1} -/turf/open/floor/plasteel/arrival {dir=9} : /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white/corner {@OLD;dir=1} -/turf/open/floor/plasteel/arrival {dir=10} : /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/cafeteria {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/arrival/corner {dir=1} : /turf/open/floor/plasteel/white/corner {@OLD} -/turf/open/floor/plasteel/arrival/corner {dir=@UNSET} : /turf/open/floor/plasteel/white/corner {@OLD} -/turf/open/floor/plasteel/arrival/corner {dir=2} : /turf/open/floor/plasteel/white/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/arrival/corner {dir=4} : /obj/effect/turf_decal/tile/blue {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/arrival/corner {dir=8} : /obj/effect/turf_decal/tile/blue {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/asteroid : /obj/effect/turf_decal/sand , /turf/open/floor/plasteel -/turf/open/floor/plasteel/bar : /obj/effect/turf_decal/tile/bar , /obj/effect/turf_decal/tile/bar {dir=1} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/barber : /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/cafeteria {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blue : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blue/side {dir=1} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blue/side {dir=@UNSET} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blue/side {dir=2} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blue/side {dir=4} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blue/side {dir=5} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blue/side {dir=6} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blue/side {dir=8} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blue/side {dir=9} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blue/side {dir=10} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blue/corner : /obj/effect/turf_decal/tile/blue {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blueyellow : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blueyellow/side {dir=1} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blueyellow/side {dir=@UNSET} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blueyellow/side {dir=2} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blueyellow/side {dir=4} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blueyellow/side {dir=5} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blueyellow/side {dir=6} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blueyellow/side {dir=8} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blueyellow/side {dir=9} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/blueyellow/side {dir=10} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown {dir=1} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown {dir=@UNSET} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown {dir=2} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown {dir=4} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown {dir=5} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown {dir=6} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown {dir=8} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown {dir=9} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=4} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown {dir=10} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/corner : /obj/effect/turf_decal/tile/brown {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms {dir=1} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms {dir=@UNSET} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms {dir=2} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms {dir=4} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms {dir=5} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms {dir=6} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms {dir=8} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms {dir=9} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=4} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms {dir=10} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms/mainframe {dir=1} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms/mainframe {dir=@UNSET} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms/mainframe {dir=2} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms/mainframe {dir=4} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms/mainframe {dir=5} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms/mainframe {dir=6} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms/mainframe {dir=8} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms/mainframe {dir=9} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=4} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brown/telecomms/mainframe {dir=10} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brownold {dir=1} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brownold {dir=@UNSET} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brownold {dir=2} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brownold {dir=4} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brownold {dir=5} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brownold {dir=6} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brownold {dir=8} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brownold {dir=9} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=4} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brownold {dir=10} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/brownold/corner : /obj/effect/turf_decal/tile/brown {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/caution {dir=1} : /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel/dark/corner {@OLD;dir=1} -/turf/open/floor/plasteel/caution {dir=@UNSET} : /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/caution {dir=2} : /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/caution {dir=4} : /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel/dark/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/caution {dir=5} : /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel/checker {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/caution {dir=6} : /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/caution {dir=8} : /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark/corner {@OLD;dir=1} -/turf/open/floor/plasteel/caution {dir=9} : /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark/corner {@OLD;dir=1} -/turf/open/floor/plasteel/caution {dir=10} : /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/checker {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/caution/corner {dir=1} : /turf/open/floor/plasteel/dark/corner {@OLD} -/turf/open/floor/plasteel/caution/corner {dir=@UNSET} : /turf/open/floor/plasteel/dark/corner {@OLD} -/turf/open/floor/plasteel/caution/corner {dir=2} : /turf/open/floor/plasteel/dark/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/caution/corner {dir=4} : /obj/effect/turf_decal/tile/yellow {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/caution/corner {dir=8} : /obj/effect/turf_decal/tile/yellow {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/cmo : /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/dark/telecomms/mainframe : /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/dark/telecomms/server : /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/dark/telecomms/server/walkway : /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side {dir=1} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side {dir=@UNSET} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side {dir=2} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side {dir=4} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side {dir=5} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side {dir=6} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side {dir=8} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side {dir=9} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side {dir=10} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/corner : /obj/effect/turf_decal/tile/blue {dir=@OLD} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side/telecomms {dir=1} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side/telecomms {dir=@UNSET} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side/telecomms {dir=2} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side/telecomms {dir=4} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side/telecomms {dir=5} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side/telecomms {dir=6} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side/telecomms {dir=8} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side/telecomms {dir=9} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkblue/side/telecomms {dir=10} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkbrown : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkbrown/side {dir=1} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkbrown/side {dir=@UNSET} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkbrown/side {dir=2} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkbrown/side {dir=4} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkbrown/side {dir=5} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkbrown/side {dir=6} : /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=4} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkbrown/side {dir=8} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkbrown/side {dir=9} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown {dir=4} , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkbrown/side {dir=10} : /obj/effect/turf_decal/tile/brown {dir=1} , /obj/effect/turf_decal/tile/brown , /obj/effect/turf_decal/tile/brown {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkbrown/corner : /obj/effect/turf_decal/tile/brown {dir=@OLD} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side {dir=1} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side {dir=@UNSET} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side {dir=2} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side {dir=4} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side {dir=5} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side {dir=6} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side {dir=8} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side {dir=9} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side {dir=10} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/corner : /obj/effect/turf_decal/tile/green {dir=@OLD} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side/telecomms {dir=1} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side/telecomms {dir=@UNSET} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side/telecomms {dir=2} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side/telecomms {dir=4} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side/telecomms {dir=5} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side/telecomms {dir=6} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side/telecomms {dir=8} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side/telecomms {dir=9} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkgreen/side/telecomms {dir=10} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side {dir=1} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side {dir=@UNSET} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side {dir=2} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side {dir=4} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side {dir=5} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side {dir=6} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side {dir=8} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side {dir=9} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=4} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side {dir=10} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/corner : /obj/effect/turf_decal/tile/purple {dir=@OLD} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side/telecomms {dir=1} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side/telecomms {dir=@UNSET} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side/telecomms {dir=2} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side/telecomms {dir=4} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side/telecomms {dir=5} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side/telecomms {dir=6} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side/telecomms {dir=8} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side/telecomms {dir=9} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=4} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkpurple/side/telecomms {dir=10} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side {dir=1} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side {dir=@UNSET} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side {dir=2} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side {dir=4} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side {dir=5} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side {dir=6} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side {dir=8} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side {dir=9} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red {dir=4} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side {dir=10} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/corner : /obj/effect/turf_decal/tile/red {dir=@OLD} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side/telecomms {dir=1} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side/telecomms {dir=@UNSET} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side/telecomms {dir=2} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side/telecomms {dir=4} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side/telecomms {dir=5} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side/telecomms {dir=6} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side/telecomms {dir=8} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side/telecomms {dir=9} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red {dir=4} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkred/side/telecomms {dir=10} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side {dir=1} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side {dir=@UNSET} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side {dir=2} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side {dir=4} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side {dir=5} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side {dir=6} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side {dir=8} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side {dir=9} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side {dir=10} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/corner : /obj/effect/turf_decal/tile/yellow {dir=@OLD} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side/telecomms {dir=1} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side/telecomms {dir=@UNSET} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side/telecomms {dir=2} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side/telecomms {dir=4} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side/telecomms {dir=5} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side/telecomms {dir=6} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side/telecomms {dir=8} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side/telecomms {dir=9} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/darkyellow/side/telecomms {dir=10} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/escape {dir=1} : /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel/white/corner {@OLD;dir=1} -/turf/open/floor/plasteel/escape {dir=@UNSET} : /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/white/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/escape {dir=2} : /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/white/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/escape {dir=4} : /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel/white/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/escape {dir=5} : /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel/cafeteria {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/escape {dir=6} : /obj/effect/turf_decal/tile/red {dir=4} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/white/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/escape {dir=8} : /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/white/corner {@OLD;dir=1} -/turf/open/floor/plasteel/escape {dir=9} : /obj/effect/turf_decal/tile/red {dir=4} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/white/corner {@OLD;dir=1} -/turf/open/floor/plasteel/escape {dir=10} : /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/cafeteria {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/escape/corner {dir=1} : /turf/open/floor/plasteel/white/corner {@OLD} -/turf/open/floor/plasteel/escape/corner {dir=@UNSET} : /turf/open/floor/plasteel/white/corner {@OLD} -/turf/open/floor/plasteel/escape/corner {dir=2} : /turf/open/floor/plasteel/white/corner {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/escape/corner {dir=4} : /obj/effect/turf_decal/tile/red {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/escape/corner {dir=8} : /obj/effect/turf_decal/tile/red {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side {dir=1} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side {dir=@UNSET} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side {dir=2} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side {dir=4} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side {dir=5} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side {dir=6} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side {dir=8} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side {dir=9} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side {dir=10} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/corner : /obj/effect/turf_decal/tile/green {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side/telecomms {dir=1} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side/telecomms {dir=@UNSET} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side/telecomms {dir=2} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side/telecomms {dir=4} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side/telecomms {dir=5} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side/telecomms {dir=6} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side/telecomms {dir=8} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side/telecomms {dir=9} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/green/side/telecomms {dir=10} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenblue : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenblue/side {dir=1} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenblue/side {dir=@UNSET} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenblue/side {dir=2} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenblue/side {dir=4} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenblue/side {dir=5} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenblue/side {dir=6} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenblue/side {dir=8} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenblue/side {dir=9} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenblue/side {dir=10} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenyellow : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenyellow/side {dir=1} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenyellow/side {dir=@UNSET} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenyellow/side {dir=2} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenyellow/side {dir=4} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenyellow/side {dir=5} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenyellow/side {dir=6} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenyellow/side {dir=8} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenyellow/side {dir=9} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/greenyellow/side {dir=10} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/hydrofloor : /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side {dir=1} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side {dir=@UNSET} : /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side {dir=2} : /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side {dir=4} : /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side {dir=5} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side {dir=6} : /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side {dir=8} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side {dir=9} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral {dir=4} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side {dir=10} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/corner : /obj/effect/turf_decal/tile/neutral {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side/telecomms {dir=1} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral {dir=4} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side/telecomms {dir=@UNSET} : /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side/telecomms {dir=2} : /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side/telecomms {dir=4} : /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side/telecomms {dir=5} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side/telecomms {dir=6} : /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side/telecomms {dir=8} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side/telecomms {dir=9} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral {dir=4} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/neutral/side/telecomms {dir=10} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/orange : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/orange/side {dir=1} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/orange/side {dir=@UNSET} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/orange/side {dir=2} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/orange/side {dir=4} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/orange/side {dir=5} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/orange/side {dir=6} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/orange/side {dir=8} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/orange/side {dir=9} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/orange/side {dir=10} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/orange/corner : /obj/effect/turf_decal/tile/yellow {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/purple : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/purple/side {dir=1} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/purple/side {dir=@UNSET} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/purple/side {dir=2} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/purple/side {dir=4} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/purple/side {dir=5} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/purple/side {dir=6} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/purple/side {dir=8} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/purple/side {dir=9} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=4} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/purple/side {dir=10} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/purple/corner : /obj/effect/turf_decal/tile/purple {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/red : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/red/side {dir=1} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/red/side {dir=@UNSET} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/red/side {dir=2} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/red/side {dir=4} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/red/side {dir=5} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/red/side {dir=6} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/red/side {dir=8} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/red/side {dir=9} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red {dir=4} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/red/side {dir=10} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/red/corner : /obj/effect/turf_decal/tile/red {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/redside {dir=1} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/redside {dir=@UNSET} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/redside {dir=2} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/redside {dir=4} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/redside {dir=5} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/redside {dir=6} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/redside {dir=8} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/redside {dir=9} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/redside {dir=10} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/blueside {dir=1} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/blueside {dir=@UNSET} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/blueside {dir=2} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/blueside {dir=4} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/blueside {dir=5} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/blueside {dir=6} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/blueside {dir=8} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/blueside {dir=9} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redblue/blueside {dir=10} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=1} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=@UNSET} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=2} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=4} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=5} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=6} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=8} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=9} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=10} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=1} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=@UNSET} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=2} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=4} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=5} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=6} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=8} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=9} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redgreen/side {dir=10} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redyellow : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redyellow/side {dir=1} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redyellow/side {dir=@UNSET} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redyellow/side {dir=2} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redyellow/side {dir=4} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redyellow/side {dir=5} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redyellow/side {dir=6} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redyellow/side {dir=8} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redyellow/side {dir=9} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/redyellow/side {dir=10} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/airless : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/dark/airless {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/snowdin : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/dark/snowdin {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/telecomms : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/telecomms/mainframe : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/killroom : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/dark/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/side {dir=1} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/side {dir=@UNSET} : /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/side {dir=2} : /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/side {dir=4} : /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/side {dir=5} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/side {dir=6} : /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=4} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/side {dir=8} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/side {dir=9} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral {dir=4} , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/side {dir=10} : /obj/effect/turf_decal/tile/neutral {dir=1} , /obj/effect/turf_decal/tile/neutral , /obj/effect/turf_decal/tile/neutral {dir=8} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/vault/corner : /obj/effect/turf_decal/tile/neutral {dir=@OLD} , /turf/open/floor/plasteel/dark {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side {dir=1} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side {dir=@UNSET} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side {dir=2} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side {dir=4} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side {dir=5} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side {dir=6} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side {dir=8} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side {dir=9} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side {dir=10} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/corner : /obj/effect/turf_decal/tile/blue {dir=@OLD} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side/telecomms {dir=1} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side/telecomms {dir=@UNSET} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side/telecomms {dir=2} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side/telecomms {dir=4} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side/telecomms {dir=5} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side/telecomms {dir=6} : /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side/telecomms {dir=8} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side/telecomms {dir=9} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue {dir=4} , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteblue/side/telecomms {dir=10} : /obj/effect/turf_decal/tile/blue {dir=1} , /obj/effect/turf_decal/tile/blue , /obj/effect/turf_decal/tile/blue {dir=8} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitegreen : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitegreen/side {dir=1} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitegreen/side {dir=@UNSET} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitegreen/side {dir=2} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitegreen/side {dir=4} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitegreen/side {dir=5} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitegreen/side {dir=6} : /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitegreen/side {dir=8} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitegreen/side {dir=9} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green {dir=4} , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitegreen/side {dir=10} : /obj/effect/turf_decal/tile/green {dir=1} , /obj/effect/turf_decal/tile/green , /obj/effect/turf_decal/tile/green {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitegreen/corner : /obj/effect/turf_decal/tile/green {dir=@OLD} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side {dir=1} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side {dir=@UNSET} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side {dir=2} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side {dir=4} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side {dir=5} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side {dir=6} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side {dir=8} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side {dir=9} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=4} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side {dir=10} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/corner : /obj/effect/turf_decal/tile/purple {dir=@OLD} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side/telecomms {dir=1} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side/telecomms {dir=@UNSET} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side/telecomms {dir=2} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side/telecomms {dir=4} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side/telecomms {dir=5} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side/telecomms {dir=6} : /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=4} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side/telecomms {dir=8} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side/telecomms {dir=9} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple {dir=4} , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitepurple/side/telecomms {dir=10} : /obj/effect/turf_decal/tile/purple {dir=1} , /obj/effect/turf_decal/tile/purple , /obj/effect/turf_decal/tile/purple {dir=8} , /turf/open/floor/plasteel/white/telecomms {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitered : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitered/side {dir=1} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitered/side {dir=@UNSET} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitered/side {dir=2} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitered/side {dir=4} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitered/side {dir=5} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitered/side {dir=6} : /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=4} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitered/side {dir=8} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitered/side {dir=9} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red {dir=4} , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitered/side {dir=10} : /obj/effect/turf_decal/tile/red {dir=1} , /obj/effect/turf_decal/tile/red , /obj/effect/turf_decal/tile/red {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whitered/corner : /obj/effect/turf_decal/tile/red {dir=@OLD} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteyellow : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteyellow/side {dir=1} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteyellow/side {dir=@UNSET} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteyellow/side {dir=2} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteyellow/side {dir=4} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteyellow/side {dir=5} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteyellow/side {dir=6} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteyellow/side {dir=8} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteyellow/side {dir=9} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteyellow/side {dir=10} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/whiteyellow/corner : /obj/effect/turf_decal/tile/yellow {dir=@OLD} , /turf/open/floor/plasteel/white {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/yellow : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/yellow/side {dir=1} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/yellow/side {dir=@UNSET} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/yellow/side {dir=2} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/yellow/side {dir=4} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/yellow/side {dir=5} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/yellow/side {dir=6} : /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/yellow/side {dir=8} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/yellow/side {dir=9} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow {dir=4} , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/yellow/side {dir=10} : /obj/effect/turf_decal/tile/yellow {dir=1} , /obj/effect/turf_decal/tile/yellow , /obj/effect/turf_decal/tile/yellow {dir=8} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plasteel/yellow/corner : /obj/effect/turf_decal/tile/yellow {dir=@OLD} , /turf/open/floor/plasteel {@OLD;dir=@SKIP} -/turf/open/floor/plating/airless/astplate : /obj/effect/turf_decal/sand/plating , /turf/open/floor/plating/airless {@OLD;dir=@SKIP} -/turf/open/floor/plating/astplate : /obj/effect/turf_decal/sand/plating , /turf/open/floor/plating {@OLD;dir=@SKIP} \ No newline at end of file diff --git a/tools/bootstrap/python b/tools/bootstrap/python index 0c8d7020206..46b146961a0 100755 --- a/tools/bootstrap/python +++ b/tools/bootstrap/python @@ -25,6 +25,12 @@ PythonDir="$Cache/python-$PythonVersion" PythonExe="$PythonDir/python.exe" Log="$Cache/last-command.log" +# function that prints an error message and exits +error_exit() { + echo "If you are seeing this message, please try to fix it by running tools/hooks/install.(bat/sh) again." + exit 1 +} + # If a portable Python for Windows is not present, search on $PATH. if [ "$(uname)" = "Linux" ] || [ ! -f "$PythonExe" ]; then # Strip the "App Execution Aliases" from $PATH. Even if the user installed @@ -55,7 +61,7 @@ if [ "$(uname)" = "Linux" ] || [ ! -f "$PythonExe" ]; then echo "Please install Python from https://www.python.org/downloads/ or using your system's package manager." fi echo - exit 1 + error_exit fi # Create a venv and activate it @@ -70,13 +76,14 @@ if [ "$(uname)" = "Linux" ] || [ ! -f "$PythonExe" ]; then PythonExe="$PythonDir/scripts/python3.exe"; else echo "bootstrap/python failed to find the python executable inside its virtualenv" - exit 1 + error_exit fi fi # Use pip to install our requirements if [ ! -f "$PythonDir/requirements.txt" ] || [ "$(b2sum < "$Sdk/requirements.txt")" != "$(b2sum < "$PythonDir/requirements.txt")" ]; then echo "Updating dependencies..." + "$PythonExe" -m ensurepip --default-pip "$PythonExe" -m pip install -U wheel "$PythonExe" -m pip install -U pip -r "$Sdk/requirements.txt" cp "$Sdk/requirements.txt" "$PythonDir/requirements.txt" diff --git a/tools/bootstrap/python_.ps1 b/tools/bootstrap/python_.ps1 index 17b0eabeb29..f37589f1064 100644 --- a/tools/bootstrap/python_.ps1 +++ b/tools/bootstrap/python_.ps1 @@ -9,7 +9,6 @@ # The underscore in the name is so that typing `bootstrap/python` into # PowerShell finds the `.bat` file first, which ensures this script executes # regardless of ExecutionPolicy. - $host.ui.RawUI.WindowTitle = "starting :: python $args" $ErrorActionPreference = "Stop" [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 @@ -47,7 +46,6 @@ if (!(Test-Path $PythonExe -PathType Leaf)) { "https://www.python.org/ftp/python/$PythonVersion/python-$PythonVersion-embed-amd64.zip" ` -OutFile $Archive ` -ErrorAction Stop - -UseBasicParsing [System.IO.Compression.ZipFile]::ExtractToDirectory($Archive, $PythonDir) @@ -70,7 +68,6 @@ if (!(Test-Path "$PythonDir/Scripts/pip.exe")) { Invoke-WebRequest "https://bootstrap.pypa.io/get-pip.py" ` -OutFile "$Cache/get-pip.py" ` -ErrorAction Stop - -UseBasicParsing & $PythonExe "$Cache/get-pip.py" --no-warn-script-location if ($LASTEXITCODE -ne 0) { @@ -98,9 +95,14 @@ if (!(Test-Path "$PythonDir/requirements.txt") -or ((Get-FileHash "$Tools/requir Write-Output $PythonExe | Out-File -Encoding utf8 $Log [System.String]::Join([System.Environment]::NewLine, $args) | Out-File -Encoding utf8 -Append $Log Write-Output "---" | Out-File -Encoding utf8 -Append $Log - $host.ui.RawUI.WindowTitle = "python $args" - -& $PythonExe -u $args - +$ErrorActionPreference = "Continue" +& $PythonExe -u $args 2>&1 | ForEach-Object { + $str = "$_" + if ($_.GetType() -eq [System.Management.Automation.ErrorRecord]) { + $str = $str.TrimEnd("`r`n") + } + $str | Out-File -Encoding utf8 -Append $Log + $str | Out-Host +} exit $LastExitCode diff --git a/tools/build/build.js b/tools/build/build.js index 25ec4aa49e2..bcd3cf9492b 100755 --- a/tools/build/build.js +++ b/tools/build/build.js @@ -1,4 +1,5 @@ #!/usr/bin/env node + /** * Build script for /tg/station 13 codebase. * @@ -6,28 +7,20 @@ * https://github.com/stylemistake/juke-build */ -import fs from 'fs'; -import https from 'https'; +import fs from 'node:fs'; import Juke from './juke/index.js'; -import { DreamDaemon, DreamMaker } from './lib/byond.js'; +import { DreamDaemon, DreamMaker, NamedVersionFile } from './lib/byond.js'; +import { downloadFile } from './lib/download.js'; +import { prependDefines } from './lib/tgs.js'; import { yarn } from './lib/yarn.js'; +export const TGS_MODE = process.env.CBT_BUILD_MODE === 'TGS'; + +export const DME_NAME = 'citadel'; + Juke.chdir('../..', import.meta.url); -Juke.setup({ file: import.meta.url }).then((code) => { - // We're using the currently available quirk in Juke Build, which - // prevents it from exiting on Windows, to wait on errors. - if (code !== 0 && process.argv.includes('--wait-on-error')) { - Juke.logger.error('Please inspect the error and close the window.'); - return; - } - process.exit(code); -}); -const DME_NAME = 'citadel'; -const CUTTER_SUFFIX = '.png.toml' - -// Stores the contents of dependencies.sh as a key value pair -// Best way I could figure to get ahold of this stuff +/** @type {Record} */ const dependencies = fs.readFileSync('dependencies.sh', 'utf8') .split("\n") .map((statement) => statement.replace("export", "").trim()) @@ -39,12 +32,13 @@ const dependencies = fs.readFileSync('dependencies.sh', 'utf8') }, {}) // Canonical path for the cutter exe at this moment -const getCutterPath = () => { +function getCutterPath() { const ver = dependencies.CUTTER_VERSION; const suffix = process.platform === 'win32' ? '.exe' : ''; const file_ver = ver.split('.').join('-'); + return `tools/icon_cutter/cache/hypnagogic${file_ver}${suffix}`; -}; +} const cutter_path = getCutterPath(); @@ -58,16 +52,20 @@ export const PortParameter = new Juke.Parameter({ alias: 'p', }); +export const DmVersionParameter = new Juke.Parameter({ + type: 'string', +}); + export const CiParameter = new Juke.Parameter({ type: 'boolean' }); export const ForceRecutParameter = new Juke.Parameter({ type: 'boolean', - name: "force_recut", + name: 'force-recut', }); export const SkipIconCutter = new Juke.Parameter({ - type: "boolean", - name: "skip-icon-cutter", + type: 'boolean', + name: 'skip-icon-cutter', }); export const WarningParameter = new Juke.Parameter({ @@ -83,61 +81,24 @@ export const NoWarningParameter = new Juke.Parameter({ export const CutterTarget = new Juke.Target({ onlyWhen: () => { const files = Juke.glob(cutter_path); - return files.length == 0; + return files.length === 0; }, executes: async () => { const repo = dependencies.CUTTER_REPO; const ver = dependencies.CUTTER_VERSION; - const suffix = process.platform === "win32" ? ".exe" : ""; + const suffix = process.platform === 'win32' ? '.exe' : ''; const download_from = `https://github.com/${repo}/releases/download/${ver}/hypnagogic${suffix}`; - await download_file(download_from, cutter_path); - if (process.platform !== "win32") { - await Juke.exec("chmod", ["+x", cutter_path]); + await downloadFile(download_from, cutter_path); + if (process.platform !== 'win32') { + await Juke.exec('chmod', ['+x', cutter_path]); } }, }); -async function download_file(url, file) { - return new Promise((resolve, reject) => { - let file_stream = fs.createWriteStream(file); - https - .get(url, function (response) { - if (response.statusCode === 302) { - file_stream.close(); - download_file(response.headers.location, file).then((value) => - resolve(), - ); - return; - } - if (response.statusCode !== 200) { - Juke.logger.error( - `Failed to download ${url}: Status ${response.statusCode}`, - ); - file_stream.close(); - reject(); - return; - } - response.pipe(file_stream); - - // after download completed close filestream - file_stream.on("finish", () => { - file_stream.close(); - resolve(); - }); - }) - .on("error", (err) => { - file_stream.close(); - Juke.rm(download_into); - Juke.logger.error(`Failed to download ${url}: ${err.message}`); - reject(); - }); - }); -} - export const IconCutterTarget = new Juke.Target({ parameters: [ForceRecutParameter], dependsOn: () => [CutterTarget], - inputs: ({ get }) => { + inputs: () => { const standard_inputs = [ `icons/**/*.png.toml`, `icons/**/*.dmi.toml`, @@ -152,7 +113,7 @@ export const IconCutterTarget = new Juke.Target({ ]; return [ ...standard_inputs, - ...existing_configs.map((file) => file.replace(".toml", "")), + ...existing_configs.map((file) => file.replace('.toml', '')), ]; }, outputs: ({ get }) => { @@ -162,22 +123,19 @@ export const IconCutterTarget = new Juke.Target({ ...Juke.glob(`icons/**/*.dmi.toml`), ]; return folders - .map((file) => file.replace(`.png.toml`, ".dmi")) - .map((file) => file.replace(`.dmi.toml`, ".png")); + .map((file) => file.replace(`.png.toml`, '.dmi')) + .map((file) => file.replace(`.dmi.toml`, '.png')); }, executes: async () => { await Juke.exec(cutter_path, [ - "--dont-wait", - "--templates", - "icon_cutter_templates", - "icons", + '--dont-wait', + '--templates', + 'icon_cutter_templates', + 'icons', ]); }, }); - - - export const DmMapsIncludeTarget = new Juke.Target({ executes: async () => { // include all maps @@ -185,10 +143,10 @@ export const DmMapsIncludeTarget = new Juke.Target({ const folders = [ ...Juke.glob('maps/**/*.dmm'), ]; - const content = folders + const content = `${folders .map((file) => file.replace('maps/', '')) .map((file) => `#include "${file}"`) - .join('\n') + '\n'; + .join('\n')}\n`; fs.writeFileSync('maps/templates.dm', content); }, }); @@ -196,6 +154,7 @@ export const DmMapsIncludeTarget = new Juke.Target({ export const DmTarget = new Juke.Target({ parameters: [ DefineParameter, + DmVersionParameter, WarningParameter, NoWarningParameter, SkipIconCutter, @@ -210,17 +169,23 @@ export const DmTarget = new Juke.Target({ 'html/**', 'icons/**', 'interface/**', + 'sound/**', + 'tgui/public/tgui.html', `${DME_NAME}.dme`, + NamedVersionFile, ], - outputs: [ - `${DME_NAME}.dmb`, - `${DME_NAME}.rsc`, - ], + outputs: ({ get }) => { + if (get(DmVersionParameter)) { + return []; // Always rebuild when dm version is provided + } + return [`${DME_NAME}.dmb`, `${DME_NAME}.rsc`]; + }, executes: async ({ get }) => { await DreamMaker(`${DME_NAME}.dme`, { defines: ['CBT', ...get(DefineParameter)], warningsAsErrors: get(WarningParameter).includes('error'), ignoreWarningCodes: get(NoWarningParameter), + namedDmVersion: get(DmVersionParameter), }); }, }); @@ -228,7 +193,9 @@ export const DmTarget = new Juke.Target({ export const DmTestTarget = new Juke.Target({ parameters: [ DefineParameter, + DmVersionParameter, WarningParameter, + NoWarningParameter, ], dependsOn: ({ get }) => [ get(DefineParameter).includes('ALL_MAPS') && DmMapsIncludeTarget, @@ -239,10 +206,16 @@ export const DmTestTarget = new Juke.Target({ await DreamMaker(`${DME_NAME}.test.dme`, { defines: ['CBT', 'CIBUILDING', ...get(DefineParameter)], warningsAsErrors: get(WarningParameter).includes('error'), + ignoreWarningCodes: get(NoWarningParameter), + namedDmVersion: get(DmVersionParameter), }); Juke.rm('data/logs/ci', { recursive: true }); + const options = { + dmbFile: `${DME_NAME}.test.dmb`, + namedDmVersion: get(DmVersionParameter), + }; await DreamDaemon( - `${DME_NAME}.test.dmb`, + options, '-close', '-trusted', '-verbose', @@ -253,8 +226,7 @@ export const DmTestTarget = new Juke.Target({ try { const cleanRun = fs.readFileSync('data/logs/ci/clean_run.lk', 'utf-8'); console.log(cleanRun); - } - catch (err) { + } catch (err) { Juke.logger.error('Test run was not clean, exiting'); throw new Juke.ExitCode(1); } @@ -278,24 +250,34 @@ export const TgFontTarget = new Juke.Target({ dependsOn: [YarnTarget], inputs: [ 'tgui/.yarn/install-target', - 'tgui/packages/tgfont/**/*.+(js|cjs|svg)', + 'tgui/packages/tgfont/**/*.+(js|mjs|svg)', 'tgui/packages/tgfont/package.json', ], outputs: [ 'tgui/packages/tgfont/dist/tgfont.css', - 'tgui/packages/tgfont/dist/tgfont.eot', 'tgui/packages/tgfont/dist/tgfont.woff2', ], - executes: () => yarn('tgfont:build'), + executes: async () => { + await yarn('tgfont:build'); + fs.mkdirSync('tgui/packages/tgfont/static', { recursive: true }); + fs.copyFileSync( + 'tgui/packages/tgfont/dist/tgfont.css', + 'tgui/packages/tgfont/static/tgfont.css', + ); + fs.copyFileSync( + 'tgui/packages/tgfont/dist/tgfont.woff2', + 'tgui/packages/tgfont/static/tgfont.woff2', + ); + }, }); export const TguiTarget = new Juke.Target({ dependsOn: [YarnTarget], inputs: [ 'tgui/.yarn/install-target', - 'tgui/webpack.config.js', + 'tgui/rspack.config.ts', 'tgui/**/package.json', - 'tgui/packages/**/*.+(js|jsx|cjs|ts|tsx|scss)', + 'tgui/packages/**/*.+(js|cjs|ts|tsx|jsx|scss)', ], outputs: [ 'tgui/public/tgui.bundle.css', @@ -360,10 +342,15 @@ export const BuildTarget = new Juke.Target({ }); export const ServerTarget = new Juke.Target({ + parameters: [DmVersionParameter, PortParameter], dependsOn: [BuildTarget], executes: async ({ get }) => { const port = get(PortParameter) || '1337'; - await DreamDaemon(`${DME_NAME}.dmb`, port, '-trusted'); + const options = { + dmbFile: `${DME_NAME}.dmb`, + namedDmVersion: get(DmVersionParameter), + }; + await DreamDaemon(options, port, '-trusted'); }, }); @@ -408,17 +395,6 @@ export const CleanAllTarget = new Juke.Target({ }, }); -/** - * Prepends the defines to the .dme. - * Does not clean them up, as this is intended for TGS which - * clones new copies anyway. - */ -const prependDefines = (...defines) => { - const dmeContents = fs.readFileSync(`${DME_NAME}.dme`); - const textToWrite = defines.map(define => `#define ${define}\n`); - fs.writeFileSync(`${DME_NAME}.dme`, `${textToWrite}\n${dmeContents}`); -}; - export const TgsTarget = new Juke.Target({ dependsOn: [TguiTarget], executes: async () => { @@ -427,6 +403,21 @@ export const TgsTarget = new Juke.Target({ }, }); -const TGS_MODE = process.env.CBT_BUILD_MODE === 'TGS'; +Juke.setup({ file: import.meta.url }).then((code) => { + // We're using the currently available quirk in Juke Build, which + // prevents it from exiting on Windows, to wait on errors. + if (code !== 0 && process.argv.includes('--wait-on-error')) { + Juke.logger.error('Please inspect the error and close the window.'); + return; + } + + if (TGS_MODE) { + // workaround for ESBuild process lingering + // Once https://github.com/privatenumber/esbuild-loader/pull/354 is merged and updated to, this can be removed + setTimeout(() => process.exit(code), 10000); + } else { + process.exit(code); + } +}); export default TGS_MODE ? TgsTarget : BuildTarget; diff --git a/tools/build/lib/byond.js b/tools/build/lib/byond.js index f99e6397630..242ba9f4f83 100644 --- a/tools/build/lib/byond.js +++ b/tools/build/lib/byond.js @@ -1,39 +1,51 @@ -import fs from 'fs'; -import path from 'path'; +import fs from 'node:fs'; +import path from 'node:path'; import Juke from '../juke/index.js'; import { regQuery } from './winreg.js'; /** * Cached path to DM compiler + * @type {string} */ let dmPath; -const getDmPath = async () => { +/** + * @param {string|null|undefined} namedVersion + * @returns {Promise} + */ +async function getDmPath(namedVersion) { + // Use specific named version + if (namedVersion) { + return getNamedByondVersionPath(namedVersion); + } if (dmPath) { return dmPath; } dmPath = await (async () => { // Search in array of paths const paths = [ - ...((process.env.DM_EXE && process.env.DM_EXE.split(',')) || []), + ...(process.env.DM_EXE?.split(',') || []), + ...(await getDefaultNamedByondVersionPath()), 'C:\\Program Files\\BYOND\\bin\\dm.exe', 'C:\\Program Files (x86)\\BYOND\\bin\\dm.exe', ['reg', 'HKLM\\Software\\Dantom\\BYOND', 'installpath'], ['reg', 'HKLM\\SOFTWARE\\WOW6432Node\\Dantom\\BYOND', 'installpath'], ]; - const isFile = path => { + /** + * @param {string} path + */ + const isFile = (path) => { try { return fs.statSync(path).isFile(); - } - catch (err) { + } catch (err) { return false; } }; for (let path of paths) { // Resolve a registry key if (Array.isArray(path)) { - const [type, ...args] = path; - path = await regQuery(...args); + const [_type, ...args] = path; + path = (await regQuery(args[0], args[1])) || ''; } if (!path) { continue; @@ -42,105 +54,210 @@ const getDmPath = async () => { if (isFile(path)) { return path; } - if (isFile(path + '/dm.exe')) { - return path + '/dm.exe'; + if (isFile(`${path}/dm.exe`)) { + return `${path}/dm.exe`; } - if (isFile(path + '/bin/dm.exe')) { - return path + '/bin/dm.exe'; + if (isFile(`${path}/bin/dm.exe`)) { + return `${path}/bin/dm.exe`; } } // Default paths - return ( - process.platform === 'win32' && 'dm.exe' - || 'DreamMaker' - ); + return (process.platform === 'win32' && 'dm.exe') || 'DreamMaker'; })(); return dmPath; }; /** - * @param {string} dmeFile - * @param {{ - * defines?: string[]; - * warningsAsErrors?: boolean; - * }} options + * @param {string} namedVersion + * @returns {Promise} */ -export const DreamMaker = async (dmeFile, options = {}) => { - const dmPath = await getDmPath(); +async function getNamedByondVersionPath(namedVersion) { + const all_entries = await getAllNamedDmVersions(true); + const map_entry = all_entries.find((x) => x.name === namedVersion); + if (map_entry === undefined) { + Juke.logger.error( + `No named byond version with name "${namedVersion}" found.`, + ); + throw new Juke.ExitCode(1); + } + return map_entry.path; +} + +/** @returns {Promise} */ +async function getDefaultNamedByondVersionPath() { + const all_entries = await getAllNamedDmVersions(false); + const map_entry = all_entries.find((x) => x.default === true); + if (map_entry === undefined) return []; + return [map_entry.path]; +} + +/** @type {{name: string; path: string; default: boolean;}[]} */ +let namedDmVersionList; +export const NamedVersionFile = 'tools/build/dm_versions.json'; + +/** + * @param {boolean} throw_on_fail + * @returns {{name: string; path: string; default: boolean;}[]} + */ +async function getAllNamedDmVersions( + throw_on_fail, +) { + if (!namedDmVersionList) { + if (!fs.existsSync(NamedVersionFile)) { + if (throw_on_fail) { + Juke.logger.error(`No byond version map file found.`); + throw new Juke.ExitCode(1); + } + namedDmVersionList = []; + return namedDmVersionList; + } + try { + namedDmVersionList = await Bun.file(NamedVersionFile).json(); + } catch (err) { + if (throw_on_fail) { + Juke.logger.error(`Failed to parse byond version map file. ${err}`); + throw new Juke.ExitCode(1); + } + namedDmVersionList = []; + return namedDmVersionList; + } + } + return namedDmVersionList; +} + +/** + * @param {string} dmeFile + * @param {Partial<{ + * defines: string[]; + * warningsAsErrors: boolean; + * namedDmVersion: string | null; + * ignoreWarningCodes: string[]; + * }>} options + * @return {Promise} + */ +export async function DreamMaker( + dmeFile, + options = {}, +) { + if (options.namedDmVersion !== null) { + Juke.logger.info('Using named byond version:', options.namedDmVersion); + } + const dmPath = await getDmPath(options.namedDmVersion); // Get project basename const dmeBaseName = dmeFile.replace(/\.dme$/, ''); // Make sure output files are writable + /** @param {string} name */ const testOutputFile = (name) => { try { fs.closeSync(fs.openSync(name, 'r+')); - } - catch (err) { - if (err && err.code === 'ENOENT') { + } catch (err) { + if (!err || typeof err !== 'object' || !('code' in err)) { + throw err; + } + if (err.code === 'ENOENT') { return; } - if (err && err.code === 'EBUSY') { - Juke.logger.error(`File '${name}' is locked by the DreamDaemon process.`); + if (err.code === 'EBUSY') { + Juke.logger.error( + `File '${name}' is locked by the DreamDaemon process.`, + ); Juke.logger.error(`Stop the currently running server and try again.`); throw new Juke.ExitCode(1); } throw err; } }; + + /** @param {string} dmPath */ + const testDmVersion = async (dmPath) => { + const execReturn = await Juke.exec(dmPath, [], { + silent: true, + throw: false, + }); + const version = execReturn.combined.match( + `DM compiler version (\\d+)\\.(\\d+)`, + ); + if (version == null) { + Juke.logger.error( + `Unexpected DreamMaker return, ensure "${dmPath}" is correct DM path.`, + ); + throw new Juke.ExitCode(1); + } + const requiredMajorVersion = 515; + const requiredMinorVersion = 1597; // First with -D switch functionality + const major = Number(version[1]); + const minor = Number(version[2]); + if ( + major < requiredMajorVersion || + (major === requiredMajorVersion && minor < requiredMinorVersion) + ) { + Juke.logger.error( + `${requiredMajorVersion}.${requiredMinorVersion} or later DM version required. Version ${major}.${minor} found at: ${dmPath}`, + ); + throw new Juke.ExitCode(1); + } + }; + + await testDmVersion(dmPath); testOutputFile(`${dmeBaseName}.dmb`); testOutputFile(`${dmeBaseName}.rsc`); - const runWithWarningChecks = async (dmeFile, args) => { - const execReturn = await Juke.exec(dmeFile, args); + + /** + * @param {string} dmPath + * @param {string[]} args + */ + const runWithWarningChecks = async (dmPath, args) => { + const execReturn = await Juke.exec(dmPath, args); if (options.warningsAsErrors) { const ignoredWarningCodes = options.ignoreWarningCodes ?? []; if (ignoredWarningCodes.length > 0) { Juke.logger.info( - "Ignored warning codes:", - ignoredWarningCodes.join(", ") + 'Ignored warning codes:', + ignoredWarningCodes.join(', '), ); } - const base_regex = "\\d+:warning( \\([a-z_]*\\))?:"; + const base_regex = '\\d+:warning( \\([a-z_]*\\))?:'; const with_ignores = `\\d+:warning( \\([a-z_]*\\))?:(?!(${ignoredWarningCodes .map((x) => `.*${x}.*$`) - .join("|")}))`; + .join('|')}))`; const reg = ignoredWarningCodes.length > 0 - ? new RegExp(with_ignores, "m") - : new RegExp(base_regex, "m"); + ? new RegExp(with_ignores, 'm') + : new RegExp(base_regex, 'm'); if (options.warningsAsErrors && execReturn.combined.match(reg)) { Juke.logger.error(`Compile warnings treated as errors`); throw new Juke.ExitCode(2); } } return execReturn; - } + }; // Compile - const { defines } = options; + const { defines = [] } = options; if (defines && defines.length > 0) { Juke.logger.info('Using defines:', defines.join(', ')); - try { - const injectedContent = defines - .map(x => `#define ${x}\n`) - .join(''); - fs.writeFileSync(`${dmeBaseName}.m.dme`, injectedContent); - const dmeContent = fs.readFileSync(`${dmeBaseName}.dme`); - fs.appendFileSync(`${dmeBaseName}.m.dme`, dmeContent); - await runWithWarningChecks(dmPath, [`${dmeBaseName}.m.dme`]); - fs.writeFileSync(`${dmeBaseName}.dmb`, fs.readFileSync(`${dmeBaseName}.m.dmb`)); - fs.writeFileSync(`${dmeBaseName}.rsc`, fs.readFileSync(`${dmeBaseName}.m.rsc`)); - } - finally { - Juke.rm(`${dmeBaseName}.m.*`); - } - } - else { - await runWithWarningChecks(dmPath, [dmeFile]); } + + await runWithWarningChecks(dmPath, [ + ...defines.map((def) => `-D${def}`), + dmeFile, + ]); }; -export const DreamDaemon = async (dmbFile, ...args) => { - const dmPath = await getDmPath(); +/** + * + * @param {{dmbFile: string; namedDmVersion?: string | null;}} options + * @param {any[]} args + * @returns {Promise} + */ +export async function DreamDaemon( + options, + ...args +) { + const dmPath = await getDmPath(options.namedDmVersion); const baseDir = path.dirname(dmPath); - const ddExeName = process.platform === 'win32' ? 'dreamdaemon.exe' : 'DreamDaemon'; + const ddExeName = + process.platform === 'win32' ? 'dreamdaemon.exe' : 'DreamDaemon'; const ddExePath = baseDir === '.' ? ddExeName : path.join(baseDir, ddExeName); - return Juke.exec(ddExePath, [dmbFile, ...args]); -}; + + return Juke.exec(ddExePath, [options.dmbFile, ...args]); +} diff --git a/tools/build/lib/download.js b/tools/build/lib/download.js new file mode 100644 index 00000000000..9dde45c5c5f --- /dev/null +++ b/tools/build/lib/download.js @@ -0,0 +1,44 @@ +import fs from 'node:fs'; +import https from 'node:https'; +import Juke from '../juke/index.js'; + +/** + * @param {string} url + * @param {string} file + * @returns {Promise} + */ +export function downloadFile(url, file) { + return new Promise((resolve, reject) => { + const file_stream = fs.createWriteStream(file); + https + .get(url, (response) => { + if (response.statusCode === 302 && response.headers.location) { + file_stream.close(); + downloadFile(response.headers.location, file).then(() => + resolve('ok'), + ); + return; + } + if (response.statusCode !== 200) { + Juke.logger.error( + `Failed to download ${url}: Status ${response.statusCode}`, + ); + file_stream.close(); + reject(); + return; + } + response.pipe(file_stream); + + // after download completed close filestream + file_stream.on('finish', () => { + file_stream.close(); + resolve('ok'); + }); + }) + .on('error', (err) => { + file_stream.close(); + Juke.logger.error(`Failed to download ${url}: ${err.message}`); + reject(); + }); + }); +} diff --git a/tools/build/lib/tgs.js b/tools/build/lib/tgs.js new file mode 100644 index 00000000000..9549e1f281b --- /dev/null +++ b/tools/build/lib/tgs.js @@ -0,0 +1,17 @@ +import fs from 'node:fs'; +import { DME_NAME } from "../build.js"; + +/** + * Prepends the defines to the .dme. + * Does not clean them up, as this is intended for TGS which + * clones new copies anyway. + * @param {string[]} defines + */ +export async function prependDefines(...defines) { + const fileName = `${DME_NAME}.dme`; + const dmeContents = fs.readFileSync(fileName); + + const textToWrite = defines.map((define) => `#define ${define}\n`); + + fs.writeFileSync(fileName, `${textToWrite.join("")}\n${dmeContents}`); +} diff --git a/tools/build/lib/winreg.js b/tools/build/lib/winreg.js index 0916ea3a319..28ec6623be8 100644 --- a/tools/build/lib/winreg.js +++ b/tools/build/lib/winreg.js @@ -8,12 +8,17 @@ * @license MIT */ -import { exec } from 'child_process'; -import { promisify } from 'util'; +import { exec } from 'node:child_process'; +import { promisify } from 'node:util'; +/** + * @param {string} path + * @param {string} key + * @returns {Promise} + */ export const regQuery = async (path, key) => { if (process.platform !== 'win32') { - return null; + return; } try { const command = `reg query "${path}" /v ${key}`; @@ -21,22 +26,19 @@ export const regQuery = async (path, key) => { const keyPattern = ` ${key} `; const indexOfKey = stdout.indexOf(keyPattern); if (indexOfKey === -1) { - return null; + return; } const indexOfEol = stdout.indexOf('\r\n', indexOfKey); if (indexOfEol === -1) { - return null; + return; } - const indexOfValue = stdout.indexOf( - ' ', - indexOfKey + keyPattern.length); + const indexOfValue = stdout.indexOf(' ', indexOfKey + keyPattern.length); if (indexOfValue === -1) { - return null; + return; } const value = stdout.substring(indexOfValue + 4, indexOfEol); return value; - } - catch (err) { - return null; + } catch (err) { + return; } }; diff --git a/tools/build/package.json b/tools/build/package.json index e986b24bbae..c4fb2812b9e 100644 --- a/tools/build/package.json +++ b/tools/build/package.json @@ -1,4 +1,4 @@ { - "private": true, - "type": "module" + "private": true, + "type": "module" } diff --git a/tools/ci/annotate_od.sh b/tools/ci/annotate_od.sh new file mode 100755 index 00000000000..12390908074 --- /dev/null +++ b/tools/ci/annotate_od.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +set -euo pipefail +tools/bootstrap/python -m od_annotator "$@" diff --git a/tools/ci/check_grep.sh b/tools/ci/check_grep.sh index 390549fae24..495d83e8cdf 100755 --- a/tools/ci/check_grep.sh +++ b/tools/ci/check_grep.sh @@ -49,12 +49,6 @@ part() { section "map issues" -part "merge conflicts" -if $grep -U '/obj/merge_conflict_marker' $map_files; then - echo - echo -e "${RED}ERROR: Merge conflict markers in maps. Fix it." - st=1 -fi; part "TGM" if $grep -U '^".+" = \(.+\)' $map_files; then echo @@ -157,6 +151,14 @@ fi; # st=1 # fi; +# section "516 Href Styles" +# part "byond href styles" +# if $grep "href[\s='\"\\\\]*\?" $code_files ; then +# echo +# echo -e "${RED}ERROR: BYOND requires internal href links to begin with \"byond://\".${NC}" +# st=1 +# fi; + section "common mistakes" # part "global vars" # if $grep '^/*var/' $code_files; then @@ -164,27 +166,55 @@ section "common mistakes" # echo -e "${RED}ERROR: Unmanaged global var use detected in code, please use the helpers.${NC}" # st=1 # fi; + # part "proc args with var/" # if $grep '^/[\w/]\S+\(.*(var/|, ?var/.*).*\)' $code_files; then # echo # echo -e "${RED}ERROR: Changed files contains a proc argument starting with 'var'.${NC}" # st=1 # fi; + part "improperly pathed static lists" if $grep -i 'var/list/static/.*' $code_files; then echo echo -e "${RED}ERROR: Found incorrect static list definition 'var/list/static/', it should be 'var/static/list/' instead.${NC}" st=1 fi; -part "can_perform_action argument check" -if $grep 'can_perform_action\(\s*\)' $code_files; then + +# part "src as a trait source" # ideally we'd lint / test for ANY datum reference as a trait source, but 'src' is the most common. +# if $grep -i '(add_trait|remove_trait)\(.+,\s*.+,\s*src\)' $code_files; then +# echo +# echo -e "${RED}ERROR: Using 'src' as a trait source. Source must be a string key - dont't use references to datums as a source, perhaps use 'REF(src)'.${NC}" +# st=1 +# fi; +# if $grep -i '(add_traits|remove_traits)\(.+,\s*src\)' $code_files; then +# echo +# echo -e "${RED}ERROR: Using 'src' as trait sources. Source must be a string key - dont't use references to datums as sources, perhaps use 'REF(src)'.${NC}" +# st=1 +# fi; + +part "forceMove sanity" +if $grep 'forceMove\(\s*(\w+\(\)|\w+)\s*,\s*(\w+\(\)|\w+)\s*\)' $code_files; then echo - echo -e "${RED}ERROR: Found a can_perform_action() proc with improper arguments.${NC}" + echo -e "${RED}ERROR: forceMove() call with two arguments - this is not how forceMove() is invoked! It's x.forceMove(y), not forceMove(x, y).${NC}" st=1 fi; +# part "as anything on typeless loops" +# if $grep 'var/[^/]+ as anything' $code_files; then +# echo +# echo -e "${RED}ERROR: 'as anything' used in a typeless for loop. This doesn't do anything and should be removed.${NC}" +# st=1 +# fi; + +part "as anything on internal functions" +if $grep 'var\/(turf|mob|obj|atom\/movable).+ as anything in o?(view|range|hearers)\(' $code_files; then + echo + echo -e "${RED}ERROR: 'as anything' typed for loop over an internal function. These functions have some internal optimization that relies on the loop not having 'as anything' in it.${NC}" + st=1 +fi; + part "common spelling mistakes" -# one pesky door causing this issue # if $grep -i 'centcomm' $code_files; then # echo # echo -e "${RED}ERROR: Misspelling(s) of CentCom detected in code, please remove the extra M(s).${NC}" @@ -200,21 +230,36 @@ if $grep 'NanoTrasen' $code_files; then echo -e "${RED}ERROR: Misspelling(s) of Nanotrasen detected in code, please uncapitalize the T(s).${NC}" st=1 fi; -# there is a lot, fixme! # if $grep -i'eciev' $code_files; then # echo # echo -e "${RED}ERROR: Common I-before-E typo detected in code.${NC}" # st=1 # fi; -part "updatepaths validity" -missing_txt_lines=$(find tools/UpdatePaths/scripts -type f ! -name "*.txt" | wc -l) -if [ $missing_txt_lines -gt 0 ]; then +part "Ineffective easing flags in animate()" +if $grep 'easing\w*=\w*(EASE_IN|EASE_OUT|\(EASE_IN\w*\|\w*EASE_OUT\))' $code_files; then echo - echo -e "${RED}ERROR: Found an UpdatePaths File that doesn't end in .txt! Please add the proper file extension!${NC}" + echo -e "${RED}ERROR: 'animate' was called with an easing argument and the default, LINEAR_EASING curve. This doesn't do anything and should be adjusted.${NC}" st=1 fi; +# enable this when theres a script file, this silently fails. +# part "updatepaths validity" +# missing_txt_lines=$(find tools/UpdatePaths/Scripts -type f ! -name "*.txt" | wc -l) +# if [ $missing_txt_lines -gt 0 ]; then +# echo +# echo -e "${RED}ERROR: Found an UpdatePaths File that doesn't end in .txt! Please add the proper file extension!${NC}" +# st=1 +# fi; + +# number_prefix_lines=$(find tools/UpdatePaths/Scripts -type f | wc -l) +# valid_number_prefix_lines=$(find tools/UpdatePaths/Scripts -type f | $grep -P "\d+_(.+)" | wc -l) +# if [ $valid_number_prefix_lines -ne $number_prefix_lines ]; then +# echo +# echo -e "${RED}ERROR: Detected an UpdatePaths File that doesn't start with the PR number! Please add the proper number prefix!${NC}" +# st=1 +# fi; + section "515 Proc Syntax" part "proc ref syntax" if $grep '\.proc/' $code_x_515 ; then diff --git a/tools/ci/copy_build_output.sh b/tools/ci/copy_build_output.sh new file mode 100755 index 00000000000..6cc69ea3cc8 --- /dev/null +++ b/tools/ci/copy_build_output.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +mkdir -p \ + $1/icons \ + $1/tgui/public \ + +cp citadel.dmb citadel.rsc $1/ +cp -r icons/* $1/icons/ +cp -r tgui/public/* $1/tgui/public/ diff --git a/tools/ci/install/install_byond.sh b/tools/ci/install/install_byond.sh index 2eca12163ab..ebe17f496ed 100644 --- a/tools/ci/install/install_byond.sh +++ b/tools/ci/install/install_byond.sh @@ -1,7 +1,10 @@ #!/bin/bash set -euo pipefail -source dependencies.sh +# BYOND_MAJOR and BYOND_MINOR can be explicitly set, such as in alt_byond_versions.txt +if [ -z "${BYOND_MAJOR+x}" ]; then + source dependencies.sh +fi if [ -d "$HOME/BYOND/byond/bin" ] && grep -Fxq "${BYOND_MAJOR}.${BYOND_MINOR}" $HOME/BYOND/version.txt; then diff --git a/tools/ci/install_ripgrep.sh b/tools/ci/install/install_ripgrep.sh old mode 100644 new mode 100755 similarity index 100% rename from tools/ci/install_ripgrep.sh rename to tools/ci/install/install_ripgrep.sh diff --git a/tools/ci/install/install_spaceman_dmm.sh b/tools/ci/install/install_spaceman_dmm.sh index b9cfc9cfd56..d92dca4e950 100644 --- a/tools/ci/install/install_spaceman_dmm.sh +++ b/tools/ci/install/install_spaceman_dmm.sh @@ -1,8 +1,4 @@ #!/bin/bash -## This script installs SpacemanDMM -## It will be linked to the provided filename (argument 1), in the user's home directory -## todo: is this good behavior? should we standardize the location somewhere? - set -euo pipefail source dependencies.sh diff --git a/tools/ci/od_lints.dm b/tools/ci/od_lints.dm new file mode 100644 index 00000000000..580afd3eaaa --- /dev/null +++ b/tools/ci/od_lints.dm @@ -0,0 +1,36 @@ +//1000-1999 +#pragma FileAlreadyIncluded error +#pragma MissingIncludedFile error +#pragma MisplacedDirective error +#pragma UndefineMissingDirective error +#pragma DefinedMissingParen error +#pragma ErrorDirective error +#pragma WarningDirective warning +#pragma MiscapitalizedDirective error + +//2000-2999 +#pragma SoftReservedKeyword error +#pragma DuplicateVariable error +#pragma DuplicateProcDefinition error +#pragma PointlessParentCall error +#pragma PointlessBuiltinCall error +#pragma SuspiciousMatrixCall error +#pragma FallbackBuiltinArgument error +#pragma MalformedRange error +#pragma InvalidRange error +#pragma InvalidSetStatement error +#pragma InvalidOverride error +#pragma DanglingVarType error +#pragma MissingInterpolatedExpression error +#pragma AmbiguousResourcePath error +#pragma ProcArgumentGlobal error +#pragma UnsupportedAccess disabled + +//3000-3999 +// #pragma EmptyBlock error +#pragma EmptyBlock disabled +#pragma EmptyProc disabled +#pragma UnsafeClientAccess disabled +#pragma SuspiciousSwitchCase error +#pragma AssignmentInConditional error +#pragma AmbiguousInOrder error diff --git a/tools/ci/run_server.sh b/tools/ci/run_server.sh index 1ff65243830..b7ba8e2c64a 100755 --- a/tools/ci/run_server.sh +++ b/tools/ci/run_server.sh @@ -2,8 +2,8 @@ set -euo pipefail tools/deploy.sh ci_test -mkdir ci_test/config -mkdir ci_test/data +mkdir -p ci_test/config +mkdir -p ci_test/data #test config cp tools/ci/ci_config.txt ci_test/config/config.txt @@ -11,5 +11,11 @@ cp tools/ci/config.toml ci_test/config/config.toml cd ci_test DreamDaemon citadel.dmb -close -trusted -verbose -params "log-directory=ci" + cd .. + +mkdir -p data/screenshots_new +# cp -r ci_test/data/screenshots_new data/screenshots_new +cp ci_test/data/unit_tests.json data/unit_tests.json + cat ci_test/data/logs/ci/clean_run.lk diff --git a/tools/define_sanity/check.py b/tools/define_sanity/check.py new file mode 100644 index 00000000000..a9ffda0c67a --- /dev/null +++ b/tools/define_sanity/check.py @@ -0,0 +1,117 @@ +import fnmatch +import glob +import os +import re +import sys + +parent_directory = "code/**/*.dm" + +output_file_name = "define_sanity_output.txt" +how_to_fix_message = "Please #undef the above defines or remake them as global defines in the code/__DEFINES directory." + +def green(text): + return "\033[32m" + str(text) + "\033[0m" + +def red(text): + return "\033[31m" + str(text) + "\033[0m" + +def blue(text): + return "\033[34m" + str(text) + "\033[0m" + +def post_error(define_name, file, github_error_style): + if github_error_style: + print(f"::error file={file},title=Define Sanity::{define_name} is defined locally in {file} but not undefined locally!") + else: + print(red(f"- Failure: {define_name} is defined locally in {file} but not undefined locally!")) + +# simple way to check if we're running on github actions, or on a local machine +on_github = os.getenv("GITHUB_ACTIONS") == "true" + +# This files/directories are expected to have "global" defines, so they must be exempt from this check. +# Add directories as string here to automatically be exempt in case you have a non-complaint file name. +excluded_files = [ + # Wildcard directories, all files are expected to be exempt. + "code/__DEFINES/*.dm", + "code/__HELPERS/*.dm", + "code/_globalvars/*.dm", + # TGS files come from another repository so lets not worry about them. + "code/modules/tgs/**/*.dm", + # cit specific + "code/datums/position_point_vector.dm", +] + +define_regex = re.compile(r"(\s+)?#define\s?([A-Z0-9_]+)\(?(.+)\)?") + +files_to_scan = [] + +number_of_defines = 0 + +if not on_github: + print(blue(f"Running define sanity check outside of Github Actions.\nFor assistance, a '{output_file_name}' file will be generated at the root of your directory if any errors are detected.")) + +for code_file in glob.glob(parent_directory, recursive=True): + exempt_file = False + for exempt_directory in excluded_files: + if fnmatch.fnmatch(code_file, exempt_directory): + exempt_file = True + break + + if exempt_file: + continue + + # If the "base path" of the file starts with an underscore, it's assumed to be an encapsulated file holding references to the other files in its folder and is exempt from the checks. + if os.path.basename(code_file)[0] == "_": + continue + + files_to_scan.append(code_file) + +located_error_tuples = [] + +# ugh +excluded_specific_dir = [ + "code/game/content/factions/derelict/derelict.dm", + "code/game/content/factions/derelict/derelict.dmi", + "code/game/content/factions/eldritch/eldritch.dm", + "code/game/content/factions/eldritch/eldritch.dmi", + "code/game/content/factions/fey_forest/fey_forest.dmi", +] + +for applicable_file in files_to_scan: + if applicable_file in excluded_specific_dir: + continue + with open(applicable_file, encoding="utf8") as file: + file_contents = file.read() + for define in define_regex.finditer(file_contents): + number_of_defines += 1 + define_name = define.group(2) + if not re.search("#undef\s" + define_name, file_contents): + located_error_tuples.append((define_name, applicable_file)) + +if number_of_defines == 0: + print(red("No defines found! This is likely an error.")) + sys.exit(1) + +if number_of_defines <= 1000: + print(red(f"Only found {number_of_defines} defines! Something has likely gone wrong as the number of local defines should not be this low.")) + sys.exit(1) + +if len(located_error_tuples): + + string_list = [] + for error in located_error_tuples: + if not on_github: + post_error(error[0], error[1], False) + string_list.append(f"{error[0]} is defined locally in {error[1]} but not undefined locally!") + else: + post_error(error[0], error[1], True) + + if len(string_list): + with open(output_file_name, "w") as output_file: + output_file.write("\n".join(string_list)) + output_file.write("\n\n" + how_to_fix_message) + + print(red(how_to_fix_message)) + sys.exit(1) + +else: + print(green(f"No unhandled local defines found (found {number_of_defines} defines).")) diff --git a/tools/deploy.sh b/tools/deploy.sh index baa4de5b529..9220fddab51 100755 --- a/tools/deploy.sh +++ b/tools/deploy.sh @@ -10,8 +10,8 @@ if [[ $# -eq 2 ]] ; then fi mkdir -p \ - $1/_mapload \ - $1/config.default \ + $1/_mapload \ + $1/config.default \ $1/maps \ $1/icons \ $1/sound \ diff --git a/tools/hooks/install.bat b/tools/hooks/Install.bat similarity index 100% rename from tools/hooks/install.bat rename to tools/hooks/Install.bat diff --git a/tools/hooks/README.md b/tools/hooks/README.md index c7105903972..1c34d538602 100644 --- a/tools/hooks/README.md +++ b/tools/hooks/README.md @@ -12,9 +12,9 @@ or other script you can run instead - see the links below for details. ## Hooks -* **Pre-commit**: Runs [mapmerge2] to reduce the diff on any changed maps. -* **DMI merger**: Attempts to [fix icon conflicts] when performing a Git merge. -* **DMM merger**: Attempts to [fix map conflicts] when performing a Git merge. +- **Pre-commit**: Runs [mapmerge2] to reduce the diff on any changed maps. +- **DMI merger**: Attempts to [fix icon conflicts] when performing a Git merge. +- **DMM merger**: Attempts to [fix map conflicts] when performing a Git merge. ## Adding New Hooks diff --git a/tools/hooks/install.py b/tools/hooks/install.py index 0736b29681e..554cece3ddc 100755 --- a/tools/hooks/install.py +++ b/tools/hooks/install.py @@ -49,6 +49,8 @@ def uninstall(target=None, keep=()): # Remove merge driver configuration for entry in repo.config: + if entry.level != pygit2.GIT_CONFIG_LEVEL_LOCAL: + continue match = re.match(r'^merge\.([^.]+)\.driver$', entry.name) if match and f"{match.group(1)}.merge" not in keep: print('Removing merge driver:', match.group(1)) diff --git a/tools/icon_cutter/README.md b/tools/icon_cutter/README.md index 8bd416154aa..397184ade81 100644 --- a/tools/icon_cutter/README.md +++ b/tools/icon_cutter/README.md @@ -9,7 +9,7 @@ This folder will hold a set of cached versions of hypnagogic, our icon cutter. W The cutter works off 2 inputs. A file, typically a png, and a toml config file in the format `{filename}.{other input extension}.toml` The input resource is transformed by the cutter following a set of rules set out in the .toml file. -Typically these are very basic. We have a set of templates in repo stored in [cutter_templates/](../../cutter_templates/) and most uses just copy from them. +Typically these are very basic. We have a set of templates in repo stored in [cutter_templates/](../../icon_cutter_templates/) and most uses just copy from them. You can find more information about it in its repository, found [here](https://github.com/actioninja/hypnagogic), the examples subfolder in particular contains fully detailed explanations of all the config values for the different types of cutting (there are more then one) diff --git a/tools/icon_cutter/check.py b/tools/icon_cutter/check.py index 568ec272436..29f1f8d4933 100644 --- a/tools/icon_cutter/check.py +++ b/tools/icon_cutter/check.py @@ -93,6 +93,11 @@ for cutter_template in files: output_hash[output_name] = get_file_hash(output_name) +# Sanity check +if len(output_hash) == 0: + print(f"::error output_hash dict was empty. Something has gone wrong") + sys.exit(1) + # Execute cutter if platform.system() == "Windows": subprocess.run(f"{path_to_us}\..\\build\\build.bat --force-recut --ci icon-cutter") diff --git a/tools/linux_build.py b/tools/linux_build.py deleted file mode 100644 index 8399d1a4932..00000000000 --- a/tools/linux_build.py +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env python - -import subprocess -import os -import sys -import argparse -import time -from subprocess import PIPE, STDOUT - -null = open("/dev/null", "wb") - -def wait(p): - rc = p.wait() - if rc != 0: - p = play("sound/misc/compiler-failure.ogg") - p.wait() - assert p.returncode == 0 - sys.exit(rc) - -def play(soundfile): - p = subprocess.Popen(["play", soundfile], stdout=null, stderr=null) - assert p.wait() == 0 - return p - -def stage1(): - p = subprocess.Popen("(cd tgui; /bin/bash ./build.sh)", shell=True) - wait(p) - play("sound/misc/compiler-stage1.ogg") - -def stage2(map): - if map: - txt = "-M{}".format(map) - else: - txt = '' - args = "bash tools/travis/dm.sh {} citadel.dme".format(txt) - print(args) - p = subprocess.Popen(args, shell=True) - wait(p) - -def stage3(profile_mode=False): - start_time = time.time() - play("sound/misc/compiler-stage2.ogg") - logfile = open('server.log~','w') - p = subprocess.Popen( - "DreamDaemon citadel.dmb 25001 -trusted", - shell=True, stdout=PIPE, stderr=STDOUT) - try: - while p.returncode is None: - stdout = p.stdout.readline() - if "Initializations complete" in stdout: - play("sound/misc/server-ready.ogg") - time_taken = time.time() - start_time - print("{} seconds taken to fully start".format(time_taken)) - if "Map is ready." in stdout: - time_taken = time.time() - start_time - print("{} seconds for initial map loading".format(time_taken)) - if profile_mode: - return time_taken - sys.stdout.write(stdout) - sys.stdout.flush() - logfile.write(stdout) - finally: - logfile.flush() - os.fsync(logfile.fileno()) - logfile.close() - p.kill() - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('-s','---stage',default=1,type=int) - parser.add_argument('--only',action='store_true') - parser.add_argument('-m','--map',type=str) - parser.add_argument('--profile-mode',action='store_true') - args = parser.parse_args() - stage = args.stage - assert stage in (1,2,3) - if stage == 1: - stage1() - if not args.only: - stage = 2 - if stage == 2: - stage2(args.map) - if not args.only: - stage = 3 - if stage == 3: - value = stage3(profile_mode=args.profile_mode) - with open('profile~', 'a') as f: - f.write("{}\n".format(value)) - -if __name__=='__main__': - try: - main() - except KeyboardInterrupt: - pass diff --git a/tools/maplint/README.md b/tools/maplint/README.md new file mode 100644 index 00000000000..c14aa580fb4 --- /dev/null +++ b/tools/maplint/README.md @@ -0,0 +1,224 @@ +# maplint + +maplint is a tool that lets you prohibit anti-patterns in maps through simple rules. You can use maplint to do things like ban variable edits for specific types, ban specific variable edits, ban combinations of types, etc. + +## Making lints + +To create a lint, create a new file in the `lints` folder. Lints use [YAML](https://learnxinyminutes.com/docs/yaml/), which is very expressive, though can be a little complex. If you get stuck, read other lints in this folder. + +### Typepaths + +The root of the file is your typepaths. This will match not only that type, but also subtypes. For example: + +```yml +/mob/dog: + # We'll get to this... +``` + +...will define rules for `/mob/dog`, `/mob/dog/corgi`, `/mob/dog/beagle`, etc. + +If you only want to match a specific typepath, prefix it with `=`. This: + +```yml +=/mob/dog: +``` + +...will only match `/mob/dog` specifically. + +Alternatively, if you want to match ALL types, enter a single `*`, for wildcard. + +### `banned` + +The simplest rule is to completely ban a subtype. To do this, fill with `banned: true`. + +For example, this lint will ban `/mob/dog` and all subtypes: + +```yml +/mob/dog: + banned: true # Cats FTW +``` + +### `banned_neighbors` + +If you want to ban other objects being on the same tile as another, you can specify `banned_neighbors`. + +This takes a few forms. The simplest is just a list of types to not be next to. This lint will ban either cat*toy \_or* cat_food (or their subtypes) from being on the same tile as a dog. + +```yml +/mob/dog: + banned_neighbors: + - /obj/item/cat_toy + - /obj/item/cat_food +``` + +This also supports the `=` format as specified before. This will ban `/mob/dog` being on the same tile as `/obj/item/toy` _only_. + +```yml +/mob/dog: + banned_neighbors: + - =/obj/item/toy # Only the best toys for our dogs +``` + +Anything in this list will _not_ include the object itself, meaning you can use it to make sure two of the same object are not on the same tile. For example, this lint will ban two dogs from being on the same tile: + +```yml +/mob/dog: + banned_neighbors: + - /mob/dog # We're a space station, not a dog park! +``` + +However, you can add a bit more specificity with `identical: true`. This will prohibit other instances of the _exact_ same type _and_ variable edits from being on the same tile. + +```yml +/mob/dog: + banned_neighbors: + # Purebreeds are unnatural! We're okay with dogs as long as they're different. + /mob/dog: { identical: true } +``` + +Finally, if you need maximum precision, you can specify a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) to match for a path. If we wanted to ban a `/mob/dog` from being on the same tile as `/obj/bowl/big/cat`, `/obj/bowl/small/cat`, etc, we can write: + +```yml +/mob/dog: + banned_neighbors: + CAT_BOWLS: { pattern: ^/obj/bowl/.+/cat$ } +``` + +### `banned_variables` + +To ban all variable edits, you can specify `banned_variables: true` for a typepath. For instance, if we want to block dogs from getting any var-edits, we can write: + +```yml +/mob/dog: + banned_variables: true # No var edits, no matter what +``` + +If we want to be more specific, we can write out the specific variables we want to ban. + +```yml +/mob/dog + banned_variables: + - species # Don't var-edit species, use the subtypes +``` + +We can also explicitly create allowlists and denylists of values through `allow` and `deny`. For example, if we want to make sure we're not creating invalid bowls for animals, we can write: + +```yml +/obj/bowl/dog: + banned_variables: + species: + # If we specify a species, it's gotta be a dog + allow: ["beagle", "corgi", "pomeranian"] + +/obj/bowl/humans: + banned_variables: + species: + # We're civilized, we don't want to eat from the same bowl that's var-edited for animals + deny: ["cats", "dogs"] +``` + +Similar to [banned_neighbors](#banned_neighbors), you can specify a regular expression pattern for allow/deny. + +```yml +/mob/dog: + banned_variables: + # Names must start with a capital letter + name: + allow: { pattern: "^[A-Z].*$" } +``` + +### `ignore` + +If you want to exclude certain objects and subtypes from `banned_neighbors`, you can specify objects in `ignore` that will get skipped from triggering `banned_neighbors`. + +```yml +/turf/wall: + banned_neighbors: + - /obj/structure + ignore: + - /obj/structure/sign +``` + +This lint stops structures from being put inside solid walls but we make an exception to ignore any signs and all their subtypes like `/obj/structure/sign/poster`. + +### `help` + +If you want a custom message to go with your lint, you can specify "help" in the root. + +```yml +help: Pugs haven't existed on Sol since 2450. +/mob/dog/pug: + banned: true +``` + +### `when` - Conditional Rules + +Sometimes it may be necessary for a rule to be given conditions which have to be met before it needs to be applied. All children of the when node must be satisfied for the rule to execute. + +If we wanted to create a rule which disallows the placement of access helpers when an airlock's access has been manually set via a variable edit, then we could make the following rule: + +```yml +/obj/machinery/door/airlock: + when: + - req_access_txt is set + banned_neighbors: + - /obj/effect/mapping_helper/airlock/access +``` + +The following conditions are valid: + +- **{var_name} is set**: The variable named _var_name_ has been modified. +- **{var_name} is not set**: The variable named _var_name_ has not been modified. +- **{var_name} is '{value}'**: The variable named _var_name_ has a specific value. +- **{var_name} is not '{value}'**: The variable named _var_name_ does not have a specific value. +- **{var_name} like '{regex}'**: The variable named _var_name_ matches the provided regex. + +#### `any` + +The any node may be added as a child to the when node to specify that it will be satisfied if any of its child conditions are met. + +```yml +/mob/dog: + # Rule only applies when the dog is any of the following breeds + when: + - any: + - breed is 'labrador' + - breed is 'pug' + - breed is 'corgi' + # These breeds of dogs must have a dogbed + required_neighbors: + - /obj/dogbed +``` + +#### `all` + +The all node may be added as a child to the when node to specify that it will be satisfied only when all of its child conditions are met. Note that the `all` node only makes sense to use when the parent node is an `any` node, as the default behaviour of `when` is to require all conditions to be met. + +```yml +/mob/dog: + # Rule only applies if the dog breed is capitalised and has an owner + when: + - all: + - breed like '[A-Z][a-z]*' + - owner is set + # These dogs must have a dogbed for their owner + required_neighbors: + - /obj/dogbed +``` + +### `skip_files` + +To skip processing this rule when the current filename start with a given path, either the start of a path, the full path (with extension), or regex matched. + +```yml +/area/template_noop: + banned_neighbors: + - /turf/open + - /obj + skip_files: + - _maps/templates + - _maps/minigame/deathmatch/OSHA_Violator.dmm + - pattern: "^_maps/templates/holodeck.*" +``` + +In this example, all files in '\_maps/templates' (recursively), 'OSHA_Violator.dmm', and any regex matched files for 'temples/holodeck.' will be skipped for any other rules defined in this yml. diff --git a/tools/maplint/lints/apc_pixel_shifts.yml.disabled b/tools/maplint/lints/apc_pixel_shifts.yml.disabled new file mode 100644 index 00000000000..5ad3a10d286 --- /dev/null +++ b/tools/maplint/lints/apc_pixel_shifts.yml.disabled @@ -0,0 +1,7 @@ +help: "Use the directional variants when possible." +/obj/machinery/power/apc: + banned_variables: + pixel_x: + allow: [25, -25] + pixel_y: + allow: [25, -25] diff --git a/tools/maplint/lints/area_varedits.yml b/tools/maplint/lints/area_varedits.yml new file mode 100644 index 00000000000..19c18620191 --- /dev/null +++ b/tools/maplint/lints/area_varedits.yml @@ -0,0 +1,3 @@ +help: "Please replace it with a proper area path." +/area: + banned_variables: true diff --git a/tools/maplint/lints/atmos_var_edits.yml b/tools/maplint/lints/atmos_var_edits.yml new file mode 100644 index 00000000000..508477b0a91 --- /dev/null +++ b/tools/maplint/lints/atmos_var_edits.yml @@ -0,0 +1,4 @@ +help: "Please consider making/using a subtype instead of editing this var." +/obj/machinery/atmospherics: + banned_variables: + piping_layer: diff --git a/tools/maplint/lints/base_turf.yml b/tools/maplint/lints/base_turf.yml new file mode 100644 index 00000000000..de7776f923a --- /dev/null +++ b/tools/maplint/lints/base_turf.yml @@ -0,0 +1,2 @@ +=/turf: + banned: true diff --git a/tools/maplint/lints/cable_varedits.yml b/tools/maplint/lints/cable_varedits.yml new file mode 100644 index 00000000000..b73adaae411 --- /dev/null +++ b/tools/maplint/lints/cable_varedits.yml @@ -0,0 +1,7 @@ +# /obj/structure/cable: +# banned_variables: true + +/obj/structure/pipe_cleaner: + banned_variables: + - d1 + - d2 diff --git a/tools/maplint/lints/disposals_without_trunks.yml b/tools/maplint/lints/disposals_without_trunks.yml new file mode 100644 index 00000000000..6cecdb326c7 --- /dev/null +++ b/tools/maplint/lints/disposals_without_trunks.yml @@ -0,0 +1,13 @@ +/obj/machinery/disposal: + required_neighbors: + - /obj/structure/disposalpipe/trunk + # remove when fixed + skip_files: + - maps/stations/rift/map_files/rift-04-surface1.dmm + - maps/stations/rift/map_files/rift-05-surface1.dmm + - maps/stations/tether/levels/surface1.dmm + - maps/stations/endeavour/levels/deck1.dmm + - maps/stations/endeavour/levels/deck2.dmm + - maps/stations/endeavour/levels/deck3.dmm + - maps/stations/rift/map_files/rift-05-surface2.dmm + - maps/templates/shuttles/overmaps/generic/cruiser.dmm diff --git a/tools/maplint/lints/grille_stacking.yml b/tools/maplint/lints/grille_stacking.yml new file mode 100644 index 00000000000..95617344408 --- /dev/null +++ b/tools/maplint/lints/grille_stacking.yml @@ -0,0 +1,5 @@ +help: "Please replace with the proper structure spawner." +=/obj/structure/grille: + banned_neighbors: + FULLTILE_WINDOW: + pattern: ^/obj/structure/window[/\w]*?fulltile$ diff --git a/tools/maplint/lints/heat_capacity.yml b/tools/maplint/lints/heat_capacity.yml new file mode 100644 index 00000000000..99340d12d0b --- /dev/null +++ b/tools/maplint/lints/heat_capacity.yml @@ -0,0 +1,4 @@ +help: "Do not override heat_capacity, you must use a custom subtype." +/turf: + banned_variables: + - heat_capacity diff --git a/tools/maplint/lints/identical_barricades.yml.disabled b/tools/maplint/lints/identical_barricades.yml.disabled new file mode 100644 index 00000000000..7fecc5363ab --- /dev/null +++ b/tools/maplint/lints/identical_barricades.yml.disabled @@ -0,0 +1,4 @@ +/obj/structure/barricade: + banned_neighbors: + /obj/structure/barricade: + identical: true diff --git a/tools/maplint/lints/identical_cables.yml.disabled b/tools/maplint/lints/identical_cables.yml.disabled new file mode 100644 index 00000000000..773578f4d7a --- /dev/null +++ b/tools/maplint/lints/identical_cables.yml.disabled @@ -0,0 +1,4 @@ +/obj/structure/cable: + banned_neighbors: + /obj/structure/cable: + identical: true diff --git a/tools/maplint/lints/identical_disposal_pipes.yml.disabled b/tools/maplint/lints/identical_disposal_pipes.yml.disabled new file mode 100644 index 00000000000..6099a9aa413 --- /dev/null +++ b/tools/maplint/lints/identical_disposal_pipes.yml.disabled @@ -0,0 +1,4 @@ +/obj/structure/disposalpipe: + banned_neighbors: + /obj/structure/disposalpipe: + identical: true diff --git a/tools/maplint/lints/identical_pipes.yml.disabled b/tools/maplint/lints/identical_pipes.yml.disabled new file mode 100644 index 00000000000..0c9fb456567 --- /dev/null +++ b/tools/maplint/lints/identical_pipes.yml.disabled @@ -0,0 +1,4 @@ +/obj/machinery/atmospherics/pipe: + banned_neighbors: + /obj/machinery/atmospherics/pipe: + identical: true diff --git a/tools/maplint/lints/merge_conflict_marker.yml b/tools/maplint/lints/merge_conflict_marker.yml new file mode 100644 index 00000000000..2926d917992 --- /dev/null +++ b/tools/maplint/lints/merge_conflict_marker.yml @@ -0,0 +1,4 @@ +help: This means you didn't clean up any potential merge conflicts, + make sure everything in that area is correct. +/obj/merge_conflict_marker: + banned: true diff --git a/tools/maplint/lints/multiple_area.yml b/tools/maplint/lints/multiple_area.yml new file mode 100644 index 00000000000..048bd04e5a3 --- /dev/null +++ b/tools/maplint/lints/multiple_area.yml @@ -0,0 +1,3 @@ +/area: + banned_neighbors: + - /area diff --git a/tools/maplint/lints/multiple_chairs.yml.disabled b/tools/maplint/lints/multiple_chairs.yml.disabled new file mode 100644 index 00000000000..70456b7903b --- /dev/null +++ b/tools/maplint/lints/multiple_chairs.yml.disabled @@ -0,0 +1,3 @@ +/obj/structure/bed/chair: + banned_neighbors: + - /obj/structure/bed/chair diff --git a/tools/maplint/lints/multiple_closets.yml.disabled b/tools/maplint/lints/multiple_closets.yml.disabled new file mode 100644 index 00000000000..394b44b12bb --- /dev/null +++ b/tools/maplint/lints/multiple_closets.yml.disabled @@ -0,0 +1,3 @@ +/obj/structure/closet: + banned_neighbors: + - /obj/structure/closet diff --git a/tools/maplint/lints/multiple_firelocks.yml.disabled b/tools/maplint/lints/multiple_firelocks.yml.disabled new file mode 100644 index 00000000000..e99050bbdc3 --- /dev/null +++ b/tools/maplint/lints/multiple_firelocks.yml.disabled @@ -0,0 +1,3 @@ +=/obj/machinery/door/firedoor: + banned_neighbors: + - =/obj/machinery/door/firedoor diff --git a/tools/maplint/lints/multiple_girders.yml b/tools/maplint/lints/multiple_girders.yml new file mode 100644 index 00000000000..960dff2f3e2 --- /dev/null +++ b/tools/maplint/lints/multiple_girders.yml @@ -0,0 +1,3 @@ +/obj/structure/girder: + banned_neighbors: + - /obj/structure/girder diff --git a/tools/maplint/lints/multiple_grilles.yml b/tools/maplint/lints/multiple_grilles.yml new file mode 100644 index 00000000000..d684e48748f --- /dev/null +++ b/tools/maplint/lints/multiple_grilles.yml @@ -0,0 +1,3 @@ +/obj/structure/grille: + banned_neighbors: + - /obj/structure/grille diff --git a/tools/maplint/lints/multiple_lattice.yml.disabled b/tools/maplint/lints/multiple_lattice.yml.disabled new file mode 100644 index 00000000000..87ce7fc2511 --- /dev/null +++ b/tools/maplint/lints/multiple_lattice.yml.disabled @@ -0,0 +1,3 @@ +/obj/structure/lattice: + banned_neighbors: + - /obj/structure/lattice diff --git a/tools/maplint/lints/multiple_machinery.yml.disabled b/tools/maplint/lints/multiple_machinery.yml.disabled new file mode 100644 index 00000000000..bbefab76b74 --- /dev/null +++ b/tools/maplint/lints/multiple_machinery.yml.disabled @@ -0,0 +1,4 @@ +/obj/machinery: + banned_neighbors: + /obj/machinery: + identical: true diff --git a/tools/maplint/lints/multiple_stairs.yml b/tools/maplint/lints/multiple_stairs.yml new file mode 100644 index 00000000000..1828a75ba2e --- /dev/null +++ b/tools/maplint/lints/multiple_stairs.yml @@ -0,0 +1,3 @@ +/obj/structure/stairs: + banned_neighbors: + - /obj/structure/stairs diff --git a/tools/maplint/lints/multiple_tables.yml.disabled b/tools/maplint/lints/multiple_tables.yml.disabled new file mode 100644 index 00000000000..b08b255a316 --- /dev/null +++ b/tools/maplint/lints/multiple_tables.yml.disabled @@ -0,0 +1,3 @@ +/obj/structure/table: + banned_neighbors: + - /obj/structure/table diff --git a/tools/maplint/lints/multiple_turf.yml.disabled b/tools/maplint/lints/multiple_turf.yml.disabled new file mode 100644 index 00000000000..dc724af7ae8 --- /dev/null +++ b/tools/maplint/lints/multiple_turf.yml.disabled @@ -0,0 +1,3 @@ +/turf: + banned_neighbors: + - /turf diff --git a/tools/maplint/lints/multiple_windows.yml.disabled b/tools/maplint/lints/multiple_windows.yml.disabled new file mode 100644 index 00000000000..34eb90d1b57 --- /dev/null +++ b/tools/maplint/lints/multiple_windows.yml.disabled @@ -0,0 +1,4 @@ +/obj/structure/window: + banned_neighbors: + /obj/structure/window: + identical: true diff --git a/tools/maplint/lints/pixel_varedits.yml b/tools/maplint/lints/pixel_varedits.yml new file mode 100644 index 00000000000..444c1acbc61 --- /dev/null +++ b/tools/maplint/lints/pixel_varedits.yml @@ -0,0 +1,4 @@ +"*": + banned_variables: + - step_x + - step_y diff --git a/tools/maplint/lints/stray_item.yml b/tools/maplint/lints/stray_item.yml new file mode 100644 index 00000000000..99160739098 --- /dev/null +++ b/tools/maplint/lints/stray_item.yml @@ -0,0 +1,3 @@ +/turf/closed: + banned_neighbors: + - =/obj/item diff --git a/tools/maplint/lints/wall_stacking.yml b/tools/maplint/lints/wall_stacking.yml new file mode 100644 index 00000000000..064e15a50d8 --- /dev/null +++ b/tools/maplint/lints/wall_stacking.yml @@ -0,0 +1,5 @@ +/turf/closed: + banned_neighbors: + - /obj/machinery/door/airlock + - /obj/structure/lattice + - /obj/structure/window diff --git a/tools/maplint/lints/windoor_var_edits.yml.disabled b/tools/maplint/lints/windoor_var_edits.yml.disabled new file mode 100644 index 00000000000..bbafb5ced8c --- /dev/null +++ b/tools/maplint/lints/windoor_var_edits.yml.disabled @@ -0,0 +1,6 @@ +help: "Use the directional variants when possible." +/obj/machinery/door/window: + banned_variables: + dir: + deny: [1, 2, 4, 8] + icon_state: diff --git a/tools/maplint/lints/window_pane_varedits.yml.disabled b/tools/maplint/lints/window_pane_varedits.yml.disabled new file mode 100644 index 00000000000..e88d232a93f --- /dev/null +++ b/tools/maplint/lints/window_pane_varedits.yml.disabled @@ -0,0 +1,5 @@ +help: "Use the directional variants when possible." +/obj/structure/window: + banned_variables: + dir: + deny: [1, 2, 4, 8] diff --git a/tools/maplint/source/__init__.py b/tools/maplint/source/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/maplint/source/__main__.py b/tools/maplint/source/__main__.py new file mode 100644 index 00000000000..4509ef75893 --- /dev/null +++ b/tools/maplint/source/__main__.py @@ -0,0 +1,123 @@ +import argparse +import glob +import pathlib +import traceback +import yaml + +from . import dmm, lint +from .error import MaplintError + +def green(text): + return "\033[32m" + str(text) + "\033[0m" + +def red(text): + return "\033[31m" + str(text) + "\033[0m" + +def process_dmm(map_filename, lints: dict[str, lint.Lint]) -> list[MaplintError]: + problems: list[MaplintError] = [] + + with open(map_filename, "r") as file: + try: + map_data = dmm.parse_dmm(file) + except MaplintError as error: + problems.append(error) + # No structured data to lint. + return problems + + for lint_name, lint in lints.items(): + try: + problems.extend(lint.run(map_data)) + except KeyboardInterrupt: + raise + except Exception: + problems.append(MaplintError( + f"An exception occurred, this is either a bug in maplint or a bug in a lint. \n{traceback.format_exc()}", + lint_name, + )) + + return problems + +def print_error(message: str, filename: str, line_number: int, github_error_style: bool): + if github_error_style: + print(f"::error file={filename},line={line_number},title=DMM Linter::{message}") + else: + print(red(f"- Error parsing {filename} (line {line_number}): {message}")) + +def print_maplint_error(error: MaplintError, github_error_style: bool): + print_error( + f"{f'(in pop {error.pop_id}) ' if error.pop_id else ''}{f'(at {error.coordinates}) ' if error.coordinates else ''}{error}" + (f"\n {error.help}" if error.help is not None else ""), + error.file_name, + error.line_number, + github_error_style, + ) + +def main(args): + any_failed = False + github_error_style = args.github + + lints: dict[str, lint.Lint] = {} + + lint_base = pathlib.Path(__file__).parent.parent / "lints" + lint_filenames = [] + if args.lints is None: + lint_filenames = lint_base.glob("*.yml") + else: + lint_filenames = [lint_base / f"{lint_name}.yml" for lint_name in args.lints] + + for lint_filename in lint_filenames: + try: + lints[lint_filename] = lint.Lint(yaml.safe_load(lint_filename.read_text())) + except MaplintError as error: + print_maplint_error(error, github_error_style) + any_failed = True + except Exception: + print_error("Error loading lint file.", lint_filename, 1, github_error_style) + traceback.print_exc() + any_failed = True + + for map_filename in (args.maps or glob.glob("maps/**/*.dmm", recursive = True)): + print(map_filename, end = " ") + + success = True + all_failures: list[MaplintError] = [] + + try: + problems = process_dmm(map_filename, lints) + if len(problems) > 0: + success = False + all_failures.extend(problems) + except KeyboardInterrupt: + raise + except Exception: + success = False + + all_failures.append(MaplintError( + f"An exception occurred, this is either a bug in maplint or a bug in a lint.' {traceback.format_exc()}", + map_filename, + )) + + if success: + print(green("OK")) + else: + print(red("X")) + any_failed = True + + for failure in all_failures: + print_maplint_error(failure, github_error_style) + + if any_failed: + exit(1) + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + prog = "maplint", + description = "Checks for common errors in maps.", + ) + + parser.add_argument("maps", nargs = "*") + parser.add_argument("--lints", nargs = "*") + parser.add_argument("--github", action='store_true') + + args = parser.parse_args() + + main(args) diff --git a/tools/maplint/source/common.py b/tools/maplint/source/common.py new file mode 100644 index 00000000000..44f0abaf011 --- /dev/null +++ b/tools/maplint/source/common.py @@ -0,0 +1,40 @@ +import re +from dataclasses import dataclass + +from .error import MapParseError + +REGEX_TYPEPATH = re.compile(r'^/[\w/]+$') + +class Typepath: + path: str + segments: list[str] + + def __init__(self, path): + if not REGEX_TYPEPATH.match(path): + raise MapParseError(f"Invalid typepath {path!r}.") + + self.path = path + self.segments = path.split('/')[1:] + + def __eq__(self, other): + if not isinstance(other, Typepath): + return False + + return self.path == other.path + + def __str__(self) -> str: + return self.path + +@dataclass +class Filename: + path: str + + def __str__(self) -> str: + return self.path + +@dataclass +class Null: + def __str__(self) -> str: + return "null" + +Constant = str | float | Filename | Typepath | Null | list['Constant'] | dict['Constant', 'Constant'] diff --git a/tools/maplint/source/dmm.py b/tools/maplint/source/dmm.py new file mode 100644 index 00000000000..dd9ecc821bb --- /dev/null +++ b/tools/maplint/source/dmm.py @@ -0,0 +1,184 @@ +# I know we already have one in mapmerge, but this one can afford to be significantly simpler to interface with +# by virtue of being read-only. +import re +from dataclasses import dataclass, field +from typing import IO + +from .common import Constant, Filename, Null, Typepath +from .error import MapParseError, MaplintError + +REGEX_POP_ID = re.compile(r'^"(?P.+)" = \($') +REGEX_POP_CONTENT_HEADER = re.compile(r'^(?P[/\w]+?)(?P[{,)])$') +REGEX_ROW_BEGIN = re.compile(r'^\((?P\d+),(?P\d+),(?P\d+)\) = {"$') +REGEX_VAR_EDIT = re.compile(r'^\t(?P.+?) = (?P.+?);?$') + +@dataclass +class Content: + path: Typepath + filename: str + starting_line: int + var_edits: dict[str, Constant] = field(default_factory = dict) + +@dataclass +class DMM: + pops: dict[str, list[Content]] = field(default_factory = dict) + + # Z -> X -> Y -> Pop + turfs: list[list[list[str]]] = field(default_factory = list) + + def size(self): + return (len(self.turfs[0]), len(self.turfs[0][0])) + + def turfs_for_pop(self, key: str): + for z, z_level in enumerate(self.turfs): + for x, x_level in enumerate(z_level): + for y, turf in enumerate(x_level): + if turf == key: + yield (x, y, z) + +class DMMParser: + dmm: DMM + line = 0 + + def __init__(self, reader: IO): + self.dmm = DMM() + self.reader = reader + + def parse(self): + if "dmm2tgm" not in self.next_line(): + self.raise_error("Map isn't in TGM format. Consider using StrongDMM instead of Dream Maker.\n Please also consider installing the map merge tools, found through Install.bat in the tools/hooks folder.") + + try: + while self.parse_pop(): + pass + + while self.parse_row(): + pass + except MapParseError as error: + raise self.raise_error(error) + + return self.dmm + + def next_line(self): + self.line += 1 + + try: + return next(self.reader).removesuffix("\n") + except StopIteration: + return None + + def parse_pop(self): + line = self.next_line() + if line == "": + return False + + pop_match = REGEX_POP_ID.match(line) + if pop_match is None: + self.raise_error("Pops ended too early, expected a newline in between.") + + pop_key = pop_match.group("key") + contents = [] + + while next_line := self.next_line(): + next_line = next_line.rstrip() + content_match = REGEX_POP_CONTENT_HEADER.match(next_line) + if content_match is None: + self.raise_error("Pop content didn't lead to a path") + + content = Content(Typepath(content_match.group("path")), self.reader.name, self.line) + contents.append(content) + + content_end = content_match.group("end") + + if content_end == ")": + break + elif content_end == "{": + while (var_edit := self.parse_var_edit()) is not None: + content.var_edits[var_edit[0]] = var_edit[1] + elif content_end == ",": + continue + + self.dmm.pops[pop_key] = contents + + return True + + def parse_var_edit(self): + line = self.next_line() + if line == "\t},": + return None + + var_edit_match = REGEX_VAR_EDIT.match(line) + self.expect(var_edit_match is not None, "Var edits ended too early, expected a newline in between.") + + return (var_edit_match.group("name"), self.parse_constant(var_edit_match.group("definition"))) + + def parse_constant(self, constant): + if (float_constant := self.safe_float(constant)) is not None: + return float_constant + elif re.match(r'^/[/\w]+$', constant): + return Typepath(constant) + elif re.match(r'^".*"$', constant): + # This should do escaping in the future + return constant[1:-1] + elif re.match(r'^null$', constant): + return Null() + elif re.match(r"^'.*'$", constant): + return Filename(constant[1:-1]) + elif (list_match := re.match(r'^list\((?P.*)\)$', constant)): + return ["NYI: list"] + else: + self.raise_error(f"Unknown constant type: {constant}") + + def parse_row(self): + line = self.next_line() + + if line is None: + return False + + if line == "": + # Starting a new z level + return True + + row_match = REGEX_ROW_BEGIN.match(line) + self.expect(row_match is not None, "Rows ended too early, expected a newline in between.") + self.expect(row_match.group("y") == "1", "TGM should only be producing individual rows.") + + x = int(row_match.group("x")) - 1 + z = int(row_match.group("z")) - 1 + + if len(self.dmm.turfs) <= z: + self.dmm.turfs.append([]) + self.expect(len(self.dmm.turfs) == z + 1, "Z coordinate is not sequential") + + z_level = self.dmm.turfs[z] + self.expect(len(z_level) == x, "X coordinate is not sequential") + + contents = [] + + while (next_line := self.next_line()) is not None: + next_line = next_line.rstrip() + if next_line == '"}': + break + + self.expect(next_line in self.dmm.pops, f"Pop {next_line} is not defined") + contents.append(next_line) + + z_level.append(contents) + + return True + + def safe_float(self, value): + try: + return float(value) + except ValueError: + return None + + def expect(self, condition, message): + if not condition: + self.raise_error(message) + + def raise_error(self, message): + raise MaplintError(message, self.reader.name, self.line) + +def parse_dmm(reader: IO): + return DMMParser(reader).parse() diff --git a/tools/maplint/source/error.py b/tools/maplint/source/error.py new file mode 100644 index 00000000000..f12a629c2fd --- /dev/null +++ b/tools/maplint/source/error.py @@ -0,0 +1,28 @@ +from typing import Optional + +"""Linting error with associated filename and line number.""" +class MaplintError(Exception): + """The DMM file name the exception occurred in""" + file_name = "unknown" + + """The line the error occurred on""" + line_number = 1 + + """The optional coordinates""" + coordinates: Optional[str] = None + + """The optional pop ID""" + pop_id: Optional[str] = None + + """The optional help message""" + help: Optional[str] = None + + def __init__(self, message: str, file_name: str, line_number = 1): + Exception.__init__(self, message) + + self.file_name = file_name + self.line_number = line_number + +"""A parsing error that must be upgrading to a linting error by parse().""" +class MapParseError(Exception): + pass diff --git a/tools/maplint/source/lint.py b/tools/maplint/source/lint.py new file mode 100644 index 00000000000..59c63be1d1c --- /dev/null +++ b/tools/maplint/source/lint.py @@ -0,0 +1,496 @@ +import re +from typing import Optional, Union + +from .common import Constant, Typepath +from .dmm import DMM, Content +from .error import MaplintError, MapParseError + +def expect(condition, message): + if not condition: + raise MapParseError(message) + +"""Create an error linked to a specific content instance""" +def fail_content(content: Content, message: str) -> MaplintError: + return MaplintError(message, content.filename, content.starting_line) + +class TypepathExtra: + typepath: Typepath + exact: bool = False + wildcard: bool = False + + def __init__(self, typepath): + if typepath == '*': + self.wildcard = True + return + + if typepath.startswith('='): + self.exact = True + typepath = typepath[1:] + + self.typepath = Typepath(typepath) + + def matches_path(self, path: Typepath): + if self.wildcard: + return True + + if self.exact: + return self.typepath == path + + if len(self.typepath.segments) > len(path.segments): + return False + + return self.typepath.segments == path.segments[:len(self.typepath.segments)] + +class AtomNeighbor: + identical: bool = False + typepath: Optional[TypepathExtra] = None + pattern: Optional[re.Pattern] = None + ignore: list[TypepathExtra] = [] + + def __init__(self, typepath, data = {}): + if typepath.upper() != typepath: + self.typepath = TypepathExtra(typepath) + + if data is None: + return + + expect(isinstance(data, dict), "Banned neighbor must be a dictionary.") + + if "identical" in data: + self.identical = data.pop("identical") + expect(isinstance(self.identical, bool), "identical must be a boolean.") + + if "pattern" in data: + self.pattern = re.compile(data.pop("pattern")) + + if "ignore" in data: + ignore_data = data.pop("ignore") + expect(isinstance(ignore_data, list), "ignore must be a list of typepaths.") + self.ignore = [TypepathExtra(tp) for tp in ignore_data] + + expect(len(data) == 0, f"Unknown key in banned neighbor: {', '.join(data.keys())}.") + + def matches(self, identified: Content, neighbor: Content): + if self.identical: + if identified.path != neighbor.path: + return False + + if identified.var_edits != neighbor.var_edits: + return False + + return True + + if self.typepath is not None: + if self.typepath.matches_path(neighbor.path): + return True + + if self.pattern is not None: + if self.pattern.match(str(neighbor.path)): + return True + + return False + + def to_string(self) -> str: + if (self.typepath is not None): + return self.typepath.typepath.path + elif (self.pattern is not None): + return self.pattern.pattern + +Choices = list[Constant] | re.Pattern + +def extract_choices(data, key) -> Optional[Choices]: + if key not in data: + return None + + constants_data = data.pop(key) + + if isinstance(constants_data, list): + constants: list[Constant] = [] + + for constant_data in constants_data: + if isinstance(constant_data, str): + constants.append(constant_data) + elif isinstance(constant_data, int): + constants.append(float(constant_data)) + elif isinstance(constant_data, float): + constants.append(constant_data) + + return constants + elif isinstance(constants_data, dict): + if "pattern" in constants_data: + pattern = constants_data.pop("pattern") + return re.compile(pattern) + + raise MapParseError(f"Unknown key in {key}: {', '.join(constants_data.keys())}.") + + raise MapParseError(f"{key} must be a list of constants, or a pattern") + +class BannedVariable: + variable: str + allow: Optional[Choices] = None + deny: Optional[Choices] = None + + def __init__(self, variable, data = {}): + self.variable = variable + + if data is None: + return + + self.allow = extract_choices(data, "allow") + self.deny = extract_choices(data, "deny") + + expect(len(data) == 0, f"Unknown key in banned variable {variable}: {', '.join(data.keys())}.") + + def run(self, identified: Content) -> str: + if identified.var_edits[self.variable] is None: + return None + + if self.allow is not None: + if isinstance(self.allow, list): + if identified.var_edits[self.variable] not in self.allow: + return f"Must be one of {', '.join(map(str, self.allow))}" + elif not self.allow.match(str(identified.var_edits[self.variable])): + return f"Must match {self.allow.pattern}" + + return None + + if self.deny is not None: + if isinstance(self.deny, list): + if identified.var_edits[self.variable] in self.deny: + return f"Must not be one of {', '.join(map(str, self.deny))}" + elif self.deny.match(str(identified.var_edits[self.variable])): + return f"Must not match {self.deny.pattern}" + + return None + + return f"This variable is not allowed for this type." + +# Base class for conditional rules +class ConditionalRule: + def is_met(self, identified: Content) -> bool: + raise NotImplementedError("This method should be implemented by subclasses.") + + def match_string(self, parent_intersection: bool) -> str: + raise NotImplementedError("This method should be implemented by subclasses") + +# A single conditional expression +class WhenCondition(ConditionalRule): + condition: str + match_set: Optional[re.Match[str]] + match_not_set: Optional[re.Match[str]] + match_equal: Optional[re.Match[str]] + match_not_equal: Optional[re.Match[str]] + match_like: Optional[re.Match[str]] + + def __init__(self, condition: str): + self.condition = condition + self.match_set = re.match("(.+) is set", condition) + self.match_not_set = re.match("(.+) is not set", condition) + self.match_equal = re.match("(.+) is '(.+)'", condition) + self.match_not_equal = re.match("(.+) is not '(.+)'", condition) + self.match_like = re.match("(.+) like '(.+)'", condition) + matches = 0 + if self.match_set is not None: + matches = matches + 1 + if self.match_not_set is not None: + matches = matches + 1 + if self.match_equal is not None: + matches = matches + 1 + if self.match_not_equal is not None: + matches = matches + 1 + if self.match_like is not None: + matches = matches + 1 + if (matches != 1): + raise RuntimeError(f"Conditional rule must be either is set, is not set, is 'value', is not 'value', or like 'regex'. Instead found: {condition}") + + def is_met(self, identified: Content) -> bool: + var_edits = identified.var_edits + + if self.match_set is not None: + var_name = self.match_set.group(1) + return var_name in var_edits + + elif self.match_not_set is not None: + var_name = self.match_not_set.group(1) + return var_name not in var_edits + + elif self.match_equal is not None: + var_name = self.match_equal.group(1) + expected_value = self.match_equal.group(2) + if var_name not in var_edits: + return False + if (isinstance(var_edits[var_name], float)): + # If something is a float (number), check it as an int and a float + # Hack for integer value parsing + if var_edits[var_name] % 1 == 0: + return str(int(var_edits[var_name])).strip() == expected_value.strip() + return str(var_edits[var_name]).strip() == expected_value.strip() + + elif self.match_not_equal is not None: + var_name = self.match_not_equal.group(1) + unexpected_value = self.match_not_equal.group(2) + if var_name not in var_edits: + return True + if (isinstance(var_edits[var_name], float)): + # If something is a float (number), check it as an int and a float + # Hack for integer value parsing + if var_edits[var_name] % 1 == 0: + return str(int(var_edits[var_name])).strip() != unexpected_value.strip() + return str(var_edits[var_name]).strip() != unexpected_value.strip() + + elif self.match_like is not None: + var_name = self.match_like.group(1) + pattern = self.match_like.group(2) + return (var_name in var_edits) and re.match(pattern, str(var_edits[var_name])) + + return False + + def match_string(self, parent_intersection: bool) -> str: + return self.condition + +# A conditional group (Joining with AND and OR) +class WhenGroup(ConditionalRule): + conditions: list[ConditionalRule] + all_group: bool + + def __init__(self, conditions: list[Union[dict, str]], all_group: bool = True): + self.conditions = [self.parse_condition(condition) for condition in conditions] + self.all_group = all_group + + def parse_condition(self, condition: Union[dict, str]) -> ConditionalRule: + if isinstance(condition, dict): + if "all" in condition: + return WhenGroup(condition["all"], all_group=True) + elif "any" in condition: + return WhenGroup(condition["any"], all_group=False) + else: + raise RuntimeError(f"Unknown conditional group in when clause: {list(condition.keys())[0]}") + elif isinstance(condition, str): + return WhenCondition(condition) + else: + raise RuntimeError(f"Invalid condition type: {type(condition)}") + + def is_met(self, identified: Content) -> bool: + if self.all_group: + # For `all` group, all conditions must be met + return all(condition.is_met(identified) for condition in self.conditions) + else: + # For `any` group, only one condition must be met + return any(condition.is_met(identified) for condition in self.conditions) + + # Add parenthesis where required + def match_string(self, parent_intersection: bool) -> str: + match_symbol = " and " if self.all_group else " or " + match_text = match_symbol.join(condition.match_string(self.all_group) for condition in self.conditions); + if (self.all_group == False and parent_intersection == True and len(self.conditions) > 1): + return f"({match_text})" + else: + return match_text + +class When: + root_group: WhenGroup + + def __init__(self, conditions: list[Union[dict, str]]): + expect(isinstance(conditions, list), "when must be a list of conditions.") + # Default to 'all' group if there are multiple conditions with no explicit 'any' or 'all' + if len(conditions) > 1 and not any(isinstance(cond, dict) for cond in conditions): + self.root_group = WhenGroup(conditions, all_group=True) + else: + self.root_group = WhenGroup(conditions) + + def evaluate(self, identified: Content) -> bool: + return self.root_group.is_met(identified) + + def match_string(self) -> str: + return f" when {self.root_group.match_string(True)}"; + +class Rules: + banned: bool = False + banned_neighbors: list[AtomNeighbor] = [] + banned_variables: bool | list[BannedVariable] = [] + required_neighbors: list[AtomNeighbor] = [] + ignored_neighbors: list[AtomNeighbor] = [] + when: Optional[When] = None + skip_files: list[Union[str, re.Pattern]] = [] + + def __init__(self, data): + expect(isinstance(data, dict), "Lint rules must be a dictionary.") + + if "ignore" in data: + ignored_neighbors_data = data.pop("ignore") + expect(isinstance(ignored_neighbors_data, list), "'ignore' must be a list of typepaths.") + self.ignored_neighbors = [TypepathExtra(tp) for tp in ignored_neighbors_data] + + if "banned" in data: + self.banned = data.pop("banned") + expect(isinstance(self.banned, bool), "banned must be a boolean.") + + if "banned_neighbors" in data: + banned_neighbors_data = data.pop("banned_neighbors") + + expect(isinstance(banned_neighbors_data, list) or isinstance(banned_neighbors_data, dict), "banned_neighbors must be a list, or a dictionary keyed by type.") + + if isinstance(banned_neighbors_data, dict): + self.banned_neighbors = [AtomNeighbor(typepath, data) for typepath, data in banned_neighbors_data.items()] + else: + self.banned_neighbors = [AtomNeighbor(typepath) for typepath in banned_neighbors_data] + + if "required_neighbors" in data: + required_neighbors_data = data.pop("required_neighbors") + + expect(isinstance(required_neighbors_data, list) or isinstance(required_neighbors_data, dict), "required_neighbors must be a list, or a dictionary keyed by type.") + + if isinstance(required_neighbors_data, dict): + self.required_neighbors = [AtomNeighbor(typepath, data) for typepath, data in required_neighbors_data.items()] + else: + self.required_neighbors = [AtomNeighbor(typepath) for typepath in required_neighbors_data] + + if "banned_variables" in data: + banned_variables_data = data.pop("banned_variables") + if banned_variables_data == True: + self.banned_variables = True + else: + expect(isinstance(banned_variables_data, list) or isinstance(banned_variables_data, dict), "banned_variables must be a list, or a dictionary keyed by variable.") + + if isinstance(banned_variables_data, dict): + self.banned_variables = [BannedVariable(variable, data) for variable, data in banned_variables_data.items()] + else: + self.banned_variables = [BannedVariable(variable) for variable in banned_variables_data] + + if "when" in data: + self.when = When(data.pop("when")) + + if "skip_files" in data: + skip_files_data = data.pop("skip_files") + expect(isinstance(skip_files_data, list), "skip_files must be a list.") + self.skip_files = [] + for entry in skip_files_data: + if isinstance(entry, str): + self.skip_files.append(entry) + elif isinstance(entry, dict) and "pattern" in entry: + pattern = entry.pop("pattern") + self.skip_files.append(re.compile(pattern)) + expect(len(entry) == 0, f"Unknown key in skip_files entry: {', '.join(entry.keys())}.") + else: + raise MapParseError("skip_files entries must be strings or dicts with a 'pattern' key.") + + expect(len(data) == 0, f"Unknown lint rules: {', '.join(data.keys())}.") + + def run(self, identified: Content, contents: list[Content], identified_index) -> list[MaplintError]: + failures: list[MaplintError] = [] + when_text = self.when.match_string() if self.when is not None else "" + + if self.skip_files: + filename = getattr(identified, "filename", None) + if filename is not None: + norm = str(filename).replace("\\", "/") + for entry in self.skip_files: + if isinstance(entry, str): + if entry in norm: + return failures + else: + if entry.search(norm): + return failures + + # If a when is present and is unmet, skip evaluation of this rule + if self.when and not self.when.evaluate(identified): + return failures + + if self.banned: + failures.append(fail_content(identified, f"Typepath {identified.path} is banned{when_text}.")) + + for banned_neighbor in self.banned_neighbors: + ignored = False + for neighbor in contents[:identified_index] + contents[identified_index + 1:]: + if any(ignore.matches_path(neighbor.path) for ignore in self.ignored_neighbors): + ignored = True + break + if ignored: + continue + + for neighbor in contents[:identified_index] + contents[identified_index + 1:]: + if not banned_neighbor.matches(identified, neighbor): + continue + + failures.append(fail_content(identified, f"Typepath {identified.path} has a banned path on the same tile{when_text}: {neighbor.path}")) + + for required_neighbor in self.required_neighbors: + found = False + for neighbor in contents[:identified_index] + contents[identified_index + 1:]: + if required_neighbor.matches(identified, neighbor): + found = True + break + if found == False: + failures.append(fail_content(identified, f"Typepath {identified.path} is missing a required neighbor{when_text}: {required_neighbor.to_string()}")) + + if self.banned_variables == True: + if len(identified.var_edits) > 0: + failures.append(fail_content(identified, f"Typepath {identified.path} should not have any variable edits{when_text}.")) + else: + assert isinstance(self.banned_variables, list) + for banned_variable in self.banned_variables: + if banned_variable.variable in identified.var_edits: + ban_reason = banned_variable.run(identified) + if ban_reason is None: + continue + failures.append(fail_content(identified, f"Typepath {identified.path} has a banned variable (set to {identified.var_edits[banned_variable.variable]}){when_text}: {banned_variable.variable}. {ban_reason}")) + + return failures + +class Lint: + help: Optional[str] = None + rules: dict[TypepathExtra, Rules] + disabled: bool = False + + def __init__(self, data): + expect(isinstance(data, dict), "Lint must be a dictionary.") + + if "help" in data: + self.help = data.pop("help") + + expect(isinstance(self.help, str) or self.help is None, "Lint help must be a string.") + + self.rules = {} + + for typepath, rules in data.items(): + self.rules[TypepathExtra(typepath)] = Rules(rules) + + def run(self, map_data: DMM) -> list[MaplintError]: + all_failures: list[MaplintError] = [] + (width, height) = map_data.size() + + for pop, contents in map_data.pops.items(): + for typepath_extra, rules in self.rules.items(): + for content_index, content in enumerate(contents): + if not typepath_extra.matches_path(content.path): + continue + + failures = rules.run(content, contents, content_index) + if len(failures) == 0: + continue + + coordinates = map_data.turfs_for_pop(pop) + coordinate_texts = [] + + for _ in range(3): + coordinate = next(coordinates, None) + if coordinate is None: + break + + x = coordinate[0] + 1 + y = height - coordinate[1] + z = coordinate[2] + 1 + + coordinate_texts.append(f"({x}, {y}, {z})") + + leftover_coordinates = sum(1 for _ in coordinates) + if leftover_coordinates > 0: + coordinate_texts.append(f"and {leftover_coordinates} more") + + for failure in failures: + failure.coordinates = ', '.join(coordinate_texts) + failure.help = self.help + failure.pop_id = pop + all_failures.append(failure) + + return list(set(all_failures)) diff --git a/tools/midi2piano/MidiDependencies/ __init__.py b/tools/midi2piano/MidiDependencies/ __init__.py new file mode 100644 index 00000000000..a585f127f18 --- /dev/null +++ b/tools/midi2piano/MidiDependencies/ __init__.py @@ -0,0 +1 @@ +from MidiDependencies.midi import * diff --git a/tools/midi2piano/midi/midi.py b/tools/midi2piano/MidiDependencies/midi.py similarity index 99% rename from tools/midi2piano/midi/midi.py rename to tools/midi2piano/MidiDependencies/midi.py index c1c6df64078..ef0e2e67a81 100644 --- a/tools/midi2piano/midi/midi.py +++ b/tools/midi2piano/MidiDependencies/midi.py @@ -1645,4 +1645,3 @@ def _encode(events_lol, unknown_callback=None, never_add_eot=False, data.append(_ber_compressed_int(dtime)+event_data) return b''.join(data) - diff --git a/tools/midi2piano/README.txt b/tools/midi2piano/README.txt index 798e9ba00a7..1314bc6b68e 100644 --- a/tools/midi2piano/README.txt +++ b/tools/midi2piano/README.txt @@ -23,11 +23,10 @@ OCTAVE_TRANSPOSE - amount of octaves you melody will be shifted by FLOAT_PRECISION - read comment Additional notes: -1. Unlike previous midi2piano, this tool optimizes sheet music to fit more in less lines. +1. Unlike previous midi2piano, this tool optimizes sheet music to fit more in less lines. 2. If two notes are less than 50 ms apart, they are chorded. BYOND works in 1/10th of a second so 50 ms is time quanta. 4. MIDI event set_tempo is NOT supported. If your MIDI file uses set_tempo to change BPM significantly, consider using some other midi file. This tool is considered final. -Made by EditorRUS/Delta Epsilon from Animus Station, ss13.ru -Contact me in Discord if you find any major issues: DeltaEpsilon#7787 \ No newline at end of file +Made by Delta Epsilon from Animus Station, ss13.ru diff --git a/tools/midi2piano/easygui/__init__.py b/tools/midi2piano/easygui/__init__.py deleted file mode 100644 index bf746ce4a91..00000000000 --- a/tools/midi2piano/easygui/__init__.py +++ /dev/null @@ -1,2492 +0,0 @@ -""" -@version: 0.96(2010-08-29) - -@note: -ABOUT EASYGUI - -EasyGui provides an easy-to-use interface for simple GUI interaction -with a user. It does not require the programmer to know anything about -tkinter, frames, widgets, callbacks or lambda. All GUI interactions are -invoked by simple function calls that return results. - -@note: -WARNING about using EasyGui with IDLE - -You may encounter problems using IDLE to run programs that use EasyGui. Try it -and find out. EasyGui is a collection of Tkinter routines that run their own -event loops. IDLE is also a Tkinter application, with its own event loop. The -two may conflict, with unpredictable results. If you find that you have -problems, try running your EasyGui program outside of IDLE. - -Note that EasyGui requires Tk release 8.0 or greater. - -@note: -LICENSE INFORMATION - -EasyGui version 0.96 - -Copyright (c) 2010, Stephen Raymond Ferg - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - - 3. The name of the author may not be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, -STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING -IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, -EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -@note: -ABOUT THE EASYGUI LICENSE - -This license is what is generally known as the "modified BSD license", -aka "revised BSD", "new BSD", "3-clause BSD". -See http://www.opensource.org/licenses/bsd-license.php - -This license is GPL-compatible. -See http://en.wikipedia.org/wiki/License_compatibility -See http://www.gnu.org/licenses/license-list.html#GPLCompatibleLicenses - -The BSD License is less restrictive than GPL. -It allows software released under the license to be incorporated into proprietary products. -Works based on the software may be released under a proprietary license or as closed source software. -http://en.wikipedia.org/wiki/BSD_licenses#3-clause_license_.28.22New_BSD_License.22.29 - -""" -egversion = __doc__.split()[1] - -__all__ = ['ynbox' - , 'ccbox' - , 'boolbox' - , 'indexbox' - , 'msgbox' - , 'buttonbox' - , 'integerbox' - , 'multenterbox' - , 'enterbox' - , 'exceptionbox' - , 'choicebox' - , 'codebox' - , 'textbox' - , 'diropenbox' - , 'fileopenbox' - , 'filesavebox' - , 'passwordbox' - , 'multpasswordbox' - , 'multchoicebox' - , 'abouteasygui' - , 'egversion' - , 'egdemo' - , 'EgStore' - ] - -import sys, os -import string -import pickle -import traceback - - -#-------------------------------------------------- -# check python version and take appropriate action -#-------------------------------------------------- -""" -From the python documentation: - -sys.hexversion contains the version number encoded as a single integer. This is -guaranteed to increase with each version, including proper support for non- -production releases. For example, to test that the Python interpreter is at -least version 1.5.2, use: - -if sys.hexversion >= 0x010502F0: - # use some advanced feature - ... -else: - # use an alternative implementation or warn the user - ... -""" - - -if sys.hexversion >= 0x020600F0: - runningPython26 = True -else: - runningPython26 = False - -if sys.hexversion >= 0x030000F0: - runningPython3 = True -else: - runningPython3 = False - -try: - from PIL import Image as PILImage - from PIL import ImageTk as PILImageTk - PILisLoaded = True -except: - PILisLoaded = False - - -if runningPython3: - from tkinter import * - import tkinter.filedialog as tk_FileDialog - from io import StringIO -else: - from Tkinter import * - import tkFileDialog as tk_FileDialog - from StringIO import StringIO - -def write(*args): - args = [str(arg) for arg in args] - args = " ".join(args) - sys.stdout.write(args) - -def writeln(*args): - write(*args) - sys.stdout.write("\n") - -say = writeln - - -if TkVersion < 8.0 : - stars = "*"*75 - writeln("""\n\n\n""" + stars + """ -You are running Tk version: """ + str(TkVersion) + """ -You must be using Tk version 8.0 or greater to use EasyGui. -Terminating. -""" + stars + """\n\n\n""") - sys.exit(0) - -def dq(s): - return '"%s"' % s - -rootWindowPosition = "+300+200" - -PROPORTIONAL_FONT_FAMILY = ("MS", "Sans", "Serif") -MONOSPACE_FONT_FAMILY = ("Courier") - -PROPORTIONAL_FONT_SIZE = 10 -MONOSPACE_FONT_SIZE = 9 #a little smaller, because it it more legible at a smaller size -TEXT_ENTRY_FONT_SIZE = 12 # a little larger makes it easier to see - -#STANDARD_SELECTION_EVENTS = ["Return", "Button-1"] -STANDARD_SELECTION_EVENTS = ["Return", "Button-1", "space"] - -# Initialize some global variables that will be reset later -__choiceboxMultipleSelect = None -__widgetTexts = None -__replyButtonText = None -__choiceboxResults = None -__firstWidget = None -__enterboxText = None -__enterboxDefaultText="" -__multenterboxText = "" -choiceboxChoices = None -choiceboxWidget = None -entryWidget = None -boxRoot = None -ImageErrorMsg = ( - "\n\n---------------------------------------------\n" - "Error: %s\n%s") -#------------------------------------------------------------------- -# various boxes built on top of the basic buttonbox -#----------------------------------------------------------------------- - -#----------------------------------------------------------------------- -# ynbox -#----------------------------------------------------------------------- -def ynbox(msg="Shall I continue?" - , title=" " - , choices=("Yes", "No") - , image=None - ): - """ - Display a msgbox with choices of Yes and No. - - The default is "Yes". - - The returned value is calculated this way:: - if the first choice ("Yes") is chosen, or if the dialog is cancelled: - return 1 - else: - return 0 - - If invoked without a msg argument, displays a generic request for a confirmation - that the user wishes to continue. So it can be used this way:: - if ynbox(): pass # continue - else: sys.exit(0) # exit the program - - @arg msg: the msg to be displayed. - @arg title: the window title - @arg choices: a list or tuple of the choices to be displayed - """ - return boolbox(msg, title, choices, image=image) - - -#----------------------------------------------------------------------- -# ccbox -#----------------------------------------------------------------------- -def ccbox(msg="Shall I continue?" - , title=" " - , choices=("Continue", "Cancel") - , image=None - ): - """ - Display a msgbox with choices of Continue and Cancel. - - The default is "Continue". - - The returned value is calculated this way:: - if the first choice ("Continue") is chosen, or if the dialog is cancelled: - return 1 - else: - return 0 - - If invoked without a msg argument, displays a generic request for a confirmation - that the user wishes to continue. So it can be used this way:: - - if ccbox(): - pass # continue - else: - sys.exit(0) # exit the program - - @arg msg: the msg to be displayed. - @arg title: the window title - @arg choices: a list or tuple of the choices to be displayed - """ - return boolbox(msg, title, choices, image=image) - - -#----------------------------------------------------------------------- -# boolbox -#----------------------------------------------------------------------- -def boolbox(msg="Shall I continue?" - , title=" " - , choices=("Yes","No") - , image=None - ): - """ - Display a boolean msgbox. - - The default is the first choice. - - The returned value is calculated this way:: - if the first choice is chosen, or if the dialog is cancelled: - returns 1 - else: - returns 0 - """ - reply = buttonbox(msg=msg, choices=choices, title=title, image=image) - if reply == choices[0]: return 1 - else: return 0 - - -#----------------------------------------------------------------------- -# indexbox -#----------------------------------------------------------------------- -def indexbox(msg="Shall I continue?" - , title=" " - , choices=("Yes","No") - , image=None - ): - """ - Display a buttonbox with the specified choices. - Return the index of the choice selected. - """ - reply = buttonbox(msg=msg, choices=choices, title=title, image=image) - index = -1 - for choice in choices: - index = index + 1 - if reply == choice: return index - raise AssertionError( - "There is a program logic error in the EasyGui code for indexbox.") - - -#----------------------------------------------------------------------- -# msgbox -#----------------------------------------------------------------------- -def msgbox(msg="(Your message goes here)", title=" ", ok_button="OK",image=None,root=None): - """ - Display a messagebox - """ - if type(ok_button) != type("OK"): - raise AssertionError("The 'ok_button' argument to msgbox must be a string.") - - return buttonbox(msg=msg, title=title, choices=[ok_button], image=image,root=root) - - -#------------------------------------------------------------------- -# buttonbox -#------------------------------------------------------------------- -def buttonbox(msg="",title=" " - ,choices=("Button1", "Button2", "Button3") - , image=None - , root=None - ): - """ - Display a msg, a title, and a set of buttons. - The buttons are defined by the members of the choices list. - Return the text of the button that the user selected. - - @arg msg: the msg to be displayed. - @arg title: the window title - @arg choices: a list or tuple of the choices to be displayed - """ - global boxRoot, __replyButtonText, __widgetTexts, buttonsFrame - - - # Initialize __replyButtonText to the first choice. - # This is what will be used if the window is closed by the close button. - __replyButtonText = choices[0] - - if root: - root.withdraw() - boxRoot = Toplevel(master=root) - boxRoot.withdraw() - else: - boxRoot = Tk() - boxRoot.withdraw() - - boxRoot.protocol('WM_DELETE_WINDOW', denyWindowManagerClose ) - boxRoot.title(title) - boxRoot.iconname('Dialog') - boxRoot.geometry(rootWindowPosition) - boxRoot.minsize(400, 100) - - # ------------- define the messageFrame --------------------------------- - messageFrame = Frame(master=boxRoot) - messageFrame.pack(side=TOP, fill=BOTH) - - # ------------- define the imageFrame --------------------------------- - tk_Image = None - if image: - imageFilename = os.path.normpath(image) - junk,ext = os.path.splitext(imageFilename) - - if os.path.exists(imageFilename): - if ext.lower() in [".gif", ".pgm", ".ppm"]: - tk_Image = PhotoImage(master=boxRoot, file=imageFilename) - else: - if PILisLoaded: - try: - pil_Image = PILImage.open(imageFilename) - tk_Image = PILImageTk.PhotoImage(pil_Image, master=boxRoot) - except: - msg += ImageErrorMsg % (imageFilename, - "\nThe Python Imaging Library (PIL) could not convert this file to a displayable image." - "\n\nPIL reports:\n" + exception_format()) - - else: # PIL is not loaded - msg += ImageErrorMsg % (imageFilename, - "\nI could not import the Python Imaging Library (PIL) to display the image.\n\n" - "You may need to install PIL\n" - "(http://www.pythonware.com/products/pil/)\n" - "to display " + ext + " image files.") - - else: - msg += ImageErrorMsg % (imageFilename, "\nImage file not found.") - - if tk_Image: - imageFrame = Frame(master=boxRoot) - imageFrame.pack(side=TOP, fill=BOTH) - label = Label(imageFrame,image=tk_Image) - label.image = tk_Image # keep a reference! - label.pack(side=TOP, expand=YES, fill=X, padx='1m', pady='1m') - - # ------------- define the buttonsFrame --------------------------------- - buttonsFrame = Frame(master=boxRoot) - buttonsFrame.pack(side=TOP, fill=BOTH) - - # -------------------- place the widgets in the frames ----------------------- - messageWidget = Message(messageFrame, text=msg, width=400) - messageWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE)) - messageWidget.pack(side=TOP, expand=YES, fill=X, padx='3m', pady='3m') - - __put_buttons_in_buttonframe(choices) - - # -------------- the action begins ----------- - # put the focus on the first button - __firstWidget.focus_force() - - boxRoot.deiconify() - boxRoot.mainloop() - boxRoot.destroy() - if root: root.deiconify() - return __replyButtonText - - -#------------------------------------------------------------------- -# integerbox -#------------------------------------------------------------------- -def integerbox(msg="" - , title=" " - , default="" - , lowerbound=0 - , upperbound=99 - , image = None - , root = None - , **invalidKeywordArguments - ): - """ - Show a box in which a user can enter an integer. - - In addition to arguments for msg and title, this function accepts - integer arguments for "default", "lowerbound", and "upperbound". - - The default argument may be None. - - When the user enters some text, the text is checked to verify that it - can be converted to an integer between the lowerbound and upperbound. - - If it can be, the integer (not the text) is returned. - - If it cannot, then an error msg is displayed, and the integerbox is - redisplayed. - - If the user cancels the operation, None is returned. - - NOTE that the "argLowerBound" and "argUpperBound" arguments are no longer - supported. They have been replaced by "upperbound" and "lowerbound". - """ - if "argLowerBound" in invalidKeywordArguments: - raise AssertionError( - "\nintegerbox no longer supports the 'argLowerBound' argument.\n" - + "Use 'lowerbound' instead.\n\n") - if "argUpperBound" in invalidKeywordArguments: - raise AssertionError( - "\nintegerbox no longer supports the 'argUpperBound' argument.\n" - + "Use 'upperbound' instead.\n\n") - - if default != "": - if type(default) != type(1): - raise AssertionError( - "integerbox received a non-integer value for " - + "default of " + dq(str(default)) , "Error") - - if type(lowerbound) != type(1): - raise AssertionError( - "integerbox received a non-integer value for " - + "lowerbound of " + dq(str(lowerbound)) , "Error") - - if type(upperbound) != type(1): - raise AssertionError( - "integerbox received a non-integer value for " - + "upperbound of " + dq(str(upperbound)) , "Error") - - if msg == "": - msg = ("Enter an integer between " + str(lowerbound) - + " and " - + str(upperbound) - ) - - while 1: - reply = enterbox(msg, title, str(default), image=image, root=root) - if reply == None: return None - - try: - reply = int(reply) - except: - msgbox ("The value that you entered:\n\t%s\nis not an integer." % dq(str(reply)) - , "Error") - continue - - if reply < lowerbound: - msgbox ("The value that you entered is less than the lower bound of " - + str(lowerbound) + ".", "Error") - continue - - if reply > upperbound: - msgbox ("The value that you entered is greater than the upper bound of " - + str(upperbound) + ".", "Error") - continue - - # reply has passed all validation checks. - # It is an integer between the specified bounds. - return reply - -#------------------------------------------------------------------- -# multenterbox -#------------------------------------------------------------------- -def multenterbox(msg="Fill in values for the fields." - , title=" " - , fields=() - , values=() - ): - r""" - Show screen with multiple data entry fields. - - If there are fewer values than names, the list of values is padded with - empty strings until the number of values is the same as the number of names. - - If there are more values than names, the list of values - is truncated so that there are as many values as names. - - Returns a list of the values of the fields, - or None if the user cancels the operation. - - Here is some example code, that shows how values returned from - multenterbox can be checked for validity before they are accepted:: - ---------------------------------------------------------------------- - msg = "Enter your personal information" - title = "Credit Card Application" - fieldNames = ["Name","Street Address","City","State","ZipCode"] - fieldValues = [] # we start with blanks for the values - fieldValues = multenterbox(msg,title, fieldNames) - - # make sure that none of the fields was left blank - while 1: - if fieldValues == None: break - errmsg = "" - for i in range(len(fieldNames)): - if fieldValues[i].strip() == "": - errmsg += ('"%s" is a required field.\n\n' % fieldNames[i]) - if errmsg == "": - break # no problems found - fieldValues = multenterbox(errmsg, title, fieldNames, fieldValues) - - writeln("Reply was: %s" % str(fieldValues)) - ---------------------------------------------------------------------- - - @arg msg: the msg to be displayed. - @arg title: the window title - @arg fields: a list of fieldnames. - @arg values: a list of field values - """ - return __multfillablebox(msg,title,fields,values,None) - - -#----------------------------------------------------------------------- -# multpasswordbox -#----------------------------------------------------------------------- -def multpasswordbox(msg="Fill in values for the fields." - , title=" " - , fields=tuple() - ,values=tuple() - ): - r""" - Same interface as multenterbox. But in multpassword box, - the last of the fields is assumed to be a password, and - is masked with asterisks. - - Example - ======= - - Here is some example code, that shows how values returned from - multpasswordbox can be checked for validity before they are accepted:: - msg = "Enter logon information" - title = "Demo of multpasswordbox" - fieldNames = ["Server ID", "User ID", "Password"] - fieldValues = [] # we start with blanks for the values - fieldValues = multpasswordbox(msg,title, fieldNames) - - # make sure that none of the fields was left blank - while 1: - if fieldValues == None: break - errmsg = "" - for i in range(len(fieldNames)): - if fieldValues[i].strip() == "": - errmsg = errmsg + ('"%s" is a required field.\n\n' % fieldNames[i]) - if errmsg == "": break # no problems found - fieldValues = multpasswordbox(errmsg, title, fieldNames, fieldValues) - - writeln("Reply was: %s" % str(fieldValues)) - """ - return __multfillablebox(msg,title,fields,values,"*") - -def bindArrows(widget): - widget.bind("", tabRight) - widget.bind("" , tabLeft) - - widget.bind("",tabRight) - widget.bind("" , tabLeft) - -def tabRight(event): - boxRoot.event_generate("") - -def tabLeft(event): - boxRoot.event_generate("") - -#----------------------------------------------------------------------- -# __multfillablebox -#----------------------------------------------------------------------- -def __multfillablebox(msg="Fill in values for the fields." - , title=" " - , fields=() - , values=() - , mask = None - ): - global boxRoot, __multenterboxText, __multenterboxDefaultText, cancelButton, entryWidget, okButton - - choices = ["OK", "Cancel"] - if len(fields) == 0: return None - - fields = list(fields[:]) # convert possible tuples to a list - values = list(values[:]) # convert possible tuples to a list - - if len(values) == len(fields): pass - elif len(values) > len(fields): - fields = fields[0:len(values)] - else: - while len(values) < len(fields): - values.append("") - - boxRoot = Tk() - - boxRoot.protocol('WM_DELETE_WINDOW', denyWindowManagerClose ) - boxRoot.title(title) - boxRoot.iconname('Dialog') - boxRoot.geometry(rootWindowPosition) - boxRoot.bind("", __multenterboxCancel) - - # -------------------- put subframes in the boxRoot -------------------- - messageFrame = Frame(master=boxRoot) - messageFrame.pack(side=TOP, fill=BOTH) - - #-------------------- the msg widget ---------------------------- - messageWidget = Message(messageFrame, width="4.5i", text=msg) - messageWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE)) - messageWidget.pack(side=RIGHT, expand=1, fill=BOTH, padx='3m', pady='3m') - - global entryWidgets - entryWidgets = [] - - lastWidgetIndex = len(fields) - 1 - - for widgetIndex in range(len(fields)): - argFieldName = fields[widgetIndex] - argFieldValue = values[widgetIndex] - entryFrame = Frame(master=boxRoot) - entryFrame.pack(side=TOP, fill=BOTH) - - # --------- entryWidget ---------------------------------------------- - labelWidget = Label(entryFrame, text=argFieldName) - labelWidget.pack(side=LEFT) - - entryWidget = Entry(entryFrame, width=40,highlightthickness=2) - entryWidgets.append(entryWidget) - entryWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,TEXT_ENTRY_FONT_SIZE)) - entryWidget.pack(side=RIGHT, padx="3m") - - bindArrows(entryWidget) - - entryWidget.bind("", __multenterboxGetText) - entryWidget.bind("", __multenterboxCancel) - - # for the last entryWidget, if this is a multpasswordbox, - # show the contents as just asterisks - if widgetIndex == lastWidgetIndex: - if mask: - entryWidgets[widgetIndex].configure(show=mask) - - # put text into the entryWidget - entryWidgets[widgetIndex].insert(0,argFieldValue) - widgetIndex += 1 - - # ------------------ ok button ------------------------------- - buttonsFrame = Frame(master=boxRoot) - buttonsFrame.pack(side=BOTTOM, fill=BOTH) - - okButton = Button(buttonsFrame, takefocus=1, text="OK") - bindArrows(okButton) - okButton.pack(expand=1, side=LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') - - # for the commandButton, bind activation events to the activation event handler - commandButton = okButton - handler = __multenterboxGetText - for selectionEvent in STANDARD_SELECTION_EVENTS: - commandButton.bind("<%s>" % selectionEvent, handler) - - - # ------------------ cancel button ------------------------------- - cancelButton = Button(buttonsFrame, takefocus=1, text="Cancel") - bindArrows(cancelButton) - cancelButton.pack(expand=1, side=RIGHT, padx='3m', pady='3m', ipadx='2m', ipady='1m') - - # for the commandButton, bind activation events to the activation event handler - commandButton = cancelButton - handler = __multenterboxCancel - for selectionEvent in STANDARD_SELECTION_EVENTS: - commandButton.bind("<%s>" % selectionEvent, handler) - - - # ------------------- time for action! ----------------- - entryWidgets[0].focus_force() # put the focus on the entryWidget - boxRoot.mainloop() # run it! - - # -------- after the run has completed ---------------------------------- - boxRoot.destroy() # button_click didn't destroy boxRoot, so we do it now - return __multenterboxText - - -#----------------------------------------------------------------------- -# __multenterboxGetText -#----------------------------------------------------------------------- -def __multenterboxGetText(event): - global __multenterboxText - - __multenterboxText = [] - for entryWidget in entryWidgets: - __multenterboxText.append(entryWidget.get()) - boxRoot.quit() - - -def __multenterboxCancel(event): - global __multenterboxText - __multenterboxText = None - boxRoot.quit() - - -#------------------------------------------------------------------- -# enterbox -#------------------------------------------------------------------- -def enterbox(msg="Enter something." - , title=" " - , default="" - , strip=True - , image=None - , root=None - ): - """ - Show a box in which a user can enter some text. - - You may optionally specify some default text, which will appear in the - enterbox when it is displayed. - - Returns the text that the user entered, or None if he cancels the operation. - - By default, enterbox strips its result (i.e. removes leading and trailing - whitespace). (If you want it not to strip, use keyword argument: strip=False.) - This makes it easier to test the results of the call:: - - reply = enterbox(....) - if reply: - ... - else: - ... - """ - result = __fillablebox(msg, title, default=default, mask=None,image=image,root=root) - if result and strip: - result = result.strip() - return result - - -def passwordbox(msg="Enter your password." - , title=" " - , default="" - , image=None - , root=None - ): - """ - Show a box in which a user can enter a password. - The text is masked with asterisks, so the password is not displayed. - Returns the text that the user entered, or None if he cancels the operation. - """ - return __fillablebox(msg, title, default, mask="*",image=image,root=root) - - -def __fillablebox(msg - , title="" - , default="" - , mask=None - , image=None - , root=None - ): - """ - Show a box in which a user can enter some text. - You may optionally specify some default text, which will appear in the - enterbox when it is displayed. - Returns the text that the user entered, or None if he cancels the operation. - """ - - global boxRoot, __enterboxText, __enterboxDefaultText - global cancelButton, entryWidget, okButton - - if title == None: title == "" - if default == None: default = "" - __enterboxDefaultText = default - __enterboxText = __enterboxDefaultText - - if root: - root.withdraw() - boxRoot = Toplevel(master=root) - boxRoot.withdraw() - else: - boxRoot = Tk() - boxRoot.withdraw() - - boxRoot.protocol('WM_DELETE_WINDOW', denyWindowManagerClose ) - boxRoot.title(title) - boxRoot.iconname('Dialog') - boxRoot.geometry(rootWindowPosition) - boxRoot.bind("", __enterboxCancel) - - # ------------- define the messageFrame --------------------------------- - messageFrame = Frame(master=boxRoot) - messageFrame.pack(side=TOP, fill=BOTH) - - # ------------- define the imageFrame --------------------------------- - tk_Image = None - if image: - imageFilename = os.path.normpath(image) - junk,ext = os.path.splitext(imageFilename) - - if os.path.exists(imageFilename): - if ext.lower() in [".gif", ".pgm", ".ppm"]: - tk_Image = PhotoImage(master=boxRoot, file=imageFilename) - else: - if PILisLoaded: - try: - pil_Image = PILImage.open(imageFilename) - tk_Image = PILImageTk.PhotoImage(pil_Image, master=boxRoot) - except: - msg += ImageErrorMsg % (imageFilename, - "\nThe Python Imaging Library (PIL) could not convert this file to a displayable image." - "\n\nPIL reports:\n" + exception_format()) - - else: # PIL is not loaded - msg += ImageErrorMsg % (imageFilename, - "\nI could not import the Python Imaging Library (PIL) to display the image.\n\n" - "You may need to install PIL\n" - "(http://www.pythonware.com/products/pil/)\n" - "to display " + ext + " image files.") - - else: - msg += ImageErrorMsg % (imageFilename, "\nImage file not found.") - - if tk_Image: - imageFrame = Frame(master=boxRoot) - imageFrame.pack(side=TOP, fill=BOTH) - label = Label(imageFrame,image=tk_Image) - label.image = tk_Image # keep a reference! - label.pack(side=TOP, expand=YES, fill=X, padx='1m', pady='1m') - - # ------------- define the buttonsFrame --------------------------------- - buttonsFrame = Frame(master=boxRoot) - buttonsFrame.pack(side=TOP, fill=BOTH) - - - # ------------- define the entryFrame --------------------------------- - entryFrame = Frame(master=boxRoot) - entryFrame.pack(side=TOP, fill=BOTH) - - # ------------- define the buttonsFrame --------------------------------- - buttonsFrame = Frame(master=boxRoot) - buttonsFrame.pack(side=TOP, fill=BOTH) - - #-------------------- the msg widget ---------------------------- - messageWidget = Message(messageFrame, width="4.5i", text=msg) - messageWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE)) - messageWidget.pack(side=RIGHT, expand=1, fill=BOTH, padx='3m', pady='3m') - - # --------- entryWidget ---------------------------------------------- - entryWidget = Entry(entryFrame, width=40) - bindArrows(entryWidget) - entryWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,TEXT_ENTRY_FONT_SIZE)) - if mask: - entryWidget.configure(show=mask) - entryWidget.pack(side=LEFT, padx="3m") - entryWidget.bind("", __enterboxGetText) - entryWidget.bind("", __enterboxCancel) - # put text into the entryWidget - entryWidget.insert(0,__enterboxDefaultText) - - # ------------------ ok button ------------------------------- - okButton = Button(buttonsFrame, takefocus=1, text="OK") - bindArrows(okButton) - okButton.pack(expand=1, side=LEFT, padx='3m', pady='3m', ipadx='2m', ipady='1m') - - # for the commandButton, bind activation events to the activation event handler - commandButton = okButton - handler = __enterboxGetText - for selectionEvent in STANDARD_SELECTION_EVENTS: - commandButton.bind("<%s>" % selectionEvent, handler) - - - # ------------------ cancel button ------------------------------- - cancelButton = Button(buttonsFrame, takefocus=1, text="Cancel") - bindArrows(cancelButton) - cancelButton.pack(expand=1, side=RIGHT, padx='3m', pady='3m', ipadx='2m', ipady='1m') - - # for the commandButton, bind activation events to the activation event handler - commandButton = cancelButton - handler = __enterboxCancel - for selectionEvent in STANDARD_SELECTION_EVENTS: - commandButton.bind("<%s>" % selectionEvent, handler) - - # ------------------- time for action! ----------------- - entryWidget.focus_force() # put the focus on the entryWidget - boxRoot.deiconify() - boxRoot.mainloop() # run it! - - # -------- after the run has completed ---------------------------------- - if root: root.deiconify() - boxRoot.destroy() # button_click didn't destroy boxRoot, so we do it now - return __enterboxText - - -def __enterboxGetText(event): - global __enterboxText - - __enterboxText = entryWidget.get() - boxRoot.quit() - - -def __enterboxRestore(event): - global entryWidget - - entryWidget.delete(0,len(entryWidget.get())) - entryWidget.insert(0, __enterboxDefaultText) - - -def __enterboxCancel(event): - global __enterboxText - - __enterboxText = None - boxRoot.quit() - -def denyWindowManagerClose(): - """ don't allow WindowManager close - """ - x = Tk() - x.withdraw() - x.bell() - x.destroy() - - - -#------------------------------------------------------------------- -# multchoicebox -#------------------------------------------------------------------- -def multchoicebox(msg="Pick as many items as you like." - , title=" " - , choices=() - , **kwargs - ): - """ - Present the user with a list of choices. - allow him to select multiple items and return them in a list. - if the user doesn't choose anything from the list, return the empty list. - return None if he cancelled selection. - - @arg msg: the msg to be displayed. - @arg title: the window title - @arg choices: a list or tuple of the choices to be displayed - """ - if len(choices) == 0: choices = ["Program logic error - no choices were specified."] - - global __choiceboxMultipleSelect - __choiceboxMultipleSelect = 1 - return __choicebox(msg, title, choices) - - -#----------------------------------------------------------------------- -# choicebox -#----------------------------------------------------------------------- -def choicebox(msg="Pick something." - , title=" " - , choices=() - ): - """ - Present the user with a list of choices. - return the choice that he selects. - return None if he cancels the selection selection. - - @arg msg: the msg to be displayed. - @arg title: the window title - @arg choices: a list or tuple of the choices to be displayed - """ - if len(choices) == 0: choices = ["Program logic error - no choices were specified."] - - global __choiceboxMultipleSelect - __choiceboxMultipleSelect = 0 - return __choicebox(msg,title,choices) - - -#----------------------------------------------------------------------- -# __choicebox -#----------------------------------------------------------------------- -def __choicebox(msg - , title - , choices - ): - """ - internal routine to support choicebox() and multchoicebox() - """ - global boxRoot, __choiceboxResults, choiceboxWidget, defaultText - global choiceboxWidget, choiceboxChoices - #------------------------------------------------------------------- - # If choices is a tuple, we make it a list so we can sort it. - # If choices is already a list, we make a new list, so that when - # we sort the choices, we don't affect the list object that we - # were given. - #------------------------------------------------------------------- - choices = list(choices[:]) - if len(choices) == 0: - choices = ["Program logic error - no choices were specified."] - defaultButtons = ["OK", "Cancel"] - - # make sure all choices are strings - for index in range(len(choices)): - choices[index] = str(choices[index]) - - lines_to_show = min(len(choices), 20) - lines_to_show = 20 - - if title == None: title = "" - - # Initialize __choiceboxResults - # This is the value that will be returned if the user clicks the close icon - __choiceboxResults = None - - boxRoot = Tk() - boxRoot.protocol('WM_DELETE_WINDOW', denyWindowManagerClose ) - screen_width = boxRoot.winfo_screenwidth() - screen_height = boxRoot.winfo_screenheight() - root_width = int((screen_width * 0.8)) - root_height = int((screen_height * 0.5)) - root_xpos = int((screen_width * 0.1)) - root_ypos = int((screen_height * 0.05)) - - boxRoot.title(title) - boxRoot.iconname('Dialog') - rootWindowPosition = "+0+0" - boxRoot.geometry(rootWindowPosition) - boxRoot.expand=NO - boxRoot.minsize(root_width, root_height) - rootWindowPosition = "+" + str(root_xpos) + "+" + str(root_ypos) - boxRoot.geometry(rootWindowPosition) - - # ---------------- put the frames in the window ----------------------------------------- - message_and_buttonsFrame = Frame(master=boxRoot) - message_and_buttonsFrame.pack(side=TOP, fill=X, expand=NO) - - messageFrame = Frame(message_and_buttonsFrame) - messageFrame.pack(side=LEFT, fill=X, expand=YES) - #messageFrame.pack(side=TOP, fill=X, expand=YES) - - buttonsFrame = Frame(message_and_buttonsFrame) - buttonsFrame.pack(side=RIGHT, expand=NO, pady=0) - #buttonsFrame.pack(side=TOP, expand=YES, pady=0) - - choiceboxFrame = Frame(master=boxRoot) - choiceboxFrame.pack(side=BOTTOM, fill=BOTH, expand=YES) - - # -------------------------- put the widgets in the frames ------------------------------ - - # ---------- put a msg widget in the msg frame------------------- - messageWidget = Message(messageFrame, anchor=NW, text=msg, width=int(root_width * 0.9)) - messageWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE)) - messageWidget.pack(side=LEFT, expand=YES, fill=BOTH, padx='1m', pady='1m') - - # -------- put the choiceboxWidget in the choiceboxFrame --------------------------- - choiceboxWidget = Listbox(choiceboxFrame - , height=lines_to_show - , borderwidth="1m" - , relief="flat" - , bg="white" - ) - - if __choiceboxMultipleSelect: - choiceboxWidget.configure(selectmode=MULTIPLE) - - choiceboxWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE)) - - # add a vertical scrollbar to the frame - rightScrollbar = Scrollbar(choiceboxFrame, orient=VERTICAL, command=choiceboxWidget.yview) - choiceboxWidget.configure(yscrollcommand = rightScrollbar.set) - - # add a horizontal scrollbar to the frame - bottomScrollbar = Scrollbar(choiceboxFrame, orient=HORIZONTAL, command=choiceboxWidget.xview) - choiceboxWidget.configure(xscrollcommand = bottomScrollbar.set) - - # pack the Listbox and the scrollbars. Note that although we must define - # the textArea first, we must pack it last, so that the bottomScrollbar will - # be located properly. - - bottomScrollbar.pack(side=BOTTOM, fill = X) - rightScrollbar.pack(side=RIGHT, fill = Y) - - choiceboxWidget.pack(side=LEFT, padx="1m", pady="1m", expand=YES, fill=BOTH) - - #--------------------------------------------------- - # sort the choices - # eliminate duplicates - # put the choices into the choiceboxWidget - #--------------------------------------------------- - for index in range(len(choices)): - choices[index] = str(choices[index]) - - if runningPython3: - choices.sort(key=str.lower) - else: - choices.sort( lambda x,y: cmp(x.lower(), y.lower())) # case-insensitive sort - - lastInserted = None - choiceboxChoices = [] - for choice in choices: - if choice == lastInserted: pass - else: - choiceboxWidget.insert(END, choice) - choiceboxChoices.append(choice) - lastInserted = choice - - boxRoot.bind('', KeyboardListener) - - # put the buttons in the buttonsFrame - if len(choices) > 0: - okButton = Button(buttonsFrame, takefocus=YES, text="OK", height=1, width=6) - bindArrows(okButton) - okButton.pack(expand=NO, side=TOP, padx='2m', pady='1m', ipady="1m", ipadx="2m") - - # for the commandButton, bind activation events to the activation event handler - commandButton = okButton - handler = __choiceboxGetChoice - for selectionEvent in STANDARD_SELECTION_EVENTS: - commandButton.bind("<%s>" % selectionEvent, handler) - - # now bind the keyboard events - choiceboxWidget.bind("", __choiceboxGetChoice) - choiceboxWidget.bind("", __choiceboxGetChoice) - else: - # now bind the keyboard events - choiceboxWidget.bind("", __choiceboxCancel) - choiceboxWidget.bind("", __choiceboxCancel) - - cancelButton = Button(buttonsFrame, takefocus=YES, text="Cancel", height=1, width=6) - bindArrows(cancelButton) - cancelButton.pack(expand=NO, side=BOTTOM, padx='2m', pady='1m', ipady="1m", ipadx="2m") - - # for the commandButton, bind activation events to the activation event handler - commandButton = cancelButton - handler = __choiceboxCancel - for selectionEvent in STANDARD_SELECTION_EVENTS: - commandButton.bind("<%s>" % selectionEvent, handler) - - - # add special buttons for multiple select features - if len(choices) > 0 and __choiceboxMultipleSelect: - selectionButtonsFrame = Frame(messageFrame) - selectionButtonsFrame.pack(side=RIGHT, fill=Y, expand=NO) - - selectAllButton = Button(selectionButtonsFrame, text="Select All", height=1, width=6) - bindArrows(selectAllButton) - - selectAllButton.bind("",__choiceboxSelectAll) - selectAllButton.pack(expand=NO, side=TOP, padx='2m', pady='1m', ipady="1m", ipadx="2m") - - clearAllButton = Button(selectionButtonsFrame, text="Clear All", height=1, width=6) - bindArrows(clearAllButton) - clearAllButton.bind("",__choiceboxClearAll) - clearAllButton.pack(expand=NO, side=TOP, padx='2m', pady='1m', ipady="1m", ipadx="2m") - - - # -------------------- bind some keyboard events ---------------------------- - boxRoot.bind("", __choiceboxCancel) - - # --------------------- the action begins ----------------------------------- - # put the focus on the choiceboxWidget, and the select highlight on the first item - choiceboxWidget.select_set(0) - choiceboxWidget.focus_force() - - # --- run it! ----- - boxRoot.mainloop() - - boxRoot.destroy() - return __choiceboxResults - - -def __choiceboxGetChoice(event): - global boxRoot, __choiceboxResults, choiceboxWidget - - if __choiceboxMultipleSelect: - __choiceboxResults = [choiceboxWidget.get(index) for index in choiceboxWidget.curselection()] - - else: - choice_index = choiceboxWidget.curselection() - __choiceboxResults = choiceboxWidget.get(choice_index) - - # writeln("Debugging> mouse-event=", event, " event.type=", event.type) - # writeln("Debugging> choice=", choice_index, __choiceboxResults) - boxRoot.quit() - - -def __choiceboxSelectAll(event): - global choiceboxWidget, choiceboxChoices - - choiceboxWidget.selection_set(0, len(choiceboxChoices)-1) - -def __choiceboxClearAll(event): - global choiceboxWidget, choiceboxChoices - - choiceboxWidget.selection_clear(0, len(choiceboxChoices)-1) - - - -def __choiceboxCancel(event): - global boxRoot, __choiceboxResults - - __choiceboxResults = None - boxRoot.quit() - - -def KeyboardListener(event): - global choiceboxChoices, choiceboxWidget - key = event.keysym - if len(key) <= 1: - if key in string.printable: - # Find the key in the list. - # before we clear the list, remember the selected member - try: - start_n = int(choiceboxWidget.curselection()[0]) - except IndexError: - start_n = -1 - - ## clear the selection. - choiceboxWidget.selection_clear(0, 'end') - - ## start from previous selection +1 - for n in range(start_n+1, len(choiceboxChoices)): - item = choiceboxChoices[n] - if item[0].lower() == key.lower(): - choiceboxWidget.selection_set(first=n) - choiceboxWidget.see(n) - return - else: - # has not found it so loop from top - for n in range(len(choiceboxChoices)): - item = choiceboxChoices[n] - if item[0].lower() == key.lower(): - choiceboxWidget.selection_set(first = n) - choiceboxWidget.see(n) - return - - # nothing matched -- we'll look for the next logical choice - for n in range(len(choiceboxChoices)): - item = choiceboxChoices[n] - if item[0].lower() > key.lower(): - if n > 0: - choiceboxWidget.selection_set(first = (n-1)) - else: - choiceboxWidget.selection_set(first = 0) - choiceboxWidget.see(n) - return - - # still no match (nothing was greater than the key) - # we set the selection to the first item in the list - lastIndex = len(choiceboxChoices)-1 - choiceboxWidget.selection_set(first = lastIndex) - choiceboxWidget.see(lastIndex) - return - -#----------------------------------------------------------------------- -# exception_format -#----------------------------------------------------------------------- -def exception_format(): - """ - Convert exception info into a string suitable for display. - """ - return "".join(traceback.format_exception( - sys.exc_info()[0] - , sys.exc_info()[1] - , sys.exc_info()[2] - )) - -#----------------------------------------------------------------------- -# exceptionbox -#----------------------------------------------------------------------- -def exceptionbox(msg=None, title=None): - """ - Display a box that gives information about - an exception that has just been raised. - - The caller may optionally pass in a title for the window, or a - msg to accompany the error information. - - Note that you do not need to (and cannot) pass an exception object - as an argument. The latest exception will automatically be used. - """ - if title == None: title = "Error Report" - if msg == None: - msg = "An error (exception) has occurred in the program." - - codebox(msg, title, exception_format()) - -#------------------------------------------------------------------- -# codebox -#------------------------------------------------------------------- - -def codebox(msg="" - , title=" " - , text="" - ): - """ - Display some text in a monospaced font, with no line wrapping. - This function is suitable for displaying code and text that is - formatted using spaces. - - The text parameter should be a string, or a list or tuple of lines to be - displayed in the textbox. - """ - return textbox(msg, title, text, codebox=1 ) - -#------------------------------------------------------------------- -# textbox -#------------------------------------------------------------------- -def textbox(msg="" - , title=" " - , text="" - , codebox=0 - ): - """ - Display some text in a proportional font with line wrapping at word breaks. - This function is suitable for displaying general written text. - - The text parameter should be a string, or a list or tuple of lines to be - displayed in the textbox. - """ - - if msg == None: msg = "" - if title == None: title = "" - - global boxRoot, __replyButtonText, __widgetTexts, buttonsFrame - global rootWindowPosition - choices = ["OK"] - __replyButtonText = choices[0] - - - boxRoot = Tk() - - boxRoot.protocol('WM_DELETE_WINDOW', denyWindowManagerClose ) - - screen_width = boxRoot.winfo_screenwidth() - screen_height = boxRoot.winfo_screenheight() - root_width = int((screen_width * 0.8)) - root_height = int((screen_height * 0.5)) - root_xpos = int((screen_width * 0.1)) - root_ypos = int((screen_height * 0.05)) - - boxRoot.title(title) - boxRoot.iconname('Dialog') - rootWindowPosition = "+0+0" - boxRoot.geometry(rootWindowPosition) - boxRoot.expand=NO - boxRoot.minsize(root_width, root_height) - rootWindowPosition = "+" + str(root_xpos) + "+" + str(root_ypos) - boxRoot.geometry(rootWindowPosition) - - mainframe = Frame(master=boxRoot) - mainframe.pack(side=TOP, fill=BOTH, expand=YES) - - # ---- put frames in the window ----------------------------------- - # we pack the textboxFrame first, so it will expand first - textboxFrame = Frame(mainframe, borderwidth=3) - textboxFrame.pack(side=BOTTOM , fill=BOTH, expand=YES) - - message_and_buttonsFrame = Frame(mainframe) - message_and_buttonsFrame.pack(side=TOP, fill=X, expand=NO) - - messageFrame = Frame(message_and_buttonsFrame) - messageFrame.pack(side=LEFT, fill=X, expand=YES) - - buttonsFrame = Frame(message_and_buttonsFrame) - buttonsFrame.pack(side=RIGHT, expand=NO) - - # -------------------- put widgets in the frames -------------------- - - # put a textArea in the top frame - if codebox: - character_width = int((root_width * 0.6) / MONOSPACE_FONT_SIZE) - textArea = Text(textboxFrame,height=25,width=character_width, padx="2m", pady="1m") - textArea.configure(wrap=NONE) - textArea.configure(font=(MONOSPACE_FONT_FAMILY, MONOSPACE_FONT_SIZE)) - - else: - character_width = int((root_width * 0.6) / MONOSPACE_FONT_SIZE) - textArea = Text( - textboxFrame - , height=25 - , width=character_width - , padx="2m" - , pady="1m" - ) - textArea.configure(wrap=WORD) - textArea.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE)) - - - # some simple keybindings for scrolling - mainframe.bind("" , textArea.yview_scroll( 1,PAGES)) - mainframe.bind("", textArea.yview_scroll(-1,PAGES)) - - mainframe.bind("", textArea.xview_scroll( 1,PAGES)) - mainframe.bind("" , textArea.xview_scroll(-1,PAGES)) - - mainframe.bind("", textArea.yview_scroll( 1,UNITS)) - mainframe.bind("" , textArea.yview_scroll(-1,UNITS)) - - - # add a vertical scrollbar to the frame - rightScrollbar = Scrollbar(textboxFrame, orient=VERTICAL, command=textArea.yview) - textArea.configure(yscrollcommand = rightScrollbar.set) - - # add a horizontal scrollbar to the frame - bottomScrollbar = Scrollbar(textboxFrame, orient=HORIZONTAL, command=textArea.xview) - textArea.configure(xscrollcommand = bottomScrollbar.set) - - # pack the textArea and the scrollbars. Note that although we must define - # the textArea first, we must pack it last, so that the bottomScrollbar will - # be located properly. - - # Note that we need a bottom scrollbar only for code. - # Text will be displayed with wordwrap, so we don't need to have a horizontal - # scroll for it. - if codebox: - bottomScrollbar.pack(side=BOTTOM, fill=X) - rightScrollbar.pack(side=RIGHT, fill=Y) - - textArea.pack(side=LEFT, fill=BOTH, expand=YES) - - - # ---------- put a msg widget in the msg frame------------------- - messageWidget = Message(messageFrame, anchor=NW, text=msg, width=int(root_width * 0.9)) - messageWidget.configure(font=(PROPORTIONAL_FONT_FAMILY,PROPORTIONAL_FONT_SIZE)) - messageWidget.pack(side=LEFT, expand=YES, fill=BOTH, padx='1m', pady='1m') - - # put the buttons in the buttonsFrame - okButton = Button(buttonsFrame, takefocus=YES, text="OK", height=1, width=6) - okButton.pack(expand=NO, side=TOP, padx='2m', pady='1m', ipady="1m", ipadx="2m") - - # for the commandButton, bind activation events to the activation event handler - commandButton = okButton - handler = __textboxOK - for selectionEvent in ["Return","Button-1","Escape"]: - commandButton.bind("<%s>" % selectionEvent, handler) - - - # ----------------- the action begins ---------------------------------------- - try: - # load the text into the textArea - if type(text) == type("abc"): pass - else: - try: - text = "".join(text) # convert a list or a tuple to a string - except: - msgbox("Exception when trying to convert "+ str(type(text)) + " to text in textArea") - sys.exit(16) - textArea.insert(END,text, "normal") - - except: - msgbox("Exception when trying to load the textArea.") - sys.exit(16) - - try: - okButton.focus_force() - except: - msgbox("Exception when trying to put focus on okButton.") - sys.exit(16) - - boxRoot.mainloop() - - # this line MUST go before the line that destroys boxRoot - areaText = textArea.get(0.0,END) - boxRoot.destroy() - return areaText # return __replyButtonText - -#------------------------------------------------------------------- -# __textboxOK -#------------------------------------------------------------------- -def __textboxOK(event): - global boxRoot - boxRoot.quit() - - - -#------------------------------------------------------------------- -# diropenbox -#------------------------------------------------------------------- -def diropenbox(msg=None - , title=None - , default=None - ): - """ - A dialog to get a directory name. - Note that the msg argument, if specified, is ignored. - - Returns the name of a directory, or None if user chose to cancel. - - If the "default" argument specifies a directory name, and that - directory exists, then the dialog box will start with that directory. - """ - title=getFileDialogTitle(msg,title) - localRoot = Tk() - localRoot.withdraw() - if not default: default = None - f = tk_FileDialog.askdirectory( - parent=localRoot - , title=title - , initialdir=default - , initialfile=None - ) - localRoot.destroy() - if not f: return None - return os.path.normpath(f) - - - -#------------------------------------------------------------------- -# getFileDialogTitle -#------------------------------------------------------------------- -def getFileDialogTitle(msg - , title - ): - if msg and title: return "%s - %s" % (title,msg) - if msg and not title: return str(msg) - if title and not msg: return str(title) - return None # no message and no title - -#------------------------------------------------------------------- -# class FileTypeObject for use with fileopenbox -#------------------------------------------------------------------- -class FileTypeObject: - def __init__(self,filemask): - if len(filemask) == 0: - raise AssertionError('Filetype argument is empty.') - - self.masks = [] - - if type(filemask) == type("abc"): # a string - self.initializeFromString(filemask) - - elif type(filemask) == type([]): # a list - if len(filemask) < 2: - raise AssertionError('Invalid filemask.\n' - +'List contains less than 2 members: "%s"' % filemask) - else: - self.name = filemask[-1] - self.masks = list(filemask[:-1] ) - else: - raise AssertionError('Invalid filemask: "%s"' % filemask) - - def __eq__(self,other): - if self.name == other.name: return True - return False - - def add(self,other): - for mask in other.masks: - if mask in self.masks: pass - else: self.masks.append(mask) - - def toTuple(self): - return (self.name,tuple(self.masks)) - - def isAll(self): - if self.name == "All files": return True - return False - - def initializeFromString(self, filemask): - # remove everything except the extension from the filemask - self.ext = os.path.splitext(filemask)[1] - if self.ext == "" : self.ext = ".*" - if self.ext == ".": self.ext = ".*" - self.name = self.getName() - self.masks = ["*" + self.ext] - - def getName(self): - e = self.ext - if e == ".*" : return "All files" - if e == ".txt": return "Text files" - if e == ".py" : return "Python files" - if e == ".pyc" : return "Python files" - if e == ".xls": return "Excel files" - if e.startswith("."): - return e[1:].upper() + " files" - return e.upper() + " files" - - -#------------------------------------------------------------------- -# fileopenbox -#------------------------------------------------------------------- -def fileopenbox(msg=None - , title=None - , default="*" - , filetypes=None - ): - """ - A dialog to get a file name. - - About the "default" argument - ============================ - The "default" argument specifies a filepath that (normally) - contains one or more wildcards. - fileopenbox will display only files that match the default filepath. - If omitted, defaults to "*" (all files in the current directory). - - WINDOWS EXAMPLE:: - ...default="c:/myjunk/*.py" - will open in directory c:\myjunk\ and show all Python files. - - WINDOWS EXAMPLE:: - ...default="c:/myjunk/test*.py" - will open in directory c:\myjunk\ and show all Python files - whose names begin with "test". - - - Note that on Windows, fileopenbox automatically changes the path - separator to the Windows path separator (backslash). - - About the "filetypes" argument - ============================== - If specified, it should contain a list of items, - where each item is either:: - - a string containing a filemask # e.g. "*.txt" - - a list of strings, where all of the strings except the last one - are filemasks (each beginning with "*.", - such as "*.txt" for text files, "*.py" for Python files, etc.). - and the last string contains a filetype description - - EXAMPLE:: - filetypes = ["*.css", ["*.htm", "*.html", "HTML files"] ] - - NOTE THAT - ========= - - If the filetypes list does not contain ("All files","*"), - it will be added. - - If the filetypes list does not contain a filemask that includes - the extension of the "default" argument, it will be added. - For example, if default="*abc.py" - and no filetypes argument was specified, then - "*.py" will automatically be added to the filetypes argument. - - @rtype: string or None - @return: the name of a file, or None if user chose to cancel - - @arg msg: the msg to be displayed. - @arg title: the window title - @arg default: filepath with wildcards - @arg filetypes: filemasks that a user can choose, e.g. "*.txt" - """ - localRoot = Tk() - localRoot.withdraw() - - initialbase, initialfile, initialdir, filetypes = fileboxSetup(default,filetypes) - - #------------------------------------------------------------ - # if initialfile contains no wildcards; we don't want an - # initial file. It won't be used anyway. - # Also: if initialbase is simply "*", we don't want an - # initialfile; it is not doing any useful work. - #------------------------------------------------------------ - if (initialfile.find("*") < 0) and (initialfile.find("?") < 0): - initialfile = None - elif initialbase == "*": - initialfile = None - - f = tk_FileDialog.askopenfilename(parent=localRoot - , title=getFileDialogTitle(msg,title) - , initialdir=initialdir - , initialfile=initialfile - , filetypes=filetypes - ) - - localRoot.destroy() - - if not f: return None - return os.path.normpath(f) - - -#------------------------------------------------------------------- -# filesavebox -#------------------------------------------------------------------- -def filesavebox(msg=None - , title=None - , default="" - , filetypes=None - ): - """ - A file to get the name of a file to save. - Returns the name of a file, or None if user chose to cancel. - - The "default" argument should contain a filename (i.e. the - current name of the file to be saved). It may also be empty, - or contain a filemask that includes wildcards. - - The "filetypes" argument works like the "filetypes" argument to - fileopenbox. - """ - - localRoot = Tk() - localRoot.withdraw() - - initialbase, initialfile, initialdir, filetypes = fileboxSetup(default,filetypes) - - f = tk_FileDialog.asksaveasfilename(parent=localRoot - , title=getFileDialogTitle(msg,title) - , initialfile=initialfile - , initialdir=initialdir - , filetypes=filetypes - ) - localRoot.destroy() - if not f: return None - return os.path.normpath(f) - - -#------------------------------------------------------------------- -# -# fileboxSetup -# -#------------------------------------------------------------------- -def fileboxSetup(default,filetypes): - if not default: default = os.path.join(".","*") - initialdir, initialfile = os.path.split(default) - if not initialdir : initialdir = "." - if not initialfile: initialfile = "*" - initialbase, initialext = os.path.splitext(initialfile) - initialFileTypeObject = FileTypeObject(initialfile) - - allFileTypeObject = FileTypeObject("*") - ALL_filetypes_was_specified = False - - if not filetypes: filetypes= [] - filetypeObjects = [] - - for filemask in filetypes: - fto = FileTypeObject(filemask) - - if fto.isAll(): - ALL_filetypes_was_specified = True # remember this - - if fto == initialFileTypeObject: - initialFileTypeObject.add(fto) # add fto to initialFileTypeObject - else: - filetypeObjects.append(fto) - - #------------------------------------------------------------------ - # make sure that the list of filetypes includes the ALL FILES type. - #------------------------------------------------------------------ - if ALL_filetypes_was_specified: - pass - elif allFileTypeObject == initialFileTypeObject: - pass - else: - filetypeObjects.insert(0,allFileTypeObject) - #------------------------------------------------------------------ - # Make sure that the list includes the initialFileTypeObject - # in the position in the list that will make it the default. - # This changed between Python version 2.5 and 2.6 - #------------------------------------------------------------------ - if len(filetypeObjects) == 0: - filetypeObjects.append(initialFileTypeObject) - - if initialFileTypeObject in (filetypeObjects[0], filetypeObjects[-1]): - pass - else: - if runningPython26: - filetypeObjects.append(initialFileTypeObject) - else: - filetypeObjects.insert(0,initialFileTypeObject) - - filetypes = [fto.toTuple() for fto in filetypeObjects] - - return initialbase, initialfile, initialdir, filetypes - -#------------------------------------------------------------------- -# utility routines -#------------------------------------------------------------------- -# These routines are used by several other functions in the EasyGui module. - -def __buttonEvent(event): - """ - Handle an event that is generated by a person clicking a button. - """ - global boxRoot, __widgetTexts, __replyButtonText - __replyButtonText = __widgetTexts[event.widget] - boxRoot.quit() # quit the main loop - - -def __put_buttons_in_buttonframe(choices): - """Put the buttons in the buttons frame - """ - global __widgetTexts, __firstWidget, buttonsFrame - - __firstWidget = None - __widgetTexts = {} - - i = 0 - - for buttonText in choices: - tempButton = Button(buttonsFrame, takefocus=1, text=buttonText) - bindArrows(tempButton) - tempButton.pack(expand=YES, side=LEFT, padx='1m', pady='1m', ipadx='2m', ipady='1m') - - # remember the text associated with this widget - __widgetTexts[tempButton] = buttonText - - # remember the first widget, so we can put the focus there - if i == 0: - __firstWidget = tempButton - i = 1 - - # for the commandButton, bind activation events to the activation event handler - commandButton = tempButton - handler = __buttonEvent - for selectionEvent in STANDARD_SELECTION_EVENTS: - commandButton.bind("<%s>" % selectionEvent, handler) - -#----------------------------------------------------------------------- -# -# class EgStore -# -#----------------------------------------------------------------------- -class EgStore: - r""" -A class to support persistent storage. - -You can use EgStore to support the storage and retrieval -of user settings for an EasyGui application. - - -# Example A -#----------------------------------------------------------------------- -# define a class named Settings as a subclass of EgStore -#----------------------------------------------------------------------- -class Settings(EgStore): -:: - def __init__(self, filename): # filename is required - #------------------------------------------------- - # Specify default/initial values for variables that - # this particular application wants to remember. - #------------------------------------------------- - self.userId = "" - self.targetServer = "" - - #------------------------------------------------- - # For subclasses of EgStore, these must be - # the last two statements in __init__ - #------------------------------------------------- - self.filename = filename # this is required - self.restore() # restore values from the storage file if possible - - - -# Example B -#----------------------------------------------------------------------- -# create settings, a persistent Settings object -#----------------------------------------------------------------------- -settingsFile = "myApp_settings.txt" -settings = Settings(settingsFile) - -user = "obama_barak" -server = "whitehouse1" -settings.userId = user -settings.targetServer = server -settings.store() # persist the settings - -# run code that gets a new value for userId, and persist the settings -user = "biden_joe" -settings.userId = user -settings.store() - - -# Example C -#----------------------------------------------------------------------- -# recover the Settings instance, change an attribute, and store it again. -#----------------------------------------------------------------------- -settings = Settings(settingsFile) -settings.userId = "vanrossum_g" -settings.store() - -""" - def __init__(self, filename): # obtaining filename is required - self.filename = None - raise NotImplementedError() - - def restore(self): - """ - Set the values of whatever attributes are recoverable - from the pickle file. - - Populate the attributes (the __dict__) of the EgStore object - from the attributes (the __dict__) of the pickled object. - - If the pickled object has attributes that have been initialized - in the EgStore object, then those attributes of the EgStore object - will be replaced by the values of the corresponding attributes - in the pickled object. - - If the pickled object is missing some attributes that have - been initialized in the EgStore object, then those attributes - of the EgStore object will retain the values that they were - initialized with. - - If the pickled object has some attributes that were not - initialized in the EgStore object, then those attributes - will be ignored. - - IN SUMMARY: - - After the recover() operation, the EgStore object will have all, - and only, the attributes that it had when it was initialized. - - Where possible, those attributes will have values recovered - from the pickled object. - """ - if not os.path.exists(self.filename): return self - if not os.path.isfile(self.filename): return self - - try: - f = open(self.filename,"rb") - unpickledObject = pickle.load(f) - f.close() - - for key in list(self.__dict__.keys()): - default = self.__dict__[key] - self.__dict__[key] = unpickledObject.__dict__.get(key,default) - except: - pass - - return self - - def store(self): - """ - Save the attributes of the EgStore object to a pickle file. - Note that if the directory for the pickle file does not already exist, - the store operation will fail. - """ - f = open(self.filename, "wb") - pickle.dump(self, f) - f.close() - - - def kill(self): - """ - Delete my persistent file (i.e. pickle file), if it exists. - """ - if os.path.isfile(self.filename): - os.remove(self.filename) - return - - def __str__(self): - """ - return my contents as a string in an easy-to-read format. - """ - # find the length of the longest attribute name - longest_key_length = 0 - keys = [] - for key in self.__dict__.keys(): - keys.append(key) - longest_key_length = max(longest_key_length, len(key)) - - keys.sort() # sort the attribute names - lines = [] - for key in keys: - value = self.__dict__[key] - key = key.ljust(longest_key_length) - lines.append("%s : %s\n" % (key,repr(value)) ) - return "".join(lines) # return a string showing the attributes - - - - -#----------------------------------------------------------------------- -# -# test/demo easygui -# -#----------------------------------------------------------------------- -def egdemo(): - """ - Run the EasyGui demo. - """ - # clear the console - writeln("\n" * 100) - - intro_message = ("Pick the kind of box that you wish to demo.\n" - + "\n * Python version " + sys.version - + "\n * EasyGui version " + egversion - + "\n * Tk version " + str(TkVersion) - ) - - #========================================== END DEMONSTRATION DATA - - - while 1: # do forever - choices = [ - "msgbox", - "buttonbox", - "buttonbox(image) -- a buttonbox that displays an image", - "choicebox", - "multchoicebox", - "textbox", - "ynbox", - "ccbox", - "enterbox", - "enterbox(image) -- an enterbox that displays an image", - "exceptionbox", - "codebox", - "integerbox", - "boolbox", - "indexbox", - "filesavebox", - "fileopenbox", - "passwordbox", - "multenterbox", - "multpasswordbox", - "diropenbox", - "About EasyGui", - " Help" - ] - choice = choicebox(msg=intro_message - , title="EasyGui " + egversion - , choices=choices) - - if not choice: return - - reply = choice.split() - - if reply[0] == "msgbox": - reply = msgbox("short msg", "This is a long title") - writeln("Reply was: %s" % repr(reply)) - - elif reply[0] == "About": - reply = abouteasygui() - - elif reply[0] == "Help": - _demo_help() - - elif reply[0] == "buttonbox": - reply = buttonbox() - writeln("Reply was: %s" % repr(reply)) - - title = "Demo of Buttonbox with many, many buttons!" - msg = "This buttonbox shows what happens when you specify too many buttons." - reply = buttonbox(msg=msg, title=title, choices=choices) - writeln("Reply was: %s" % repr(reply)) - - elif reply[0] == "buttonbox(image)": - _demo_buttonbox_with_image() - - elif reply[0] == "boolbox": - reply = boolbox() - writeln("Reply was: %s" % repr(reply)) - - elif reply[0] == "enterbox": - image = "python_and_check_logo.gif" - message = "Enter the name of your best friend."\ - "\n(Result will be stripped.)" - reply = enterbox(message, "Love!", " Suzy Smith ") - writeln("Reply was: %s" % repr(reply)) - - message = "Enter the name of your best friend."\ - "\n(Result will NOT be stripped.)" - reply = enterbox(message, "Love!", " Suzy Smith ",strip=False) - writeln("Reply was: %s" % repr(reply)) - - reply = enterbox("Enter the name of your worst enemy:", "Hate!") - writeln("Reply was: %s" % repr(reply)) - - elif reply[0] == "enterbox(image)": - image = "python_and_check_logo.gif" - message = "What kind of snake is this?" - reply = enterbox(message, "Quiz",image=image) - writeln("Reply was: %s" % repr(reply)) - - elif reply[0] == "exceptionbox": - try: - thisWillCauseADivideByZeroException = 1/0 - except: - exceptionbox() - - elif reply[0] == "integerbox": - reply = integerbox( - "Enter a number between 3 and 333", - "Demo: integerbox WITH a default value", - 222, 3, 333) - writeln("Reply was: %s" % repr(reply)) - - reply = integerbox( - "Enter a number between 0 and 99", - "Demo: integerbox WITHOUT a default value" - ) - writeln("Reply was: %s" % repr(reply)) - - elif reply[0] == "diropenbox" : _demo_diropenbox() - elif reply[0] == "fileopenbox": _demo_fileopenbox() - elif reply[0] == "filesavebox": _demo_filesavebox() - - elif reply[0] == "indexbox": - title = reply[0] - msg = "Demo of " + reply[0] - choices = ["Choice1", "Choice2", "Choice3", "Choice4"] - reply = indexbox(msg, title, choices) - writeln("Reply was: %s" % repr(reply)) - - elif reply[0] == "passwordbox": - reply = passwordbox("Demo of password box WITHOUT default" - + "\n\nEnter your secret password", "Member Logon") - writeln("Reply was: %s" % str(reply)) - - reply = passwordbox("Demo of password box WITH default" - + "\n\nEnter your secret password", "Member Logon", "alfie") - writeln("Reply was: %s" % str(reply)) - - elif reply[0] == "multenterbox": - msg = "Enter your personal information" - title = "Credit Card Application" - fieldNames = ["Name","Street Address","City","State","ZipCode"] - fieldValues = [] # we start with blanks for the values - fieldValues = multenterbox(msg,title, fieldNames) - - # make sure that none of the fields was left blank - while 1: - if fieldValues == None: break - errmsg = "" - for i in range(len(fieldNames)): - if fieldValues[i].strip() == "": - errmsg = errmsg + ('"%s" is a required field.\n\n' % fieldNames[i]) - if errmsg == "": break # no problems found - fieldValues = multenterbox(errmsg, title, fieldNames, fieldValues) - - writeln("Reply was: %s" % str(fieldValues)) - - elif reply[0] == "multpasswordbox": - msg = "Enter logon information" - title = "Demo of multpasswordbox" - fieldNames = ["Server ID", "User ID", "Password"] - fieldValues = [] # we start with blanks for the values - fieldValues = multpasswordbox(msg,title, fieldNames) - - # make sure that none of the fields was left blank - while 1: - if fieldValues == None: break - errmsg = "" - for i in range(len(fieldNames)): - if fieldValues[i].strip() == "": - errmsg = errmsg + ('"%s" is a required field.\n\n' % fieldNames[i]) - if errmsg == "": break # no problems found - fieldValues = multpasswordbox(errmsg, title, fieldNames, fieldValues) - - writeln("Reply was: %s" % str(fieldValues)) - - elif reply[0] == "ynbox": - title = "Demo of ynbox" - msg = "Were you expecting the Spanish Inquisition?" - reply = ynbox(msg, title) - writeln("Reply was: %s" % repr(reply)) - if reply: - msgbox("NOBODY expects the Spanish Inquisition!", "Wrong!") - - elif reply[0] == "ccbox": - title = "Demo of ccbox" - reply = ccbox(msg,title) - writeln("Reply was: %s" % repr(reply)) - - elif reply[0] == "choicebox": - title = "Demo of choicebox" - longchoice = "This is an example of a very long option which you may or may not wish to choose."*2 - listChoices = ["nnn", "ddd", "eee", "fff", "aaa", longchoice - , "aaa", "bbb", "ccc", "ggg", "hhh", "iii", "jjj", "kkk", "LLL", "mmm" , "nnn", "ooo", "ppp", "qqq", "rrr", "sss", "ttt", "uuu", "vvv"] - - msg = "Pick something. " + ("A wrapable sentence of text ?! "*30) + "\nA separate line of text."*6 - reply = choicebox(msg=msg, choices=listChoices) - writeln("Reply was: %s" % repr(reply)) - - msg = "Pick something. " - reply = choicebox(msg=msg, title=title, choices=listChoices) - writeln("Reply was: %s" % repr(reply)) - - msg = "Pick something. " - reply = choicebox(msg="The list of choices is empty!", choices=[]) - writeln("Reply was: %s" % repr(reply)) - - elif reply[0] == "multchoicebox": - listChoices = ["aaa", "bbb", "ccc", "ggg", "hhh", "iii", "jjj", "kkk" - , "LLL", "mmm" , "nnn", "ooo", "ppp", "qqq" - , "rrr", "sss", "ttt", "uuu", "vvv"] - - msg = "Pick as many choices as you wish." - reply = multchoicebox(msg,"Demo of multchoicebox", listChoices) - writeln("Reply was: %s" % repr(reply)) - - elif reply[0] == "textbox": _demo_textbox(reply[0]) - elif reply[0] == "codebox": _demo_codebox(reply[0]) - - else: - msgbox("Choice\n\n" + choice + "\n\nis not recognized", "Program Logic Error") - return - - -def _demo_textbox(reply): - text_snippet = ((\ -"""It was the best of times, and it was the worst of times. The rich ate cake, and the poor had cake recommended to them, but wished only for enough cash to buy bread. The time was ripe for revolution! """ \ -*5)+"\n\n")*10 - title = "Demo of textbox" - msg = "Here is some sample text. " * 16 - reply = textbox(msg, title, text_snippet) - writeln("Reply was: %s" % str(reply)) - -def _demo_codebox(reply): - code_snippet = ("dafsdfa dasflkj pp[oadsij asdfp;ij asdfpjkop asdfpok asdfpok asdfpok"*3) +"\n"+\ -"""# here is some dummy Python code -for someItem in myListOfStuff: - do something(someItem) - do something() - do something() - if somethingElse(someItem): - doSomethingEvenMoreInteresting() - -"""*16 - msg = "Here is some sample code. " * 16 - reply = codebox(msg, "Code Sample", code_snippet) - writeln("Reply was: %s" % repr(reply)) - - -def _demo_buttonbox_with_image(): - - msg = "Do you like this picture?\nIt is " - choices = ["Yes","No","No opinion"] - - for image in [ - "python_and_check_logo.gif" - ,"python_and_check_logo.jpg" - ,"python_and_check_logo.png" - ,"zzzzz.gif"]: - - reply=buttonbox(msg + image,image=image,choices=choices) - writeln("Reply was: %s" % repr(reply)) - - -def _demo_help(): - savedStdout = sys.stdout # save the sys.stdout file object - sys.stdout = capturedOutput = StringIO() - help("easygui") - sys.stdout = savedStdout # restore the sys.stdout file object - codebox("EasyGui Help",text=capturedOutput.getvalue()) - -def _demo_filesavebox(): - filename = "myNewFile.txt" - title = "File SaveAs" - msg ="Save file as:" - - f = filesavebox(msg,title,default=filename) - writeln("You chose to save file: %s" % f) - -def _demo_diropenbox(): - title = "Demo of diropenbox" - msg = "Pick the directory that you wish to open." - d = diropenbox(msg, title) - writeln("You chose directory...: %s" % d) - - d = diropenbox(msg, title,default="./") - writeln("You chose directory...: %s" % d) - - d = diropenbox(msg, title,default="c:/") - writeln("You chose directory...: %s" % d) - - -def _demo_fileopenbox(): - msg = "Python files" - title = "Open files" - default="*.py" - f = fileopenbox(msg,title,default=default) - writeln("You chose to open file: %s" % f) - - default="./*.gif" - filetypes = ["*.jpg",["*.zip","*.tgs","*.gz", "Archive files"],["*.htm", "*.html","HTML files"]] - f = fileopenbox(msg,title,default=default,filetypes=filetypes) - writeln("You chose to open file: %s" % f) - - """#deadcode -- testing ---------------------------------------- - f = fileopenbox(None,None,default=default) - writeln("You chose to open file: %s" % f) - - f = fileopenbox(None,title,default=default) - writeln("You chose to open file: %s" % f) - - f = fileopenbox(msg,None,default=default) - writeln("You chose to open file: %s" % f) - - f = fileopenbox(default=default) - writeln("You chose to open file: %s" % f) - - f = fileopenbox(default=None) - writeln("You chose to open file: %s" % f) - #----------------------------------------------------deadcode """ - - -def _dummy(): - pass - -EASYGUI_ABOUT_INFORMATION = ''' -======================================================================== -0.96(2010-08-29) -======================================================================== -This version fixes some problems with version independence. - -BUG FIXES ------------------------------------------------------- - * A statement with Python 2.x-style exception-handling syntax raised - a syntax error when running under Python 3.x. - Thanks to David Williams for reporting this problem. - - * Under some circumstances, PIL was unable to display non-gif images - that it should have been able to display. - The cause appears to be non-version-independent import syntax. - PIL modules are now imported with a version-independent syntax. - Thanks to Horst Jens for reporting this problem. - -LICENSE CHANGE ------------------------------------------------------- -Starting with this version, EasyGui is licensed under what is generally known as -the "modified BSD license" (aka "revised BSD", "new BSD", "3-clause BSD"). -This license is GPL-compatible but less restrictive than GPL. -Earlier versions were licensed under the Creative Commons Attribution License 2.0. - - -======================================================================== -0.95(2010-06-12) -======================================================================== - -ENHANCEMENTS ------------------------------------------------------- - * Previous versions of EasyGui could display only .gif image files using the - msgbox "image" argument. This version can now display all image-file formats - supported by PIL the Python Imaging Library) if PIL is installed. - If msgbox is asked to open a non-gif image file, it attempts to import - PIL and to use PIL to convert the image file to a displayable format. - If PIL cannot be imported (probably because PIL is not installed) - EasyGui displays an error message saying that PIL must be installed in order - to display the image file. - - Note that - http://www.pythonware.com/products/pil/ - says that PIL doesn't yet support Python 3.x. - - -======================================================================== -0.94(2010-06-06) -======================================================================== - -ENHANCEMENTS ------------------------------------------------------- - * The codebox and textbox functions now return the contents of the box, rather - than simply the name of the button ("Yes"). This makes it possible to use - codebox and textbox as data-entry widgets. A big "thank you!" to Dominic - Comtois for requesting this feature, patiently explaining his requirement, - and helping to discover the tkinter techniques to implement it. - - NOTE THAT in theory this change breaks backward compatibility. But because - (in previous versions of EasyGui) the value returned by codebox and textbox - was meaningless, no application should have been checking it. So in actual - practice, this change should not break backward compatibility. - - * Added support for SPACEBAR to command buttons. Now, when keyboard - focus is on a command button, a press of the SPACEBAR will act like - a press of the ENTER key; it will activate the command button. - - * Added support for keyboard navigation with the arrow keys (up,down,left,right) - to the fields and buttons in enterbox, multenterbox and multpasswordbox, - and to the buttons in choicebox and all buttonboxes. - - * added highlightthickness=2 to entry fields in multenterbox and - multpasswordbox. Now it is easier to tell which entry field has - keyboard focus. - - -BUG FIXES ------------------------------------------------------- - * In EgStore, the pickle file is now opened with "rb" and "wb" rather than - with "r" and "w". This change is necessary for compatibility with Python 3+. - Thanks to Marshall Mattingly for reporting this problem and providing the fix. - - * In integerbox, the actual argument names did not match the names described - in the docstring. Thanks to Daniel Zingaro of at University of Toronto for - reporting this problem. - - * In integerbox, the "argLowerBound" and "argUpperBound" arguments have been - renamed to "lowerbound" and "upperbound" and the docstring has been corrected. - - NOTE THAT THIS CHANGE TO THE ARGUMENT-NAMES BREAKS BACKWARD COMPATIBILITY. - If argLowerBound or argUpperBound are used, an AssertionError with an - explanatory error message is raised. - - * In choicebox, the signature to choicebox incorrectly showed choicebox as - accepting a "buttons" argument. The signature has been fixed. - - -======================================================================== -0.93(2009-07-07) -======================================================================== - -ENHANCEMENTS ------------------------------------------------------- - - * Added exceptionbox to display stack trace of exceptions - - * modified names of some font-related constants to make it - easier to customize them - - -======================================================================== -0.92(2009-06-22) -======================================================================== - -ENHANCEMENTS ------------------------------------------------------- - - * Added EgStore class to to provide basic easy-to-use persistence. - -BUG FIXES ------------------------------------------------------- - - * Fixed a bug that was preventing Linux users from copying text out of - a textbox and a codebox. This was not a problem for Windows users. - -''' - -def abouteasygui(): - """ - shows the easygui revision history - """ - codebox("About EasyGui\n"+egversion,"EasyGui",EASYGUI_ABOUT_INFORMATION) - return None - - - -if __name__ == '__main__': - if True: - egdemo() - else: - # test the new root feature - root = Tk() - msg = """This is a test of a main Tk() window in which we will place an easygui msgbox. - It will be an interesting experiment.\n\n""" - messageWidget = Message(root, text=msg, width=1000) - messageWidget.pack(side=TOP, expand=YES, fill=X, padx='3m', pady='3m') - messageWidget = Message(root, text=msg, width=1000) - messageWidget.pack(side=TOP, expand=YES, fill=X, padx='3m', pady='3m') - - - msgbox("this is a test of passing in boxRoot", root=root) - msgbox("this is a second test of passing in boxRoot", root=root) - - reply = enterbox("Enter something", root=root) - writeln("You wrote:", reply) - - reply = enterbox("Enter something else", root=root) - writeln("You wrote:", reply) - root.destroy() diff --git a/tools/midi2piano/midi/__init__.py b/tools/midi2piano/midi/__init__.py deleted file mode 100644 index f9dff3f378f..00000000000 --- a/tools/midi2piano/midi/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from midi.midi import * diff --git a/tools/midi2piano/midi2piano.py b/tools/midi2piano/midi2piano.py index 6fbbcdf3dc4..b2c0639827b 100644 --- a/tools/midi2piano/midi2piano.py +++ b/tools/midi2piano/midi2piano.py @@ -3,9 +3,12 @@ This module allows user to convert MIDI melodies to SS13 sheet music ready for copy-and-paste """ from functools import reduce -import midi as mi -import easygui as egui -import pyperclip as pclip +import MidiDependencies as mi +import tkinter as tk +from tkinter import filedialog +from tkinter import messagebox +root = tk.Tk() +root.withdraw() LINE_LENGTH_LIM = 300 LINES_LIMIT = 1000 @@ -105,9 +108,8 @@ def obtain_midi_file(): """ Asks user to select MIDI and returns this file opened in binary mode for reading """ - file = egui.fileopenbox(msg='Choose MIDI file to convert', - title='MIDI file selection', - filetypes=[['*.mid', 'MID files']]) + messagebox.showinfo("Midi2Piano Information", "Choose a MIDI file to convert") + file = filedialog.askopenfilename(title='MIDI file selection',filetypes=[['*.mid', 'MID files']]) if not file: return None file = open(file, mode='rb').read() @@ -306,6 +308,6 @@ def main_cycle(): split_music = explode_sheet_music(sheet_music) sheet_music = finalize_sheet_music(split_music, most_frequent_dur) - pclip.copy(sheet_music) + root.clipboard_append(sheet_music) main_cycle() diff --git a/tools/midi2piano/pyperclip/__init__.py b/tools/midi2piano/pyperclip/__init__.py deleted file mode 100644 index a3db1b35742..00000000000 --- a/tools/midi2piano/pyperclip/__init__.py +++ /dev/null @@ -1,103 +0,0 @@ -""" -Pyperclip - -A cross-platform clipboard module for Python. (only handles plain text for now) -By Al Sweigart al@inventwithpython.com -BSD License - -Usage: - import pyperclip - pyperclip.copy('The text to be copied to the clipboard.') - spam = pyperclip.paste() - - if not pyperclip.copy: - print("Copy functionality unavailable!") - -On Windows, no additional modules are needed. -On Mac, the module uses pbcopy and pbpaste, which should come with the os. -On Linux, install xclip or xsel via package manager. For example, in Debian: -sudo apt-get install xclip - -Otherwise on Linux, you will need the gtk or PyQt4 modules installed. - -gtk and PyQt4 modules are not available for Python 3, -and this module does not work with PyGObject yet. -""" -__version__ = '1.5.27' - -import platform -import os -import subprocess -from .clipboards import (init_osx_clipboard, - init_gtk_clipboard, init_qt_clipboard, - init_xclip_clipboard, init_xsel_clipboard, - init_klipper_clipboard, init_no_clipboard) -from .windows import init_windows_clipboard - -# `import PyQt4` sys.exit()s if DISPLAY is not in the environment. -# Thus, we need to detect the presence of $DISPLAY manually -# and not load PyQt4 if it is absent. -HAS_DISPLAY = os.getenv("DISPLAY", False) -CHECK_CMD = "where" if platform.system() == "Windows" else "which" - - -def _executable_exists(name): - return subprocess.call([CHECK_CMD, name], - stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0 - - -def determine_clipboard(): - # Determine the OS/platform and set - # the copy() and paste() functions accordingly. - if 'cygwin' in platform.system().lower(): - # FIXME: pyperclip currently does not support Cygwin, - # see https://github.com/asweigart/pyperclip/issues/55 - pass - elif os.name == 'nt' or platform.system() == 'Windows': - return init_windows_clipboard() - if os.name == 'mac' or platform.system() == 'Darwin': - return init_osx_clipboard() - if HAS_DISPLAY: - # Determine which command/module is installed, if any. - try: - import gtk # check if gtk is installed - except ImportError: - pass - else: - return init_gtk_clipboard() - - try: - import PyQt4 # check if PyQt4 is installed - except ImportError: - pass - else: - return init_qt_clipboard() - - if _executable_exists("xclip"): - return init_xclip_clipboard() - if _executable_exists("xsel"): - return init_xsel_clipboard() - if _executable_exists("klipper") and _executable_exists("qdbus"): - return init_klipper_clipboard() - - return init_no_clipboard() - - -def set_clipboard(clipboard): - global copy, paste - - clipboard_types = {'osx': init_osx_clipboard, - 'gtk': init_gtk_clipboard, - 'qt': init_qt_clipboard, - 'xclip': init_xclip_clipboard, - 'xsel': init_xsel_clipboard, - 'klipper': init_klipper_clipboard, - 'windows': init_windows_clipboard, - 'no': init_no_clipboard} - - copy, paste = clipboard_types[clipboard]() - - -copy, paste = determine_clipboard() - -__all__ = ["copy", "paste"] diff --git a/tools/midi2piano/pyperclip/clipboards.py b/tools/midi2piano/pyperclip/clipboards.py deleted file mode 100644 index 5eac945628a..00000000000 --- a/tools/midi2piano/pyperclip/clipboards.py +++ /dev/null @@ -1,134 +0,0 @@ -import sys -import subprocess -from .exceptions import PyperclipException - -EXCEPT_MSG = """ - Pyperclip could not find a copy/paste mechanism for your system. - For more information, please visit https://pyperclip.readthedocs.org """ -PY2 = sys.version_info[0] == 2 -text_type = unicode if PY2 else str - - -def init_osx_clipboard(): - def copy_osx(text): - p = subprocess.Popen(['pbcopy', 'w'], - stdin=subprocess.PIPE, close_fds=True) - p.communicate(input=text.encode('utf-8')) - - def paste_osx(): - p = subprocess.Popen(['pbpaste', 'r'], - stdout=subprocess.PIPE, close_fds=True) - stdout, stderr = p.communicate() - return stdout.decode('utf-8') - - return copy_osx, paste_osx - - -def init_gtk_clipboard(): - import gtk - - def copy_gtk(text): - global cb - cb = gtk.Clipboard() - cb.set_text(text) - cb.store() - - def paste_gtk(): - clipboardContents = gtk.Clipboard().wait_for_text() - # for python 2, returns None if the clipboard is blank. - if clipboardContents is None: - return '' - else: - return clipboardContents - - return copy_gtk, paste_gtk - - -def init_qt_clipboard(): - # $DISPLAY should exist - from PyQt4.QtGui import QApplication - - app = QApplication([]) - - def copy_qt(text): - cb = app.clipboard() - cb.setText(text) - - def paste_qt(): - cb = app.clipboard() - return text_type(cb.text()) - - return copy_qt, paste_qt - - -def init_xclip_clipboard(): - def copy_xclip(text): - p = subprocess.Popen(['xclip', '-selection', 'c'], - stdin=subprocess.PIPE, close_fds=True) - p.communicate(input=text.encode('utf-8')) - - def paste_xclip(): - p = subprocess.Popen(['xclip', '-selection', 'c', '-o'], - stdout=subprocess.PIPE, close_fds=True) - stdout, stderr = p.communicate() - return stdout.decode('utf-8') - - return copy_xclip, paste_xclip - - -def init_xsel_clipboard(): - def copy_xsel(text): - p = subprocess.Popen(['xsel', '-b', '-i'], - stdin=subprocess.PIPE, close_fds=True) - p.communicate(input=text.encode('utf-8')) - - def paste_xsel(): - p = subprocess.Popen(['xsel', '-b', '-o'], - stdout=subprocess.PIPE, close_fds=True) - stdout, stderr = p.communicate() - return stdout.decode('utf-8') - - return copy_xsel, paste_xsel - - -def init_klipper_clipboard(): - def copy_klipper(text): - p = subprocess.Popen( - ['qdbus', 'org.kde.klipper', '/klipper', 'setClipboardContents', - text.encode('utf-8')], - stdin=subprocess.PIPE, close_fds=True) - p.communicate(input=None) - - def paste_klipper(): - p = subprocess.Popen( - ['qdbus', 'org.kde.klipper', '/klipper', 'getClipboardContents'], - stdout=subprocess.PIPE, close_fds=True) - stdout, stderr = p.communicate() - - # Workaround for https://bugs.kde.org/show_bug.cgi?id=342874 - # TODO: https://github.com/asweigart/pyperclip/issues/43 - clipboardContents = stdout.decode('utf-8') - # even if blank, Klipper will append a newline at the end - assert len(clipboardContents) > 0 - # make sure that newline is there - assert clipboardContents.endswith('\n') - if clipboardContents.endswith('\n'): - clipboardContents = clipboardContents[:-1] - return clipboardContents - - return copy_klipper, paste_klipper - - -def init_no_clipboard(): - class ClipboardUnavailable(object): - def __call__(self, *args, **kwargs): - raise PyperclipException(EXCEPT_MSG) - - if PY2: - def __nonzero__(self): - return False - else: - def __bool__(self): - return False - - return ClipboardUnavailable(), ClipboardUnavailable() diff --git a/tools/midi2piano/pyperclip/exceptions.py b/tools/midi2piano/pyperclip/exceptions.py deleted file mode 100644 index c5ba3e75d2a..00000000000 --- a/tools/midi2piano/pyperclip/exceptions.py +++ /dev/null @@ -1,11 +0,0 @@ -import ctypes - - -class PyperclipException(RuntimeError): - pass - - -class PyperclipWindowsException(PyperclipException): - def __init__(self, message): - message += " (%s)" % ctypes.WinError() - super(PyperclipWindowsException, self).__init__(message) diff --git a/tools/midi2piano/pyperclip/windows.py b/tools/midi2piano/pyperclip/windows.py deleted file mode 100644 index a12932a575f..00000000000 --- a/tools/midi2piano/pyperclip/windows.py +++ /dev/null @@ -1,151 +0,0 @@ -""" -This module implements clipboard handling on Windows using ctypes. -""" -import time -import contextlib -import ctypes -from ctypes import c_size_t, sizeof, c_wchar_p, get_errno, c_wchar -from .exceptions import PyperclipWindowsException - - -class CheckedCall(object): - def __init__(self, f): - super(CheckedCall, self).__setattr__("f", f) - - def __call__(self, *args): - ret = self.f(*args) - if not ret and get_errno(): - raise PyperclipWindowsException("Error calling " + self.f.__name__) - return ret - - def __setattr__(self, key, value): - setattr(self.f, key, value) - - -def init_windows_clipboard(): - from ctypes.wintypes import (HGLOBAL, LPVOID, DWORD, LPCSTR, INT, HWND, - HINSTANCE, HMENU, BOOL, UINT, HANDLE) - - windll = ctypes.windll - - safeCreateWindowExA = CheckedCall(windll.user32.CreateWindowExA) - safeCreateWindowExA.argtypes = [DWORD, LPCSTR, LPCSTR, DWORD, INT, INT, - INT, INT, HWND, HMENU, HINSTANCE, LPVOID] - safeCreateWindowExA.restype = HWND - - safeDestroyWindow = CheckedCall(windll.user32.DestroyWindow) - safeDestroyWindow.argtypes = [HWND] - safeDestroyWindow.restype = BOOL - - OpenClipboard = windll.user32.OpenClipboard - OpenClipboard.argtypes = [HWND] - OpenClipboard.restype = BOOL - - safeCloseClipboard = CheckedCall(windll.user32.CloseClipboard) - safeCloseClipboard.argtypes = [] - safeCloseClipboard.restype = BOOL - - safeEmptyClipboard = CheckedCall(windll.user32.EmptyClipboard) - safeEmptyClipboard.argtypes = [] - safeEmptyClipboard.restype = BOOL - - safeGetClipboardData = CheckedCall(windll.user32.GetClipboardData) - safeGetClipboardData.argtypes = [UINT] - safeGetClipboardData.restype = HANDLE - - safeSetClipboardData = CheckedCall(windll.user32.SetClipboardData) - safeSetClipboardData.argtypes = [UINT, HANDLE] - safeSetClipboardData.restype = HANDLE - - safeGlobalAlloc = CheckedCall(windll.kernel32.GlobalAlloc) - safeGlobalAlloc.argtypes = [UINT, c_size_t] - safeGlobalAlloc.restype = HGLOBAL - - safeGlobalLock = CheckedCall(windll.kernel32.GlobalLock) - safeGlobalLock.argtypes = [HGLOBAL] - safeGlobalLock.restype = LPVOID - - safeGlobalUnlock = CheckedCall(windll.kernel32.GlobalUnlock) - safeGlobalUnlock.argtypes = [HGLOBAL] - safeGlobalUnlock.restype = BOOL - - GMEM_MOVEABLE = 0x0002 - CF_UNICODETEXT = 13 - - @contextlib.contextmanager - def window(): - """ - Context that provides a valid Windows hwnd. - """ - # we really just need the hwnd, so setting "STATIC" - # as predefined lpClass is just fine. - hwnd = safeCreateWindowExA(0, b"STATIC", None, 0, 0, 0, 0, 0, - None, None, None, None) - try: - yield hwnd - finally: - safeDestroyWindow(hwnd) - - @contextlib.contextmanager - def clipboard(hwnd): - """ - Context manager that opens the clipboard and prevents - other applications from modifying the clipboard content. - """ - # We may not get the clipboard handle immediately because - # some other application is accessing it (?) - # We try for at least 500ms to get the clipboard. - t = time.time() + 0.5 - success = False - while time.time() < t: - success = OpenClipboard(hwnd) - if success: - break - time.sleep(0.01) - if not success: - raise PyperclipWindowsException("Error calling OpenClipboard") - - try: - yield - finally: - safeCloseClipboard() - - def copy_windows(text): - # This function is heavily based on - # http://msdn.com/ms649016#_win32_Copying_Information_to_the_Clipboard - with window() as hwnd: - # http://msdn.com/ms649048 - # If an application calls OpenClipboard with hwnd set to NULL, - # EmptyClipboard sets the clipboard owner to NULL; - # this causes SetClipboardData to fail. - # => We need a valid hwnd to copy something. - with clipboard(hwnd): - safeEmptyClipboard() - - if text: - # http://msdn.com/ms649051 - # If the hMem parameter identifies a memory object, - # the object must have been allocated using the - # function with the GMEM_MOVEABLE flag. - count = len(text) + 1 - handle = safeGlobalAlloc(GMEM_MOVEABLE, - count * sizeof(c_wchar)) - locked_handle = safeGlobalLock(handle) - - ctypes.memmove(c_wchar_p(locked_handle), c_wchar_p(text), count * sizeof(c_wchar)) - - safeGlobalUnlock(handle) - safeSetClipboardData(CF_UNICODETEXT, handle) - - def paste_windows(): - with clipboard(None): - handle = safeGetClipboardData(CF_UNICODETEXT) - if not handle: - # GetClipboardData may return NULL with errno == NO_ERROR - # if the clipboard is empty. - # (Also, it may return a handle to an empty buffer, - # but technically that's not empty) - return "" - return c_wchar_p(handle).value - - return copy_windows, paste_windows diff --git a/tools/od_annotator/__main__.py b/tools/od_annotator/__main__.py new file mode 100644 index 00000000000..357adccfe91 --- /dev/null +++ b/tools/od_annotator/__main__.py @@ -0,0 +1,50 @@ +import sys +import re + +def green(text): + return "\033[32m" + str(text) + "\033[0m" + +def red(text): + return "\033[31m" + str(text) + "\033[0m" + +def annotate(raw_output): + # Remove ANSI escape codes + raw_output = re.sub(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]', '', raw_output) + + print("::group::OpenDream Output") + print(raw_output) + print("::endgroup::") + + annotation_regex = r'((?PError|Warning) (?POD(?P\d{4})) at (?P(?P.+):(?P\d+):(?P\d+)|): (?P.+))' + failures_detected = False + expected_failure_case_detected = False # this is just here so this script breaks if we forget to set it to True when we expect a failure. remove this when we have handled the expected failure + + print("OpenDream Code Annotations:") + for annotation in re.finditer(annotation_regex, raw_output): + message = annotation['message'] + if message == "Unimplemented proc & var warnings are currently suppressed": # this happens every single run, it's important to know about it but we don't need to throw an error + message += " (This is expected and can be ignored)" # also there's no location for it to annotate to since it's an failure. + expected_failure_case_detected = True + + if annotation['type'] == "Error": + failures_detected = True + + error_string = f"{annotation['errorcode']}: {message}" + + if annotation['location'] == "": + print(f"::{annotation['type']} file=,line=,col=::{error_string}") + else: + print(f"::{annotation['type']} file={annotation['filename']},line={annotation['line']},col={annotation['column']}::{error_string}") + + if failures_detected: + sys.exit(1) + return + + if not expected_failure_case_detected: + print(red("Failed to detect the expected failure case! If you have recently changed how we work with OpenDream Pragmas, please fix the od_annotator script!")) + sys.exit(1) + return + + print(green("No OpenDream issues found!")) + +annotate(sys.stdin.read()) diff --git a/tools/pull_request_hooks/autoChangelog.js b/tools/pull_request_hooks/autoChangelog.js index bc24481f182..2e9f656fe7e 100644 --- a/tools/pull_request_hooks/autoChangelog.js +++ b/tools/pull_request_hooks/autoChangelog.js @@ -1,42 +1,42 @@ import { parseChangelog } from "./changelogParser.js"; const safeYml = (string) => - string.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n"); + string.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n"); export function changelogToYml(changelog, login) { - const author = changelog.author || login; - const ymlLines = []; + const author = changelog.author || login; + const ymlLines = []; - ymlLines.push(`author: "${safeYml(author)}"`); - ymlLines.push(`delete-after: True`); - ymlLines.push(`changes:`); + ymlLines.push(`author: "${safeYml(author)}"`); + ymlLines.push(`delete-after: True`); + ymlLines.push(`changes:`); - for (const change of changelog.changes) { - ymlLines.push( - ` - ${change.type.changelogKey}: "${safeYml(change.description)}"` - ); - } + for (const change of changelog.changes) { + ymlLines.push( + ` - ${change.type.changelogKey}: "${safeYml(change.description)}"`, + ); + } - return ymlLines.join("\n"); + return ymlLines.join("\n"); } export async function processAutoChangelog({ github, context }) { - const changelog = parseChangelog(context.payload.pull_request.body); - if (!changelog || changelog.changes.length === 0) { - console.log("no changelog found"); - return; - } + const changelog = parseChangelog(context.payload.pull_request.body); + if (!changelog || changelog.changes.length === 0) { + console.log("no changelog found"); + return; + } - const yml = changelogToYml( - changelog, - context.payload.pull_request.user.login - ); + const yml = changelogToYml( + changelog, + context.payload.pull_request.user.login, + ); - github.rest.repos.createOrUpdateFileContents({ - owner: context.repo.owner, - repo: context.repo.repo, - path: `html/changelogs/AutoChangeLog-pr-${context.payload.pull_request.number}.yml`, - message: `Automatic changelog for PR #${context.payload.pull_request.number} [ci skip]`, - content: Buffer.from(yml).toString("base64"), - }); + github.rest.repos.createOrUpdateFileContents({ + owner: context.repo.owner, + repo: context.repo.repo, + path: `html/changelogs/AutoChangeLog-pr-${context.payload.pull_request.number}.yml`, + message: `Automatic changelog for PR #${context.payload.pull_request.number} [ci skip]`, + content: Buffer.from(yml).toString("base64"), + }); } diff --git a/tools/pull_request_hooks/autoChangelog.test.js b/tools/pull_request_hooks/autoChangelog.test.js index e7c29c384a9..821c29c6a42 100644 --- a/tools/pull_request_hooks/autoChangelog.test.js +++ b/tools/pull_request_hooks/autoChangelog.test.js @@ -3,19 +3,19 @@ import { changelogToYml } from "./autoChangelog.js"; import { parseChangelog } from "./changelogParser.js"; assert.equal( - changelogToYml( - parseChangelog(` + changelogToYml( + parseChangelog(` My cool PR! :cl: DenverCoder9 add: Adds new stuff add: Adds more stuff /:cl: - `) - ), + `), + ), - `author: "DenverCoder9" + `author: "DenverCoder9" delete-after: True changes: - rscadd: "Adds new stuff" - - rscadd: "Adds more stuff"` + - rscadd: "Adds more stuff"`, ); diff --git a/tools/pull_request_hooks/autoLabel.js b/tools/pull_request_hooks/autoLabel.js new file mode 100644 index 00000000000..2aa77b2cf6e --- /dev/null +++ b/tools/pull_request_hooks/autoLabel.js @@ -0,0 +1,276 @@ +import * as autoLabelConfig from "./autoLabelConfig.js"; + +/** + * Precompute a lowercase keyword → changelog label map + */ +const keywordToClLabel = (() => { + const map = {}; + for (const [label, { keywords }] of Object.entries( + autoLabelConfig.changelog_labels, + )) { + for (const keyword of keywords) { + map[keyword.toLowerCase()] = label; + } + } + return map; +})(); + +/** + * Precompute title keyword Sets per label for O(1) lookup + */ +const titleKeywordSets = (() => { + const map = {}; + for (const [label, { keywords }] of Object.entries( + autoLabelConfig.title_labels, + )) { + map[label] = new Set(keywords.map((k) => k.toLowerCase())); + } + return map; +})(); + +/** + * Precompute filepaths Sets per label for O(1) lookup + */ +const fileLabelFilepathSets = (() => { + const map = {}; + for (const [ + label, + { filepaths = [], file_extensions = [], add_only }, + ] of Object.entries(autoLabelConfig.file_labels)) { + map[label] = { + filepaths: new Set(filepaths), + file_extensions: new Set(file_extensions), + add_only, + }; + } + return map; +})(); + +/** + * Checks the body (primarily the changelog) for labels to add + */ +function check_body_for_labels(body) { + const labels_to_add = []; + + // detect "fixes #1234" or "resolves #1234" in body + const fix_regex = + /\b(?:fix(?:es|ed)?|resolve[sd]?)\s*(?:#\d+|https:\/\/github\.com\/\S+\/issues\/\d+)/gim; + if (fix_regex.test(body)) { + labels_to_add.push("Fix"); + } + + const lines = body.split("\n"); + let inChangelog = false; + + for (const line of lines) { + if (line.startsWith(":cl:")) { + inChangelog = true; + continue; + } + if (line.startsWith("/:cl:")) break; + if (!inChangelog) continue; + + // see if the first segment of the line is one of the keywords + const keyword = line.split(":")[0]?.toLowerCase(); + const found_label = keywordToClLabel[keyword]; + if (!found_label) continue; + + // don't add a billion tags if they forgot to clear all the default ones + const line_text = line.split(":")[1]?.trim(); + const { default_text, alt_default_text } = + autoLabelConfig.changelog_labels[found_label]; + + if (line_text !== default_text && line_text !== alt_default_text) { + labels_to_add.push(found_label); + } + } + + return labels_to_add; +} + +/** + * Checks the title for labels to add (O(1) keyword lookup) + */ +function check_title_for_labels(title) { + const title_lower = title.toLowerCase(); + const labels_to_add = []; + + for (const [label, keywordSet] of Object.entries(titleKeywordSets)) { + for (const keyword of keywordSet) { + if (title_lower.includes(keyword)) { + labels_to_add.push(label); + break; + } + } + } + return labels_to_add; +} + +/** + * Checks changed files for labels to add/remove (O(1) filepath lookup) + */ +async function check_diff_files_for_labels(github, context) { + const labels_to_add = []; + const labels_to_remove = []; + + try { + // Use github.paginate to fetch all files (up to ~3000 max) + const allFiles = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + per_page: 100, // max per request + }); + + if (!allFiles?.length) { + console.error("No files returned in pagination."); + return { labels_to_add, labels_to_remove }; + } + + // Set of changed filenames for quick lookup + const changedFiles = new Set(allFiles.map((f) => f.filename)); + + for (const [ + label, + { filepaths = new Set(), file_extensions = new Set(), add_only }, + ] of Object.entries(fileLabelFilepathSets)) { + let found = false; + + // Filepath-based matching + for (const filename of changedFiles) { + for (const path of filepaths) { + if (filename.includes(path)) { + found = true; + break; + } + } + if (found) break; + } + + // File extension-based matching + if (!found && file_extensions.size) { + for (const filename of changedFiles) { + for (const ext of file_extensions) { + if (filename.endsWith(ext)) { + found = true; + break; + } + } + if (found) break; + } + } + + if (found) { + labels_to_add.push(label); + } else if (!add_only) { + labels_to_remove.push(label); + } + } + } catch (error) { + console.error("Error fetching paginated files:", error); + } + + return { labels_to_add, labels_to_remove }; +} + +/** + * Main function to get the updated label set + */ +export async function get_updated_label_set({ github, context }) { + const { pull_request } = context.payload; + const { + body = "", + diff_url, + labels = [], + mergeable, + title = "", + } = pull_request; + + const updated_labels = new Set(labels.map((l) => l.name)); + + // Always check file diffs + if (diff_url) { + const { labels_to_add, labels_to_remove } = + await check_diff_files_for_labels(github, context); + labels_to_add.forEach((label) => updated_labels.add(label)); + labels_to_remove.forEach((label) => updated_labels.delete(label)); + } + + // Always check body/title (otherwise we can lose the changelog labels) + if (title) + check_title_for_labels(title).forEach((label) => updated_labels.add(label)); + if (body) + check_body_for_labels(body).forEach((label) => updated_labels.add(label)); + + // Keep track of labels that were manually added/removed by maintainers in the events. + // And make sure they -stay- added/removed. + try { + const events = await github.paginate( + github.rest.issues.listEventsForTimeline, + { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + per_page: 100, + }, + ); + + for (const eventData of events) { + // Skip all bot actions + if (eventData.actor?.login === "github-actions[bot]") { + continue; + } + if (eventData.event === "labeled") { + updated_labels.add(eventData.label.name); + } else if (eventData.event === "unlabeled") { + updated_labels.delete(eventData.label.name); + } + } + } catch (error) { + console.error("Error fetching paginated events:", error); + } + + // Always remove Test Merge Candidate + updated_labels.delete("Test Merge Candidate"); + + // Handle merge conflict label + let merge_conflict = mergeable === false; + // null means it was not reported yet + // it is not normally included in the payload - a "get" is needed + if (mergeable === null) { + try { + let response = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pull_request.number, + }); + // failed to find? still processing? try again in a few seconds + + if (response.data.mergeable === null) { + console.log("Awaiting GitHub response for merge status..."); + await new Promise((r) => setTimeout(r, 10000)); + response = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pull_request.number, + }); + if (response.data.mergeable === null) { + throw new Error("Merge status not available"); + } + } + + merge_conflict = response.data.mergeable === false; + } catch (e) { + console.error(e); + } + } + + if (merge_conflict) { + updated_labels.add("Merge Conflict"); + } else { + updated_labels.delete("Merge Conflict"); + } + + // return the labels to the action, which will apply it + return [...updated_labels]; +} diff --git a/tools/pull_request_hooks/autoLabel.test.js b/tools/pull_request_hooks/autoLabel.test.js new file mode 100644 index 00000000000..09a3946f52f --- /dev/null +++ b/tools/pull_request_hooks/autoLabel.test.js @@ -0,0 +1,59 @@ +import { strict as assert } from "node:assert"; +import { get_updated_label_set } from "./autoLabel.js"; + +const empty_pr = { + action: "opened", + pull_request: { + body: "This PR will have no labels", + title: "Pr with no labels", + mergeable: true, + }, +}; +const empty_label_set = await get_updated_label_set({ + github: null, + context: { payload: empty_pr }, +}); +assert.equal(empty_label_set.length, 0, "No labels should be added"); + +const cl = ` +My Awesome PR + +:cl: Awesome Dude +add: Adds Awesome Stuff +refactor: refactored some code +:/cl: +`; +const cl_pr = { + action: "opened", + pull_request: { + body: cl, + title: "Awesome PR", + mergeable: false, + }, +}; +const cl_label_set = await get_updated_label_set({ + github: null, + context: { payload: cl_pr }, +}); +assert.ok( + cl_label_set.includes("Merge Conflict"), + "Merge Conflict label should be added", +); +assert.ok(cl_label_set.includes("Feature"), "Feature label should be added"); +assert.ok( + !cl_label_set.includes("Refactor"), + "Refactor label should not be added", +); + +const title_pr = { + action: "opened", + pull_request: { + title: "Logging is important", + mergeable: true, + }, +}; +const title_label_set = await get_updated_label_set({ + github: null, + context: { payload: title_pr }, +}); +assert.ok(title_label_set.includes("Logging"), "Logging label should be added"); diff --git a/tools/pull_request_hooks/autoLabelConfig.js b/tools/pull_request_hooks/autoLabelConfig.js new file mode 100644 index 00000000000..c54561c8f40 --- /dev/null +++ b/tools/pull_request_hooks/autoLabelConfig.js @@ -0,0 +1,127 @@ +// File Labels +// +// Add a label based on if a file is modified in the diff +// +// You can optionally set add_only to make the label one-way - +// if the edit to the file is removed in a later commit, +// the label will not be removed +export const file_labels = { + GitHub: { + filepaths: [".github/"], + }, + SQL: { + filepaths: ["SQL/"], + }, + "Map Edit": { + filepaths: ["maps/"], + file_extensions: [".dmm"], + }, + Tools: { + filepaths: ["tools/"], + }, + "Config Update": { + filepaths: [ + "config/", + "code/controllers/configuration/entries/", + "code/controllers/toml_config/entries/", + ], + add_only: true, + }, + Sprites: { + filepaths: ["icons/"], + file_extensions: [".dmi"], + add_only: true, + }, + Sound: { + filepaths: ["sound/"], + file_extensions: [".ogg"], + add_only: true, + }, + UI: { + filepaths: ["tgui/"], + add_only: true, + }, +}; + +// Title Labels +// +// Add a label based on keywords in the title +export const title_labels = { + Logging: { + keywords: ["log", "logging"], + }, + Removal: { + keywords: ["remove", "delete"], + }, + Refactor: { + keywords: ["refactor"], + }, + "Unit Tests": { + keywords: ["unit test"], + }, + "April Fools": { + keywords: ["[april fools]"], + }, + "Do Not Merge": { + keywords: ["[dnm]", "[do not merge]"], + }, + "Test Merge Only": { + keywords: ["[tm only]", "[test merge only]"], + }, +}; + +// Changelog Labels +// +// Adds labels based on keywords in the changelog +// TODO use the existing changelog parser +export const changelog_labels = { + Fix: { + default_text: "fixed a few things", + keywords: ["fix", "fixes", "bugfix"], + }, + "Quality of Life": { + default_text: "made something easier to use", + keywords: ["qol"], + }, + Sound: { + default_text: "added/modified/removed audio or sound effects", + keywords: ["sound"], + }, + Feature: { + default_text: "Added new mechanics or gameplay changes", + alt_default_text: "Added more things", + keywords: ["add", "adds", "rscadd"], + }, + Removal: { + default_text: "Removed old things", + keywords: ["del", "dels", "rscdel"], + }, + Sprites: { + default_text: "added/modified/removed some icons or images", + keywords: ["image"], + }, + "Grammar and Formatting": { + default_text: "fixed a few typos", + keywords: ["typo", "spellcheck"], + }, + Balance: { + default_text: "rebalanced something", + keywords: ["balance"], + }, + "Code Improvement": { + default_text: "changed some code", + keywords: ["code_imp", "code"], + }, + Refactor: { + default_text: "refactored some code", + keywords: ["refactor"], + }, + "Config Update": { + default_text: "changed some config setting", + keywords: ["config"], + }, + Administration: { + default_text: "messed with admin stuff", + keywords: ["admin"], + }, +}; diff --git a/tools/pull_request_hooks/changelogConfig.js b/tools/pull_request_hooks/changelogConfig.js index 545a6d551eb..1d0a2344b7c 100644 --- a/tools/pull_request_hooks/changelogConfig.js +++ b/tools/pull_request_hooks/changelogConfig.js @@ -8,120 +8,120 @@ * doesn't exist. */ export const CHANGELOG_ENTRIES = [ - [ - ["rscadd", "add", "adds"], - { - placeholders: [ - "Added new mechanics or gameplay changes", - "Added more things", - ], - }, - ], + [ + ["rscadd", "add", "adds"], + { + placeholders: [ + "Added new mechanics or gameplay changes", + "Added more things", + ], + }, + ], - [ - ["bugfix", "fix", "fixes"], - { - placeholders: ["fixed a few things"], - }, - ], + [ + ["bugfix", "fix", "fixes"], + { + placeholders: ["fixed a few things"], + }, + ], - [ - ["rscdel", "del", "dels"], - { - placeholders: ["Removed old things"], - }, - ], + [ + ["rscdel", "del", "dels"], + { + placeholders: ["Removed old things"], + }, + ], - [ - ["tweak"], - { - placeholders: ["tweaked a few things"], - }, - ], + [ + ["tweak"], + { + placeholders: ["tweaked a few things"], + }, + ], - [ - ["qol"], - { - placeholders: ["made something easier to use"], - }, - ], + [ + ["qol"], + { + placeholders: ["made something easier to use"], + }, + ], - [ - ["soundadd"], - { - placeholders: ["added a new sound thingy"], - }, - ], + [ + ["soundadd"], + { + placeholders: ["added a new sound thingy"], + }, + ], - [ - ["sounddel"], - { - placeholders: ["removed an old sound thingy"], - }, - ], + [ + ["sounddel"], + { + placeholders: ["removed an old sound thingy"], + }, + ], - [ - ["imageadd"], - { - placeholders: ["added some icons and images"], - }, - ], + [ + ["imageadd"], + { + placeholders: ["added some icons and images"], + }, + ], - [ - ["imagedel"], - { - placeholders: ["deleted some icons and images"], - }, - ], + [ + ["imagedel"], + { + placeholders: ["deleted some icons and images"], + }, + ], - [ - ["spellcheck", "typo"], - { - placeholders: ["fixed a few typos"], - }, - ], + [ + ["spellcheck", "typo"], + { + placeholders: ["fixed a few typos"], + }, + ], - [ - ["balance"], - { - placeholders: ["rebalanced something"], - }, - ], + [ + ["balance"], + { + placeholders: ["rebalanced something"], + }, + ], - [ - ["code_imp", "code"], - { - placeholders: ["changed some code"], - }, - ], + [ + ["code_imp", "code"], + { + placeholders: ["changed some code"], + }, + ], - [ - ["refactor"], - { - placeholders: ["refactored some code"], - }, - ], + [ + ["refactor"], + { + placeholders: ["refactored some code"], + }, + ], - [ - ["config"], - { - placeholders: ["changed some config setting"], - }, - ], + [ + ["config"], + { + placeholders: ["changed some config setting"], + }, + ], - [ - ["admin"], - { - placeholders: ["messed with admin stuff"], - }, - ], + [ + ["admin"], + { + placeholders: ["messed with admin stuff"], + }, + ], - [ - ["server"], - { - placeholders: ["something server ops should know"], - }, - ], + [ + ["server"], + { + placeholders: ["something server ops should know"], + }, + ], ]; // Valid changelog openers diff --git a/tools/pull_request_hooks/changelogParser.js b/tools/pull_request_hooks/changelogParser.js index 2daa2a57989..03b55e14e60 100644 --- a/tools/pull_request_hooks/changelogParser.js +++ b/tools/pull_request_hooks/changelogParser.js @@ -40,9 +40,9 @@ function parseChangelogBody(lines, openTag) { if (match) { const [_, type, description] = match; - const entry = CHANGELOG_KEYS_TO_ENTRY[type]; + const entry = CHANGELOG_KEYS_TO_ENTRY[type.toLowerCase()]; - if (entry.placeholders.includes(description)) { + if (!entry || entry.placeholders.includes(description)) { continue; } @@ -64,6 +64,9 @@ function parseChangelogBody(lines, openTag) { } export function parseChangelog(text) { + if (text == null) { + return undefined; + } const lines = text.split("\n").map((line) => line.trim()); for (let index = 0; index < lines.length; index++) { diff --git a/tools/pull_request_hooks/changelogParser.test.js b/tools/pull_request_hooks/changelogParser.test.js index bcf044ee36d..fdb460ed34a 100644 --- a/tools/pull_request_hooks/changelogParser.test.js +++ b/tools/pull_request_hooks/changelogParser.test.js @@ -14,6 +14,19 @@ assert.equal(basicChangelog.changes.length, 1); assert.equal(basicChangelog.changes[0].type.changelogKey, "rscadd"); assert.equal(basicChangelog.changes[0].description, "Adds new stuff"); +// Case-insensitivity test +const mixedCaseChangelog = parseChangelog(` + My cool PR! + :cl: DenverCoder9 + Add: Adds new stuff + /:cl: +`); + +assert.equal(mixedCaseChangelog.author, "DenverCoder9"); +assert.equal(mixedCaseChangelog.changes.length, 1); +assert.equal(mixedCaseChangelog.changes[0].type.changelogKey, "rscadd"); +assert.equal(mixedCaseChangelog.changes[0].description, "Adds new stuff"); + // Multi-line test const multiLineChangelog = parseChangelog(` My cool PR! @@ -27,8 +40,8 @@ assert.equal(multiLineChangelog.author, undefined); assert.equal(multiLineChangelog.changes.length, 1); assert.equal(multiLineChangelog.changes[0].type.changelogKey, "rscadd"); assert.equal( - multiLineChangelog.changes[0].description, - "Adds new stuff\nto the game" + multiLineChangelog.changes[0].description, + "Adds new stuff\nto the game", ); // Placeholders diff --git a/tools/pull_request_hooks/removeGuideComments.js b/tools/pull_request_hooks/removeGuideComments.js new file mode 100644 index 00000000000..66846ad3c85 --- /dev/null +++ b/tools/pull_request_hooks/removeGuideComments.js @@ -0,0 +1,49 @@ +import fs from "fs"; + +const REGEX_COMMENT = //g; + +// Make sure we only remove default comments +const comments = []; + +for (const match of fs + .readFileSync(".github/PULL_REQUEST_TEMPLATE.md", { encoding: "utf8" }) + .matchAll(REGEX_COMMENT)) { + comments.push(match[0]); +} + +function escapeRegex(string) { + return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); +} + +export async function removeGuideComments({ github, context }) { + const originalBody = ( + await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + }) + ).data.body; + + if (!originalBody) { + console.log("PR body is empty, skipping..."); + return; + } + + let newBody = originalBody; + + for (const comment of comments) { + newBody = newBody.replace( + new RegExp(`^\\s*${escapeRegex(comment)}\\s*`, "gm"), + "\n", + ); + } + + if (newBody !== originalBody) { + await github.rest.pulls.update({ + pull_number: context.payload.pull_request.number, + repo: context.repo.repo, + owner: context.repo.owner, + body: newBody, + }); + } +} diff --git a/tools/requirements.txt b/tools/requirements.txt index ce4c953d5eb..b4e6c5cdf3e 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,10 +1,10 @@ -pygit2==1.15 +pygit2==1.19.0 bidict==0.23.1 -Pillow==11.0.0 +Pillow==11.1.0 # changelogs -PyYaml==6.0.1 -beautifulsoup4==4.9.3 +PyYaml==6.0.3 +beautifulsoup4==4.14.3 # icon cutter -numpy==2.3.3 +numpy==2.3.5 diff --git a/tools/ss13_genchangelog.py b/tools/ss13_genchangelog.py index c999e42a247..7f040cbbc8a 100644 --- a/tools/ss13_genchangelog.py +++ b/tools/ss13_genchangelog.py @@ -128,6 +128,7 @@ for fileName in glob.glob(os.path.join(args.ymlDir, "*.yml")): (change_type, _) = dictToTuples(change)[0] if change_type not in validPrefixes: print(' {0}: Invalid prefix {1}'.format(fileName, change_type), file=sys.stderr) + sys.exit(1) author_entries += [change] new += 1 currentEntries[today][cl['author']] = author_entries diff --git a/tools/tgs4_scripts/PreCompile.bat b/tools/tgs4_scripts/PreCompile.bat deleted file mode 100644 index 25b55cb66d9..00000000000 --- a/tools/tgs4_scripts/PreCompile.bat +++ /dev/null @@ -1,15 +0,0 @@ -@echo off -cd /D "%~dp0" -set TG_BOOTSTRAP_CACHE=%cd% -IF NOT %1 == "" ( - rem TGS4: we are passed the game directory on the command line - cd %1 -) ELSE IF EXIST "..\Game\B\citadel.dmb" ( - rem TGS3: Game/B/citadel.dmb exists, so build in Game/A - cd ..\Game\A -) ELSE ( - rem TGS3: Otherwise build in Game/B - cd ..\Game\B -) -set CBT_BUILD_MODE=TGS -tools\build\build diff --git a/tools/tgs4_scripts/InstallDeps.sh b/tools/tgs_scripts/InstallDeps.sh similarity index 84% rename from tools/tgs4_scripts/InstallDeps.sh rename to tools/tgs_scripts/InstallDeps.sh index e006a72b95a..260d0ed6515 100644 --- a/tools/tgs4_scripts/InstallDeps.sh +++ b/tools/tgs_scripts/InstallDeps.sh @@ -4,11 +4,11 @@ set +e has_git="$(command -v git)" has_curl="$(command -v curl)" -has_sudo="$(command -v sudo)" -has_unzip="$(command -v unzip)" has_cargo="$(command -v ~/.cargo/bin/cargo)" -has_ytdlp="$(command -v ~/.local/bin/yt-dlp)" +has_sudo="$(command -v sudo)" +has_ytdlp="$(command -v yt-dlp)" has_uv="$(command -v ~/.local/bin/uv)" +has_unzip="$(command -v unzip)" set -e set -x @@ -41,8 +41,18 @@ if ! [ -x "$has_cargo" ]; then . ~/.profile fi -# install yt-dlp -if ! [ -x "$has_ytdlp" ]; then - echo "Installing ytdlp..." - ~/.local/bin/uv tool install yt-dlp +if ! [ -x "$has_uv" ]; then + echo "Installing uv..." + curl https://astral.sh/uv/install.sh -LsSf | sh -s + . ~/.profile +fi + +# install or update yt-dlp when not present, or if it is present with uv, +# which we assume was used to install it +if ! [ -x "$has_ytdlp" ]; then + echo "Installing yt-dlp with uv..." + ~/.local/bin/uv tool install yt-dlp +else + echo "Ensuring yt-dlp is up-to-date with uv..." + ~/.local/bin/uv tool upgrade yt-dlp fi diff --git a/tools/tgs_scripts/PreCompile.bat b/tools/tgs_scripts/PreCompile.bat new file mode 100644 index 00000000000..43d3066ae89 --- /dev/null +++ b/tools/tgs_scripts/PreCompile.bat @@ -0,0 +1,6 @@ +@echo off +cd /D "%~dp0" +set TG_BOOTSTRAP_CACHE=%cd% +cd %1 +set CBT_BUILD_MODE=TGS +tools\build\build diff --git a/tools/tgs4_scripts/PreCompile.sh b/tools/tgs_scripts/PreCompile.sh similarity index 93% rename from tools/tgs4_scripts/PreCompile.sh rename to tools/tgs_scripts/PreCompile.sh index af7a2a0139a..c3545072bb2 100755 --- a/tools/tgs4_scripts/PreCompile.sh +++ b/tools/tgs_scripts/PreCompile.sh @@ -2,6 +2,9 @@ ./InstallDeps.sh +set -e +set -x + #load dep exports #need to switch to game dir for Dockerfile weirdness original_dir=$PWD @@ -25,7 +28,7 @@ fi echo "Deploying rust-g..." git checkout "$RUST_G_VERSION" -env PKG_CONFIG_ALLOW_CROSS=1 ~/.cargo/bin/cargo build --ignore-rust-version --release --target=i686-unknown-linux-gnu --features=all +env PKG_CONFIG_ALLOW_CROSS=1 ~/.cargo/bin/cargo build --ignore-rust-version --release --target=i686-unknown-linux-gnu cp -f target/i686-unknown-linux-gnu/release/librust_g.so "$1/librust_g.so" cd .. diff --git a/tools/tgs4_scripts/WatchdogLaunch.sh b/tools/tgs_scripts/WatchdogLaunch.sh similarity index 100% rename from tools/tgs4_scripts/WatchdogLaunch.sh rename to tools/tgs_scripts/WatchdogLaunch.sh diff --git a/tools/tgs_test/Program.cs b/tools/tgs_test/Program.cs new file mode 100644 index 00000000000..8bc77e9e8e1 --- /dev/null +++ b/tools/tgs_test/Program.cs @@ -0,0 +1,364 @@ +// Simple app meant to test tgstation's TGS integration given a fresh TGS install with the default account +// +// Args: Repository Owner/Name, TGS instance path, TGS API port, Pushed commit hash (For .tgs.yml access), GitHub Token, (OPTIONAL) PR Number + +using System.Reflection; +using System.Text; + +using Octokit; +using Tgstation.Server.Api; +using Tgstation.Server.Api.Models.Request; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Response; +using Tgstation.Server.Client; +using Tgstation.Server.Common.Extensions; +using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.Serialization; + +Console.WriteLine("Parsing args..."); + +if (args.Length < 5 || args.Length > 6) +{ + Console.WriteLine($"Incorrect number of args: {args.Length}. Expected 5-6"); + return 1; +} + +var repoSlug = args[0]; +var instancePath = args[1]; +var tgsApiPortString = args[2]; +var pushedCommitHash = args[3]; +var gitHubToken = args[4]; + +int? pullRequest = default; +if(args.Length == 6) +{ + if (!Int32.TryParse(args[5], out int prNumber)) + { + Console.WriteLine($"Invalid repo slug: {repoSlug}"); + return 10; + } + + pullRequest = prNumber; +} + +var repoSlugSplits = repoSlug.Split('/', StringSplitOptions.RemoveEmptyEntries); +if(repoSlugSplits.Length != 2) +{ + Console.WriteLine($"Invalid repo slug: {repoSlug}"); + return 2; +} + +var repoOwner = repoSlugSplits[0]; +var repoName = repoSlugSplits[1]; + +if (!ushort.TryParse(tgsApiPortString, out var tgsApiPort)) +{ + Console.WriteLine($"Invalid port: {tgsApiPortString}"); + return 3; +} + +try +{ + Console.WriteLine($"Retrieving .tgs.yml (@{pushedCommitHash})..."); + var assemblyName = Assembly.GetExecutingAssembly().GetName(); + var gitHubClient = new GitHubClient( + new ProductHeaderValue( + assemblyName.Name, + assemblyName.Version!.Semver().ToString())) + { + Credentials = new Credentials(gitHubToken) + }; + + var tgsYmlContent = await gitHubClient.Repository.Content.GetRawContentByRef(repoOwner, repoName, ".tgs.yml", pushedCommitHash); + var tgsYmlString = Encoding.UTF8.GetString(tgsYmlContent); + + var deserializer = new DeserializerBuilder() + .WithNamingConvention(new UnderscoredNamingConvention()) + .Build(); + + var tgsYml = deserializer.Deserialize(tgsYmlString); + + const int SupportedTgsYmlVersion = 1; + if (tgsYml.Version != SupportedTgsYmlVersion) + { + Console.WriteLine($"Unsupported .tgs.yml version: {tgsYml.Version}. Expected {SupportedTgsYmlVersion}"); + return 4; + } + + var targetByondVersion = Version.Parse(tgsYml.Byond); + + Console.WriteLine($".tgs.yml Security level: {tgsYml.Security}"); + + Console.WriteLine("Downloading and checking BYOND version in dependencies.sh..."); + var dependenciesShContent = await gitHubClient.Repository.Content.GetRawContentByRef(repoOwner, repoName, "dependencies.sh", pushedCommitHash); + var dependenciesSh = Encoding.UTF8.GetString(dependenciesShContent); + var dependenciesShLines = dependenciesSh.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + + int dependenciesShByondMajor = 0; + int dependenciesShByondMinor = 0; + foreach(var dependenciesShLine in dependenciesShLines) + { + var trimmedLine = dependenciesShLine.Trim(); + var lineSplit = trimmedLine.Split('=', StringSplitOptions.RemoveEmptyEntries); + if (lineSplit.Length != 2) + continue; + + if (lineSplit[0].EndsWith("BYOND_MAJOR")) + dependenciesShByondMajor = Int32.Parse(lineSplit[1]); + else if (lineSplit[0].EndsWith("BYOND_MINOR")) + dependenciesShByondMinor = Int32.Parse(lineSplit[1]); + } + + var dependenciesByondVersion = new Version(dependenciesShByondMajor, dependenciesShByondMinor); + if(dependenciesByondVersion != targetByondVersion) + { + Console.WriteLine($".tgs.yml BYOND version does not match dependencies.sh! Expected {dependenciesByondVersion} got {targetByondVersion}!"); + return 5; + } + + // Connect to TGS + var clientFactory = new ServerClientFactory( + new System.Net.Http.Headers.ProductHeaderValue( + assemblyName.Name!, + assemblyName.Version!.Semver().ToString())); + + var tgsApiUrl = new Uri($"http://127.0.0.1:{tgsApiPort}"); + var giveUpAt = DateTimeOffset.UtcNow.AddMinutes(2); + IServerClient client; + for (var I = 1; ; ++I) + { + try + { + Console.WriteLine($"TGS Connection Attempt {I}..."); + client = await clientFactory.CreateFromLogin( + tgsApiUrl, + DefaultCredentials.AdminUserName, + DefaultCredentials.DefaultAdminUserPassword); + break; + } + catch (HttpRequestException) + { + //migrating, to be expected + if (DateTimeOffset.UtcNow > giveUpAt) + throw; + await Task.Delay(TimeSpan.FromSeconds(1)); + } + catch (ServiceUnavailableException) + { + // migrating, to be expected + if (DateTimeOffset.UtcNow > giveUpAt) + throw; + await Task.Delay(TimeSpan.FromSeconds(1)); + } + } + + + Console.WriteLine("Getting TGS information..."); + + var tgsInfo = await client.ServerInformation(default); + + var scriptDictionaryToUse = tgsInfo.WindowsHost ? tgsYml.WindowsScripts : tgsYml.LinuxScripts; + Console.WriteLine($"Downloading {scriptDictionaryToUse.Count} EventScripts..."); + + var scriptDownloadTasks = new Dictionary>(); + foreach (var scriptKvp in scriptDictionaryToUse) + { + scriptDownloadTasks.Add( + scriptKvp.Key, + gitHubClient.Repository.Content.GetRawContentByRef(repoOwner, repoName, scriptKvp.Value, pushedCommitHash)); + } + + await Task.WhenAll(scriptDownloadTasks.Values); + + Console.WriteLine("Setting up TGS instance..."); + + var instance = await client.Instances.CreateOrAttach( + new InstanceCreateRequest + { + ConfigurationType = ConfigurationType.HostWrite, + Name = "tgstation", + Path = instancePath + }, + default); + + instance = await client.Instances.Update( + new InstanceUpdateRequest + { + Id = instance.Id, + Online = true + }, + default); + + var instanceClient = client.Instances.CreateClient(instance); + + Console.WriteLine("Cloning main branch of repo..."); + var repoCloneJob = await instanceClient.Repository.Clone( + new RepositoryCreateRequest + { + Origin = new Uri($"http://github.com/{repoSlug}"), + UpdateSubmodules = true, + }, + default); + + Console.WriteLine("Installing BYOND..."); + var byondInstallJob = await instanceClient.Engine.SetActiveVersion( + new EngineVersionRequest + { + EngineVersion = new EngineVersion + { + Version = targetByondVersion, + Engine = EngineType.Byond, + } + }, + null, + default); + + Console.WriteLine("Updating server/compiler settings..."); + await instanceClient.DreamMaker.Update( + new DreamMakerRequest + { + ApiValidationSecurityLevel = tgsYml.Security + }, + default); + + await instanceClient.DreamDaemon.Update( + new DreamDaemonRequest + { + SecurityLevel = tgsYml.Security, + Visibility = DreamDaemonVisibility.Invisible + }, + default); + + Console.WriteLine("Uploading EventScripts..."); + foreach (var scriptDownloadKvp in scriptDownloadTasks) + { + var scriptContent = await scriptDownloadKvp.Value; + + var memoryStream = new MemoryStream(scriptContent); + await instanceClient.Configuration.Write( + new ConfigurationFileRequest + { + Path = $"EventScripts/{scriptDownloadKvp.Key}" + }, + memoryStream, + default); + } + + Console.WriteLine("Creating GameStaticFiles structure..."); + var staticFileDownloadTasks = new Dictionary>>(); + foreach (var staticFile in tgsYml.StaticFiles) + { + if (!staticFile.Populate) + { + Console.WriteLine($"Creating empty directory GameStaticFiles/{staticFile.Name}..."); + await instanceClient.Configuration.CreateDirectory(new ConfigurationFileRequest + { + Path = $"GameStaticFiles/{staticFile.Name}" + }, + default); + } + else + { + // not by ref here as we are relying on master being not broken + Console.WriteLine($"Enumerating repo path {staticFile.Name}..."); + var repositoryFilesToUpload = new Queue(await gitHubClient.Repository.Content.GetAllContents(repoOwner, repoName, staticFile.Name)); + while (repositoryFilesToUpload.Count != 0) + { + var repositoryFileToUpload = repositoryFilesToUpload.Dequeue(); + if (repositoryFileToUpload.Type == ContentType.File) + { + // serial because easier to track errors + Console.WriteLine($"Transferring {repositoryFileToUpload.Path}..."); + var fileContent = await gitHubClient.Repository.Content.GetRawContent(repoOwner, repoName, repositoryFileToUpload.Path); + using var memoryStream = new MemoryStream(fileContent); + await instanceClient.Configuration.Write(new ConfigurationFileRequest + { + Path = $"GameStaticFiles/{repositoryFileToUpload.Path}" + }, + memoryStream, + default); + } + else + { + Console.WriteLine($"Enumerating repo path {repositoryFileToUpload.Path}..."); + var additionalFiles = await gitHubClient.Repository.Content.GetAllContents(repoOwner, repoName, repositoryFileToUpload.Path); + foreach (var additionalFile in additionalFiles) + repositoryFilesToUpload.Enqueue(additionalFile); + } + } + } + } + + async Task WaitForJob(JobResponse originalJob, int timeout) + { + Console.WriteLine($"Waiting for job \"{originalJob.Description}\"..."); + var job = originalJob; + var previousProgress = job.Progress; + do + { + if (job.Progress != previousProgress) + Console.WriteLine($"Progress: {previousProgress = job.Progress}"); + + await Task.Delay(TimeSpan.FromSeconds(1)); + job = await instanceClient!.Jobs.GetId(job, default); + --timeout; + } + while (!job.StoppedAt.HasValue && timeout > 0); + + if (!job.StoppedAt.HasValue) + { + await instanceClient!.Jobs.Cancel(job, default); + Console.WriteLine($"Timed out!"); + return false; + } + else if (job.ExceptionDetails != null) + { + Console.WriteLine($"Error: {job.ExceptionDetails}"); + return false; + } + + return true; + } + + if (!await WaitForJob(byondInstallJob.InstallJob!, 120)) + return 6; + + if (!await WaitForJob(repoCloneJob.ActiveJob!, 600)) + return 7; + + if (pullRequest.HasValue) + { + Console.WriteLine($"Applying test merge #{pullRequest}..."); + var testMergeJob = await instanceClient.Repository.Update(new RepositoryUpdateRequest + { + NewTestMerges = new List + { + new TestMergeParameters + { + Comment = "Active Pull Request", + Number = pullRequest.Value, + TargetCommitSha = pushedCommitHash + } + } + }, default); + if (!await WaitForJob(testMergeJob.ActiveJob!, 60)) + return 11; + } + + Console.WriteLine("Deploying..."); + var deploymentJob = await instanceClient.DreamMaker.Compile(default); + if (!await WaitForJob(deploymentJob, 1800)) + return 8; + + Console.WriteLine("Launching..."); + var launchJob = await instanceClient.DreamDaemon.Start(default); + if (!await WaitForJob(launchJob, 300)) + return 9; + + return 0; +} +catch (Exception ex) +{ + Console.WriteLine(ex); + return 4; +} diff --git a/tools/tgs_test/README.md b/tools/tgs_test/README.md new file mode 100644 index 00000000000..436f21d894c --- /dev/null +++ b/tools/tgs_test/README.md @@ -0,0 +1,11 @@ +# TGS Test Script + +This is a simple app that does a few things + +- Downloads .tgs.yml information from a specific commit of a given repository. +- Checks that the BYOND version in the .tgs.yml file matches the dependencies.sh version. +- Connects to a TGS instance via command line parameters. +- Uses the .tgs.yml information to automatically set up a TGS instance. +- Runs a TGS deploy/launch and validates that they succeeded. + +Look for its invocation in the GitHub workflows diff --git a/tools/tgs_test/StaticFile.cs b/tools/tgs_test/StaticFile.cs new file mode 100644 index 00000000000..135f159ad6e --- /dev/null +++ b/tools/tgs_test/StaticFile.cs @@ -0,0 +1,5 @@ +sealed class StaticFile +{ + public string Name { get; set; } = String.Empty; + public bool Populate { get; set; } +} diff --git a/tools/tgs_test/TgsYml.cs b/tools/tgs_test/TgsYml.cs new file mode 100644 index 00000000000..c738ac4fdef --- /dev/null +++ b/tools/tgs_test/TgsYml.cs @@ -0,0 +1,15 @@ +using Tgstation.Server.Api.Models; + +sealed class TgsYml +{ + public int Version { get; set; } + + public string Byond { get; set; } = String.Empty; + + public List StaticFiles { get; set; } = new List(); + + public Dictionary WindowsScripts { get; set; } = new Dictionary(); + public Dictionary LinuxScripts { get; set; } = new Dictionary(); + + public DreamDaemonSecurity Security { get; set; } +} diff --git a/tools/tgs_test/Tgstation.TgsTest.csproj b/tools/tgs_test/Tgstation.TgsTest.csproj new file mode 100644 index 00000000000..967fbd42951 --- /dev/null +++ b/tools/tgs_test/Tgstation.TgsTest.csproj @@ -0,0 +1,18 @@ + + + + 2.0.0 + Exe + net8.0 + enable + enable + + + + + + + + + + diff --git a/tools/tgs_test/Tgstation.TgsTest.sln b/tools/tgs_test/Tgstation.TgsTest.sln new file mode 100644 index 00000000000..37a7a119a88 --- /dev/null +++ b/tools/tgs_test/Tgstation.TgsTest.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.4.33213.308 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tgstation.TgsTest", "Tgstation.TgsTest.csproj", "{3146D745-AAE5-4205-8FF2-0CE471B47B4E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3146D745-AAE5-4205-8FF2-0CE471B47B4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3146D745-AAE5-4205-8FF2-0CE471B47B4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3146D745-AAE5-4205-8FF2-0CE471B47B4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3146D745-AAE5-4205-8FF2-0CE471B47B4E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {001721B4-7740-419D-837E-26CE9DABCAB8} + EndGlobalSection +EndGlobal diff --git a/tools/ticked_file_enforcement/schemas/citadel_dme.json b/tools/ticked_file_enforcement/schemas/citadel_dme.json new file mode 100644 index 00000000000..ce8504c7380 --- /dev/null +++ b/tools/ticked_file_enforcement/schemas/citadel_dme.json @@ -0,0 +1,7 @@ +{ + "file": "citadel.dme", + "scannable_directory": "code/", + "subdirectories": true, + "excluded_files": [], + "forbidden_includes": ["code/modules/tgs/**/*.dm", "code/unit_tests/[!_]*.dm"] +} diff --git a/tools/ticked_file_enforcement/schemas/unit_tests.json b/tools/ticked_file_enforcement/schemas/unit_tests.json new file mode 100644 index 00000000000..1f1457ed883 --- /dev/null +++ b/tools/ticked_file_enforcement/schemas/unit_tests.json @@ -0,0 +1,7 @@ +{ + "file": "code/unit_tests/_unit_tests.dm", + "scannable_directory": "code/unit_tests/", + "subdirectories": false, + "excluded_files": ["find_reference_sanity.dm"], + "forbidden_includes": [] +} diff --git a/tools/ticked_file_enforcement/ticked_file_enforcement.py b/tools/ticked_file_enforcement/ticked_file_enforcement.py new file mode 100644 index 00000000000..86c399c7355 --- /dev/null +++ b/tools/ticked_file_enforcement/ticked_file_enforcement.py @@ -0,0 +1,156 @@ +import codecs +import fnmatch +import functools +import glob +import json +import os +import sys + +# simple way to check if we're running on github actions, or on a local machine +on_github = os.getenv("GITHUB_ACTIONS") == "true" + +def green(text): + return "\033[32m" + str(text) + "\033[0m" + +def red(text): + return "\033[31m" + str(text) + "\033[0m" + +def blue(text): + return "\033[34m" + str(text) + "\033[0m" + +schema = json.load(sys.stdin) +file_reference = schema["file"] +file_reference_basename = os.path.basename(file_reference) +scannable_directory = schema["scannable_directory"] +subdirectories = schema["subdirectories"] +FORBIDDEN_INCLUDES = schema["forbidden_includes"] +excluded_files = schema["excluded_files"] + +def post_error(string): + print(red(f"Ticked File Enforcement [{file_reference}]: " + string)) + if on_github: + print(f"::error file={file_reference},line=1,title=Ticked File Enforcement::{string}") + +for excluded_file in excluded_files: + full_file_path = scannable_directory + excluded_file + if not os.path.isfile(full_file_path): + post_error(f"Excluded file {full_file_path} does not exist, please remove it!") + sys.exit(1) + +file_extensions = ("dm", "dmf") + +reading = False +lines = [] +total = 0 + +with open(file_reference, 'r') as file: + for line in file: + total += 1 + line = line.strip() + + if line == "// BEGIN_INCLUDE": + reading = True + continue + elif line == "// END_INCLUDE": + break + elif not reading: + continue + + lines.append(line) + +offset = total - len(lines) +print(blue(f"Ticked File Enforcement: {offset} lines were ignored in output for [{file_reference}].")) +fail_no_include = False + +scannable_files = [] +for file_extension in file_extensions: + compiled_directory = f"{scannable_directory}/**/*.{file_extension}" + scannable_files += glob.glob(compiled_directory, recursive=True) + +if len(scannable_files) == 0: + post_error(f"No files were found in {scannable_directory}. Ticked File Enforcement has failed!") + sys.exit(1) + +for code_file in scannable_files: + dm_path = "" + + if subdirectories is True: + dm_path = code_file.replace('/', '\\') + else: + dm_path = os.path.basename(code_file) + + included = f"#include \"{dm_path}\"" in lines + + forbid_include = False + for forbidable in FORBIDDEN_INCLUDES: + if not fnmatch.fnmatch(code_file, forbidable): + continue + + forbid_include = True + + if included: + post_error(f"{dm_path} should NOT be included.") + fail_no_include = True + + if forbid_include: + continue + + if not included: + if(dm_path == file_reference_basename): + continue + + if(dm_path in excluded_files): + continue + + post_error(f"Missing include for {dm_path}.") + fail_no_include = True + +if fail_no_include: + sys.exit(1) + +def compare_lines(a, b): + # Remove initial include as well as the final quotation mark + a = a[len("#include \""):-1].lower() + b = b[len("#include \""):-1].lower() + + split_by_period = a.split('.') + a_suffix = "" + if len(split_by_period) >= 2: + a_suffix = split_by_period[len(split_by_period) - 1] + split_by_period = b.split('.') + b_suffix = "" + if len(split_by_period) >= 2: + b_suffix = split_by_period[len(split_by_period) - 1] + + a_segments = a.split('\\') + b_segments = b.split('\\') + + for (a_segment, b_segment) in zip(a_segments, b_segments): + a_is_file = a_segment.endswith(file_extensions) + b_is_file = b_segment.endswith(file_extensions) + + # code\something.dm will ALWAYS come before code\directory\something.dm + if a_is_file and not b_is_file: + return -1 + + if b_is_file and not a_is_file: + return 1 + + # interface\something.dm will ALWAYS come after code\something.dm + if a_segment != b_segment: + # if we're at the end of a compare, then this is about the file name + # files with longer suffixes come after ones with shorter ones + if a_suffix != b_suffix: + return (a_suffix > b_suffix) - (a_suffix < b_suffix) + return (a_segment > b_segment) - (a_segment < b_segment) + + print(f"Two lines were exactly the same ({a} vs. {b})") + sys.exit(1) + +sorted_lines = sorted(lines, key = functools.cmp_to_key(compare_lines)) +for (index, line) in enumerate(lines): + if sorted_lines[index] != line: + post_error(f"The include at line {index + offset} is out of order ({line}, expected {sorted_lines[index]})") + sys.exit(1) + +print(green(f"Ticked File Enforcement: [{file_reference}] All includes (for {len(scannable_files)} scanned files) are in order!")) diff --git a/tools/validate_dme.py b/tools/validate_dme.py deleted file mode 100644 index 684a6d419e7..00000000000 --- a/tools/validate_dme.py +++ /dev/null @@ -1,95 +0,0 @@ -import fnmatch -import functools -import glob -import sys - -reading = False - -FORBID_INCLUDE = [ - # Included by tgs/includes.dm - r'code/modules/tgs/**/*.dm', - - # Included by _unit_test.dm - r'code/modules/unit_tests/[!_]*.dm', - -] - -lines = [] -total = 0 -for line in sys.stdin: - total+=1 - line = line.strip() - - if line == "// BEGIN_INCLUDE": - reading = True - continue - elif line == "// END_INCLUDE": - break - elif not reading: - continue - - lines.append(line) - -offset = total - len(lines) -print(f"{offset} lines were ignored in output") -fail_no_include = False - -for code_file in glob.glob("code/**/*.dm", recursive=True): - dm_path = code_file.replace('/', '\\') - - included = f"#include \"{dm_path}\"" in lines - forbid_include = False - - for forbid in FORBID_INCLUDE: - if not fnmatch.fnmatch(code_file, forbid): - continue - - forbid_include = True - - if included: - print(f"{dm_path} should not be included") - print(f"::error file={code_file},line=1,title=DME Validator::File should not be included") - fail_no_include = True - - if forbid_include: - continue - - if not included: - print(f"{dm_path} is not included") - print(f"::error file={code_file},line=1,title=DME Validator::File is not included") - fail_no_include = True - -if fail_no_include: - sys.exit(1) - -def compare_lines(a, b): - # Remove initial include as well as the final quotation mark - a = a[len("#include \""):-1].lower() - b = b[len("#include \""):-1].lower() - - a_segments = a.split('\\') - b_segments = b.split('\\') - - for (a_segment, b_segment) in zip(a_segments, b_segments): - a_is_file = a_segment.endswith(".dm") - b_is_file = b_segment.endswith(".dm") - - # code\something.dm will ALWAYS come before code\directory\something.dm - if a_is_file and not b_is_file: - return -1 - - if b_is_file and not a_is_file: - return 1 - - # interface\something.dm will ALWAYS come after code\something.dm - if a_segment != b_segment: - return (a_segment > b_segment) - (a_segment < b_segment) - - raise f"Two lines were exactly the same ({a} vs. {b})" - -sorted_lines = sorted(lines, key = functools.cmp_to_key(compare_lines)) -for (index, line) in enumerate(lines): - if sorted_lines[index] != line: - print(f"The include at line {index + offset} is out of order ({line}, expected {sorted_lines[index]})") - print(f"::error file=tgstation.dme,line={index+offset},title=DME Validator::The include at line {index + offset} is out of order ({line}, expected {sorted_lines[index]})") - sys.exit(1)