code:
import fs from 'fs';
import path from 'path';
import Juke from '../juke/index.js';
import { regQuery } from './winreg.js';

/**
 * Cached path to DM compiler
 */
let dmPath;

const getDmPath = async () => {
  if (dmPath) {
    return dmPath;
  }
  dmPath = await (async () => {
    // Search in array of paths
    const paths = [
      ...((process.env.DM_EXE && process.env.DM_EXE.split(',')) || []),
      '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 => {
      try {
        return fs.statSync(path).isFile();
      }
      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);
      }
      if (!path) {
        continue;
      }
      // Check if path exists
      if (isFile(path)) {
        return path;
      }
      if (isFile(path + '/dm.exe')) {
        return path + '/dm.exe';
      }
      if (isFile(path + '/bin/dm.exe')) {
        return path + '/bin/dm.exe';
      }
    }
    // Default paths
    return (
      process.platform === 'win32' && 'dm.exe'
      || 'DreamMaker'
    );
  })();
  return dmPath;
};

/**
 * @param {string} dmeFile
 * @param {{
 *   defines?: string[];
 *   warningsAsErrors?: boolean;
 * }} options
 */
export const DreamMaker = async (dmeFile, options = {}) => {
  const dmPath = await getDmPath();
  // Get project basename
  const dmeBaseName = dmeFile.replace(/\.dme$/, '');
  // Make sure output files are writable
  const testOutputFile = (name) => {
    try {
      fs.closeSync(fs.openSync(name, 'r+'));
    }
    catch (err) {
      if (err && err.code === 'ENOENT') {
        return;
      }
      if (err && 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;
    }
  };
  testOutputFile(`${dmeBaseName}.dmb`);
  testOutputFile(`${dmeBaseName}.rsc`);
  const runWithWarningChecks = async (dmeFile, args) => {
    const execReturn = await Juke.exec(dmeFile, args);
    if (options.warningsAsErrors && execReturn.combined.match(/\d+:warning: /)) {
      Juke.logger.error(`Compile warnings treated as errors`);
      throw new Juke.ExitCode(2);
    }
    return execReturn;
  }
  // Compile
  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]);
  }
};

export const DreamDaemon = async (dmbFile, ...args) => {
  const dmPath = await getDmPath();
  const baseDir = path.dirname(dmPath);
  const ddExeName = process.platform === 'win32' ? 'dreamdaemon.exe' : 'DreamDaemon';
  const ddExePath = baseDir === '.' ? ddExeName : path.join(baseDir, ddExeName);
  return Juke.exec(ddExePath, [dmbFile, ...args]);
};

repo_name: AnalWerewolf/sundown

license: mit


code:
/**
 * Tools for dealing with Windows Registry bullshit.
 *
 * Adapted from `tgui/packages/tgui-dev-server/winreg.js`.
 *
 * @file
 * @copyright 2021 Aleksej Komarov
 * @license MIT
 */

import { exec } from 'child_process';
import { promisify } from 'util';

export const regQuery = async (path, key) => {
  if (process.platform !== 'win32') {
    return null;
  }
  try {
    const command = `reg query "${path}" /v ${key}`;
    const { stdout } = await promisify(exec)(command);
    const keyPattern = `    ${key}    `;
    const indexOfKey = stdout.indexOf(keyPattern);
    if (indexOfKey === -1) {
      return null;
    }
    const indexOfEol = stdout.indexOf('\r\n', indexOfKey);
    if (indexOfEol === -1) {
      return null;
    }
    const indexOfValue = stdout.indexOf(
      '    ',
      indexOfKey + keyPattern.length);
    if (indexOfValue === -1) {
      return null;
    }
    const value = stdout.substring(indexOfValue + 4, indexOfEol);
    return value;
  }
  catch (err) {
    return null;
  }
};


code:
import Juke from '../juke/index.js';

let yarnPath;

export const yarn = (...args) => {
  if (!yarnPath) {
    yarnPath = Juke.glob('./tgui/.yarn/releases/*.cjs')[0]
      .replace('/tgui/', '/');
  }
  return Juke.exec('node', [
    yarnPath,
    ...args.filter((arg) => typeof arg === 'string'),
  ], {
    cwd: './tgui',
  });
};


code:
if(!(Test-Path -Path "C:/byond")){
    bash tools/ci/download_byond.sh
    [System.IO.Compression.ZipFile]::ExtractToDirectory("C:/byond.zip", "C:/")
    Remove-Item C:/byond.zip
}

bash tools/ci/install_node.sh
bash tools/build/build -Werror

exit $LASTEXITCODE


code:
#!/bin/bash
set -euo pipefail

source dependencies.sh

cd $HOME/SpacemanDMM

if [ ! -d .git ]
then
	git init
	git remote add origin https://github.com/SpaceManiac/SpacemanDMM.git
fi

git fetch origin --depth=1 $SPACEMAN_DMM_COMMIT_HASH
git reset --hard FETCH_HEAD

cargo build --release --bin $1
cp target/release/$1 ~
~/$1 --version


code:
#!/bin/bash
set -euo pipefail

md5sum -c - <<< "4d783933c74290b3a219068790b2046f *html/changelogs/example.yml"
python3 tools/ss13_genchangelog.py html/changelogs


code:
#!/bin/bash
if [ -n "$1" ]
then
    dme=$1
else
    echo "ERROR: Specify a DME to check"
    exit 1
fi

if [[ $(awk '/BEGIN_FILE_DIR/{flag=1;next}/END_FILE_DIR/{flag=0}flag' $dme | wc -l) -ne 1 ]]
then
    echo "ERROR: File DIR was ticked, please untick it, see: https://tgstation13.org/phpBB/viewtopic.php?f=5&t=321 for more"
    exit 1
fi


code:
#!/bin/bash
set -euo pipefail

#nb: must be bash to support shopt globstar
shopt -s globstar

st=0

if grep -El '^\".+\" = \(.+\)' _maps/**/*.dmm;	then
    echo "ERROR: Non-TGM formatted map detected. Please convert it using Map Merger!"
    st=1
fi;
if grep -P 'Merge conflict marker' _maps/**/*.dmm; then
    echo "ERROR: Merge conflict markers detected in map, please resolve all merge failures!"
    st=1
fi;
if grep -P '^\ttag = \"icon' _maps/**/*.dmm;	then
    echo "ERROR: tag vars from icon state generation detected in maps, please remove them."
    st=1
fi;
if grep -P 'step_[xy]' _maps/**/*.dmm;	then
    echo "ERROR: step_x/step_y variables detected in maps, please remove them."
    st=1
fi;
if grep -P 'pixel_[^xy]' _maps/**/*.dmm;	then
    echo "ERROR: incorrect pixel offset variables detected in maps, please remove them."
    st=1
fi;
echo "Checking for cable varedits"
if grep -P '/obj/structure/cable(/\w+)+\{' _maps/**/*.dmm;	then
    echo "ERROR: vareditted cables detected, please remove them."
    st=1
fi;
if grep -P '\td[1-2] =' _maps/**/*.dmm;	then
    echo "ERROR: d1/d2 cable variables detected in maps, please remove them."
    st=1
fi;
echo "Checking for stacked cables"
if grep -P '"\w+" = \(\n([^)]+\n)*/obj/structure/cable,\n([^)]+\n)*/obj/structure/cable,\n([^)]+\n)*/area/.+\)' _maps/**/*.dmm;	then
    echo "found multiple cables on the same tile, please remove them."
    st=1
fi;
if grep -P '^/area/.+[\{]' _maps/**/*.dmm;	then
    echo "ERROR: Vareditted /area path use detected in maps, please replace with proper paths."
    st=1
fi;
if grep -P '\W\/turf\s*[,\){]' _maps/**/*.dmm; then
    echo "ERROR: base /turf path use detected in maps, please replace with proper paths."
    st=1
fi;
if grep -P '^/*var/' code/**/*.dm; then
    echo "ERROR: Unmanaged global var use detected in code, please use the helpers."
    st=1
fi;
echo "Checking for space indentation"
if grep -P '(^ {2})|(^ [^ * ])|(^    +)' code/**/*.dm; then
    echo "space indentation detected"
    st=1
fi;
echo "Checking for mixed indentation"
if grep -P '^\t+ [^ *]' code/**/*.dm; then
    echo "mixed <tab><space> indentation detected"
    st=1
fi;
nl='
'
nl=$'\n'
while read f; do
    t=$(tail -c2 "$f"; printf x); r1="${nl}$"; r2="${nl}${r1}"
    if [[ ! ${t%x} =~ $r1 ]]; then
        echo "file $f is missing a trailing newline"
        st=1
    fi;
done < <(find . -type f -name '*.dm')
if grep -P '^/[\w/]\S+\(.*(var/|, ?var/.*).*\)' code/**/*.dm; then
    echo "changed files contains proc argument starting with 'var'"
    st=1
fi;
if grep -i 'centcomm' code/**/*.dm; then
    echo "ERROR: Misspelling(s) of CENTCOM detected in code, please remove the extra M(s)."
    st=1
fi;
if grep -i 'centcomm' _maps/**/*.dmm; then
    echo "ERROR: Misspelling(s) of CENTCOM detected in maps, please remove the extra M(s)."
    st=1
fi;
if grep -ni 'nanotransen' code/**/*.dm; then
    echo "Misspelling(s) of nanotrasen detected in code, please remove the extra N(s)."
    st=1
fi;
if grep -ni 'nanotransen' _maps/**/*.dmm; then
    echo "Misspelling(s) of nanotrasen detected in maps, please remove the extra N(s)."
    st=1
fi;
if ls _maps/*.json | grep -P "[A-Z]"; then
    echo "Uppercase in a map json detected, these must be all lowercase."
    st=1
fi;
if grep -i '/obj/effect/mapping_helpers/custom_icon' _maps/**/*.dmm; then
    echo "Custom icon helper found. Please include dmis as standard assets instead for built-in maps."
    st=1
fi;
for json in _maps/*.json
do
    map_path=$(jq -r '.map_path' $json)
    while read map_file; do
        filename="_maps/$map_path/$map_file"
        if [ ! -f $filename ]
        then
            echo "found invalid file reference to $filename in _maps/$json"
            st=1
        fi
    done < <(jq -r '[.map_file] | flatten | .[]' $json)
done

exit $st


code:
#!/bin/bash
set -euo pipefail

find . -name "*.php" -print0 | xargs -0 -n1 php -l
find . -name "*.json" -not -path "*/node_modules/*" -print0 | xargs -0 python3 ./tools/json_verifier.py


code:
SQL_ENABLED
ADDRESS 127.0.0.1
PORT 3306
FEEDBACK_DATABASE tg_ci
FEEDBACK_TABLEPREFIX
FEEDBACK_LOGIN root
FEEDBACK_PASSWORD
LAVALAND_BUDGET 0
SPACE_BUDGET 0


code:
#!/bin/bash
set -e
source dependencies.sh
echo "Downloading BYOND version $BYOND_MAJOR.$BYOND_MINOR"
curl "http://www.byond.com/download/build/$BYOND_MAJOR/$BYOND_MAJOR.${BYOND_MINOR}_byond.zip" -o C:/byond.zip


code:
#!/bin/bash
set -euo pipefail

source dependencies.sh

if [ -d "$HOME/BYOND/byond/bin" ] && grep -Fxq "${BYOND_MAJOR}.${BYOND_MINOR}" $HOME/BYOND/version.txt;
then
  echo "Using cached directory."
else
  echo "Setting up BYOND."
  rm -rf "$HOME/BYOND"
  mkdir -p "$HOME/BYOND"
  cd "$HOME/BYOND"
  curl "http://www.byond.com/download/build/${BYOND_MAJOR}/${BYOND_MAJOR}.${BYOND_MINOR}_byond_linux.zip" -o byond.zip
  unzip byond.zip
  rm byond.zip
  cd byond
  make here
  echo "$BYOND_MAJOR.$BYOND_MINOR" > "$HOME/BYOND/version.txt"
  cd ~/
fi


code:
#!/bin/bash
set -euo pipefail

source dependencies.sh

if [[ -e ~/.nvm/nvm.sh ]]; then
	source ~/.nvm/nvm.sh
	nvm install $NODE_VERSION
	nvm use $NODE_VERSION
fi


code:
#!/usr/bin/env bash
set -euo pipefail

source dependencies.sh

mkdir -p ~/.byond/bin
wget -nv -O ~/.byond/bin/librust_g.so "https://github.com/tgstation/rust-g/releases/download/$RUST_G_VERSION/librust_g.so"
chmod +x ~/.byond/bin/librust_g.so
ldd ~/.byond/bin/librust_g.so


code:
#!/bin/bash
set -euo pipefail

source dependencies.sh

if [ ! -f ~/$1 ]; then
	mkdir -p "$HOME/SpacemanDMM"
	CACHEFILE="$HOME/SpacemanDMM/$1"

	if ! [ -f "$CACHEFILE.version" ] || ! grep -Fxq "$SPACEMAN_DMM_VERSION" "$CACHEFILE.version"; then
		wget -O "$CACHEFILE" "https://github.com/SpaceManiac/SpacemanDMM/releases/download/$SPACEMAN_DMM_VERSION/$1"
		chmod +x "$CACHEFILE"
		echo "$SPACEMAN_DMM_VERSION" >"$CACHEFILE.version"
	fi

	ln -s "$CACHEFILE" ~/$1
fi

~/$1 --version


code:
#!/bin/bash
set -euo pipefail

tools/deploy.sh ci_test
mkdir ci_test/config

#test config
cp tools/ci/ci_config.txt ci_test/config/config.txt

cd ci_test
DreamDaemon tgstation.dmb -close -trusted -verbose -params "log-directory=ci"
cd ..
cat ci_test/data/logs/ci/clean_run.lk


code:
#!/bin/bash

#Run this in the repo root after compiling
#First arg is path to where you want to deploy
#creates a work tree free of everything except what's necessary to run the game

#second arg is working directory if necessary
if [[ $# -eq 2 ]] ; then
  cd $2
fi

mkdir -p \
    $1/_maps \
    $1/icons/runtime \
    $1/sound/runtime \
    $1/strings \
    $1/tgui/public \
    $1/tgui/packages/tgfont/dist

if [ -d ".git" ]; then
  mkdir -p $1/.git/logs
  cp -r .git/logs/* $1/.git/logs/
fi

cp tgstation.dmb tgstation.rsc $1/
cp -r _maps/* $1/_maps/
cp -r icons/runtime/* $1/icons/runtime/
cp -r sound/runtime/* $1/sound/runtime/
cp -r strings/* $1/strings/
cp -r tgui/public/* $1/tgui/public/
cp -r tgui/packages/tgfont/dist/* $1/tgui/packages/tgfont/dist/

#remove .dm files from _maps

#this regrettably doesn't work with windows find
#find $1/_maps -name "*.dm" -type f -delete

#dlls on windows
if [ "$(uname -o)" = "Msys" ]; then
	cp ./*.dll $1/
fi


code:
This is a script to role members in a discord if they have their ckey linked. It should only have to be ran **once**, since the game will handle any other new roles

Requirements:
  - Python 3 (Tested on 3.6.4)
  - `Mysql-Connector` module
  - `Requests` module

How to use:
  1. Ensure you have the correct dependencies installed
  2. Fill out the config sections in the script (Discord details and SS13 server details)
  3. Run the script, and leave it running until its done. It will give progress indicators as it runs


code:
# Script to role discord members who have already associated their BYOND account
# Author: AffectedArc07

# From Discord API:
# Clients are allowed 120 events every 60 seconds, meaning you can send on average at a rate of up to 2 events per second.

# So lets send every 0.6 seconds to ensure we arent rate capped

####### CONFIG ######

# Discord section. Make sure the IDs are strings to avoid issues with IDs that start with a 0
botToken = "Put your discord bot token here"
guildID = "000000000000000000"
roleID = "000000000000000000"

# SS13 Database section
dbHost = "127.0.0.1"
dbUser = "root"
dbPass = "your password here"
dbDatabase = "tg_db"

##### DO NOT TOUCH ANYTHING BELOW HERE UNLESS YOURE FAMILIAR WITH PYTHON #####
import requests, mysql.connector, time

# Connect to DB
dbCon = mysql.connector.connect(
    host = dbHost,
    user = dbUser,
    passwd = dbPass,
    database = dbDatabase
)
cur = dbCon.cursor()

# Grab all users who need to be processed
cur.execute("SELECT byond_key, discord_id FROM player WHERE discord_id IS NOT NULL")
usersToProcess = cur.fetchall()

# We dont need the DB anymore, so close it up
dbCon.close()

# Calculate a total for better monitoring
total = len(usersToProcess)
count = 0
print("Found "+str(total)+" accounts to process.")

# Now the actual processing
for user in usersToProcess:
    count += 1  # Why the fuck does python not have ++
    # user[0] = ckey, user[1] = discord ID
    print("Processing "+str(user[0])+" (Discord ID: " + str(user[1]) + ") | User "+str(count)+"/"+str(total))
    url = "https://discord.com/api/guilds/"+str(guildID)+"/members/"+str(user[1])+"/roles/"+str(roleID)
    response = requests.put(url, headers={"Authorization": "Bot "+str(botToken)})
    # Adding a role returns a code 204, not a code 200. Dont ask
    if response.status_code != 204:
        print("WARNING: Returned non-204 status code. Request used: PUT "+str(url))

    # Sleep for 0.6. This way we stay under discords rate limiting.
    time.sleep(0.6)


code:
@call "%~dp0\..\bootstrap\python.bat" -m dmi.merge_driver --posthoc %*
@pause

path: tools/dmi/Resolve Icon Conflicts.bat


code:
# Tools for working with modern DreamMaker icon files (PNGs + metadata)

import math
from PIL import Image
from PIL.PngImagePlugin import PngInfo

DEFAULT_SIZE = 32, 32
LOOP_UNLIMITED = 0
LOOP_ONCE = 1

NORTH = 1
SOUTH = 2
EAST = 4
WEST = 8
SOUTHEAST = SOUTH | EAST
SOUTHWEST = SOUTH | WEST
NORTHEAST = NORTH | EAST
NORTHWEST = NORTH | WEST

CARDINALS = [NORTH, SOUTH, EAST, WEST]
DIR_ORDER = [SOUTH, NORTH, EAST, WEST, SOUTHEAST, SOUTHWEST, NORTHEAST, NORTHWEST]
DIR_NAMES = {
    'SOUTH': SOUTH,
    'NORTH': NORTH,
    'EAST': EAST,
    'WEST': WEST,
    'SOUTHEAST': SOUTHEAST,
    'SOUTHWEST': SOUTHWEST,
    'NORTHEAST': NORTHEAST,
    'NORTHWEST': NORTHWEST,
    **{str(x): x for x in DIR_ORDER},
    **{x: x for x in DIR_ORDER},
    '0': SOUTH,
    None: SOUTH,
}

class Dmi:
    version = "4.0"

    def __init__(self, width, height):
        self.width = width
        self.height = height
        self.states = []

    @classmethod
    def from_file(cls, fname):
        image = Image.open(fname)
        if image.mode != 'RGBA':
            image = image.convert('RGBA')

        # no metadata = regular image file
        if 'Description' not in image.info:
            dmi = Dmi(*image.size)
            state = dmi.state("")
            state.frame(image)
            return dmi

        # read metadata
        metadata = image.info['Description']
        line_iter = iter(metadata.splitlines())
        assert next(line_iter) == "# BEGIN DMI"
        assert next(line_iter) == f"version = {cls.version}"

        dmi = Dmi(*DEFAULT_SIZE)
        state = None

        for line in line_iter:
            if line == "# END DMI":
                break
            key, value = line.lstrip().split(" = ")
            if key == 'width':
                dmi.width = int(value)
            elif key == 'height':
                dmi.height = int(value)
            elif key == 'state':
                state = dmi.state(unescape(value))
            elif key == 'dirs':
                state.dirs = int(value)
            elif key == 'frames':
                state._nframes = int(value)
            elif key == 'delay':
                state.delays = [parse_num(x) for x in value.split(',')]
            elif key == 'loop':
                state.loop = int(value)
            elif key == 'rewind':
                state.rewind = parse_bool(value)
            elif key == 'hotspot':
                x, y, frm = [int(x) for x in value.split(',')]
                state.hotspot(frm - 1, x, y)
            elif key == 'movement':
                state.movement = parse_bool(value)
            else:
                raise NotImplementedError(key)

        # cut image into frames
        width, height = image.size
        gridwidth = width // dmi.width
        i = 0
        for state in dmi.states:
            for frame in range(state._nframes):
                for dir in range(state.dirs):
                    px = dmi.width * (i % gridwidth)
                    py = dmi.height * (i // gridwidth)
                    im = image.crop((px, py, px + dmi.width, py + dmi.height))
                    assert im.size == (dmi.width, dmi.height)
                    state.frames.append(im)
                    i += 1
            state._nframes = None

        return dmi

    def state(self, *args, **kwargs):
        s = State(self, *args, **kwargs)
        self.states.append(s)
        return s

    @property
    def default_state(self):
        return self.states[0]

    def get_state(self, name):
        for state in self.states:
            if state.name == name:
                return state
        raise KeyError(name)

    def _assemble_comment(self):
        comment = "# BEGIN DMI\n"
        comment += f"version = {self.version}\n"
        comment += f"\twidth = {self.width}\n"
        comment += f"\theight = {self.height}\n"
        for state in self.states:
            comment += f"state = {escape(state.name)}\n"
            comment += f"\tdirs = {state.dirs}\n"
            comment += f"\tframes = {state.framecount}\n"
            if state.framecount > 1 and len(state.delays):  # any(x != 1 for x in state.delays):
                comment += "\tdelay = " + ",".join(map(str, state.delays)) + "\n"
            if state.loop != 0:
                comment += f"\tloop = {state.loop}\n"
            if state.rewind:
                comment += "\trewind = 1\n"
            if state.movement:
                comment += "\tmovement = 1\n"
            if state.hotspots and any(state.hotspots):
                current = None
                for i, value in enumerate(state.hotspots):
                    if value != current:
                        x, y = value
                        comment += f"\thotspot = {x},{y},{i + 1}\n"
                        current = value
        comment += "# END DMI"
        return comment

    def to_file(self, filename, *, palette=False):
        # assemble comment
        comment = self._assemble_comment()

        # assemble spritesheet
        W, H = self.width, self.height
        num_frames = sum(len(state.frames) for state in self.states)
        sqrt = math.ceil(math.sqrt(num_frames))
        output = Image.new('RGBA', (sqrt * W, math.ceil(num_frames / sqrt) * H))

        i = 0
        for state in self.states:
            for frame in state.frames:
                output.paste(frame, ((i % sqrt) * W, (i // sqrt) * H))
                i += 1

        # save
        pnginfo = PngInfo()
        pnginfo.add_text('Description', comment, zip=True)
        if palette:
            output = output.convert('P')
        output.save(filename, 'png', optimize=True, pnginfo=pnginfo)

class State:
    def __init__(self, dmi, name, *, loop=LOOP_UNLIMITED, rewind=False, movement=False, dirs=1):
        self.dmi = dmi
        self.name = name
        self.loop = loop
        self.rewind = rewind
        self.movement = movement
        self.dirs = dirs

        self._nframes = None  # used during loading only
        self.frames = []
        self.delays = []
        self.hotspots = None

    @property
    def framecount(self):
        if self._nframes is not None:
            return self._nframes
        else:
            return len(self.frames) // self.dirs

    def frame(self, image, *, delay=1):
        assert image.size == (self.dmi.width, self.dmi.height)
        self.delays.append(delay)
        self.frames.append(image)

    def hotspot(self, first_frame, x, y):
        if self.hotspots is None:
            self.hotspots = [None] * self.framecount
        for i in range(first_frame, self.framecount):
            self.hotspots[i] = x, y

    def _frame_index(self, frame=0, dir=None):
        ofs = DIR_ORDER.index(DIR_NAMES[dir])
        if ofs >= self.dirs:
            ofs = 0
        return frame * self.dirs + ofs

    def get_frame(self, *args, **kwargs):
        return self.frames[self._frame_index(*args, **kwargs)]

def escape(text):
    text = text.replace('\\', '\\\\')
    text = text.replace('"', '\\"')
    return f'"{text}"'

def unescape(text, quote='"'):
    if text == 'null':
        return None
    if not (text.startswith(quote) and text.endswith(quote)):
        raise ValueError(text)
    text = text[1:-1]
    text = text.replace('\\"', '"')
    text = text.replace('\\\\', '\\')
    return text

def parse_num(value):
    if '.' in value:
        return float(value)
    return int(value)

def parse_bool(value):
    if value not in ('0', '1'):
        raise ValueError(value)
    return value == '1'


code:
#!/usr/bin/env python3
import sys
import dmi
from hooks.merge_frontend import MergeDriver

def images_equal(left, right):
    if left.size != right.size:
        return False
    w, h = left.size
    left_load, right_load = left.load(), right.load()
    for y in range(0, h):
        for x in range(0, w):
            lpixel, rpixel = left_load[x, y], right_load[x, y]
            # quietly ignore changes where both pixels are fully transparent
            if lpixel != rpixel and (lpixel[3] != 0 or rpixel[3] != 0):
                return False
    return True

def states_equal(left, right):
    result = True

    # basic properties
    for attr in ('loop', 'rewind', 'movement', 'dirs', 'delays', 'hotspots', 'framecount'):
        lval, rval = getattr(left, attr), getattr(right, attr)
        if lval != rval:
            result = False

    # frames
    for (left_frame, right_frame) in zip(left.frames, right.frames):
        if not images_equal(left_frame, right_frame):
            result = False

    return result

def key_of(state):
    return (state.name, state.movement)

def dictify(sheet):
    result = {}
    for state in sheet.states:
        k = key_of(state)
        if k in result:
            print(f"    duplicate {k!r}")
        result[k] = state
    return result

def three_way_merge(base, left, right):
    base_dims = base.width, base.height
    if base_dims != (left.width, left.height) or base_dims != (right.width, right.height):
        print("Dimensions have changed:")
        print(f"    Base: {base.width} x {base.height}")
        print(f"    Ours: {left.width} x {left.height}")
        print(f"    Theirs: {right.width} x {right.height}")
        return True, None

    base_states, left_states, right_states = dictify(base), dictify(left), dictify(right)

    new_left = {k: v for k, v in left_states.items() if k not in base_states}
    new_right = {k: v for k, v in right_states.items() if k not in base_states}
    new_both = {}
    conflicts = []
    for key, state in list(new_left.items()):
        in_right = new_right.get(key, None)
        if in_right:
            if states_equal(state, in_right):
                # allow it
                new_both[key] = state
            else:
                # generate conflict states
                print(f" C: {state.name!r}: added differently in both!")
                state.name = f"{state.name} !CONFLICT! left"
                conflicts.append(state)
                in_right.name = f"{state.name} !CONFLICT! right"
                conflicts.append(in_right)
            # don't add it a second time
            del new_left[key]
            del new_right[key]

    final_states = []
    # add states that are currently in the base
    for state in base.states:
        in_left = left_states.get(key_of(state), None)
        in_right = right_states.get(key_of(state), None)
        left_equals = in_left and states_equal(state, in_left)
        right_equals = in_right and states_equal(state, in_right)

        if not in_left and not in_right:
            # deleted in both left and right, it's just deleted
            print(f"    {state.name!r}: deleted in both")
        elif not in_left:
            # left deletes
            print(f"    {state.name!r}: deleted in left")
            if not right_equals:
                print(f"    ... but modified in right")
                final_states.append(in_right)
        elif not in_right:
            # right deletes
            print(f"    {state.name!r}: deleted in right")
            if not left_equals:
                print(f"    ... but modified in left")
                final_states.append(in_left)
        elif left_equals and right_equals:
            # changed in neither
            #print(f"Same in both: {state.name!r}")
            final_states.append(state)
        elif left_equals:
            # changed only in right
            print(f"    {state.name!r}: changed in left")
            final_states.append(in_right)
        elif right_equals:
            # changed only in left
            print(f"    {state.name!r}: changed in right")
            final_states.append(in_left)
        elif states_equal(in_left, in_right):
            # changed in both, to the same thing
            print(f"    {state.name!r}: changed same in both")
            final_states.append(in_left)  # either or
        else:
            # changed in both
            name = state.name
            print(f" C: {name!r}: changed differently in both!")
            state.name = f"{name} !CONFLICT! base"
            conflicts.append(state)
            in_left.name = f"{name} !CONFLICT! left"
            conflicts.append(in_left)
            in_right.name = f"{name} !CONFLICT! right"
            conflicts.append(in_right)

    # add states which both left and right added the same
    for key, state in new_both.items():
        print(f"    {state.name!r}: added same in both")
        final_states.append(state)

    # add states that are brand-new in the left
    for key, state in new_left.items():
        print(f"    {state.name!r}: added in left")
        final_states.append(state)

    # add states that are brand-new in the right
    for key, state in new_right.items():
        print(f"    {state.name!r}: added in right")
        final_states.append(state)

    final_states.extend(conflicts)
    merged = dmi.Dmi(base.width, base.height)
    merged.states = final_states
    return len(conflicts), merged

class DmiDriver(MergeDriver):
    driver_id = 'dmi'

    def merge(self, base, left, right):
        icon_base = dmi.Dmi.from_file(base)
        icon_left = dmi.Dmi.from_file(left)
        icon_right = dmi.Dmi.from_file(right)
        trouble, merge_result = three_way_merge(icon_base, icon_left, icon_right)
        return not trouble, merge_result

    def to_file(self, outfile, merge_result):
        merge_result.to_file(outfile)

    def post_announce(self, success, merge_result):
        if not success:
            print("!!! Manual merge required!")
            if merge_result:
                print("    A best-effort merge was performed. You must edit the icon and remove all")
                print("    icon states marked with !CONFLICT!, leaving only the desired icon.")
            else:
                print("    The icon was totally unable to be merged, you must start with one version")
                print("    or the other and manually resolve the conflict.")
            print("    Information about which states conflicted is listed above.")

if __name__ == '__main__':
    exit(DmiDriver().main())


code:
import os
import sys
from dmi import *

def _self_test():
    # test: can we load every DMI in the tree
    count = 0
    for dirpath, dirnames, filenames in os.walk('.'):
        if '.git' in dirnames:
            dirnames.remove('.git')
        for filename in filenames:
            if filename.endswith('.dmi'):
                fullpath = os.path.join(dirpath, filename)
                try:
                    Dmi.from_file(fullpath)
                except Exception:
                    print('Failed on:', fullpath)
                    raise
                count += 1

    print(f"{os.path.relpath(__file__)}: successfully parsed {count} .dmi files")

def _usage():
    print(f"Usage:")
    print(f"    tools{os.sep}bootstrap{os.sep}python -m {__spec__.name}")
    exit(1)

def _main():
    if len(sys.argv) == 1:
        return _self_test()

    return _usage()

if __name__ == '__main__':
    _main()


code:
// BEGIN_INTERNALS
/*
MAP_ICON_TYPE: 0
AUTO_FILE_DIR: OFF
*/
// END_INTERNALS


code:
#!/usr/bin/env python

import re, os, sys, fnmatch

# Regex pattern to extract the directory path in a #define FILE_DIR
filedir_pattern = re.compile(r'^#define\s*FILE_DIR\s*"(.*?)"')

# Regex pattern to extract any single quoted piece of text. This can also
# match single quoted strings inside of double quotes, which is part of a
# regular text string and should not be replaced. The replacement function
# however will any match that doesn't appear to be a filename so these
# extra matches should not be a problem.
rename_pattern = re.compile(r"'(.+?)'")

# Only filenames matching this pattern will have their resources renamed
source_pattern = re.compile(r"^.*?\.(dm|dmm)$")

# Open the .dme file and return a list of all FILE_DIR paths in it
def read_filedirs(filename):
    result = []
    dme_file = file(filename, "rt")

    # Read each line from the file and check for regex pattern match
    for row in dme_file:
        match = filedir_pattern.match(row)
        if match:
            result.append(match.group(1))

    dme_file.close()
    return result

# Search through a list of directories, and build a dictionary which
# maps every file to its full pathname (relative to the .dme file)
# If the same filename appears in more than one directory, the earlier
# directory in the list takes preference.
def index_files(file_dirs):
    result = {}

    # Reverse the directory list so the earlier directories take precedence
    # by replacing the previously indexed file of the same name
    for directory in reversed(file_dirs):
        for name in os.listdir(directory):
            # Replace backslash path separators on Windows with forward slash
            # Force "name" to lowercase when used as a key since BYOND resource
            # names are case insensitive, even on Linux.
            if name.find(".") == -1:
                continue
            result[name.lower()] = directory.replace('\\', '/') + '/' + name

    return result

# Recursively search for every .dm/.dmm file in the .dme file directory. For
# each file, search it for any resource names in single quotes, and replace
# them with the full path previously found by index_files()
def rewrite_sources(resources):
    # Create a closure for the regex replacement function to capture the
    # resources dictionary which can't be passed directly to this function
    def replace_func(name):
        key = name.group(1).lower()
        if key in resources:
            replacement = resources[key]
        else:
            replacement = name.group(1)
        return "'" + replacement + "'"

    # Search recursively for all .dm and .dmm files
    for (dirpath, dirs, files) in os.walk("."):
        for name in files:
            if source_pattern.match(name):
                path = dirpath + '/' + name
                source_file = file(path, "rt")
                output_file = file(path + ".tmp", "wt")

                # Read file one line at a time and perform replacement of all
                # single quoted resource names with the fullpath to that resource
                # file. Write the updated text back out to a temporary file.
                for row in source_file:
                    row = rename_pattern.sub(replace_func, row)
                    output_file.write(row)

                output_file.close()
                source_file.close()

                # Delete original source file and replace with the temporary
                # output. On Windows, an atomic rename() operation is not
                # possible like it is under POSIX.
                os.remove(path)
                os.rename(path + ".tmp", path)

dirs = read_filedirs("tgstation.dme");
resources = index_files(dirs)
rewrite_sources(resources)


code:
@call "%~dp0\..\bootstrap\python" -m hooks.install %*
@pause


code:
# Git Integration Hooks

This folder contains installable scripts for Git [hooks] and [merge drivers].
Use of these hooks and drivers is optional and they must be installed
explicitly before they take effect.

To install the current set of hooks, or update if new hooks are added, run
`Install.bat` (Windows) or `tools/hooks/install.sh` (Unix-like) as appropriate.

If your Git GUI does not support a given hook, there is usually a `.bat` file
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.

## Adding New Hooks

New Git [hooks] may be added by creating a file named `<hook-name>.hook` in
this directory. Git determines what hooks are available and what their names
are.

New Git [merge drivers] may be added by adding a shell script named `<ext>.merge`
and updating `.gitattributes` in the root of the repository to include the line
`*.<ext> merge=<ext>`.

Adding or removing hooks or merge drivers requires running the install script
again, but modifying them does not. See existing `.hook` and `.merge` files for examples.

[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
[merge drivers]: https://git-scm.com/docs/gitattributes#_performing_a_three_way_merge
[mapmerge2]: ../mapmerge2/README.md
[fix icon conflicts]: https://tgstation13.org/wiki/Resolving_icon_conflicts
[fix map conflicts]: https://tgstation13.org/wiki/Map_Merger


code:
@call "%~dp0\..\bootstrap\python" -m hooks.install --uninstall %*
@pause


code:
#!/bin/sh
exec tools/bootstrap/python -m dmi.merge_driver "$@"


code:
#!/bin/sh
exec tools/bootstrap/python -m mapmerge2.merge_driver "$@"


code:
#!/usr/bin/env python3
# hooks/install.py
#
# This script is configured by adding `*.hook` and `*.merge` files in the same
# directory. Such files should be `#!/bin/sh` scripts, usually invoking Python.
# This installer will have to be re-run any time a hook or merge file is added
# or removed, but not when they are changed.
#
# Merge drivers will also need a corresponding entry in the `.gitattributes`
# file.

import os
import stat
import glob
import re
import pygit2
import shlex

def write_hook(fname, command):
    with open(fname, 'w', encoding='utf-8', newline='\n') as f:
        print("#!/bin/sh", file=f)
        print("exec", command, file=f)

    # chmod +x
    st = os.stat(fname)
    if not hasattr(st, 'st_file_attributes'):
        os.chmod(fname, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)

def _find_stuff(target=None):
    repo_dir = pygit2.discover_repository(target or os.getcwd())
    repo = pygit2.Repository(repo_dir)
    # Strips any active worktree to find the hooks directory.
    root_repo_dir = re.sub(r'/.git/worktrees/[^/]+/', '/.git/', repo_dir)
    hooks_dir = os.path.join(root_repo_dir, 'hooks')
    return repo, hooks_dir

def uninstall(target=None, keep=()):
    repo, hooks_dir = _find_stuff(target)

    # Remove hooks
    for fname in glob.glob(os.path.join(hooks_dir, '*')):
        _, shortname = os.path.split(fname)
        if not fname.endswith('.sample') and f"{shortname}.hook" not in keep:
            print('Removing hook:', shortname)
            os.unlink(fname)

    # Remove merge driver configuration
    for entry in repo.config:
        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))
            del repo.config[entry.name]

def install(target=None):
    repo, hooks_dir = _find_stuff(target)
    tools_hooks = os.path.split(__file__)[0]

    keep = set()
    for full_path in glob.glob(os.path.join(tools_hooks, '*.hook')):
        _, fname = os.path.split(full_path)
        name, _ = os.path.splitext(fname)
        print('Installing hook:', name)
        keep.add(fname)
        relative_path = shlex.quote(os.path.relpath(full_path, repo.workdir).replace('\\', '/'))
        write_hook(os.path.join(hooks_dir, name), f'{relative_path} "$@"')

    # Use libgit2 config manipulation to set the merge driver config.
    for full_path in glob.glob(os.path.join(tools_hooks, '*.merge')):
        # Merge drivers are documented here: https://git-scm.com/docs/gitattributes
        _, fname = os.path.split(full_path)
        name, _ = os.path.splitext(fname)
        print('Installing merge driver:', name)
        keep.add(fname)
        # %P: "real" path of the file, should not usually be read or modified
        # %O: ancestor's version
        # %A: current version, and also the output path
        # %B: other branches' version
        # %L: conflict marker size
        relative_path = shlex.quote(os.path.relpath(full_path, repo.workdir).replace('\\', '/'))
        repo.config[f"merge.{name}.driver"] = f'{relative_path} %P %O %A %B %L'

    uninstall(target, keep=keep)

def main(argv):
    if len(argv) <= 1:
        return install()
    elif argv[1] == '--uninstall':
        return uninstall()
    else:
        print("Usage: python -m hooks.install [--uninstall]")
        return 1

if __name__ == '__main__':
    import sys
    exit(main(sys.argv))


code:
#!/bin/sh
exec "$(dirname "$0")/../bootstrap/python" -m hooks.install "$@"


code:
# merge_frontend.py
import sys
import io
import os
import pygit2
import collections
import typing

ENCODING = 'utf-8'

class MergeReturn(typing.NamedTuple):
    success: bool
    merge_result: typing.Optional[object]

class MergeDriver:
    driver_id: typing.Optional[str] = None

    def pre_announce(self, path: str):
        """
        Called before merge() is called, with a human-friendly path for output.
        """
        print(f"Merging {self.driver_id}: {path}")

    def merge(self, base: typing.BinaryIO, left: typing.BinaryIO, right: typing.BinaryIO) -> MergeReturn:
        """
        Read from three BinaryIOs: base (common ancestor), left (ours), and
        right (theirs). Perform the actual three-way merge operation. Leave
        conflict markers if necessary.

        Return (False, None) to indicate the merge driver totally failed.
        Return (False, merge_result) if the result contains conflict markers.
        Return (True, merge_result) if everything went smoothly.
        """
        raise NotImplementedError

    def to_file(self, output: typing.BinaryIO, merge_result: object):
        """
        Save the merge() result to the given output stream.
        Override this if the merge() result is not bytes or str.
        """
        if isinstance(merge_result, bytes):
            output.write(merge_result)
        elif isinstance(merge_result, str):
            with io.TextIOWrapper(output, ENCODING) as f:
                f.write(merge_result)
        else:
            raise NotImplementedError

    def post_announce(self, success: bool, merge_result: object):
        """
        Called after merge() is called, to warn the user if action is needed.
        """
        if not success:
            print("!!! Manual merge required")
            if merge_result:
                print("    A best-effort merge was performed. You must finish the job yourself.")
            else:
                print("    No merge was possible. You must resolve the conflict yourself.")

    def main(self, args: typing.List[str] = None):
        return _main(self, args or sys.argv[1:])

def _main(driver: MergeDriver, args: typing.List[str]):
    if len(args) > 0 and args[0] == '--posthoc':
        return _posthoc_main(driver, args[1:])
    else:
        return _driver_main(driver, args)

def _driver_main(driver: MergeDriver, args: typing.List[str]):
    """
    Act like a normal Git merge driver, called by Git during a merge.
    """
    if len(args) != 5:
        print("merge driver called with wrong number of arguments")
        print("    usage: %P %O %A %B %L")
        return 1

    path, path_base, path_left, path_right, _ = args
    driver.pre_announce(path)

    with open(path_base, 'rb') as io_base:
        with open(path_left, 'rb') as io_left:
            with open(path_right, 'rb') as io_right:
                success, merge_result = driver.merge(io_base, io_left, io_right)

    if merge_result:
        # If we got anything, write it to the working directory.
        with open(path_left, 'wb') as io_output:
            driver.to_file(io_output, merge_result)

    driver.post_announce(success, merge_result)
    if not success:
        # If we were not successful, do not mark the conflict as resolved.
        return 1

def _posthoc_main(driver: MergeDriver, args: typing.List[str]):
    """
    Apply merge driver logic to a repository which is already in a conflicted
    state, running the driver on any conflicted files.
    """
    repo_dir = pygit2.discover_repository(os.getcwd())
    repo = pygit2.Repository(repo_dir)
    conflicts = repo.index.conflicts
    if not conflicts:
        print("There are no unresolved conflicts.")
        return 0

    all_success = True
    index_changed = False
    any_attempted = False
    for base, left, right in list(conflicts):
        if not base or not left or not right:
            # (not left) or (not right): deleted in one branch, modified in the other.
            # (not base): added differently in both branches.
            # In either case, there's nothing we can do for now.
            continue

        path = left.path
        if not _applies_to(repo, driver, path):
            # Skip the file if it's not the right extension.
            continue

        any_attempted = True
        driver.pre_announce(path)
        io_base = io.BytesIO(repo[base.id].data)
        io_left = io.BytesIO(repo[left.id].data)
        io_right = io.BytesIO(repo[right.id].data)
        success, merge_result = driver.merge(io_base, io_left, io_right)
        if merge_result:
            # If we got anything, write it to the working directory.
            with open(os.path.join(repo.workdir, path), 'wb') as io_output:
                driver.to_file(io_output, merge_result)

            if success:
                # If we were successful, mark the conflict as resolved.
                with open(os.path.join(repo.workdir, path), 'rb') as io_readback:
                    contents = io_readback.read()
                merged_id = repo.create_blob(contents)
                repo.index.add(pygit2.IndexEntry(path, merged_id, left.mode))
                del conflicts[path]
                index_changed = True
        if not success:
            all_success = False
        driver.post_announce(success, merge_result)

    if index_changed:
        repo.index.write()

    if not any_attempted:
        print("There are no unresolved", driver.driver_id, "conflicts.")

    if not all_success:
        # Not usually observed, but indicate the failure just in case.
        return 1

def _applies_to(repo: pygit2.Repository, driver: MergeDriver, path: str):
    """
    Check if the current merge driver is a candidate to handle a given path.
    """
    if not driver.driver_id:
        raise ValueError('Driver must have ID to perform post-hoc merge')
    return repo.get_attr(path, 'merge') == driver.driver_id


code:
#!/bin/sh
exec tools/bootstrap/python -m mapmerge2.precommit


code:
#!/bin/sh
set -e
if [ "$*" = "-m precommit" ]; then
	echo "Hooks are being updated..."
	echo "Details: https://github.com/tgstation/tgstation/pull/55658"
	if [ "$(uname -o)" = "Msys" ]; then
		tools/hooks/Install.bat
	else
		tools/hooks/install.sh
	fi
	echo "---------------"
	exec tools/hooks/pre-commit.hook
else
	echo "tools/hooks/python.sh is replaced by tools/bootstrap/python"
	echo "Details: https://github.com/tgstation/tgstation/pull/55658"
	exit 1
fi


code:
import sys
import json

if len(sys.argv) <= 1:
    exit(1)

status = 0

for file in sys.argv[1:]:
    with open(file, encoding="ISO-8859-1") as f:
        try:
            json.load(f)
        except ValueError as exception:
            print("JSON error in {}".format(file))
            print(exception)
            status = 1
        else:
            print("Valid {}".format(file))

exit(status)


code:
#!/usr/bin/env python3
from http.server import HTTPServer, SimpleHTTPRequestHandler
import os

class CORSRequestHandler(SimpleHTTPRequestHandler):
    def end_headers(self):
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET')
        self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate')
        return super(CORSRequestHandler, self).end_headers()

os.makedirs('../data/asset-store/', exist_ok=True)
os.chdir('../data/asset-store/')
httpd = HTTPServer(('localhost', 58715), CORSRequestHandler)
httpd.serve_forever()


code:
@echo off
rem Cheridan asked for this. - N3X
call "%~dp0\bootstrap\python" ss13_genchangelog.py ../html/changelogs
pause


code:
@echo off
echo Installing hooks for next time...
call "%~dp0\..\bootstrap\python.bat" -m hooks.install
echo.
echo Fixing things up...
call "%~dp0\..\bootstrap\python.bat" -m mapmerge2.fixup
pause

path: tools/mapmerge2/I Forgot To Map Merge.bat


code:
# Map Merger

The **Map Merger** is a collection of scripts that keep this repository's maps
in a format which is easier to track in Git and less likely to cause merge
conflicts. When merge conflicts do occur, it can sometimes resolve them.

For detailed troubleshooting instructions and other tips, visit the
[Map Merger] wiki article.

## Installation

To install the [Git hooks], open the `tools/hooks/` folder and double-click
`Install.bat`. Linux users run `tools/hooks/install.sh`.

## Manual Use

If using a Git GUI which is not compatible with the hooks:

* Before committing, double-click `Run Before Committing.bat`
* When a merge has map conflicts, double-click `Resolve Map Conflicts.bat`

The console will show whether the operation succeeded.

For more details, see the [Map Merger] wiki article.

## What Map Merging Is

The "map merge" operation describes the process of rewriting a map file written
by the DreamMaker map editor to A) use a format more amenable to Git's conflict
resolution and B) differ in the least amount textually from the previous
version of the map while maintaining all the actual changes. It requires an old
version of the map to use as a reference and a new version of the map which
contains the desired changes.

Map Merge 2 adds multi-Z support, automatic handling of key overflow, better
merge conflict prevention, and a real merge conflict resolver.

## Code Structure

Frontend scripts are meant to be run directly. They obey the environment
variables `TGM` to set whether files are saved in TGM (1) or DMM (0) format,
and `MAPROOT` to determine where maps are kept. By default, TGM is used and
the map root is autodetected. Each script may either prompt for the desired map
or be run with command-line parameters indicating which maps to act on. The
scripts include:

* `convert.py` for converting maps to and from the TGM format. Used by
  `tgm2dmm.bat` and `dmm2tgm.bat`.
* `mapmerge.py` for running the map merge on map backups saved by
  `Prepare Maps.bat`. Used by `mapmerge.bat`

Implementation modules:

* `dmm.py` includes the map reader and writer.
* `mapmerge.py` includes the implementation of the map merge operation.
* `frontend.py` includes the common code for the frontend scripts.

`precommit.py` is run by the [Git hooks] if installed, and merges the new
version of any map saved in the index (`git add`ed) with the old version stored
in Git when run.

[Map Merger]: https://tgstation13.org/wiki/Map_Merger
[Git hooks]: ../hooks/README.md


code:
@call "%~dp0\..\bootstrap\python.bat" -m mapmerge2.merge_driver --posthoc %*
@pause


code:
@call "%~dp0\..\bootstrap\python" -m mapmerge2.precommit --use-workdir %*
@pause


code:
#!/usr/bin/env python3
from . import frontend, dmm

if __name__ == '__main__':
    settings = frontend.read_settings()
    for fname in frontend.process(settings, "convert"):
        dmm.DMM.from_file(fname).to_file(fname, tgm = settings.tgm)


code:
# Tools for working with DreamMaker maps

import io
import bidict
import random
from collections import namedtuple

TGM_HEADER = "//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE"
ENCODING = 'utf-8'

Coordinate = namedtuple('Coordinate', ['x', 'y', 'z'])

class DMM:
    __slots__ = ['key_length', 'size', 'dictionary', 'grid', 'header']

    def __init__(self, key_length, size):
        self.key_length = key_length
        self.size = size
        self.dictionary = bidict.bidict()
        self.grid = {}
        self.header = None

    @staticmethod
    def from_file(fname):
        with open(fname, 'r', encoding=ENCODING) as f:
            return _parse(f.read())

    @staticmethod
    def from_bytes(bytes):
        return _parse(bytes.decode(ENCODING))

    def to_file(self, fname, *, tgm = True):
        self._presave_checks()
        with open(fname, 'w', newline='\n', encoding=ENCODING) as f:
            (save_tgm if tgm else save_dmm)(self, f)

    def to_bytes(self, *, tgm = True):
        self._presave_checks()
        bio = io.BytesIO()
        with io.TextIOWrapper(bio, newline='\n', encoding=ENCODING) as f:
            (save_tgm if tgm else save_dmm)(self, f)
            f.flush()
            return bio.getvalue()

    def get_or_generate_key(self, tile):
        try:
            return self.dictionary.inv[tile]
        except KeyError:
            key = self.generate_new_key()
            self.dictionary[key] = tile
            return key

    def get_tile(self, coord):
        return self.dictionary[self.grid[coord]]

    def set_tile(self, coord, tile):
        tile = tuple(tile)
        self.grid[coord] = self.get_or_generate_key(tile)

    def generate_new_key(self):
        self._ensure_free_keys(1)
        max_key = max_key_for(self.key_length)
        # choose one of the free keys at random
        key = random.randint(0, max_key - 1)
        while key in self.dictionary:
            key = random.randint(0, max_key - 1)
        return key

    def overwrite_key(self, key, fixed, bad_keys):
        try:
            self.dictionary[key] = fixed
            return None
        except bidict.DuplicationError:
            old_key = self.dictionary.inv[fixed]
            bad_keys[key] = old_key
            print(f"Merging '{num_to_key(key, self.key_length)}' into '{num_to_key(old_key, self.key_length)}'")
            return old_key

    def reassign_bad_keys(self, bad_keys):
        if not bad_keys:
            return
        for k, v in self.grid.items():
            # reassign the grid entries which used the old key
            self.grid[k] = bad_keys.get(v, v)

    def _presave_checks(self):
        # last-second handling of bogus keys to help prevent and fix broken maps
        self._ensure_free_keys(0)
        max_key = max_key_for(self.key_length)
        bad_keys = {key: 0 for key in self.dictionary.keys() if key > max_key}
        if bad_keys:
            print(f"Warning: fixing {len(bad_keys)} overflowing keys")
            for k in bad_keys:
                # create a new non-bogus key and transfer that value to it
                new_key = bad_keys[k] = self.generate_new_key()
                self.dictionary.forceput(new_key, self.dictionary[k])
                print(f"    {num_to_key(k, self.key_length, True)} -> {num_to_key(new_key, self.key_length)}")

        # handle entries in the dictionary which have atoms in the wrong order
        keys = list(self.dictionary.keys())
        for key in keys:
            value = self.dictionary[key]
            if is_bad_atom_ordering(num_to_key(key, self.key_length, True), value):
                fixed = tuple(fix_atom_ordering(value))
                self.overwrite_key(key, fixed, bad_keys)

        self.reassign_bad_keys(bad_keys)

    def _ensure_free_keys(self, desired):
        # ensure that free keys exist by increasing the key length if necessary
        free_keys = max_key_for(self.key_length) - len(self.dictionary)
        while free_keys < desired:
            if self.key_length >= MAX_KEY_LENGTH:
                raise KeyTooLarge(f"can't expand beyond key length {MAX_KEY_LENGTH} ({len(self.dictionary)} keys)")
            self.key_length += 1
            free_keys = max_key_for(self.key_length) - len(self.dictionary)
        return free_keys

    @property
    def coords_zyx(self):
        for z in range(1, self.size.z + 1):
            for y in range(1, self.size.y + 1):
                for x in range(1, self.size.x + 1):
                    yield (z, y, x)

    @property
    def coords_z(self):
        return range(1, self.size.z + 1)

    @property
    def coords_yx(self):
        for y in range(1, self.size.y + 1):
            for x in range(1, self.size.x + 1):
                yield (y, x)

    def __repr__(self):
        return f"DMM(size={self.size}, key_length={self.key_length}, dictionary_size={len(self.dictionary)})"

# ----------
# key handling

# Base 52 a-z A-Z dictionary for fast conversion
MAX_KEY_LENGTH = 3  # things will get ugly fast if you exceed this
BASE = 52
base52 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
base52_r = {x: i for i, x in enumerate(base52)}
assert len(base52) == BASE and len(base52_r) == BASE

def key_to_num(key):
    num = 0
    for ch in key:
        num = BASE * num + base52_r[ch]
    return num

def num_to_key(num, key_length, allow_overflow=False):
    if num >= (BASE ** key_length if allow_overflow else max_key_for(key_length)):
        raise KeyTooLarge(f"num={num} does not fit in key_length={key_length}")

    result = ''
    while num:
        result = base52[num % BASE] + result
        num //= BASE

    assert len(result) <= key_length
    return base52[0] * (key_length - len(result)) + result

def max_key_for(key_length):
    # keys only go up to "ymo" = 65534, under-estimated just in case
    # https://secure.byond.com/forum/?post=2340796#comment23770802
    return min(65530, BASE ** key_length)

class KeyTooLarge(Exception):
    pass

# ----------
# An actual atom parser

def parse_map_atom(atom):
    try:
        i = atom.index('{')
    except ValueError:
        return atom, {}

    path, rest = atom[:i], atom[i+1:]
    vars = {}

    in_string = False
    in_name = False
    escaping = False
    current_name = ''
    current = ''
    for ch in rest:
        if escaping:
            escaping = False
            current += ch
        elif ch == '\\':
            escaping = True
        elif ch == '"':
            in_string = not in_string
            current += ch
        elif in_string:
            current += ch
        elif ch == ';':
            vars[current_name.strip()] = current.strip()
            current_name = current = ''
        elif ch == '=':
            current_name = current
            current = ''
        elif ch == '}':
            vars[current_name.strip()] = current.strip()
            break
        elif ch not in ' ':
            current += ch

    return path, vars

def is_bad_atom_ordering(key, atoms):
    seen_turfs = 0
    seen_areas = 0
    can_fix = False
    for each in atoms:
        if each.startswith('/turf'):
            if seen_turfs == 1:
                print(f"Warning: key '{key}' has multiple turfs!")
            if seen_areas:
                print(f"Warning: key '{key}' has area before turf (autofixing...)")
                can_fix = True
            seen_turfs += 1
        elif each.startswith('/area'):
            if seen_areas == 1:
                print(f"Warning: key '{key}' has multiple areas!!!")
            seen_areas += 1
        else:
            if (seen_turfs or seen_areas) and not can_fix:
                print(f"Warning: key '{key}' has movable after turf or area (autofixing...)")
                can_fix = True
    if not seen_areas or not seen_turfs:
        print(f"Warning: key '{key}' is missing either a turf or area")
    return can_fix

def split_atom_groups(atoms):
    movables, turfs, areas = [], [], []
    for each in atoms:
        if each.startswith('/turf'):
            turfs.append(each)
        elif each.startswith('/area'):
            areas.append(each)
        else:
            movables.append(each)
    return movables, turfs, areas

def fix_atom_ordering(atoms):
    movables, turfs, areas = split_atom_groups(atoms)
    movables.extend(turfs)
    movables.extend(areas)
    return movables

# ----------
# TGM writer

def save_tgm(dmm, output):
    output.write(f"{TGM_HEADER}\n")
    if dmm.header:
        output.write(f"{dmm.header}\n")

    # write dictionary in tgm format
    for key, value in sorted(dmm.dictionary.items()):
        output.write(f'"{num_to_key(key, dmm.key_length)}" = (\n')
        for idx, thing in enumerate(value):
            in_quote_block = False
            in_varedit_block = False
            for char in thing:
                if in_quote_block:
                    if char == '"':
                        in_quote_block = False
                    output.write(char)
                elif char == '"':
                    in_quote_block = True
                    output.write(char)
                elif not in_varedit_block:
                    if char == "{":
                        in_varedit_block = True
                        output.write("{\n\t")
                    else:
                        output.write(char)
                elif char == ";":
                    output.write(";\n\t")
                elif char == "}":
                    output.write("\n\t}")
                    in_varedit_block = False
                else:
                    output.write(char)
            if idx < len(value) - 1:
                output.write(",\n")
        output.write(")\n")

    # thanks to YotaXP for finding out about this one
    max_x, max_y, max_z = dmm.size
    for z in range(1, max_z + 1):
        output.write("\n")
        for x in range(1, max_x + 1):
            output.write(f"({x},{1},{z}) = {{\"\n")
            for y in range(max_y, 0, -1):
                output.write(f"{num_to_key(dmm.grid[x, y, z], dmm.key_length)}\n")
            output.write("\"}\n")

# ----------
# DMM writer

def save_dmm(dmm, output):
    if dmm.header:
        output.write(f"{dmm.header}\n")

    # writes a tile dictionary the same way Dreammaker does
    for key, value in sorted(dmm.dictionary.items()):
        output.write(f'"{num_to_key(key, dmm.key_length)}" = ({",".join(value)})\n')

    output.write("\n")

    # writes a map grid the same way Dreammaker does
    max_x, max_y, max_z = dmm.size
    for z in range(1, max_z + 1):
        output.write(f"(1,1,{z}) = {{\"\n")

        for y in range(max_y, 0, -1):
            for x in range(1, max_x + 1):
                try:
                    output.write(num_to_key(dmm.grid[x, y, z], dmm.key_length))
                except KeyError:
                    print(f"Key error: ({x}, {y}, {z})")
            output.write("\n")
        output.write("\"}\n")

# ----------
# Parser

def _parse(map_raw_text):
    in_comment_line = False
    comment_trigger = False

    in_quote_block = False
    in_key_block = False
    in_data_block = False
    in_varedit_block = False
    after_data_block = False
    escaping = False
    skip_whitespace = False

    dictionary = bidict.bidict()
    duplicate_keys = {}
    curr_key_len = 0
    curr_key = 0
    curr_datum = ""
    curr_data = list()

    in_map_block = False
    in_coord_block = False
    in_map_string = False
    base_x = 0
    adjust_y = True

    curr_num = ""
    reading_coord = "x"

    key_length = 0

    maxx = 0
    maxy = 0
    maxz = 0

    curr_x = 0
    curr_y = 0
    curr_z = 0
    grid = dict()

    it = iter(map_raw_text)

    # map block
    for char in it:
        if char == "\n":
            in_comment_line = False
            comment_trigger = False
            continue
        elif in_comment_line:
            continue
        elif char in "\r\t":
            continue

        if char == "/" and not in_quote_block:
            if comment_trigger:
                in_comment_line = True
                continue
            else:
                comment_trigger = True
        else:
            comment_trigger = False

        if in_data_block:

            if in_varedit_block:

                if in_quote_block:
                    if char == "\\":
                        curr_datum = curr_datum + char
                        escaping = True

                    elif escaping:
                        curr_datum = curr_datum + char
                        escaping = False

                    elif char == "\"":
                        curr_datum = curr_datum + char
                        in_quote_block = False

                    else:
                        curr_datum = curr_datum + char

                else:
                    if skip_whitespace and char == " ":
                        skip_whitespace = False
                        continue
                    skip_whitespace = False

                    if char == "\"":
                        curr_datum = curr_datum + char
                        in_quote_block = True

                    elif char == ";":
                        skip_whitespace = True
                        curr_datum = curr_datum + char

                    elif char == "}":
                        curr_datum = curr_datum + char
                        in_varedit_block = False

                    else:
                        curr_datum = curr_datum + char

            elif char == "{":
                curr_datum = curr_datum + char
                in_varedit_block = True

            elif char == ",":
                curr_data.append(curr_datum)
                curr_datum = ""

            elif char == ")":
                curr_data.append(curr_datum)
                curr_data = tuple(curr_data)
                try:
                    dictionary[curr_key] = curr_data
                except bidict.ValueDuplicationError:
                    # if the map has duplicate values, eliminate them now
                    duplicate_keys[curr_key] = dictionary.inv[curr_data]
                curr_data = list()
                curr_datum = ""
                curr_key = 0
                curr_key_len = 0
                in_data_block = False
                after_data_block = True

            else:
                curr_datum = curr_datum + char

        elif in_key_block:
            if char == "\"":
                in_key_block = False
                if key_length == 0:
                    key_length = curr_key_len
                else:
                    assert key_length == curr_key_len
            else:
                curr_key = BASE * curr_key + base52_r[char]
                curr_key_len += 1

        # else we're looking for a key block, a data block or the map block
        elif char == "\"":
            in_key_block = True
            after_data_block = False

        elif char == "(":
            if after_data_block:
                in_coord_block = True
                after_data_block = False
                curr_key = 0
                curr_key_len = 0
                break
            else:
                in_data_block = True
                after_data_block = False

    # grid block
    for char in it:
        if char == "\r":
            continue

        if in_coord_block:
            if char == ",":
                if reading_coord == "x":
                    curr_x = int(curr_num)
                    if curr_x > maxx:
                        maxx = curr_x
                    base_x = curr_x
                    curr_num = ""
                    reading_coord = "y"
                elif reading_coord == "y":
                    curr_y = int(curr_num)
                    if curr_y > maxy:
                        maxy = curr_y
                    curr_num = ""
                    reading_coord = "z"
                else:
                    raise ValueError("too many dimensions")

            elif char == ")":
                curr_z = int(curr_num)
                if curr_z > maxz:
                    maxz = curr_z
                in_coord_block = False
                reading_coord = "x"
                curr_num = ""

            else:
                curr_num = curr_num + char

        elif in_map_string:
            if char == "\"":
                in_map_string = False
                adjust_y = True
                curr_y -= 1

            elif char == "\n":
                if adjust_y:
                    adjust_y = False
                else:
                    curr_y += 1
                curr_x = base_x
            else:
                curr_key = BASE * curr_key + base52_r[char]
                curr_key_len += 1
                if curr_key_len == key_length:
                    grid[curr_x, curr_y, curr_z] = duplicate_keys.get(curr_key, curr_key)
                    if curr_x > maxx:
                        maxx = curr_x
                    curr_x += 1
                    curr_key = 0
                    curr_key_len = 0

        # else look for coordinate block or a map string
        elif char == "(":
            in_coord_block = True
        elif char == "\"":
            in_map_string = True

    if curr_y > maxy:
        maxy = curr_y

    if not grid:
        # Usually caused by unbalanced quotes.
        max_key = num_to_key(max(dictionary.keys()), key_length, True)
        raise ValueError(f"dmm failed to parse, check for a syntax error near or after key {max_key!r}")

    # Convert from raw .dmm coordinates to DM/BYOND coordinates by flipping Y
    grid2 = dict()
    for (x, y, z), tile in grid.items():
        grid2[x, maxy + 1 - y, z] = tile

    data = DMM(key_length, Coordinate(maxx, maxy, maxz))
    data.dictionary = dictionary
    data.grid = grid2
    return data


code:
import os
import sys
from .dmm import *

def _self_test():
    # test: can we load every DMM in the tree
    count = 0
    for dirpath, dirnames, filenames in os.walk('.'):
        if '.git' in dirnames:
            dirnames.remove('.git')
        for filename in filenames:
            if filename.endswith('.dmm'):
                fullpath = os.path.join(dirpath, filename)
                try:
                    DMM.from_file(fullpath)
                except Exception:
                    print('Failed on:', fullpath)
                    raise
                count += 1

    print(f"{os.path.relpath(__file__)}: successfully parsed {count} .dmm files")

def _usage():
    print(f"Usage:")
    print(f"    tools{os.sep}bootstrap{os.sep}python -m {__spec__.name}")
    exit(1)

def _main():
    if len(sys.argv) == 1:
        return _self_test()

    return _usage()

if __name__ == '__main__':
    _main()


code:
#!/usr/bin/env python3
import os
import pygit2
from . import dmm
from .mapmerge import merge_map

STATUS_INDEX = (pygit2.GIT_STATUS_INDEX_NEW
    | pygit2.GIT_STATUS_INDEX_MODIFIED
    | pygit2.GIT_STATUS_INDEX_DELETED
    | pygit2.GIT_STATUS_INDEX_RENAMED
    | pygit2.GIT_STATUS_INDEX_TYPECHANGE
)
STATUS_WT = (pygit2.GIT_STATUS_WT_NEW
    | pygit2.GIT_STATUS_WT_MODIFIED
    | pygit2.GIT_STATUS_WT_DELETED
    | pygit2.GIT_STATUS_WT_RENAMED
    | pygit2.GIT_STATUS_WT_TYPECHANGE
)
ABBREV_LEN = 12
TGM_HEADER = dmm.TGM_HEADER.encode(dmm.ENCODING)

def walk_tree(tree, *, _prefix=''):
    for child in tree:
        if isinstance(child, pygit2.Tree):
            yield from walk_tree(child, _prefix=f'{_prefix}{child.name}/')
        else:
            yield f'{_prefix}{child.name}', child

def insert_into_tree(repo, tree_builder, path, blob_oid):
    try:
        first, rest = path.split('/', 1)
    except ValueError:
        tree_builder.insert(path, blob_oid, pygit2.GIT_FILEMODE_BLOB)
    else:
        inner = repo.TreeBuilder(tree_builder.get(first))
        insert_into_tree(repo, inner, rest, blob_oid)
        tree_builder.insert(first, inner.write(), pygit2.GIT_FILEMODE_TREE)

def main(repo):
    if repo.index.conflicts:
        print("You need to resolve merge conflicts first.")
        return 1

    # Ensure the index is clean.
    for path, status in repo.status().items():
        if status & pygit2.GIT_STATUS_IGNORED:
            continue
        if status & STATUS_INDEX:
            print("You have changes staged for commit. Commit them or unstage them first.")
            print("If you are about to commit maps for the first time, run `Run Before Committing.bat`.")
            return 1
        if path.endswith(".dmm") and (status & STATUS_WT):
            print("You have modified maps. Commit them first.")
            print("If you are about to commit maps for the first time, run `Run Before Committing.bat`.")
            return 1

    # Read the HEAD commit.
    head_commit = repo[repo.head.target]
    head_files = {}
    for path, blob in walk_tree(head_commit.tree):
        if path.endswith(".dmm"):
            data = blob.read_raw()
            if not data.startswith(TGM_HEADER):
                head_files[path] = dmm.DMM.from_bytes(data)

    if not head_files:
        print("All committed maps appear to be in the correct format.")
        print("If you are about to commit maps for the first time, run `Run Before Committing.bat`.")
        return 1

    # Work backwards to find a base for each map, converting as found.
    converted = {}
    if len(head_commit.parents) != 1:
        print("Unable to automatically fix anything because HEAD is a merge commit.")
        return 1
    commit_message_lines = []
    working_commit = head_commit.parents[0]
    while len(converted) < len(head_files):
        for path in head_files.keys() - converted.keys():
            try:
                blob = working_commit.tree[path]
            except KeyError:
                commit_message_lines.append(f"{'new':{ABBREV_LEN}}: {path}")
                print(f"Converting new map: {path}")
                converted[path] = head_files[path]
            else:
                data = blob.read_raw()
                if data.startswith(TGM_HEADER):
                    str_id = str(working_commit.id)[:ABBREV_LEN]
                    commit_message_lines.append(f"{str_id}: {path}")
                    print(f"Converting map: {path}")
                    converted[path] = merge_map(head_files[path], dmm.DMM.from_bytes(data))
        if len(working_commit.parents) != 1:
            print("A merge commit was encountered before good versions of these maps were found:")
            print("\n".join(f"    {x}" for x in head_files.keys() - converted.keys()))
            return 1
        working_commit = working_commit.parents[0]

    # Okay, do the actual work.
    tree_builder = repo.TreeBuilder(head_commit.tree)
    for path, merged_map in converted.items():
        blob_oid = repo.create_blob(merged_map.to_bytes())
        insert_into_tree(repo, tree_builder, path, blob_oid)
        repo.index.add(pygit2.IndexEntry(path, blob_oid, repo.index[path].mode))
        merged_map.to_file(os.path.join(repo.workdir, path))

    # Save the index.
    repo.index.write()

    # Commit the index to the current branch.
    signature = pygit2.Signature(repo.config['user.name'], repo.config['user.email'])
    joined = "\n".join(commit_message_lines)
    repo.create_commit(
        repo.head.name,
        signature,  # author
        signature,  # committer
        f'Convert maps to TGM\n\n{joined}\n\nAutomatically commited by: {os.path.relpath(__file__, repo.workdir)}',
        tree_builder.write(),
        [head_commit.id],
    )

    # Success.
    print("Successfully committed a fixup. Push as needed.")
    return 0

if __name__ == '__main__':
    exit(main(pygit2.Repository(pygit2.discover_repository(os.getcwd()))))


code:
# Common code for the frontend interface of map tools
import sys
import os
import pathlib
import shutil
from collections import namedtuple

Settings = namedtuple('Settings', ['map_folder', 'tgm'])
MapsToRun = namedtuple('MapsToRun', ['files', 'indices'])

def string_to_num(s):
    try:
        return int(s)
    except ValueError:
        return -1

def read_settings():
    # discover map folder if needed
    try:
        map_folder = os.environ['MAPROOT']
    except KeyError:
        map_folder = '_maps/'
        for _ in range(8):
            if os.path.exists(map_folder):
                break
            map_folder = os.path.join('..', map_folder)
        else:
            map_folder = None

    # assume TGM is True by default
    tgm = os.environ.get('TGM', "1") == "1"

    return Settings(map_folder, tgm)

def pretty_path(settings, path_str):
    if settings.map_folder:
        return path_str[len(os.path.commonpath([settings.map_folder, path_str]))+1:]
    else:
        return path_str

def prompt_maps(settings, verb):
    if not settings.map_folder:
        print("Could not autodetect the _maps folder, set MAPROOT")
        exit(1)

    list_of_files = list()
    for root, directories, filenames in os.walk(settings.map_folder):
        for filename in [f for f in filenames if f.endswith(".dmm")]:
            list_of_files.append(pathlib.Path(root, filename))

    last_dir = ""
    for i, this_file in enumerate(list_of_files):
        this_dir = this_file.parent
        if last_dir != this_dir:
            print("--------------------------------")
            last_dir = this_dir
        print("[{}]: {}".format(i, pretty_path(settings, str(this_file))))

    print("--------------------------------")
    in_list = input("List the maps you want to " + verb + " (example: 1,3-5,12):\n")
    in_list = in_list.replace(" ", "")
    in_list = in_list.split(",")

    valid_indices = list()
    for m in in_list:
        index_range = m.split("-")
        if len(index_range) == 1:
            index = string_to_num(index_range[0])
            if index >= 0 and index < len(list_of_files):
                valid_indices.append(index)
        elif len(index_range) == 2:
            index0 = string_to_num(index_range[0])
            index1 = string_to_num(index_range[1])
            if index0 >= 0 and index0 <= index1 and index1 < len(list_of_files):
                valid_indices.extend(range(index0, index1 + 1))

    return MapsToRun(list_of_files, valid_indices)

def process(settings, verb, *, modify=True, backup=None):
    if backup is None:
        backup = modify  # by default, backup when we modify
    assert modify or not backup  # doesn't make sense to backup when not modifying

    if len(sys.argv) > 1:
        maps = sys.argv[1:]
    else:
        maps = prompt_maps(settings, verb)
        maps = [str(maps.files[i]) for i in maps.indices]
        print()

    if not maps:
        print("No maps selected.")
        return

    if modify:
        print(f"Maps WILL{'' if settings.tgm else ' NOT'} be converted to tgm.")
        if backup:
            print("Backups will be created with a \".before\" extension.")
        else:
            print("Warning: backups are NOT being taken.")

    print(f"\nWill {verb} these maps:")
    for path_str in maps:
        print(pretty_path(settings, path_str))

    try:
        confirm = input(f"\nPress Enter to {verb}...\n")
    except KeyboardInterrupt:
        confirm = "^C"
    if confirm != "":
        print(f"\nAborted.")
        return

    for path_str in maps:
        print(f' - {pretty_path(settings, path_str)}')

        if backup:
            shutil.copyfile(path_str, path_str + ".before")

        try:
            yield path_str
        except Exception as e:
            print(f"Error: {e}")
        else:
            print("Succeeded.")

    print("\nFinished.")


code:
#!/usr/bin/env python3
import shutil
from collections import defaultdict
from . import frontend
from .dmm import *

def merge_map(new_map, old_map, delete_unused=False):
    if new_map.key_length != old_map.key_length:
        print("Warning: Key lengths differ, taking new map")
        print(f"  Old: {old_map.key_length}")
        print(f"  New: {new_map.key_length}")
        return new_map

    if new_map.size != old_map.size:
        print("Warning: Map dimensions differ, taking new map")
        print(f"  Old: {old_map.size}")
        print(f"  New: {new_map.size}")
        return new_map

    key_length, size = old_map.key_length, old_map.size
    merged = DMM(key_length, size)
    merged.dictionary = old_map.dictionary.copy()

    known_keys = dict()  # mapping fron 'new' key to 'merged' key
    unused_keys = set(old_map.dictionary.keys())  # keys going unused

    # step one: parse the new version, compare it to the old version, merge both
    for z, y, x in new_map.coords_zyx:
        new_key = new_map.grid[x, y, z]
        # if this key has been processed before, it can immediately be merged
        try:
            merged.grid[x, y, z] = known_keys[new_key]
            continue
        except KeyError:
            pass

        def select_key(assigned):
            merged.grid[x, y, z] = known_keys[new_key] = assigned

        old_key = old_map.grid[x, y, z]
        old_tile = old_map.dictionary[old_key]
        new_tile = new_map.dictionary[new_key]

        # this tile is the exact same as before, so the old key is used
        if new_tile == old_tile:
            select_key(old_key)
            unused_keys.remove(old_key)

        # the tile is different here, but if it exists in the merged dictionary, that key can be used
        elif new_tile in merged.dictionary.inv:
            newold_key = merged.dictionary.inv[new_tile]
            select_key(newold_key)
            unused_keys.remove(newold_key)

        # the tile is brand new and it needs a new key, but if the old key isn't being used any longer it can be used instead
        elif old_tile not in new_map.dictionary.inv and old_key in unused_keys:
            merged.dictionary[old_key] = new_tile
            select_key(old_key)
            unused_keys.remove(old_key)

        # all other options ruled out, a brand new key is generated for the brand new tile
        else:
            fresh_key = merged.generate_new_key()
            merged.dictionary[fresh_key] = new_tile
            select_key(fresh_key)

    # step two: delete unused keys
    if unused_keys:
        #print(f"Notice: Trimming {len(unused_keys)} unused dictionary keys.")
        for key in unused_keys:
            del merged.dictionary[key]

    # sanity check: that the merged map equals the new map
    for z, y, x in new_map.coords_zyx:
        new_tile = new_map.dictionary[new_map.grid[x, y, z]]
        merged_tile = merged.dictionary[merged.grid[x, y, z]]
        if new_tile != merged_tile:
            print(f"Error: the map has been mangled! This is a mapmerge bug!")
            print(f"At {x},{y},{z}.")
            print(f"Should be {new_tile}")
            print(f"Instead is {merged_tile}")
            raise RuntimeError()

    return merged

def main(settings):
    for fname in frontend.process(settings, "merge", backup=True):
        shutil.copyfile(fname, fname + ".before")
        old_map = DMM.from_file(fname + ".backup")
        new_map = DMM.from_file(fname)
        merge_map(new_map, old_map).to_file(fname, settings.tgm)

if __name__ == '__main__':
    main(frontend.read_settings())


code:
#!/usr/bin/env python3
import sys
import collections
from . import dmm, mapmerge
from hooks.merge_frontend import MergeDriver

debug_stats = collections.defaultdict(int)

def select(base, left, right, *, debug=None):
    if left == right:
        # whether or not it's in the base, both sides agree
        if debug:
            debug_stats[f"select {debug} both"] += 1
        return left
    elif base == left:
        # base == left, but right is different: accept right
        if debug:
            debug_stats[f"select {debug} right"] += 1
        return right
    elif base == right:
        # base == right, but left is different: accept left
        if debug:
            debug_stats[f"select {debug} left"] += 1
        return left
    else:
        # all three versions are different
        if debug:
            debug_stats[f"select {debug} fail"] += 1
        return None

def three_way_merge(base, left, right):
    if base.size != left.size or base.size != right.size:
        print("Dimensions have changed:")
        print(f"    Base: {base.size}")
        print(f"    Ours: {left.size}")
        print(f"    Theirs: {right.size}")
        return True, None

    trouble = False
    merged = dmm.DMM(base.key_length, base.size)
    merged.dictionary = base.dictionary.copy()

    for (z, y, x) in base.coords_zyx:
        coord = x, y, z
        base_tile = base.get_tile(coord)
        left_tile = left.get_tile(coord)
        right_tile = right.get_tile(coord)

        # try to merge the whole tiles
        whole_tile_merge = select(base_tile, left_tile, right_tile, debug='tile')
        if whole_tile_merge is not None:
            merged.set_tile(coord, whole_tile_merge)
            continue

        # try to merge each group independently (movables, turfs, areas)
        base_movables, base_turfs, base_areas = dmm.split_atom_groups(base_tile)
        left_movables, left_turfs, left_areas = dmm.split_atom_groups(left_tile)
        right_movables, right_turfs, right_areas = dmm.split_atom_groups(right_tile)

        merged_movables = select(base_movables, left_movables, right_movables, debug='movable')
        merged_turfs = select(base_turfs, left_turfs, right_turfs, debug='turf')
        merged_areas = select(base_areas, left_areas, right_areas, debug='area')

        if merged_movables is not None and merged_turfs is not None and merged_areas is not None:
            merged.set_tile(coord, merged_movables + merged_turfs + merged_areas)
            continue

        # TODO: more advanced strategies?

        # fall back to requiring manual conflict resolution
        trouble = True
        print(f" C: Both sides touch the tile at {coord}")

        if merged_movables is None:
            obj_name = "---Merge conflict marker---"
            merged_movables = left_movables + [f'/obj{{name = "{obj_name}"}}'] + right_movables
            print(f"    Left and right movable groups are split by an `/obj` named \"{obj_name}\"")
        if merged_turfs is None:
            merged_turfs = left_turfs
            print(f"    Saving turf: {', '.join(left_turfs)}")
            print(f"    Alternative: {', '.join(right_turfs)}")
            print(f"    Original:    {', '.join(base_turfs)}")
        if merged_areas is None:
            merged_areas = left_areas
            print(f"    Saving area: {', '.join(left_areas)}")
            print(f"    Alternative: {', '.join(right_areas)}")
            print(f"    Original:    {', '.join(base_areas)}")

        merged.set_tile(coord, merged_movables + merged_turfs + merged_areas)

    merged = mapmerge.merge_map(merged, base)
    return trouble, merged

class DmmDriver(MergeDriver):
    driver_id = 'dmm'

    def merge(self, base, left, right):
        map_base = dmm.DMM.from_bytes(base.read())
        map_left = dmm.DMM.from_bytes(left.read())
        map_right = dmm.DMM.from_bytes(right.read())
        trouble, merge_result = three_way_merge(map_base, map_left, map_right)
        return not trouble, merge_result

    def to_file(self, outfile, merge_result):
        outfile.write(merge_result.to_bytes())

    def post_announce(self, success, merge_result):
        if not success:
            print("!!! Manual merge required!")
            if merge_result:
                print("    A best-effort merge was performed. You must edit the map and confirm")
                print("    that all coordinates mentioned above are as desired.")
            else:
                print("    The map was totally unable to be merged; you must start with one version")
                print("    or the other and manually resolve the conflict. Information about the")
                print("    conflicting tiles is listed above.")

if __name__ == '__main__':
    exit(DmmDriver().main())


code:
#!/usr/bin/env python3
import os
import sys
import pygit2
from . import dmm
from .mapmerge import merge_map

def main(repo, *, use_workdir=False):
    if repo.index.conflicts:
        print("You need to resolve merge conflicts first.")
        return 1

    try:
        repo.lookup_reference('MERGE_HEAD')
        print("Not running mapmerge for merge commit.")
        return 0
    except KeyError:
        pass

    target_statuses = pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_INDEX_NEW
    skip_to_file_statuses = pygit2.GIT_STATUS_WT_DELETED | pygit2.GIT_STATUS_WT_MODIFIED
    if use_workdir:
        target_statuses |= pygit2.GIT_STATUS_WT_MODIFIED | pygit2.GIT_STATUS_WT_NEW
        skip_to_file_statuses &= ~pygit2.GIT_STATUS_WT_MODIFIED

    changed = 0
    for path, status in repo.status().items():
        if path.endswith(".dmm") and (status & target_statuses):
            # read the index
            index_entry = repo.index[path]
            if use_workdir:
                index_map = dmm.DMM.from_file(os.path.join(repo.workdir, path))
            else:
                index_map = dmm.DMM.from_bytes(repo[index_entry.id].read_raw())

            try:
                head_blob = repo[repo[repo.head.target].tree[path].id]
            except KeyError:
                # New map, no entry in HEAD
                print(f"Converting new map: {path}", flush=True)
                assert (status & pygit2.GIT_STATUS_INDEX_NEW)
                merged_map = index_map
            else:
                # Entry in HEAD, merge the index over it
                print(f"Converting map: {path}", flush=True)
                assert not (status & pygit2.GIT_STATUS_INDEX_NEW)
                head_map = dmm.DMM.from_bytes(head_blob.read_raw())
                merged_map = merge_map(index_map, head_map)

            # write to the index
            blob_id = repo.create_blob(merged_map.to_bytes())
            repo.index.add(pygit2.IndexEntry(path, blob_id, index_entry.mode))
            changed += 1

            # write to the working directory if that's clean
            if status & skip_to_file_statuses:
                print(f"Warning: {path} has unindexed changes, not overwriting them")
            else:
                merged_map.to_file(os.path.join(repo.workdir, path))

    if changed:
        repo.index.write()
    return 0

if __name__ == '__main__':
    repo = pygit2.Repository(pygit2.discover_repository(os.getcwd()))
    exit(main(repo, use_workdir='--use-workdir' in sys.argv))


code:
This is a remake of 2013 midi2piano tool.

Requirements:
  Python 3

Simply run midi2piano.py and choose midi file you want to convert.
The "sheet music" will be copied to the clipboard.

There are some constants defined at the top of midi2piano.py.

TICK_LAG - CHANGE THIS VALUE TO TICK LAG OF YOUR SERVER!

Change their value if needed.

LINE_LENGTH_LIM - max length of line allowed in the sheet music
LINES_LIMIT - max amount of lines allowed in the sheet music. Extra lines will be cropped.

OVERALL_IMPORT_LIM - max amount of characters allowed in the sheet music.

You can also transpose music if you need to
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.
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