tools tools tools!

This commit is contained in:
Letter N
2021-01-31 10:47:25 +08:00
parent 945599e937
commit 0901cf9f10
105 changed files with 1612 additions and 8467 deletions

View File

@@ -1,162 +0,0 @@
# Colon Locater and Reporter, by RemieRichards V1.0 - 25/10/15
# Locates the byond operator ":", which is largely frowned upon due to the fact it ignores any type safety.
# This tool produces a .txt of "filenames line,line?,line totalcolons" (where line? represents a colon in a ternary operation) from all
# .dm files in the /code directory of an SS13 codebase, but can work on any Byond project if you modify scan_dir and real_dir
# the .txt take's todays date in reverse order and adds -colon_operator_log to the end, eg: "2015/10/25-colon_operator_log.txt"
import sys
import os
from datetime import date
#Climbs up from /tools/ColonCatcher and along to ../code
scan_dir = "code" #used later to truncate log file paths
real_dir = os.path.abspath("../../"+scan_dir)
#Scan a directory, scanning any dm files it finds
def colon_scan_dir(scan_dir):
if os.path.exists(scan_dir):
if os.path.isdir(scan_dir):
output_str = ""
files_scanned = 0
files_with_colons = 0
for root, dirs, files in os.walk(scan_dir):
for f in files:
print str(f)
scan_result = scan_dm_file(os.path.join(root, f))
files_scanned += 1
if scan_result:
output_str += scan_result+"\n"
files_with_colons += 1
output_str += str(files_with_colons) + "/" + str(files_scanned) + " files have colons in them"
todays_file = str(date.today())+"-colon_operator_log.txt"
output_file = open(todays_file, "w") #w so it overrides existing files for today, there should only really be one file per day
output_file.write(output_str)
#Scan one file, returning a string as a "report" or if there are no colons, False
def scan_dm_file(_file):
if not _file.endswith(".dm"):
return False
with open(_file, "r") as dm_file:
characters = dm_file.read()
line_num = 1
colon_count = 0
last_char = ""
in_embed_statement = 0 # [ ... ] num due to embeds in embeds
in_multiline_comment = 0 #/* ... */ num due to /* /* */ */
in_singleline_comment = False #// ... \n
in_string = False # " ... "
ternary_on_line = False #If there's a ? anywhere on the line, used to report "false"-positives
lines_with_colons = []
for char in characters:
#Line info
if char == "\n" or char == "\r":
if not in_string:
ternary_on_line = False #Stop any old ternary operation
line_num += 1
in_embed_statement = 0
#Not in a comment
if (not in_singleline_comment) and (in_multiline_comment == 0):
#Not in a string
if not in_string:
if last_char == "/":
if char == "/":
in_singleline_comment = True
if char == "*":
in_multiline_comment += 1
if char == "\"":
in_string = True
#In a string
else:
if char == "\"": #Only " ends a string, as byond supports multiline strings
if last_char != "\\": #make sure it's a real " not an escaped one (\")
in_string = False
#It's not an embedded statment if it's not in a string
if char == "[":
in_embed_statement += 1
if char == "]":
in_embed_statement -= 1
in_embed_statement = max(in_embed_statement,0)
#ternary statements, True when in_embed_statement+in_string OR when not in_string
if char == "?":
if in_string:
if in_embed_statement != 0:
ternary_on_line = True
else:
ternary_on_line = True
#A Colon!
#If we're in a string, but not embedded: Ok, it's just rawtext
#If we're in a string, and embedded but NOT in a ternary operation: Bad, guaranteed to be a : used to avoid typechecks
#If we're in a string, and embedded AND in a ternary operation: Potentially Bad, this could be a : used to avoid typechecks (bad) or the middle : of the ternary operation (generally ok)
if char == ":":
if not in_string:
colon_count += 1
data = str(line_num)
if ternary_on_line:
data += "?"
if not data in lines_with_colons: #only add the line twice if it's like: 76, 76? (eg: a "bad" colon and a ternary colon)
lines_with_colons.append(data)
else:
if in_embed_statement != 0:
colon_count += 1
data = str(line_num)
if ternary_on_line:
data += "?"
if not data in lines_with_colons:
lines_with_colons.append(data)
#In a comment
else:
if char == "/":
if last_char == "*":
in_multiline_comment -= 1
in_multiline_comment = max(in_multiline_comment,0)
if char == "\n" or char == "\r":
in_singleline_comment = False
if char != "": #Spaces aren't useful to us
last_char = char
if colon_count:
file_report = ".."+scan_dir+str(_file).split(scan_dir)[1]+" " #crop it down to ..\code\DIR\FILE.dm, everything else is developer specific
first = True
for line in lines_with_colons:
if first:
first = False
file_report += "Lines: "+line
else:
file_report += ", "+line
file_report += " Total Colons: "+str(colon_count)
return file_report
else:
return False
colon_scan_dir(real_dir)
print "Done!"

0
tools/CreditsTool/UpdateCreditsDMI.sh Normal file → Executable file
View File

View File

@@ -18,3 +18,13 @@ PraiseRatvar Frozenguy5
FuryMcFlurry Fury McFlurry
vuonojenmustaturska Naksu
praisenarsie Frozenguy5
MrDoomBringer Mr. DoomBringer
Fikou Dr. Fikou
TiviPlus Tivi Plus
tralezab Trale Zab
Iamgoofball goofball
Tharcoonvagh Tharcoon
Rectification itseasytosee
ATHATH ATH1909
trollbreeder troll breeder
BuffEngineering Buff Engineering

View File

@@ -1,3 +0,0 @@
Imaging-1.1.7/
zlib/

View File

@@ -0,0 +1,2 @@
@call "%~dp0\..\bootstrap\python" -m HitboxExpander %*
@pause

View File

@@ -1,7 +1,4 @@
Setup: Unzip the zip files in third_party/ so that you have the directories
third_party/Imaging-1.1.7/ and third_party/zlib/.
Usage: python hitbox_expander.py <path_to_file.dmi or png>
Usage: tools/bootstrap/python -m HitboxExpander <path_to_file.dmi or png>
This tool expands the hitbox of the given image by 1 pixel.
Works by changing some of the fully-transparent pixels to alpha=1 black pixels.

View File

@@ -2,21 +2,10 @@ import os
import sys
import inspect
import shutil
def AddToPath(path):
if path not in sys.path:
sys.path.insert(0, path)
delimeter = ':' if os.name == "posix" else ";"
os.environ['PATH'] = path + delimeter + os.environ['PATH']
import PIL.Image as Image
current_dir = os.path.split(inspect.getfile(inspect.currentframe()))[0]
AddToPath(os.path.abspath(os.path.join(current_dir, "third_party/Imaging-1.1.7/PIL")))
AddToPath(os.path.abspath(os.path.join(current_dir, "third_party/zlib")))
import Image
import _imaging
def PngSave(im, file):
# From http://blog.client9.com/2007/08/28/python-pil-and-png-metadata-take-2.html
@@ -25,13 +14,13 @@ def PngSave(im, file):
reserved = ('interlace', 'gamma', 'dpi', 'transparency', 'aspect')
# undocumented class
import PngImagePlugin
import PIL.PngImagePlugin as PngImagePlugin
meta = PngImagePlugin.PngInfo()
# copy metadata into new object
for k,v in im.info.iteritems():
for k,v in im.info.items():
if k in reserved: continue
meta.add_text(k, v, 0)
meta.add_text(k, str(v), 0)
# and save
im.save(file, "PNG", pnginfo=meta)
@@ -44,7 +33,7 @@ def ProcessFile(path):
try:
im = Image.open(path)
print name + ": " + im.format, im.size, im.mode
print(name + ": " + im.format, im.size, im.mode)
if im.mode != "RGBA":
return
width, height = im.size
@@ -76,15 +65,19 @@ def ProcessFile(path):
pix[coords] = (0, 0, 0, 1)
PngSave(im, path)
except:
print "Could not process " + name
except Exception as e:
print("Could not process " + name)
print(e)
root_dir = os.path.abspath(os.path.join(current_dir, "../../"))
icons_dir = os.path.join(root_dir, "icons")
def Main():
if len(sys.argv) != 2:
print "Usage: hitbox_expander.py filename.dmi"
if os.name == 'nt':
print("Usage: drag-and-drop a .dmi onto `Hitbox Expander.bat`\n or")
with open(os.path.join(current_dir, "README.txt")) as f:
print(f.read())
return 0
try:
@@ -101,7 +94,7 @@ def Main():
ProcessFile(path)
return 0
print "File not found: " + sys.argv[1]
print("File not found: " + sys.argv[1])
if __name__ == "__main__":
Main()

View File

@@ -0,0 +1,3 @@
#!/bin/sh
set -e
exec "$(dirname "$0")/../bootstrap/python" -m HitboxExpander "$@"

Binary file not shown.

View File

@@ -0,0 +1,166 @@
//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"

View File

@@ -1,4 +1,4 @@
// DM Environment file for HumanScissors.dme.
// 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.
@@ -10,9 +10,10 @@
// END_FILE_DIR
// BEGIN_PREFERENCES
#define DEBUG
// END_PREFERENCES
// BEGIN_INCLUDE
#include "HumanScissors.dm"
#include "HubMigrator.dm"
// END_INCLUDE

View File

@@ -1,7 +0,0 @@
This small Byond program takes all the icons in SpritesToSnip.dmi,
cuts them using all the icons in CookieCutter.dmi, and produces a file save
dialog for you to download the resulting DMI.
Useful for cutting up species sprites from full body ones. Or whatever else.
--Arokha/Aronai

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,56 +0,0 @@
/*
These are simple defaults for your project.
*/
world
fps = 25 // 25 frames per second
icon_size = 32 // 32x32 icon size by default
view = 6 // show up to 6 tiles outward from center (13x13 view)
// Make objects move 8 pixels per tick when walking
//usr << ftp(usr.working,"[usr.outfile].dmi")
mob
step_size = 8
obj
step_size = 8
client/verb/split_sprites()
set name = "Begin The Decimation"
set desc = "Loads SpritesToSnip.dmi and cuts them with CookieCutter.dmi"
set category = "Here"
var/icon/SpritesToSnip = icon('SpritesToSnip.dmi')
var/icon/CookieCutter = icon('CookieCutter.dmi')
var/icon/RunningOutput = new ()
//For each original project
for(var/OriginalState in icon_states(SpritesToSnip))
//For each piece we're going to cut
for(var/CutterState in icon_states(CookieCutter))
//The fully assembled icon to cut
var/icon/Original = icon(SpritesToSnip,OriginalState)
//Our cookie cutter sprite
var/icon/Cutter = icon(CookieCutter,CutterState)
//We have to make these all black to cut with
Cutter.Blend(rgb(0,0,0),ICON_MULTIPLY)
//Blend with AND to cut
Original.Blend(Cutter,ICON_AND) //AND, not ADD
//Make a useful name
var/good_name = "[OriginalState]_[CutterState]"
//Add to the output with the good name
RunningOutput.Insert(Original,good_name)
//Give the output
usr << ftp(RunningOutput,"CutUpPeople.dmi")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 839 B

View File

@@ -0,0 +1,74 @@
This is @Cyberboss rage code
The goal is a one stop solution for hosting /tg/station on linux via Docker. Will not work with Docker on Windows.
This requires Docker with the `docker-compose` command to be installed on your system. See ubuntu instructions [here](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository). If you fail to find the `docker-ce` package refer to [this StackOverflow answer](https://unix.stackexchange.com/a/363058).
Some basic configuration options in `docker-compose.yml` before starting:
- Change TGS_ADMIN_CKEY to your ckey so you may have initial control over the server.
- Change TGS_SCHEMA_MAJOR_VERSION to your repo's latest schema major version.
- Change TGS_SCHEMA_MINOR_VERSION to your repo's latest schema minor version.
- If you want to change the MariaDB password, there are three locations in the file it must be changed from its default value of `ChangeThisInBothMariaDBAndTgsConnectionString`.
- Change TGS_BYOND to set the initial BYOND version.
- Ports are mapped in the form `<external>:<internal>` NEVER change the internal port. If you want to prevent a service from being exposed, delete/comment out the entire line.
- The first (3306) is the exposed mariadb port. Do not expose this over the internet without changing the password. In general it's a bad idea.
- The second (1337) is the exposed DreamDaemon port
- The third (5000) is the exposed TGS API port. Do not expose this over the internet. Setup an HTTPS reverse proxy instead.
- Change TGS_REPO to set the repository used. Note, this must be a /tg/ derivative from at least 2019 that implements the latest TGS [DreamMaker API](https://github.com/tgstation/tgstation-server#integrating). Repositories that follow tgstation/tgstation will have this automatically. It also must contain a prefixed SQL schema setup file.
To launch, change to this directory and run `docker-compose up`. The initial setup will take a long time. If that fails, Ctrl+C out, run `docker-compose down`, remove `./TGS_Instances` and `./Database`, and try again. Once setup is complete, you can either leave the terminal running, or `Ctrl+C` out (this will stop DreamDaemon) and run `docker-compose -d` to run it in the background.
What it does:
- Starts mariadb with the data files in `./Database` on port 3306
- Installs and starts Tgs4 (using latest stable docker tag, no HTTPS) on port 5000. Configuration in `./TGS_Config`, logs in `./TGS_Logs`.
- Configures a TGS instance for tgstation in `./TGS_Instances` (SetupProgram)
- The instance is configured to autostart
- Repo is cloned from the origin specified in the `docker-compose.yml`
- BYOND version is set to the latest one specified in the `docker-compose.yml`
- A script will be run to setup dependencies. This does the following every time the game is built:
- Reads dependency information from `dependencies.sh` in the root of the repository
- Installs the following necessary packages into the TGS image
- Rust/cargo
- git
- cmake
- grep
- g++-6
- g++-6-multilib
- mysql-client
- libmariadb-dev:i386
- libssl-dev:i386
- Builds rust-g in `./TGS_Instances/main/Configuration/EventScripts/rust-g` and copies the artifact to the game directory.
- Builds BSQL in `./TGS_Instances/main/Configuration/EventScripts/BSQL` and copies the artifact to the game directory.
- Sets up `./TGS_Instances/main/Configuration/GameStaticFiles/config` with the initial repository config.
- Sets up `./TGS_Instances/main/Configuration/GameStaticFiles/data`.
- If it doesn't exist, create the `ss13_db` database on the mariadb server and populate it with the repository's.
- Start DreamDaemon and configure it to autostart and keep it running via TGS.
- Updates will be pulled from the default repository branch and deployed every hour
What it DOESN'T do:
- Configure sane MariaDB security
- TGS Test merging
- TGS Chat bots
- Handle updating BYOND versions
- Handle updating the database schema
- Manage TGS users, permissions, or change the default admin password
- Provide HTTPS for TGS
- Expose the DB to the internet UNLESS you have port 3306 forwarded for some ungodly reason
- Port forward or setup firewall rules for DreamDaemon or TGS
- Notify you of TGS errors past initial setup
- Keep MariaDB logs
- Backup ANYTHING
- Pretend like it's a long term solution
This is enough to host a production level server !!!IN THEORY!!! This script guarantees nothing and comes with no warranty
You can change the TGS_BYOND and TGS_REPO variables when setting up the first time. But further configuration must be done with TGS itself.
You can connect to TGS with [Tgstation.Server.ControlPanel](https://github.com/tgstation/Tgstation.Server.ControlPanel/releases) (Binaries provided for windows, must be compiled manually on Linux).
- Connect to `http://localhost:5000`. Be sure to `Use Plain HTTP` and `Default Credentials`
You should learn how to manually setup TGS if you truly want control over what your server does.
You have been warned.

View File

@@ -0,0 +1,8 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
# THIS SHOULD NOT BE USED TO CREATE THE PRODUCTION BUILD IT'S FOR DEBUGGING ONLY
FROM mcr.microsoft.com/dotnet/core/sdk:3.1
WORKDIR /app
COPY . .
ENTRYPOINT ["dotnet", "run"]

View File

@@ -0,0 +1,78 @@
#!/bin/bash
# REPO MAINTAINERS: KEEP CHANGES TO THIS IN SYNC WITH /tools/tgs4_scripts/PreCompile.sh
set -e
set -x
#load dep exports
#need to switch to game dir for Dockerfile weirdness
original_dir=$PWD
cd "$1"
. dependencies.sh
cd "$original_dir"
#find out what we have (+e is important for this)
set +e
has_git="$(command -v git)"
has_cargo="$(command -v ~/.cargo/bin/cargo)"
has_sudo="$(command -v sudo)"
has_grep="$(command -v grep)"
DATABASE_EXISTS="$(mysqlshow --host mariadb --port 3306 --user=root --password=$MYSQL_ROOT_PASSWORD ss13_db| grep -v Wildcard | grep -o ss13_db)"
set -e
# install cargo if needful
if ! [ -x "$has_cargo" ]; then
echo "Installing rust..."
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-host i686-unknown-linux-gnu
. ~/.profile
fi
# apt packages, libssl needed by rust-g but not included in TGS barebones install
if ! { [ -x "$has_git" ] && [ -x "$has_grep" ] && [ -f "/usr/lib/i386-linux-gnu/libssl.so" ] && [ -f "/usr/bin/mysql" ] && [ -d "/usr/include/mysql" ]; }; then
echo "Installing apt dependencies..."
if ! [ -x "$has_sudo" ]; then
dpkg --add-architecture i386
apt-get update
apt-get install -y git libssl-dev:i386 grep mysql-client
rm -rf /var/lib/apt/lists/*
else
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install -y git libssl-dev:i386 grep mysql-client
sudo rm -rf /var/lib/apt/lists/*
fi
fi
#update rust-g
if [ ! -d "rust-g" ]; then
echo "Cloning rust-g..."
git clone https://github.com/tgstation/rust-g
else
echo "Fetching rust-g..."
cd rust-g
git fetch
cd ..
fi
echo "Deploying rust-g..."
cd rust-g
git checkout "$RUST_G_VERSION"
~/.cargo/bin/cargo build --release
mv target/release/librust_g.so "$1/rust_g"
cd ..
if [ ! -d "../GameStaticFiles/config" ]; then
echo "Creating initial config..."
cp -r "$1/config" "../GameStaticFiles/config"
echo -e "SQL_ENABLED\nFEEDBACK_TABLEPREFIX SS13_\nADDRESS mariadb\nPORT 3306\nFEEDBACK_DATABASE ss13_db\nFEEDBACK_LOGIN root\nFEEDBACK_PASSWORD $MYSQL_ROOT_PASSWORD\nASYNC_QUERY_TIMEOUT 10\nBLOCKING_QUERY_TIMEOUT 5\nBSQL_THREAD_LIMIT 50" > "../GameStaticFiles/config/dbconfig.txt"
echo "$TGS_ADMIN_CKEY = Host" > "../GameStaticFiles/config/admins.txt"
fi
if [ "$DATABASE_EXISTS" != "ss13_db" ]; then
echo "Creating initial SS13 database..."
mysql -u root --password=$MYSQL_ROOT_PASSWORD -h mariadb -P 3306 -e 'CREATE DATABASE IF NOT EXISTS ss13_db;'
cat "$1/$TGS_PREFIXED_SCHEMA_FILE"
mysql -u root --password=$MYSQL_ROOT_PASSWORD -h mariadb -P 3306 ss13_db < "$1/$TGS_PREFIXED_SCHEMA_FILE"
mysql -u root --password=$MYSQL_ROOT_PASSWORD -h mariadb -P 3306 ss13_db -e "INSERT INTO \`SS13_schema_revision\` (\`major\`, \`minor\`) VALUES ($TGS_SCHEMA_MAJOR_VERSION, $TGS_SCHEMA_MINOR_VERSION)"
fi

View File

@@ -0,0 +1,208 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Client;
using Tgstation.Server.Client.Components;
namespace SetupProgram
{
class Program
{
public static async Task<int> Main()
{
var repo = Environment.GetEnvironmentVariable("TGS_REPO")?.Trim();
if (String.IsNullOrWhiteSpace(repo))
{
Console.WriteLine("ERROR: Environment variable TGS_REPO not set to a git url!");
return 1;
}
var byondStr = Environment.GetEnvironmentVariable("TGS_BYOND")?.Trim();
if (String.IsNullOrWhiteSpace(byondStr) || !Version.TryParse(byondStr, out Version byond) || byond.Build != -1)
{
Console.WriteLine("ERROR: Environment variable TGS_BYOND not set to a valid BYOND version!");
return 2;
}
var clientFactory = new ServerClientFactory(new ProductHeaderValue("LinuxOneShot", "1.0.0"));
IServerClient serverClient = null;
Instance instance = null;
IInstanceClient instanceClient;
void CreateInstanceClient() => instanceClient = serverClient.Instances.CreateClient(instance);
async Task CreateAdminClient()
{
Console.WriteLine("Attempting to reestablish connection to TGS (120s max wait)...");
var giveUpAt = DateTimeOffset.Now.AddSeconds(60);
do
{
try
{
serverClient = await clientFactory.CreateServerClient(new Uri("http://tgs:80"), User.AdminName, User.DefaultAdminPassword, default, default);
if (instance != null)
CreateInstanceClient();
break;
}
catch (HttpRequestException)
{
//migrating, to be expected
if (DateTimeOffset.Now > giveUpAt)
throw;
await Task.Delay(TimeSpan.FromSeconds(1));
}
catch (ServiceUnavailableException)
{
// migrating, to be expected
if (DateTimeOffset.Now > giveUpAt)
throw;
await Task.Delay(TimeSpan.FromSeconds(1));
}
} while (true);
}
async Task WaitForJob(Job originalJob, CancellationToken cancellationToken)
{
var job = originalJob;
int? lastProgress = null;
do
{
try
{
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
job = await instanceClient.Jobs.GetId(job, cancellationToken).ConfigureAwait(false);
if (job.Progress != lastProgress)
{
Console.WriteLine($"Progress: {job.Progress}");
lastProgress = job.Progress;
}
}
catch (UnauthorizedException)
{
await CreateAdminClient();
}
}
while (!job.StoppedAt.HasValue);
if (job.ExceptionDetails != null)
{
Console.WriteLine(job.ExceptionDetails);
Environment.Exit(3);
}
}
await CreateAdminClient();
Console.WriteLine("Listing instances...");
var instances = await serverClient.Instances.List(default);
if (instances.Any())
{
Console.WriteLine("One or more instances already exist, aborting!");
return 3;
}
Console.WriteLine("Creating instance...");
instance = await serverClient.Instances.CreateOrAttach(new Instance
{
ConfigurationType = ConfigurationType.HostWrite,
Name = "AutoInstance",
Path = "/tgs4_instances/main"
}, default);
Console.WriteLine("Onlining instance...");
instance.Online = true;
instance = await serverClient.Instances.Update(instance, default);
CreateInstanceClient();
Console.WriteLine("Starting repo clone...");
var cloneJobTask = instanceClient.Repository.Clone(new Repository
{
Origin = repo
}, default);
Console.WriteLine($"Starting BYOND install {byond}...");
var byondInstallTask = instanceClient.Byond.SetActiveVersion(new Byond
{
Version = byond
}, default);
Console.WriteLine("Setting DD Settings to Ultrasafe|Startup Timeout=120|AutoStart=true|HeartbeatSeconds=120...");
var ddUpdateTask = instanceClient.DreamDaemon.Update(new DreamDaemon
{
AutoStart = true,
SecurityLevel = DreamDaemonSecurity.Ultrasafe,
HeartbeatSeconds = 120,
StartupTimeout = 120
}, default);
Console.WriteLine("Setting API validation security level to trusted...");
var dmUpdateTask = instanceClient.DreamMaker.Update(new DreamMaker
{
ApiValidationSecurityLevel = DreamDaemonSecurity.Trusted
}, default);
Console.WriteLine("Uploading EventScripts/PreCompile.sh...");
var configurationTask = instanceClient.Configuration.Write(new ConfigurationFile
{
Path = "/EventScripts/PreCompile.sh",
Content = File.ReadAllBytes("PreCompile.sh")
}, default);
Console.WriteLine("Creating GameStaticFiles/data...");
var configTask2 = instanceClient.Configuration.CreateDirectory(new ConfigurationFile
{
IsDirectory = true,
Path = "/GameStaticFiles/data"
}, default);
Console.WriteLine("Waiting for previous requests...");
await Task.WhenAll(
cloneJobTask,
byondInstallTask,
ddUpdateTask,
dmUpdateTask,
configurationTask,
configTask2);
Console.WriteLine("Waiting for BYOND install...");
var installJob = await byondInstallTask;
await WaitForJob(installJob.InstallJob, default);
Console.WriteLine("Waiting for Repo clone...");
var cloneJob = await cloneJobTask;
await WaitForJob(cloneJob.ActiveJob, default);
await CreateAdminClient();
Console.WriteLine("Starting deployment...");
var deployJobTask = instanceClient.DreamMaker.Compile(default);
Console.WriteLine("Enabling auto updates every hour...");
instance.AutoUpdateInterval = 60;
await serverClient.Instances.Update(instance, default);
Console.WriteLine("Waiting for deployment job...");
var deployJob = await deployJobTask;
await WaitForJob(deployJob, default);
await CreateAdminClient();
Console.WriteLine("Launching watchdog...");
var launchJob = await instanceClient.DreamDaemon.Start(default);
await WaitForJob(launchJob, default);
Console.WriteLine("Complete!");
return 0;
}
}
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Tgstation.Server.Client" Version="6.1.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,29 @@
{
"Database": {
"DatabaseType": "MariaDB",
"ResetAdminPassword": false
},
"General": {
"MinimumPasswordLength": 15,
"GitHubAccessToken": "",
"ByondTopicTimeout": 5000,
"RestartTimeout": 10000,
"UseExperimentalWatchdog": false
},
"FileLogging": {
"Directory": "/tgs_logs",
"LogLevel": "Debug"
},
"ControlPanel": {
"Enable": true,
"AllowAnyOrigin": false,
"AllowedOrigins": null
},
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://0.0.0.0:80"
}
}
}
}

View File

@@ -0,0 +1,49 @@
version: '3.7'
services:
mariadb:
image: mariadb
restart: always
ports:
- "3306:3306"
volumes:
- "./Database:/var/lib/mysql"
logging:
driver: none
environment:
MYSQL_INITDB_SKIP_TZINFO: 1
MYSQL_ROOT_PASSWORD: ChangeThisInBothMariaDBAndTgsConnectionString
tgs:
environment:
MYSQL_ROOT_PASSWORD: ChangeThisInBothMariaDBAndTgsConnectionString
Database__ConnectionString: "Password=ChangeThisInBothMariaDBAndTgsConnectionString;Server=mariadb;User Id=root;Database=tgs4"
TGS_ADMIN_CKEY: <YOUR BYOND USERNAME HERE>
TGS_PREFIXED_SCHEMA_FILE: SQL/tgstation_schema_prefixed.sql
TGS_SCHEMA_MAJOR_VERSION: 5
TGS_SCHEMA_MINOR_VERSION: 9
cap_add:
- SYS_NICE
image: "tgstation/server:latest"
depends_on:
- mariadb
ports:
- "1337:1337"
- "5000:80"
restart: always
init: true
volumes:
- "./TGS_Logs:/tgs_logs"
- "./TGS_Config:/config_data"
- "./TGS_Instances:/tgs4_instances"
logging:
driver: none
setup:
environment:
TGS_BYOND: 513.1514
TGS_REPO: https://github.com/tgstation/tgstation
build:
context: ./SetupProgram
dockerfile: Dockerfile
depends_on:
- tgs
- mariadb
restart: "no"

View File

@@ -1,20 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 11.00
# Visual Studio 2010
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapAtmosFixer", "MapAtmosFixer\MapAtmosFixer.csproj", "{7A901E97-C798-4B17-A9A9-5548C6DCDB56}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x86 = Debug|x86
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7A901E97-C798-4B17-A9A9-5548C6DCDB56}.Debug|x86.ActiveCfg = Debug|x86
{7A901E97-C798-4B17-A9A9-5548C6DCDB56}.Debug|x86.Build.0 = Debug|x86
{7A901E97-C798-4B17-A9A9-5548C6DCDB56}.Release|x86.ActiveCfg = Release|x86
{7A901E97-C798-4B17-A9A9-5548C6DCDB56}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProductVersion>8.0.30703</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{7A901E97-C798-4B17-A9A9-5548C6DCDB56}</ProjectGuid>
<OutputType>Exe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>MapAtmosFixer</RootNamespace>
<AssemblyName>MapAtmosFixer</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<TargetFrameworkProfile>Client</TargetFrameworkProfile>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<PlatformTarget>x86</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<PlatformTarget>x86</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Mapatmosfixer.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -1,371 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
namespace MapAtmosFixer
{
internal enum Objtype
{
Null,
Manifold,
Pump,
Pipe,
Scrubber,
Vent,
Mixer,
Filter,
Injector,
Temp
}
internal static class Mapatmosfixer
{
/// <summary>
/// Used to make displayed lines shorter.
/// </summary>
/// <param name="path">The long path</param>
/// <param name="masterpath">The path to short with</param>
/// <returns>Shorted path</returns>
public static string ShortenPath(string path, ref string masterpath)
{
return path.Substring(masterpath.Length, path.Length - masterpath.Length);
}
/// <summary>
/// Returns true if 'str' starts with 'start'
/// </summary>
/// <param name="str">String to test</param>
/// <param name="start"></param>
/// <returns></returns>
public static bool StartsWith(string str, string start)
{
if (start.Length > str.Length)
return false;
return str.Substring(0, start.Length) == start;
}
/// <summary>
/// Method to perform a simple regex match.
/// </summary>
/// <param name="pattern">Regex pattern</param>
/// <param name="txt">String to match</param>
/// <returns>Collection of matches</returns>
public static GroupCollection Regex(string pattern, string txt)
{
var rgx = new Regex(pattern, RegexOptions.IgnoreCase);
MatchCollection matches = rgx.Matches(txt);
if (matches.Count == 0)
return null;
GroupCollection groups = matches[0].Groups;
if (groups.Count <= 1)
return null;
return groups;
}
/// <summary>
/// Gets the type of the object in the path
/// </summary>
/// <param name="path">String path</param>
/// <returns></returns>
public static Objtype GetType(string path)
{
if (StartsWith(path, "/obj/machinery/atmospherics/pipe/manifold"))
return Objtype.Manifold;
if (StartsWith(path, "/obj/machinery/atmospherics/pipe/simple"))
return Objtype.Pipe;
switch (path)
{
case "/obj/machinery/atmospherics/binary/pump":
return Objtype.Pump;
case "/obj/machinery/atmospherics/unary/vent_scrubber":
return Objtype.Scrubber;
case "/obj/machinery/atmospherics/unary/vent_pump":
return Objtype.Vent;
case "/obj/machinery/atmospherics/trinary/filter":
return Objtype.Filter;
case "/obj/machinery/atmospherics/trinary/mixer":
return Objtype.Mixer;
case "/obj/machinery/atmospherics/unary/heat_reservoir/heater":
return Objtype.Temp;
case "/obj/machinery/atmospherics/unary/cold_sink/freezer":
return Objtype.Temp;
case "/obj/machinery/atmospherics/unary/outlet_injector":
return Objtype.Injector;
default:
return Objtype.Null;
}
}
/// <summary>
/// Updates an objects path with its corresponding icon_state
/// </summary>
/// <param name="path">Object path to change</param>
/// <param name="iconstate">Iconstate it has</param>
/// <param name="objtype">Type of the object</param>
public static void ProcessIconstate(ref string path, string iconstate, Objtype objtype)
{
switch (objtype)
{
case Objtype.Pipe:
switch (iconstate)
{
case "intact":
path = "/obj/machinery/atmospherics/pipe/simple/general/visible";
return;
case "intact-f":
path = "/obj/machinery/atmospherics/pipe/simple/general/hidden";
return;
case "intact-b":
path = "/obj/machinery/atmospherics/pipe/simple/supply/visible";
return;
case "intact-b-f":
path = "/obj/machinery/atmospherics/pipe/simple/supply/hidden";
return;
case "intact-r":
path = "/obj/machinery/atmospherics/pipe/simple/scrubbers/visible";
return;
case "intact-r-f":
path = "/obj/machinery/atmospherics/pipe/simple/scrubbers/hidden";
return;
case "intact-y":
path = "/obj/machinery/atmospherics/pipe/simple/yellow/visible";
return;
case "intact-y-f":
path = "/obj/machinery/atmospherics/pipe/simple/yellow/hidden";
return;
case "intact-g":
path = "/obj/machinery/atmospherics/pipe/simple/green/visible";
return;
case "intact-g-f":
path = "/obj/machinery/atmospherics/pipe/simple/green/hidden";
return;
case "intact-c":
path = "/obj/machinery/atmospherics/pipe/simple/cyan/visible";
return;
case "intact-c-f":
path = "/obj/machinery/atmospherics/pipe/simple/cyan/hidden";
return;
case "intact-p":
path = "/obj/machinery/atmospherics/pipe/simple/supplymain/visible";
return;
case "intact-p-f":
path = "/obj/machinery/atmospherics/pipe/simple/supplymain/hidden";
return;
}
return;
case Objtype.Manifold:
switch (iconstate)
{
case "manifold":
path = "/obj/machinery/atmospherics/pipe/manifold/general/visible";
return;
case "manifold-f":
path = "/obj/machinery/atmospherics/pipe/manifold/general/hidden";
return;
case "manifold-b":
path = "/obj/machinery/atmospherics/pipe/manifold/supply/visible";
return;
case "manifold-b-f":
path = "/obj/machinery/atmospherics/pipe/manifold/supply/hidden";
return;
case "manifold-r":
path = "/obj/machinery/atmospherics/pipe/manifold/scrubbers/visible";
return;
case "manifold-r-f":
path = "/obj/machinery/atmospherics/pipe/manifold/scrubbers/hidden";
return;
case "manifold-c":
path = "/obj/machinery/atmospherics/pipe/manifold/cyan/visible";
return;
case "manifold-c-f":
path = "/obj/machinery/atmospherics/pipe/manifold/cyan/hidden";
return;
case "manifold-y":
path = "/obj/machinery/atmospherics/pipe/manifold/yellow/visible";
return;
case "manifold-y-f":
path = "/obj/machinery/atmospherics/pipe/manifold/yellow/hidden";
return;
case "manifold-g":
path = "/obj/machinery/atmospherics/pipe/manifold/green/visible";
return;
case "manifold-g-f":
path = "/obj/machinery/atmospherics/pipe/manifold/green/hidden";
return;
case "manifold-p":
path = "/obj/machinery/atmospherics/pipe/manifold/supplymain/visible";
return;
case "manifold-p-f":
path = "/obj/machinery/atmospherics/pipe/manifold/supplymain/hidden";
return;
}
return;
}
}
/// <summary>
/// Processes one object and its parameters
/// </summary>
/// <param name="line"></param>
public static void ProcessObject(ref string line)
{
GroupCollection g = Regex(@"^(.+)\{(.+)\}", line);
if (g == null)
return;
string path = g[1].Value;
string stringtags = g[2].Value;
Objtype objtype = GetType(path);
if (objtype == Objtype.Null)
return;
var tags = new List<string>(stringtags.Split(new[] {"; "}, StringSplitOptions.None));
for (int i = 0; i < tags.Count; i++)
{
string tag = tags[i];
GroupCollection g2 = Regex(@"^(.+)[ ]=[ ](.+)", tag);
if (g2 == null)
continue;
string name = g2[1].Value;
string value = g2[2].Value.Trim(new[] {'"'});
//Removes icon_state from heaters/freezers
if (objtype == Objtype.Temp)
{
if (name == "icon_state")
{
tags.RemoveAt(i);
i--;
}
continue;
}
//General removal of tags we shouldn't have
if (name == "pipe_color" || name == "color" || name == "level" ||
(objtype != Objtype.Pump && name == "name")
|| (objtype == Objtype.Pump && name == "icon_state"))
{
tags.RemoveAt(i);
i--;
continue;
}
//Processes icon_state into correct path
if (name == "icon_state")
{
ProcessIconstate(ref path, value, objtype);
tags.RemoveAt(i);
i--;
continue;
}
//Fixes up injector
if (objtype == Objtype.Injector && name == "on")
{
path = "/obj/machinery/atmospherics/unary/outlet_injector/on";
tags.RemoveAt(i);
i--;
}
}
stringtags = String.Join("; ", tags);
line = String.Format("{0}{{{1}}}", path, stringtags);
}
/// <summary>
/// This fixes connectors to their proper path, if they ain't on plating, they should be visible.
/// </summary>
/// <param name="line"></param>
public static void FixConnector(ref string line)
{
//Dirty shit, don't read this
if (line.Contains("/obj/machinery/atmospherics/portables_connector") &&
//!line.Contains("/turf/open/floor/plating") && // Most of the time connectors on plating want to be visible..
!line.Contains("/obj/machinery/atmospherics/portables_connector/visible")) // Makes sure we don't update same line twice
{
line = line.Replace("/obj/machinery/atmospherics/portables_connector",
"/obj/machinery/atmospherics/portables_connector/visible");
}
}
/// <summary>
/// Processes a line of objects
/// </summary>
/// <param name="line"></param>
public static void ProcessObjectline(ref string line)
{
FixConnector(ref line);
string[] objs = line.Split(',');
for (int i = 0; i < objs.Length; i++)
{
try
{
ProcessObject(ref objs[i]);
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
line = String.Join(",", objs);
}
/// <summary>
/// Processes a whole .dmm
/// </summary>
/// <param name="file"></param>
public static void Process(string file)
{
string[] lines = File.ReadAllLines(file, Encoding.Default);
for (int i = 0; i < lines.Length; i++)
{
string line = lines[i];
if (line.Length == 0)
continue;
if (line[0] != '"')
continue;
GroupCollection g = Regex(@"^""([\w]+)""[ ]\=[ ]\((.+)\)", line);
if (g == null)
continue;
string letters = g[1].Value;
string types = g[2].Value;
ProcessObjectline(ref types);
line = String.Format("\"{0}\" = ({1})", letters, types);
lines[i] = line;
}
File.WriteAllLines(file, lines, Encoding.Default);
}
internal static void Init(string file)
{
//string exepath = "C:\\Users\\Daniel\\Documents\\GitHub\\-tg-station";
//string file = "C:\\Users\\Daniel\\Documents\\GitHub\\-tg-station\\_maps\\map_files\\tgstation.2.1.3.dmm";
Process(file);
Console.WriteLine("Done");
Console.Read();
}
}
}

View File

@@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace MapAtmosFixer
{
class Program
{
static void Main(string[] args)
{
if (args.Length < 1)
{
Console.WriteLine("Please drag-drop file onto the .exe");
Console.Read();
return;
}
string file = args[0];
if (Path.GetExtension(file) != ".dmm")
{
Console.WriteLine("File not a map.");
Console.Read();
return;
}
Mapatmosfixer.Init(file);
}
}
}

View File

@@ -1,36 +0,0 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MapAtmosFixer")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("MapAtmosFixer")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("87bc6d96-c3ae-4644-ac20-033d8ec401e5")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@@ -1,9 +0,0 @@
A handy tool used to clean up all shit mappers leave behind when doing atmosia code.
Basically it removes most edited variables and instead let the code and object do the work, less hacky shit and everything can be edited in-code without having to edit the map.
How to use:
Drag-n-drop an .dmm onto the compiled .exe
The program will run and then overwrite the same .dmm
Mapmerge tool is not necessary to be run with this.

View File

@@ -60,7 +60,7 @@ var/admin_substr = "admins=" // search for this to locate # of admins
src << link(serverlink)
/proc/extract(var/data, var/type = PLAYERS)
/proc/extract(data, type = PLAYERS)
var/nextpos = 0

View File

@@ -1,153 +1,142 @@
/*
Written by contributor Doohl for the /tg/station Open Source project, hosted on Google Code.
(2012)
NOTE: The below functions are part of BYOND user Deadron's "TextHandling" library.
[ http://www.byond.com/developer/Deadron/TextHandling ]
* Written by contributor Doohl for the /tg/station Open Source project, hosted on Google Code.
* (2012)
*
* NOTE: The below functions are part of BYOND user Deadron's "TextHandling" library.
* [ http://www.byond.com/developer/Deadron/TextHandling ]
*/
proc
///////////////////
// Reading files //
///////////////////
dd_file2list(file_path, separator = "\n")
var/file
if (isfile(file_path))
file = file_path
else
file = file(file_path)
return dd_text2list(file2text(file), separator)
/// Reading files
/proc/dd_file2list(file_path, separator = "\n")
var/file
if (isfile(file_path))
file = file_path
else
file = file(file_path)
return dd_text2list(file2text(file), separator)
////////////////////
// Replacing text //
////////////////////
dd_replacetext(text, search_string, replacement_string)
/// Replacing text
/proc/dd_replacetext(text, search_string, replacement_string)
// A nice way to do this is to split the text into an array based on the search_string,
// then put it back together into text using replacement_string as the new separator.
var/list/textList = dd_text2list(text, search_string)
return dd_list2text(textList, replacement_string)
dd_replaceText(text, search_string, replacement_string)
var/list/textList = dd_text2List(text, search_string)
return dd_list2text(textList, replacement_string)
/proc/dd_replaceText(text, search_string, replacement_string)
var/list/textList = dd_text2List(text, search_string)
return dd_list2text(textList, replacement_string)
/////////////////////
// Prefix checking //
/////////////////////
dd_hasprefix(text, prefix)
var/start = 1
var/end = length(prefix) + 1
return findtext(text, prefix, start, end)
///Prefix checking
/proc/dd_hasprefix(text, prefix)
var/start = 1
var/end = lentext(prefix) + 1
return findtext(text, prefix, start, end)
dd_hasPrefix(text, prefix)
var/start = 1
var/end = length(prefix) + 1
return findtextEx(text, prefix, start, end)
/proc/dd_hasPrefix(text, prefix)
var/start = 1
var/end = lentext(prefix) + 1
return findtextEx(text, prefix, start, end)
/////////////////////
// Suffix checking //
/////////////////////
dd_hassuffix(text, suffix)
var/start = length(text) - length(suffix)
if (start)
return findtext(text, suffix, start)
///Suffix checking
/proc/dd_hassuffix(text, suffix)
var/start = length(text) - length(suffix)
if (start)
return findtext(text, suffix, start)
dd_hasSuffix(text, suffix)
var/start = length(text) - length(suffix)
if (start)
return findtextEx(text, suffix, start)
/proc/dd_hasSuffix(text, suffix)
var/start = length(text) - length(suffix)
if (start)
return findtextEx(text, suffix, start)
/////////////////////////////
// Turning text into lists //
/////////////////////////////
dd_text2list(text, separator)
var/textlength = length(text)
var/separatorlength = length(separator)
var/list/textList = new /list()
var/searchPosition = 1
var/findPosition = 1
var/buggyText
while (1) // Loop forever.
findPosition = findtext(text, separator, searchPosition, 0)
buggyText = copytext(text, searchPosition, findPosition) // Everything from searchPosition to findPosition goes into a list element.
textList += "[buggyText]" // Working around weird problem where "text" != "text" after this copytext().
/// Turning text into lists
/proc/dd_text2list(text, separator)
var/textlength = lentext(text)
var/separatorlength = lentext(separator)
var/list/textList = new /list()
var/searchPosition = 1
var/findPosition = 1
var/buggyText
while (1) // Loop forever.
findPosition = findtext(text, separator, searchPosition, 0)
buggyText = copytext(text, searchPosition, findPosition) // Everything from searchPosition to findPosition goes into a list element.
textList += "[buggyText]" // Working around weird problem where "text" != "text" after this copytext().
searchPosition = findPosition + separatorlength // Skip over separator.
if (findPosition == 0) // Didn't find anything at end of string so stop here.
return textList
else
if (searchPosition > textlength) // Found separator at very end of string.
textList += "" // So add empty element.
return textList
dd_text2List(text, separator)
var/textlength = length(text)
var/separatorlength = length(separator)
var/list/textList = new /list()
var/searchPosition = 1
var/findPosition = 1
var/buggyText
while (1) // Loop forever.
findPosition = findtextEx(text, separator, searchPosition, 0)
buggyText = copytext(text, searchPosition, findPosition) // Everything from searchPosition to findPosition goes into a list element.
textList += "[buggyText]" // Working around weird problem where "text" != "text" after this copytext().
searchPosition = findPosition + separatorlength // Skip over separator.
if (findPosition == 0) // Didn't find anything at end of string so stop here.
return textList
else
if (searchPosition > textlength) // Found separator at very end of string.
textList += "" // So add empty element.
return textList
dd_list2text(list/the_list, separator)
var/total = the_list.len
if (total == 0) // Nothing to work with.
return
var/newText = "[the_list[1]]" // Treats any object/number as text also.
var/count
for (count = 2, count <= total, count++)
if (separator)
newText += separator
newText += "[the_list[count]]"
return newText
dd_centertext(message, length)
var/new_message = message
var/size = length(message)
if (size == length)
return new_message
if (size > length)
return copytext(new_message, 1, length + 1)
// Need to pad text to center it.
var/delta = length - size
if (delta == 1)
// Add one space after it.
return new_message + " "
// Is this an odd number? If so, add extra space to front.
if (delta % 2)
new_message = " " + new_message
delta--
// Divide delta in 2, add those spaces to both ends.
delta = delta / 2
var/spaces = ""
for (var/count = 1, count <= delta, count++)
spaces += " "
return spaces + new_message + spaces
dd_limittext(message, length)
// Truncates text to limit if necessary.
var/size = length(message)
if (size <= length)
return message
searchPosition = findPosition + separatorlength // Skip over separator.
if (findPosition == 0) // Didn't find anything at end of string so stop here.
return textList
else
return copytext(message, 1, length + 1)
if (searchPosition > textlength) // Found separator at very end of string.
textList += "" // So add empty element.
return textList
/proc/dd_text2List(text, separator)
var/textlength = lentext(text)
var/separatorlength = lentext(separator)
var/list/textList = new /list()
var/searchPosition = 1
var/findPosition = 1
var/buggyText
while (1) // Loop forever.
findPosition = findtextEx(text, separator, searchPosition, 0)
buggyText = copytext(text, searchPosition, findPosition) // Everything from searchPosition to findPosition goes into a list element.
textList += "[buggyText]" // Working around weird problem where "text" != "text" after this copytext().
searchPosition = findPosition + separatorlength // Skip over separator.
if (findPosition == 0) // Didn't find anything at end of string so stop here.
return textList
else
if (searchPosition > textlength) // Found separator at very end of string.
textList += "" // So add empty element.
return textList
/proc/dd_list2text(list/the_list, separator)
var/total = the_list.len
if (total == 0) // Nothing to work with.
return
var/newText = "[the_list[1]]" // Treats any object/number as text also.
var/count
for (count = 2, count <= total, count++)
if (separator)
newText += separator
newText += "[the_list[count]]"
return newText
/proc/dd_centertext(message, length)
var/new_message = message
var/size = length(message)
if (size == length)
return new_message
if (size > length)
return copytext(new_message, 1, length + 1)
// Need to pad text to center it.
var/delta = length - size
if (delta == 1)
// Add one space after it.
return new_message + " "
// Is this an odd number? If so, add extra space to front.
if (delta % 2)
new_message = " " + new_message
delta--
// Divide delta in 2, add those spaces to both ends.
delta = delta / 2
var/spaces = ""
for (var/count = 1, count <= delta, count++)
spaces += " "
return spaces + new_message + spaces
/proc/dd_limittext(message, length)
// Truncates text to limit if necessary.
var/size = length(message)
if (size <= length)
return message
else
return copytext(message, 1, length + 1)

File diff suppressed because it is too large Load Diff

View File

@@ -117,14 +117,22 @@ inline void forward_progress(FILE * inputFile) {
delete(lastLine);
lastLine = currentLine;
currentLine = nextLine;
nextLine = readline(inputFile);
//strip out any timestamps.
if (nextLine->length() >= 10) {
if ((*nextLine)[0] == '[' && (*nextLine)[3] == ':' && (*nextLine)[6] == ':' && (*nextLine)[9] == ']')
nextLine->erase(0, 10);
else if (nextLine->length() >= 26 && ((*nextLine)[0] == '[' && (*nextLine)[5] == '-' && (*nextLine)[14] == ':' && (*nextLine)[20] == '.' && (*nextLine)[24] == ']'))
nextLine->erase(0, 26);
}
do {
nextLine = readline(inputFile);
//strip out rustg continuing line markers
if (safe_substr(nextLine, 0, 3) == " - ") {
nextLine->erase(0, 3);
}
//strip out any timestamps.
if (nextLine->length() >= 10) {
if ((*nextLine)[0] == '[' && (*nextLine)[3] == ':' && (*nextLine)[6] == ':' && (*nextLine)[9] == ']')
nextLine->erase(0, 10);
else if (nextLine->length() >= 26 && ((*nextLine)[0] == '[' && (*nextLine)[5] == '-' && (*nextLine)[14] == ':' && (*nextLine)[20] == '.' && (*nextLine)[24] == ']'))
nextLine->erase(0, 26);
}
} while (!endofbuffer && nextLine->length() < 1);
}
//deallocates to, copys from to to.
inline void string_send(string * &from, string * &to) {

View File

@@ -1,438 +0,0 @@
Note: The source file, src and usr are all from the FIRST of the identical runtimes. Everything else is cropped.
Total unique runtimes: 28
Total runtimes: 124723
Total unique hard deletions: 108
Total hard deletions: 1080
** Runtimes **
The following runtime has occurred 123121 time(s).
runtime error: Component is missing a pipenet! Rebuilding...
The following runtime has occurred 709 time(s).
runtime error: wrong type of value for list
proc name: updateghostimages (/mob/dead/observer/proc/updateghostimages)
source file: observer.dm,488
usr: (src)
src: Argon IXV (/mob/dead/observer)
src.loc: the asteroid sand (217,137,8) (/turf/open/floor/plating/asteroid)
The following runtime has occurred 696 time(s).
runtime error: Cannot read null.air
proc name: return air (/obj/machinery/atmospherics/pipe/return_air)
source file: pipes.dm,58
usr: null
src: the air supply pipe (/obj/machinery/atmospherics/pipe/manifold4w/supply/visible)
src.loc: the floor (169,130,1) (/turf/open/floor/plasteel)
The following runtime has occurred 48 time(s).
runtime error: bad resource file
proc name: send asset (/proc/send_asset)
source file: asset_cache.dm,44
usr: Mariabella Pycroft-Crane (/mob/living/carbon/human)
src: null
The following runtime has occurred 30 time(s).
runtime error: list index out of bounds
proc name: GetGreaterChild (/Heap/proc/GetGreaterChild)
source file: heap.dm,62
usr: null
src: /Heap (/Heap)
The following runtime has occurred 21 time(s).
runtime error: undefined proc or verb /turf/closed/wall/shuttle/high pressure movements().
The following runtime has occurred 15 time(s).
runtime error: Simple animal being instantiated in nullspace
proc name: stack trace (/proc/stack_trace)
source file: unsorted.dm,1358
usr: the Securitron (/mob/living/simple_animal/bot/secbot)
src: null
The following runtime has occurred 12 time(s).
runtime error: Cannot read null.viewavail
proc name: record (/obj/machinery/computer/monitor/proc/record)
source file: monitor.dm,41
usr: null
src: Engineering Power Monitoring C... (/obj/machinery/computer/monitor)
src.loc: space (154,151,1) (/turf/open/space)
The following runtime has occurred 11 time(s).
runtime error: Cannot read null.selection_color
proc name: SetChoices (/datum/preferences/proc/SetChoices)
source file: preferences.dm,543
usr: Sweetestbro (/mob/new_player)
src: /datum/preferences (/datum/preferences)
The following runtime has occurred 10 time(s).
runtime error: Cannot execute null.add turf().
proc name: process cell (/turf/open/process_cell)
source file: LINDA_turf_tile.dm,172
usr: null
src: the plating (102,135,1) (/turf/open/floor/plating)
The following runtime has occurred 9 time(s).
runtime error: Cannot read null.thrownby
proc name: hitby (/mob/living/carbon/human/hitby)
source file: human_defense.dm,353
usr: the monkey (662) (/mob/living/carbon/monkey)
src: Sydney Hujsak (/mob/living/carbon/human)
The following runtime has occurred 8 time(s).
runtime error: Cannot execute null.HasProximity().
proc name: Entered (/turf/Entered)
source file: turf.dm,95
usr: 0
src: the floor (99,147,1) (/turf/open/floor/plasteel)
The following runtime has occurred 5 time(s).
runtime error: bad client
proc name: open (/datum/browser/proc/open)
source file: browser.dm,106
usr: Timothy Catleay (/mob/living/carbon/human)
src: /datum/browser (/datum/browser)
The following runtime has occurred 4 time(s).
runtime error: return_file_text(): File not found
The following runtime has occurred 4 time(s).
runtime error: Cannot execute null.merge().
proc name: assume air (/turf/open/assume_air)
source file: LINDA_turf_tile.dm,56
usr: null
src: the engraved floor (95,49,5) (/turf/open/floor/engine/cult)
The following runtime has occurred 3 time(s).
runtime error: Cannot read null.gases
proc name: tile graphic (/turf/open/proc/tile_graphic)
source file: LINDA_turf_tile.dm,110
usr: null
src: the floor (81,52,5) (/turf/open/floor/plasteel/freezer)
The following runtime has occurred 3 time(s).
runtime error: undefined proc or verb /turf/closed/wall/high pressure movements().
The following runtime has occurred 2 time(s).
runtime error: Cannot modify null.loc.
proc name: ui act (/obj/machinery/suit_storage_unit/ui_act)
source file: suit_storage_unit.dm,359
usr: Cy Carter (/mob/living/carbon/human)
src: the suit storage unit (/obj/machinery/suit_storage_unit/standard_unit)
The following runtime has occurred 2 time(s).
runtime error: Cannot execute null.remove().
proc name: remove air (/turf/open/remove_air)
source file: LINDA_turf_tile.dm,62
usr: null
src: the engraved floor (171,210,5) (/turf/open/floor/engine/cult)
The following runtime has occurred 2 time(s).
runtime error: Cannot execute null.return pressure().
proc name: update pressure (/obj/mecha/working/ripley/proc/update_pressure)
source file: ripley.dm,150
usr: 0
src: the APLU \"Ripley\" (/obj/mecha/working/ripley)
src.loc: the floor (88,45,5) (/turf/open/floor/plasteel/freezer)
The following runtime has occurred 1 time(s).
runtime error: list index out of bounds
proc name: Topic (/datum/song/Topic)
source file: musician.dm,208
usr: Francisco Luque (/mob/living/carbon/human)
src: Untitled (/datum/song/handheld)
The following runtime has occurred 1 time(s).
runtime error: undefined proc or verb /turf/closed/wall/r_wall/high pressure movements().
The following runtime has occurred 1 time(s).
runtime error: bad client
proc name: show (/datum/html_interface/proc/show)
source file: html_interface.dm,227
usr: Mariabella Pycroft-Crane (/mob/living/carbon/human)
src: /datum/html_interface/nanotras... (/datum/html_interface/nanotrasen)
The following runtime has occurred 1 time(s).
runtime error: Cannot execute null.GetAtmosAdjacentTurfs().
proc name: spread smoke (/obj/effect/particle_effect/smoke/proc/spread_smoke)
source file: effects_smoke.dm,74
usr: null
src: the smoke (/obj/effect/particle_effect/smoke)
src.loc: null
The following runtime has occurred 1 time(s).
runtime error: Cannot read null.pipe_vision_img
proc name: add ventcrawl (/mob/living/proc/add_ventcrawl)
source file: ventcrawling.dm,94
usr: (src)
src: the monkey (809) (/mob/living/carbon/monkey)
src.loc: the Cloning Lab vent pump #1 (/obj/machinery/atmospherics/components/unary/vent_pump)
The following runtime has occurred 1 time(s).
runtime error: Cannot execute null.s click().
proc name: Click (/obj/screen/grab/Click)
source file: screen_objects.dm,167
usr: Hisstian Weston Chandler (/mob/living/carbon/human)
src: the disarm/kill (/obj/screen/grab)
The following runtime has occurred 1 time(s).
runtime error: list index out of bounds
proc name: mod list add (/client/proc/mod_list_add)
source file: modifyvariables.dm,161
usr: the potted plant (/mob/dead/observer)
src: MimicFaux (/client)
The following runtime has occurred 1 time(s).
runtime error: Cannot execute null.get reagent amount().
proc name: get fuel (/obj/item/weapon/weldingtool/proc/get_fuel)
source file: tools.dm,334
usr: null
src: the welding tool (/obj/item/weapon/weldingtool)
src.loc: null
** Hard deletions **
/obj/structure/lattice - 499 time(s).
/mob/new_player - 91 time(s).
/image - 87 time(s).
/mob/dead/observer - 42 time(s).
/datum/mind - 36 time(s).
/obj/item/radio/integrated/signal - 31 time(s).
/obj/effect/ebeam - 24 time(s).
/obj/machinery/camera - 15 time(s).
/obj/machinery/atmospherics/components/binary/pump - 14 time(s).
/obj/effect/decal/cleanable/trail_holder - 10 time(s).
/obj/screen/buildmode/bdir - 10 time(s).
/obj/screen/buildmode/help - 10 time(s).
/obj/screen/buildmode/mode - 10 time(s).
/obj/screen/buildmode/quit - 10 time(s).
/obj/effect/landmark/start - 8 time(s).
/obj/machinery/status_display - 7 time(s).
/obj/item/stack/sheet/cardboard - 7 time(s).
/datum/gas_mixture - 6 time(s).
/obj/machinery/requests_console - 6 time(s).
/obj/machinery/airalarm - 5 time(s).
/obj/machinery/atmospherics/components/unary/vent_pump - 5 time(s).
/obj/item/organ/hivelord_core/legion - 5 time(s).
/obj/item/weapon/electronics/airlock - 5 time(s).
/obj/effect/mist - 5 time(s).
/obj/machinery/camera/portable - 5 time(s).
/obj/machinery/porta_turret - 4 time(s).
/obj/machinery/atmospherics/components/unary/portables_connector/visible - 4 time(s).
/obj/effect/decal/cleanable/blood/gibs - 4 time(s).
/obj/machinery/atmospherics/components/unary/vent_scrubber - 4 time(s).
/obj/item/weapon/tank/internals/oxygen - 3 time(s).
/obj/item/device/assembly/signaler - 3 time(s).
/datum/event - 3 time(s).
/obj/effect/decal/cleanable/blood/footprints - 3 time(s).
/obj/machinery/camera/emp_proof - 3 time(s).
/obj/item/weapon/tank/internals/plasma - 3 time(s).
/obj/machinery/camera_assembly - 3 time(s).
/obj/machinery/camera/motion - 3 time(s).
/obj/machinery/atmospherics/pipe/simple/supply/hidden - 2 time(s).
/obj/item/mecha_parts/chassis/ripley - 2 time(s).
/obj/item/stack/sheet/metal - 2 time(s).
/obj/machinery/door/airlock/external - 2 time(s).
/obj/machinery/suit_storage_unit/engine - 2 time(s).
/obj/item/weapon/stock_parts/cell - 2 time(s).
/obj/machinery/computer/communications - 2 time(s).
/obj/item/clothing/tie/petcollar - 2 time(s).
/obj/machinery/atmospherics/components/binary/passive_gate - 2 time(s).
/obj/machinery/power/apc - 2 time(s).
/obj/structure/table/glass - 2 time(s).
/obj/item/stack/cable_coil - 2 time(s).
/obj/effect/decal/cleanable/blood/gibs/core - 2 time(s).
/obj/machinery/atmospherics/components/unary/heat_exchanger - 2 time(s).
/obj/item/mecha_parts/chassis/firefighter - 2 time(s).
/obj/machinery/porta_turret/ai - 2 time(s).
/obj/structure/closet/secure_closet/brig - 1 time(s).
/obj/effect/landmark/river_waypoint - 1 time(s).
/obj/item/clothing/head/chaplain_hood - 1 time(s).
/obj/machinery/door/window/eastleft - 1 time(s).
/obj/item/weapon/staff/broom - 1 time(s).
/obj/machinery/door/window - 1 time(s).
/obj/machinery/computer/pandemic - 1 time(s).
/obj/machinery/door/airlock/research - 1 time(s).
/obj/machinery/portable_atmospherics/pump - 1 time(s).
/obj/machinery/suit_storage_unit/ce - 1 time(s).
/obj/machinery/power/grounding_rod - 1 time(s).
/mob/camera/aiEye - 1 time(s).
/datum/data/record - 1 time(s).
/obj/machinery/firealarm - 1 time(s).
/obj/machinery/porta_turret_cover - 1 time(s).
/obj/mecha/working/ripley - 1 time(s).
/obj/machinery/portable_atmospherics/scrubber - 1 time(s).
/obj/effect/decal/cleanable/robot_debris/old - 1 time(s).
/obj/machinery/biogenerator - 1 time(s).
/obj/machinery/plantgenes - 1 time(s).
/datum/reagents - 1 time(s).
/obj/machinery/vending/hydronutrients - 1 time(s).
/obj/machinery/recycler - 1 time(s).
/obj/item/weapon/tank/jetpack/suit - 1 time(s).
/obj/machinery/vending/hydroseeds - 1 time(s).
/obj/structure/table/optable - 1 time(s).
/obj/item/device/pda/ai/pai - 1 time(s).
/obj/item/stack/sheet/mineral/plasma - 1 time(s).
/obj/machinery/chem_dispenser/drinks/beer - 1 time(s).
/obj/structure/closet/crate - 1 time(s).
/obj/item/stack/sheet/animalhide/monkey - 1 time(s).
/obj/effect/hallucination/simple/singularity - 1 time(s).
/obj - 1 time(s).
/obj/item/clothing/head/explorer - 1 time(s).
/obj/screen/grab - 1 time(s).
/obj/item/weapon/grab - 1 time(s).
/obj/machinery/atmospherics/components/unary/thermomachine/heater - 1 time(s).
/obj/machinery/atmospherics/components/unary/thermomachine/freezer - 1 time(s).
/obj/machinery/door/poddoor - 1 time(s).
/obj/item/weapon/cartridge/hos - 1 time(s).
/obj/item/device/pda/heads/hos - 1 time(s).
/obj/item/ammo_casing/energy/electrode - 1 time(s).
/obj/effect/mob_spawn/human/alive/space_bar_patron - 1 time(s).
/obj/effect/blob/core - 1 time(s).
/obj/machinery/clonepod - 1 time(s).
/obj/item/weapon/reagent_containers/spray/mister/janitor - 1 time(s).
/obj/item/clothing/head/helmet/space/hardsuit/shielded/ctf - 1 time(s).
/obj/item/weapon/card/id/syndicate - 1 time(s).
/obj/item/device/radio - 1 time(s).
/obj/machinery/r_n_d/circuit_imprinter - 1 time(s).
/obj/machinery/door/window/brigdoor - 1 time(s).
/obj/machinery/computer/rdconsole/core - 1 time(s).
/obj/structure/cable/yellow - 1 time(s).
/obj/machinery/disposal/bin - 1 time(s).
/obj/effect/mob_spawn/human/prisoner_transport - 1 time(s).

View File

@@ -0,0 +1,3 @@
@echo off
call "%~dp0\..\bootstrap\python" -m UpdatePaths %*
pause

View File

@@ -1,9 +1,10 @@
# A script and syntax for applying path updates to maps.
import re
import os
import sys
import argparse
import frontend
from dmm import *
from mapmerge2 import frontend
from mapmerge2.dmm import *
desc = """
Update dmm files given update file/string.
@@ -167,10 +168,14 @@ def main(args):
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=desc, formatter_class=argparse.RawTextHelpFormatter)
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")
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("--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("--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())
main(parser.parse_args())

View File

@@ -1,34 +1,14 @@
/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/duraskirt : /obj/item/cloning/under/misc/durathread/skirt
/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/staffassistant : /obj/item/clothing/under/misc/staffassistant
/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/croptop : /obj/item/clothing/under/misc/croptop
/obj/item/clothing/under/gear_harness : /obj/item/clothing/under/misc/gear_harness
/obj/item/clothing/under/stripper_pink : /obj/item/clothing/under/misc/stripper
/obj/item/clothing/under/stripper_green : /obj/item/clothing/under/misc/stripper/green
/obj/item/clothing/under/mankini : /obj/item/clothing/under/misc/stripper/mankini
/obj/item/clothing/under/squatter_outfit : /obj/item/clothing/under/misc/squatter
/obj/item/clothing/under/russobluecamooutfit : /obj/item/clothing/under/misc/blue_camo
/obj/item/clothing/under/keyholesweater : /obj/item/clothing/under/misc/keyholesweater
/obj/item/clothing/under/polychromic/shirt : /obj/item/clothing/under/misc/poly_shirt
/obj/item/clothing/under/polychromic/jumpsuit : /obj/item/clothing/under/misc/polyjumpsuit
/obj/item/clothing/under/polychromic/shimatank : /obj/item/clothing/under/misc/poly_tanktop
/obj/item/clothing/under/polychromic/femtank : /obj/item/clothing/under/misc/poly_tanktop/female
/obj/item/clothing/under/polychromic/shorts : /obj/item/clothing/under/misc/polyshorts
/obj/item/clothing/under/polychromic/shorts/pantsu : /obj/item/clothing/under/misc/polyshorts/pantsu
/obj/item/clothing/under/polychromic/bottomless : /obj/item/clothing/under/misc/poly_bottomless
/obj/item/clothing/under/corporateuniform : /obj/item/clothing/under/misc/corporateuniform
/obj/item/clothing/under/polychromic/shortpants : /obj/item/clothing/under/shorts/polychromic
/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
@@ -45,43 +25,25 @@
/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/telegram : /obj/item/clothing/under/suit/telegram
/obj/item/clothing/under/polychromic : /obj/item/clothing/under/suit/polychromic
/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/sweptskirt : /obj/item/clothing/under/skirt/swept
/obj/item/clothing/under/sundress : /obj/item/clothing/under/dress/sundress
/obj/item/clothing/under/sundresswhite : /obj/item/clothing/under/dress/sundress/white
/obj/item/clothing/under/greendress : /obj/item/clothing/under/dress/green
/obj/item/clothing/under/pinkdress : /obj/item/clothing/under/dress/pink
/obj/item/clothing/under/blacktango : /obj/item/clothing/under/dress/blacktango
/obj/item/clothing/under/westernbustle : /obj/item/clothing/under/dress/westernbustle
/obj/item/clothing/under/flamenco : /obj/item/clothing/under/dress/flamenco
/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/flowerdress : /obj/item/clothing/under/dress/flower
/obj/item/clothing/under/redeveninggown : /obj/item/clothing/under/dress/redeveninggown
/obj/item/clothing/under/corset : /obj/item/clothing/under/dress/corset
/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/wedding : /obj/item/clothing/under/dress/wedding
/obj/item/clothing/under/wedding/orange : /obj/item/clothing/under/dress/wedding/orange
/obj/item/clothing/under/wedding/purple : /obj/item/clothing/under/dress/wedding/purple
/obj/item/clothing/under/wedding/blue : /obj/item/clothing/under/dress/wedding/blue
/obj/item/clothing/under/wedding/red : /obj/item/clothing/under/dress/wedding/red
/obj/item/clothing/under/polychromic/skirt : /obj/item/clothing/under/dress/skirt/polychromic
/obj/item/clothing/under/polychromic/pleat : /obj/item/clothing/under/dress/skirt/polychromic/pleated
/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/cloud : /obj/item/clothing/under/costume/cloud
/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
@@ -91,7 +53,6 @@
/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/polychromic/kilt : /obj/item/clothing/under/costume/kilt/polychromic
/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
@@ -111,23 +72,6 @@
/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/christmas/christmasmaler : /obj/item/clothing/under/costume/christmas
/obj/item/clothing/under/christmas/christmasmaleg : /obj/item/clothing/under/costume/christmas/green
/obj/item/clothing/under/christmas/christmasfemaler : /obj/item/clothing/under/costume/christmas/croptop
/obj/item/clothing/under/christmas/christmasfemaleg : /obj/item/clothing/under/costume/christmas/croptop/green
/obj/item/clothing/under/lunar/qipao : /obj/item/clothing/under/costume/qipao
/obj/item/clothing/under/lunar/qipao/white : /obj/item/clothing/under/costume/qipao/red
/obj/item/clothing/under/lunar/qipao/red : /obj/item/clothing/under/costume/qipao/red
/obj/item/clothing/under/lunar/cheongsam : /obj/item/clothing/under/costume/cheongsam
/obj/item/clothing/under/lunar/cheongsam/white : /obj/item/clothing/under/costume/cheongsam/white
/obj/item/clothing/under/lunar/cheongsam/red : /obj/item/clothing/under/costume/cheongsam/red
/obj/item/clothing/under/lunasune : /obj/item/clothing/under/custom/lunasune
/obj/item/clothing/under/leoskimpy : /obj/item/clothing/under/custom/leoskimpy
/obj/item/clothing/under/mimeoveralls : /obj/item/clothing/under/custom/mimeoveralls
/obj/item/clothing/under/mw2_russian_para : /obj/item/clothing/under/custom/mw2_russian_para
/obj/item/clothing/under/trendy_fit : /obj/item/clothing/under/custom/trendy_fit
/obj/item/clothing/under/mikubikini : /obj/item/clothing/under/custom/mikubikini
/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
@@ -171,8 +115,8 @@
/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/rank/civilian/lawyer/black/alt
/obj/item/clothing/under/lawyer/blacksuit/skirt : /obj/item/clothing/under/rank/civilian/lawyer/black/alt/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
@@ -200,15 +144,14 @@
/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/chief_medical_officer/turtleneck : /obj/item/clothing/under/rank/medical/chief_medical_officer/turtleneck
/obj/item/clothing/under/rank/medical : /obj/item/clothing/under/rank/medical/doctor/nurse
/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
/obj/item/clothing/under/rank/geneticist : /obj/item/clothing/under/rank/medical/geneticist
/obj/item/clothing/under/rank/geneticist/skirt : /obj/item/clothing/under/rank/medical/geneticist/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

View File

@@ -462,4 +462,6 @@
/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/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}

117
tools/bootstrap/python Executable file
View File

@@ -0,0 +1,117 @@
#!/bin/sh
# bootstrap/python
#
# Python-finding script for all `sh` environments, including Linux, MSYS2,
# Git for Windows, and GitHub Desktop. Invokable from CLI or automation.
#
# If a python.exe installed by `python_.ps1` is present, it will be used.
# Otherwise, this script requires a system `python3` and `pip` to be provided,
# and will create a standard virtualenv in which to install `requirements.txt`.
set -e
# Convenience variables
Bootstrap="$(dirname "$0")"
Sdk="$(dirname "$Bootstrap")"
Cache="$Bootstrap/.cache"
if [ "$TG_BOOTSTRAP_CACHE" ]; then
Cache="$TG_BOOTSTRAP_CACHE"
fi
OldPWD="$PWD"
cd "$Bootstrap/../.."
. ./dependencies.sh # sets PYTHON_VERSION
cd "$OldPWD"
PythonVersion="$PYTHON_VERSION"
PythonDir="$Cache/python-$PythonVersion"
PythonExe="$PythonDir/python.exe"
Log="$Cache/last-command.log"
# 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
# Python using the Windows Store on purpose, these aliases always generate
# "Permission denied" errors when sh.exe tries to invoke them.
PATH=$(echo "$PATH" | tr ":" "\n" | grep -v "AppData/Local/Microsoft/WindowsApps" | tr "\n" ":")
# Try to find a Python executable.
if command -v python3 >/dev/null 2>&1; then
PythonExe=python3
elif command -v python >/dev/null 2>&1; then
PythonExe=python
elif command -v py >/dev/null 2>&1; then
PythonExe="py -3"
else
echo
if command -v apt-get >/dev/null 2>&1; then
echo "Please install Python using your system's package manager:"
echo " sudo apt-get install python3 python3-pip"
elif [ "$(uname -o)" = "Msys" ]; then
echo "Please run tools/bootstrap/python.bat instead of tools/bootstrap/python once to"
echo "install Python automatically, or install it from https://www.python.org/downloads/"
# TODO: give MSYS pacman advice?
elif command -v pacman >/dev/null 2>&1; then
echo "Please install Python using your system's package manager:"
echo " sudo pacman -S python python-pip"
else
echo "Please install Python from https://www.python.org/downloads/ or using your system's package manager."
fi
echo
exit 1
fi
# Create a venv and activate it
PythonDir="$Cache/venv"
if [ ! -d "$PythonDir" ]; then
echo "Creating virtualenv..."
"$PythonExe" -m venv "$PythonDir"
fi
if [ -f "$PythonDir/bin/python" ]; then
PythonExe="$PythonDir/bin/python"
elif [ -f "$PythonDir/scripts/python3.exe" ]; then
PythonExe="$PythonDir/scripts/python3.exe";
else
echo "bootstrap/python failed to find the python executable inside its virtualenv"
exit 1
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 pip install -U pip -r "$Sdk/requirements.txt"
cp "$Sdk/requirements.txt" "$PythonDir/requirements.txt"
echo "---"
fi
# Verify version and deduce the path separator
PythonMajor=${PythonVersion%%.*}
PythonMinor=${PythonVersion#*.}
PythonMinor=${PythonMinor%.*}
PATHSEP=$("$PythonExe" - "$PythonMajor" "$PythonMinor" <<'EOF'
import sys, os
if sys.version_info.major != int(sys.argv[1]) or sys.version_info.minor < int(sys.argv[2]):
print("Error: Python ", sys.argv[1], ".", sys.argv[2], " or later is required, but you have:\n", sys.version, sep="", file=sys.stderr)
exit(1)
print(os.pathsep)
EOF
)
# Cheap shell function if tee.exe is not available
if ! command -v tee >/dev/null 2>&1; then
tee() {
# Fudge: assume $1 is always "-a"
while read -r line; do
echo "$line" >> "$2"
echo "$line"
done
}
fi
# Invoke python with all command-line arguments
export PYTHONPATH="$Sdk$PATHSEP${PYTHONPATH:-}"
mkdir -p "$Cache"
printf '%s\n' "$PythonExe" "$@" > "$Log"
printf -- '---\n' >> "$Log"
exec 4>&1
exitstatus=$({ { set +e; "$PythonExe" -u "$@" 2>&1 3>&-; printf %s $? >&3; } 4>&- | tee -a "$Log" 1>&4; } 3>&1)
exec 4>&-
exit "$exitstatus"

1
tools/bootstrap/python.bat Executable file
View File

@@ -0,0 +1 @@
@call powershell.exe -NoLogo -ExecutionPolicy Bypass -File "%~dp0\python_.ps1" %*

View File

@@ -0,0 +1,6 @@
python36.zip
.
..\..\..
# Uncomment to run site.main() automatically
import site

103
tools/bootstrap/python_.ps1 Executable file
View File

@@ -0,0 +1,103 @@
# bootstrap/python_.ps1
#
# Python bootstrapping script for Windows.
#
# Automatically downloads a portable edition of a pinned Python version to
# a cache directory, installs Pip, installs `requirements.txt`, and then invokes
# Python.
#
# 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
Add-Type -AssemblyName System.IO.Compression.FileSystem
function ExtractVersion {
param([string] $Path, [string] $Key)
foreach ($Line in Get-Content $Path) {
if ($Line.StartsWith("export $Key=")) {
return $Line.Substring("export $Key=".Length)
}
}
throw "Couldn't find value for $Key in $Path"
}
# Convenience variables
$Bootstrap = Split-Path $script:MyInvocation.MyCommand.Path
$Tools = Split-Path $Bootstrap
$Cache = "$Bootstrap/.cache"
if ($Env:TG_BOOTSTRAP_CACHE) {
$Cache = $Env:TG_BOOTSTRAP_CACHE
}
$PythonVersion = ExtractVersion -Path "$Bootstrap/../../dependencies.sh" -Key "PYTHON_VERSION"
$PythonDir = "$Cache/python-$PythonVersion"
$PythonExe = "$PythonDir/python.exe"
$Log = "$Cache/last-command.log"
# Download and unzip a portable version of Python
if (!(Test-Path $PythonExe -PathType Leaf)) {
$host.ui.RawUI.WindowTitle = "Downloading Python $PythonVersion..."
New-Item $Cache -ItemType Directory -ErrorAction silentlyContinue | Out-Null
$Archive = "$Cache/python-$PythonVersion-embed.zip"
Invoke-WebRequest `
"https://www.python.org/ftp/python/$PythonVersion/python-$PythonVersion-embed-amd64.zip" `
-OutFile $Archive `
-ErrorAction Stop
[System.IO.Compression.ZipFile]::ExtractToDirectory($Archive, $PythonDir)
# Copy a ._pth file without "import site" commented, so pip will work
Copy-Item "$Bootstrap/python36._pth" $PythonDir `
-ErrorAction Stop
Remove-Item $Archive
}
# Install pip
if (!(Test-Path "$PythonDir/Scripts/pip.exe")) {
$host.ui.RawUI.WindowTitle = "Downloading Pip..."
Invoke-WebRequest "https://bootstrap.pypa.io/get-pip.py" `
-OutFile "$Cache/get-pip.py" `
-ErrorAction Stop
& $PythonExe "$Cache/get-pip.py" --no-warn-script-location
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
Remove-Item "$Cache/get-pip.py" `
-ErrorAction Stop
}
# Use pip to install our requirements
if (!(Test-Path "$PythonDir/requirements.txt") -or ((Get-FileHash "$Tools/requirements.txt").hash -ne (Get-FileHash "$PythonDir/requirements.txt").hash)) {
$host.ui.RawUI.WindowTitle = "Updating dependencies..."
& $PythonExe -m pip install -U pip -r "$Tools/requirements.txt"
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
Copy-Item "$Tools/requirements.txt" "$PythonDir/requirements.txt"
Write-Output "`n---`n"
}
# Invoke python with all command-line arguments
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"
$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

View File

@@ -22,15 +22,15 @@ 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 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."
@@ -48,16 +48,16 @@ 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;
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'
@@ -68,10 +68,10 @@ while read f; do
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 # annoy the coders instead of causing it to fail
# fi;
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

View File

@@ -8,11 +8,6 @@ mkdir ci_test/config
#test config
cp tools/ci/ci_config.txt ci_test/config/config.txt
#throw extools into ldd
cp libbyond-extools.so ~/.byond/bin/libbyond-extools.so
chmod +x ~/.byond/bin/libbyond-extools.so
ldd ~/.byond/bin/libbyond-extools.so
cd ci_test
DreamDaemon tgstation.dmb -close -trusted -verbose -params "log-directory=ci"
cd ..

View File

@@ -11,10 +11,8 @@ fi
mkdir -p \
$1/_maps \
$1/icons \
$1/sound/chatter \
$1/sound/voice/complionator \
$1/sound/instruments \
$1/icons/runtime \
$1/sound/runtime \
$1/strings
if [ -d ".git" ]; then
@@ -24,18 +22,14 @@ fi
cp tgstation.dmb tgstation.rsc $1/
cp -r _maps/* $1/_maps/
cp icons/default_title.dmi $1/icons/
cp -r sound/chatter/* $1/sound/chatter/
cp -r sound/voice/complionator/* $1/sound/voice/complionator/
cp -r sound/instruments/* $1/sound/instruments/
cp -r icons/runtime/* $1/icons/runtime/
cp -r sound/runtime/* $1/sound/runtime/
cp -r strings/* $1/strings/
cp *byond-extools.* $1/ || true
#remove .dm files from _maps
#this regrettably doesn't work with windows find
#find $1/_maps -name "*.dm" -type f -delete
#dlls on windows.
cp rust_g* $1/ || true
cp *BSQL.* $1/ || true
#dlls on windows
cp *.dll $1/ || true

View File

@@ -0,0 +1,11 @@
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

View File

@@ -0,0 +1,59 @@
# 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)

View File

@@ -0,0 +1,2 @@
@call "%~dp0\..\bootstrap\python.bat" -m dmi.merge_driver --posthoc %*
@pause

37
tools/mapmerge2/dmi.py → tools/dmi/__init__.py Normal file → Executable file
View File

@@ -12,10 +12,10 @@ NORTH = 1
SOUTH = 2
EAST = 4
WEST = 8
SOUTHEAST = SOUTH|EAST
SOUTHWEST = SOUTH|WEST
NORTHEAST = NORTH|EAST
NORTHWEST = NORTH|WEST
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]
@@ -34,6 +34,7 @@ DIR_NAMES = {
None: SOUTH,
}
class Dmi:
version = "4.0"
@@ -134,7 +135,7 @@ class Dmi:
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):
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"
@@ -175,6 +176,7 @@ class Dmi:
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
@@ -216,40 +218,31 @@ class State:
def get_frame(self, *args, **kwargs):
return self.frames[self._frame_index(*args, **kwargs)]
def escape(text):
assert '\\' not in text and '"' not in 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]
assert '\\' not in text and quote not in text
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'
if __name__ == '__main__':
# test: can we load every DMI in the tree
import os
count = 0
for dirpath, dirnames, filenames in os.walk('.'):
if '.git' in dirnames:
dirnames.remove('.git')
for filename in filenames:
if filename.endswith('.dmi'):
Dmi.from_file(os.path.join(dirpath, filename))
count += 1
print(f"Successfully parsed {count} dmi files")

View File

@@ -1,6 +1,8 @@
#!/usr/bin/env python3
import sys
import dmi
from hooks.merge_frontend import MergeDriver
def images_equal(left, right):
if left.size != right.size:
@@ -15,6 +17,7 @@ def images_equal(left, right):
return False
return True
def states_equal(left, right):
result = True
@@ -31,9 +34,11 @@ def states_equal(left, right):
return result
def key_of(state):
return (state.name, state.movement)
def dictify(sheet):
result = {}
for state in sheet.states:
@@ -43,6 +48,7 @@ def dictify(sheet):
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):
@@ -145,33 +151,31 @@ def three_way_merge(base, left, right):
merged.states = final_states
return len(conflicts), merged
def main(path, original, left, right):
print(f"Merging icon: {path}")
icon_orig = dmi.Dmi.from_file(original)
icon_left = dmi.Dmi.from_file(left)
icon_right = dmi.Dmi.from_file(right)
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.")
trouble, merged = three_way_merge(icon_orig, icon_left, icon_right)
if merged:
merged.to_file(left)
if trouble:
print("!!! Manual merge required!")
if merged:
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.")
return trouble
if __name__ == '__main__':
if len(sys.argv) != 6:
print("DMI merge driver called with wrong number of arguments")
print(" usage: merge-driver-dmi %P %O %A %B %L")
exit(1)
# "left" is also the file that ought to be overwritten
_, path, original, left, right, conflict_size_marker = sys.argv
exit(main(path, original, left, right))
exit(DmiDriver().main())

39
tools/dmi/test.py Executable file
View File

@@ -0,0 +1,39 @@
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"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()

View File

@@ -1,5 +0,0 @@
Uses PNGJ: https://code.google.com/p/pngj/.
For help, use "java -jar dmitool.jar help".
Requires Java 7.

View File

@@ -1,16 +0,0 @@
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
compile group: 'ar.com.hjg', name: 'pngj', version: '2.1.0'
}
jar {
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
manifest {
attributes 'Main-Class': 'dmitool.Main'
}
}

View File

@@ -1,8 +0,0 @@
java -jar tools/dmitool/dmitool.jar merge $1 $2 $3 $2
if [ "$?" -gt 0 ]
then
echo "Unable to automatically resolve all icon_state conflicts, please merge manually."
exit 1
fi
exit 0

Binary file not shown.

View File

@@ -1,94 +0,0 @@
""" Python 2.7 wrapper for dmitool.
"""
import os
from subprocess import Popen, PIPE
_JAVA_PATH = ["java"]
_DMITOOL_CMD = ["-jar", "dmitool.jar"]
def _dmitool_call(*dmitool_args, **popen_args):
return Popen(_JAVA_PATH + _DMITOOL_CMD + [str(arg) for arg in dmitool_args], **popen_args)
def _safe_parse(dict, key, deferred_value):
try:
dict[key] = deferred_value()
except Exception as e:
print "Could not parse property '%s': %s"%(key, e)
return e
return False
def version():
""" Returns the version as a string. """
stdout, stderr = _dmitool_call("version", stdout=PIPE).communicate()
return str(stdout).strip()
def help():
""" Returns the help text as a string. """
stdout, stderr = _dmitool_call("help", stdout=PIPE).communicate()
return str(stdout).strip()
def info(filepath):
""" Totally not a hack that parses the output from dmitool into a dictionary.
May break at any moment.
"""
subproc = _dmitool_call("info", filepath, stdout=PIPE)
stdout, stderr = subproc.communicate()
result = {}
data = stdout.split(os.linesep)[1:]
#for s in data: print s
#parse header line
if len(data) > 0:
header = data.pop(0).split(",")
#don't need to parse states, it's redundant
_safe_parse(result, "images", lambda: int(header[0].split()[0].strip()))
_safe_parse(result, "size", lambda: header[2].split()[1].strip())
#parse state information
states = []
for item in data:
if not len(item): continue
stateinfo = {}
item = item.split(",", 3)
_safe_parse(stateinfo, "name", lambda: item[0].split()[1].strip(" \""))
_safe_parse(stateinfo, "dirs", lambda: int(item[1].split()[0].strip()))
_safe_parse(stateinfo, "frames", lambda: int(item[2].split()[0].strip()))
if len(item) > 3:
stateinfo["misc"] = item[3]
states.append(stateinfo)
result["states"] = states
return result
def extract_state(input_path, output_path, icon_state, direction=None, frame=None):
""" Extracts an icon state as a png to a given path.
If provided direction should be a string, one of S, N, E, W, SE, SW, NE, NW.
If provided frame should be a frame number or a string of two frame number separated by a dash.
"""
args = ["extract", input_path, icon_state, output_path]
if direction is not None: args.extend(("direction" , str(direction)))
if frame is not None: args.extend(("frame" , str(frame)))
return _dmitool_call(*args)
def import_state(target_path, input_path, icon_state, replace=False, delays=None, rewind=False, loop=None, ismovement=False, direction=None, frame=None):
""" Inserts an input png given by the input_path into the target_path.
"""
args = ["import", target_path, icon_state, input_path]
if replace: args.append("nodup")
if rewind: args.append("rewind")
if ismovement: args.append("movement")
if delays: args.extend(("delays", ",".join(delays)))
if direction is not None: args.extend(("direction", direction))
if frame is not None: args.extend(("frame", frame))
if loop in ("inf", "infinity"):
args.append("loop")
elif loop:
args.extend(("loopn", loop))
return _dmitool_call(*args)

View File

@@ -1,6 +0,0 @@
@echo off
set tab=
echo. >> ../../.git/config
echo [merge "merge-dmi"] >> ../../.git/config
echo %tab%name = iconfile merge driver >> ../../.git/config
echo %tab%driver = ./tools/dmitool/dmimerge.sh %%O %%A %%B >> ../../.git/config

View File

@@ -1,6 +0,0 @@
F="../../.git/config"
echo '' >> $F
echo '[merge "merge-dmi"]' >> $F
echo ' name = iconfile merge driver' >> $F
echo ' driver = ./tools/dmitool/dmimerge.sh %O %A %B' >> $F

View File

@@ -1,14 +0,0 @@
1. Install java(http://www.java.com/en/download/index.jsp)
2. Make sure java is in your PATH. To test this, open git bash, and type "java". If it says unknown command, you need to add JAVA/bin to your PATH variable (A guide for this can be found at https://www.java.com/en/download/help/path.xml ).
Merging
The easiest way to do merging is to install the merge driver. For this, open `-tg-station/.git/config` in a text editor, and paste the following lines to the end of it:
[merge "merge-dmi"]
name = iconfile merge driver
driver = ./tools/dmitool/dmimerge.sh %O %A %B
You may optionally instead run git_merge_installer.bat or git_merge_installer.sh which should automatically insert these lines for you at the appropriate location.
After this, merging DMI files should happen automagically unless there are conflicts (an icon_state that both you and someone else changed).
If there are conflicts, you will unfortunately still be stuck with opening both versions in the editor, and manually resolving the issues with those states.

View File

@@ -1,455 +0,0 @@
package dmitool;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.ImageLineInt;
import ar.com.hjg.pngj.PngReader;
import ar.com.hjg.pngj.PngWriter;
import ar.com.hjg.pngj.PngjInputException;
import ar.com.hjg.pngj.chunks.PngChunkPLTE;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
public class DMI implements Comparator<IconState> {
int w, h;
List<IconState> images;
int totalImages = 0;
RGBA[] palette;
boolean isPaletted;
public DMI(int w, int h) {
this.w = w;
this.h = h;
images = new ArrayList<>();
isPaletted = false;
palette = null;
}
public DMI(String f) throws DMIException, FileNotFoundException {
this(new File(f));
}
public DMI(File f) throws DMIException, FileNotFoundException {
if(f.length() == 0) { // Empty .dmi is empty file
w = 32;
h = 32;
images = new ArrayList<>();
isPaletted = false;
palette = null;
return;
}
InputStream in = new FileInputStream(f);
PngReader pngr;
try {
pngr = new PngReader(in);
} catch(PngjInputException pie) {
throw new DMIException("Bad file format!", pie);
}
String descriptor = pngr.getMetadata().getTxtForKey("Description");
String[] lines = descriptor.split("\n");
if(Main.VERBOSITY > 0) System.out.println("Descriptor has " + lines.length + " lines.");
if(Main.VERBOSITY > 3) {
System.out.println("Descriptor:");
System.out.println(descriptor);
}
/* length 6 is:
# BEGIN DMI
version = 4.0
state = "state"
dirs = 1
frames = 1
# END DMI
*/
if(lines.length < 6) throw new DMIException(null, 0, "Descriptor too short!");
if(!"# BEGIN DMI".equals(lines[0])) throw new DMIException(lines, 0, "Expected '# BEGIN DMI'");
if(!"# END DMI".equals(lines[lines.length-1])) throw new DMIException(lines, lines.length-1, "Expected '# END DMI'");
if(!"version = 4.0".equals(lines[1])) throw new DMIException(lines, 1, "Unknown version, expected 'version = 4.0'");
this.w = 32;
this.h = 32;
int i = 2;
if(lines[i].startsWith("\twidth = ")) {
this.w = Integer.parseInt(lines[2].substring("\twidth = ".length()));
i++;
}
if(lines[i].startsWith("\theight = ")) {
this.h = Integer.parseInt(lines[3].substring("\theight = ".length()));
i++;
}
List<IconState> states = new ArrayList<>();
while(i < lines.length - 1) {
long imagesInState = 1;
if(!lines[i].startsWith("state = \"") || !lines[i].endsWith("\"")) throw new DMIException(lines, i, "Error reading state string");
String stateName = lines[i].substring("state = \"".length(), lines[i].length()-1);
i++;
int dirs = 1;
int frames = 1;
float[] delays = null;
boolean rewind = false;
int loop = -1;
String hotspot = null;
boolean movement = false;
while(lines[i].startsWith("\t")) {
if(lines[i].startsWith("\tdirs = ")) {
dirs = Integer.parseInt(lines[i].substring("\tdirs = ".length()));
imagesInState *= dirs;
i++;
} else if(lines[i].startsWith("\tframes = ")) {
frames = Integer.parseInt(lines[i].substring("\tframes = ".length()));
imagesInState *= frames;
i++;
} else if(lines[i].startsWith("\tdelay = ")) {
String delayString = lines[i].substring("\tdelay = ".length());
String[] delayVals = delayString.split(",");
delays = new float[delayVals.length];
for(int d=0; d<delays.length; d++) {
delays[d] = Float.parseFloat(delayVals[d]);
}
i++;
} else if(lines[i].equals("\trewind = 1")) {
rewind = true;
i++;
} else if(lines[i].startsWith("\tloop = ")) {
loop = Integer.parseInt(lines[i].substring("\tloop = ".length()));
i++;
} else if(lines[i].startsWith("\thotspot = ")) {
hotspot = lines[i].substring("\thotspot = ".length());
i++;
} else if(lines[i].equals("\tmovement = 1")) {
movement = true;
i++;
} else {
System.out.println("Unknown line '" + lines[i] + "' in state '" + stateName + "'!");
i++;
}
}
if(delays != null) {
if((Main.STRICT && delays.length != frames) || delays.length < frames) {
throw new DMIException(null, 0, "Frames must be equal to delays (" + stateName + "; " + frames + " frames, " + delays.length + " delays)!");
}
}
IconState is = new IconState(stateName, dirs, frames, null, delays, rewind, loop, hotspot, movement);
totalImages += imagesInState;
states.add(is);
}
images = states;
PngChunkPLTE pal = (PngChunkPLTE)pngr.getChunksList().getById1("PLTE");
isPaletted = pal != null;
if(isPaletted) {
if(Main.VERBOSITY > 0) System.out.println(pal.getNentries() + " palette entries");
palette = new RGBA[pal.getNentries()];
int[] rgb = new int[3];
for(int q=0; q<pal.getNentries(); q++) {
pal.getEntryRgb(q, rgb);
palette[q] = new RGBA(rgb[0], rgb[1], rgb[2], q==0 ? 0 : 255);
}
} else {
if(Main.VERBOSITY > 0) System.out.println("Non-paletted image");
}
int iw = pngr.imgInfo.cols;
int ih = pngr.imgInfo.rows;
if(totalImages > iw * ih)
throw new DMIException(null, 0, "Impossible number of images!");
if(Main.VERBOSITY > 0) System.out.println("Image size " + iw+"x"+ih);
int[][] px = new int[ih][];
for(int y=0; y<ih; y++) {
ImageLineInt ili = (ImageLineInt)pngr.readRow();
int[] sl = ili.getScanline();
if(sl.length != (isPaletted ? iw : iw*4))
throw new DMIException(null, 0, "Error processing image!");
px[y] = sl.clone();
}
int statesX = iw / w;
int statesY = ih / h;
int x=0, y=0;
for(IconState is: states) {
int numImages = is.dirs * is.frames;
Image[] img = new Image[numImages];
for(int q=0; q<numImages; q++) {
if(isPaletted) {
int[][] idat = new int[h][w];
for(int sy = 0; sy < h; sy++) {
for(int sx = 0; sx < w; sx++) {
idat[sy][sx] = px[y*h + sy][x*w + sx];
}
}
img[q] = new PalettedImage(w, h, idat, palette);
} else {
RGBA[][] idat = new RGBA[h][w];
for(int sy = 0; sy < h; sy++) {
for(int sx = 0; sx < w; sx++) {
idat[sy][sx] = new RGBA(px[y*h + sy][x*4*w + 4*sx], px[y*h + sy][x*4*w + 4*sx + 1], px[y*h + sy][x*4*w + 4*sx + 2], px[y*h + sy][x*4*w + 4*sx + 3]);
}
}
img[q] = new NonPalettedImage(w, h, idat);
}
x++;
if(x == statesX) {
x = 0;
y++;
if(y > statesY)
// this should NEVER happen, we pre-check it
throw new DMIException(null, 0, "CRITICAL: End of image reached with states to go!");
}
}
if(is.delays != null) {
if((Main.STRICT && is.delays.length*is.dirs != img.length) || is.delays.length*is.dirs < img.length)
throw new DMIException(null, 0, "Delay array size mismatch: " + is.delays.length*is.dirs + " vs " + img.length + "!");
}
is.images = img;
}
}
public IconState getIconState(String name) {
for(IconState is: images) {
if(is.name.equals(name)) {
return is;
}
}
return null;
}
/**
* Makes a copy, unless name is null.
*/
public void addIconState(String name, IconState is) {
if(name == null) {
images.add(is);
totalImages += is.dirs * is.frames;
} else {
IconState newState = (IconState)is.clone();
newState.name = name;
images.add(newState);
totalImages += is.dirs * is.frames;
}
}
public boolean removeIconState(String name) {
for(IconState is: images) {
if(is.name.equals(name)) {
images.remove(is);
totalImages -= is.dirs * is.frames;
return true;
}
}
return false;
}
public boolean setIconState(IconState is) {
for(int i=0; i<images.size(); i++) {
IconState ic = images.get(i);
if(ic.name.equals(is.name)) {
totalImages -= ic.dirs * ic.frames;
totalImages += is.dirs * is.frames;
images.set(i, is);
return true;
}
}
return false;
}
private static final int IEND = 0x49454e44;
private static final int zTXt = 0x7a545874;
private static final int IHDR = 0x49484452;
private static void fixChunks(DataInputStream in, DataOutputStream out) throws IOException {
if(Main.VERBOSITY > 0) System.out.println("Fixing PNG chunks...");
out.writeInt(in.readInt());
out.writeInt(in.readInt());
Deque<PNGChunk> notZTXT = new ArrayDeque<>();
PNGChunk c = null;
while(c == null || c.type != IEND) {
c = new PNGChunk(in);
if(c.type == zTXt && notZTXT != null) {
PNGChunk cc = null;
while(cc == null || cc.type != IHDR) {
cc = notZTXT.pop();
cc.write(out);
}
c.write(out);
while(notZTXT.size() != 0) {
PNGChunk pc = notZTXT.pop();
pc.write(out);
}
notZTXT = null;
} else if(notZTXT != null) {
notZTXT.add(c);
} else {
c.write(out);
}
}
if(Main.VERBOSITY > 0) System.out.println("Chunks fixed.");
}
@Override public int compare(IconState arg0, IconState arg1) {
return arg0.name.compareTo(arg1.name);
}
public void writeDMI(OutputStream os) throws IOException {
writeDMI(os, false);
}
public void writeDMI(OutputStream os, boolean sortStates) throws IOException {
if(totalImages == 0) { // Empty .dmis are empty files
os.close();
return;
}
// Setup chunk-fix buffer
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if(sortStates) {
Collections.sort(images, this);
}
// Write the dmi into the buffer
int sx = (int)Math.ceil(Math.sqrt(totalImages));
int sy = totalImages / sx;
if(sx*sy < totalImages) {
sy++;
}
if(Main.VERBOSITY > 0) System.out.println("Image size: " + w + "x" + h + "; number of images " + sx + "x" + sy + " (" + totalImages + ")");
int ix = sx * w;
int iy = sy * h;
ImageInfo ii = new ImageInfo(ix, iy, 8, true);
PngWriter out = new PngWriter(baos, ii);
out.setCompLevel(9); // Maximum compression
String description = getDescriptor();
if(Main.VERBOSITY > 0) System.out.println("Descriptor has " + (description.split("\n").length) + " lines.");
out.getMetadata().setText("Description", description, true, true);
Image[][] img = new Image[sx][sy];
{
int k = 0;
int r = 0;
for(IconState is: images) {
for(Image i: is.images) {
img[k++][r] = i;
if(k == sx) {
k = 0;
r++;
}
}
}
}
for(int irow=0; irow<iy; irow++) {
ImageLineInt ili = new ImageLineInt(ii);
int[] buf = ili.getScanline();
for(int icol=0; icol<ix; icol++) {
int imageX = icol / w;
int pixelX = icol % w;
int imageY = irow / h;
int pixelY = irow % h;
Image i = img[imageX][imageY];
if(i != null) {
RGBA c = i.getPixel(pixelX, pixelY);
buf[icol*4 ] = c.r;
buf[icol*4 + 1] = c.g;
buf[icol*4 + 2] = c.b;
buf[icol*4 + 3] = c.a;
} else {
buf[icol*4 ] = 0;
buf[icol*4 + 1] = 0;
buf[icol*4 + 2] = 0;
buf[icol*4 + 3] = 0;
}
}
out.writeRow(ili);
}
out.end();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
fixChunks(new DataInputStream(bais), new DataOutputStream(os));
}
private String getDescriptor() {
String s = "";
String n = "\n";
String q = "\"";
s += "# BEGIN DMI\n";
s += "version = 4.0\n";
s += " width = " + w + n;
s += " height = " + h + n;
for(IconState is: images) {
s += is.getDescriptorFragment();
}
s += "# END DMI\n";
return s;
}
public void printInfo() {
System.out.println(totalImages + " images, " + images.size() + " states, size "+w+"x"+h);
}
public void printStateList() {
for(IconState s: images) {
System.out.println(s.getInfoLine());
}
}
@Override public boolean equals(Object obj) {
if(obj == this) return true;
if(!(obj instanceof DMI)) return false;
DMI dmi = (DMI)obj;
// try to find a simple difference before we dive into icon_state comparisons
if(dmi.w != w || dmi.h != h) return false;
if(dmi.isPaletted != isPaletted) return false;
if(dmi.totalImages != totalImages) return false;
if(dmi.images.size() != images.size()) return false;
HashMap<String, IconState> myIS = new HashMap<>();
HashMap<String, IconState> dmiIS = new HashMap<>();
for(IconState is: images) {
myIS.put(is.name, is);
}
for(IconState is: dmi.images) {
dmiIS.put(is.name, is);
}
if(!myIS.keySet().equals(dmiIS.keySet())) return false;
for(String s: myIS.keySet()) {
if(!myIS.get(s).equals(dmiIS.get(s))) return false;
}
return true;
}
}

View File

@@ -1,194 +0,0 @@
package dmitool;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class DMIDiff {
Map<String, IconState> newIconStates;
Map<String, IconStateDiff> modifiedIconStates = new HashMap<>();
Set<String> removedIconStates;
DMIDiff() {
newIconStates = new HashMap<>();
removedIconStates = new HashSet<>();
}
public DMIDiff(DMI base, DMI mod) {
if(base.h != mod.h || base.w != mod.w) throw new IllegalArgumentException("Cannot compare non-identically-sized DMIs!");
HashMap<String, IconState> baseIS = new HashMap<>();
for(IconState is: base.images) {
baseIS.put(is.name, is);
}
HashMap<String, IconState> modIS = new HashMap<>();
for(IconState is: mod.images) {
modIS.put(is.name, is);
}
newIconStates = ((HashMap<String, IconState>)modIS.clone());
for(String s: baseIS.keySet()) {
newIconStates.remove(s);
}
removedIconStates = new HashSet<>();
removedIconStates.addAll(baseIS.keySet());
removedIconStates.removeAll(modIS.keySet());
Set<String> retainedStates = new HashSet<>();
retainedStates.addAll(baseIS.keySet());
retainedStates.retainAll(modIS.keySet());
for(String s: retainedStates) {
if(!baseIS.get(s).equals(modIS.get(s))) {
modifiedIconStates.put(s, new IconStateDiff(baseIS.get(s), modIS.get(s)));
}
}
}
/**
* ASSUMES NO MERGE CONFLICTS - MERGE DIFFS FIRST.
*/
public void applyToDMI(DMI dmi) {
for(String s: removedIconStates) {
dmi.removeIconState(s);
}
for(String s: modifiedIconStates.keySet()) {
dmi.setIconState(modifiedIconStates.get(s).newState);
}
for(String s: newIconStates.keySet()) {
dmi.addIconState(null, newIconStates.get(s));
}
}
/**
* @param other The diff to merge with
* @param conflictDMI A DMI to add conflicted icon_states to
* @param merged An empty DMIDiff to merge into
* @param aName The log name for this diff
* @param bName The log name for {@code other}
* @return A Set<String> containing all icon_states which conflicted, along with what was done in each diff, in the format "icon_state: here|there"; here and there are one of "added", "modified", and "removed"
*/
public Set<String> mergeDiff(DMIDiff other, DMI conflictDMI, DMIDiff merged, String aName, String bName) {
HashSet<String> myTouched = new HashSet<>();
myTouched.addAll(removedIconStates);
myTouched.addAll(newIconStates.keySet());
myTouched.addAll(modifiedIconStates.keySet());
HashSet<String> otherTouched = new HashSet<>();
otherTouched.addAll(other.removedIconStates);
otherTouched.addAll(other.newIconStates.keySet());
otherTouched.addAll(other.modifiedIconStates.keySet());
HashSet<String> bothTouched = (HashSet<String>)myTouched.clone();
bothTouched.retainAll(otherTouched); // this set now contains the list of icon_states that *both* diffs modified, which we'll put in conflictDMI for manual merge (unless they were deletions
if(Main.VERBOSITY > 0) {
System.out.println("a: " + Arrays.toString(myTouched.toArray()));
System.out.println("b: " + Arrays.toString(otherTouched.toArray()));
System.out.println("both: " + Arrays.toString(bothTouched.toArray()));
}
HashSet<String> whatHappened = new HashSet<>();
for(String s: bothTouched) {
String here, there;
if(removedIconStates.contains(s)) {
here = "removed";
} else if(newIconStates.containsKey(s)) {
here = "added";
} else if(modifiedIconStates.containsKey(s)) {
here = "modified";
} else {
System.out.println("Unknown error; state="+s);
here = "???";
}
if(other.removedIconStates.contains(s)) {
there = "removed";
} else if(other.newIconStates.containsKey(s)) {
there = "added";
} else if(other.modifiedIconStates.containsKey(s)) {
there = "modified";
} else {
System.out.println("Unknown error; state="+s);
there = "???";
}
whatHappened.add(s + ": " + here + "|" + there);
}
// Removals
for(String s: removedIconStates) {
if(!bothTouched.contains(s)) {
merged.removedIconStates.add(s);
}
}
for(String s: other.removedIconStates) {
if(!bothTouched.contains(s)) {
merged.removedIconStates.add(s);
}
}
// Modifications
for(String s: modifiedIconStates.keySet()) {
if(!bothTouched.contains(s)) {
merged.modifiedIconStates.put(s, modifiedIconStates.get(s));
} else {
conflictDMI.addIconState(aName + "|" + s, modifiedIconStates.get(s).newState);
}
}
for(String s: other.modifiedIconStates.keySet()) {
if(!bothTouched.contains(s)) {
merged.modifiedIconStates.put(s, other.modifiedIconStates.get(s));
} else {
conflictDMI.addIconState(bName + "|" + s, other.modifiedIconStates.get(s).newState);
}
}
// Additions
for(String s: newIconStates.keySet()) {
if(!bothTouched.contains(s)) {
merged.newIconStates.put(s, newIconStates.get(s));
} else {
conflictDMI.addIconState(aName + s, newIconStates.get(s));
}
}
for(String s: other.newIconStates.keySet()) {
if(!bothTouched.contains(s)) {
merged.newIconStates.put(s, other.newIconStates.get(s));
} else {
conflictDMI.addIconState(bName + s, other.newIconStates.get(s));
}
}
return whatHappened;
}
@Override public String toString() {
String s = "";
String t = "\t";
String q = "\"";
String n = "\n";
if(!removedIconStates.isEmpty()) {
s += "Removed:\n";
for(String state: removedIconStates)
s += t + q + state + q + n;
}
if(!modifiedIconStates.isEmpty()) {
s += "Modified:\n";
for(String state: modifiedIconStates.keySet())
s += t + q + state + q + " [" + modifiedIconStates.get(state).toString() + "]\n";
}
if(!newIconStates.isEmpty()) {
s += "Added:\n";
for(String state: newIconStates.keySet())
s += t + q + state + q + " " + newIconStates.get(state).infoStr() + n;
}
if("".equals(s))
return "No changes";
return s;
}
}

View File

@@ -1,24 +0,0 @@
package dmitool;
public class DMIException extends Exception {
String[] desc = null;
int line = 0;
public DMIException(String[] descriptor, int line, String what) {
super(what);
desc = descriptor;
this.line = line;
}
public DMIException(String what) {
super(what);
}
public DMIException(String what, Exception cause) {
super(what, cause);
}
@Override public String getMessage() {
if(desc != null)
return "\"" + desc[line] + "\" - " + super.getMessage();
return super.getMessage();
}
}

View File

@@ -1,281 +0,0 @@
package dmitool;
import java.util.Arrays;
import ar.com.hjg.pngj.ImageInfo;
import ar.com.hjg.pngj.ImageLineInt;
import ar.com.hjg.pngj.PngWriter;
import ar.com.hjg.pngj.PngReader;
import ar.com.hjg.pngj.PngjInputException;
import java.io.InputStream;
import java.io.OutputStream;
public class IconState {
String name;
int dirs;
int frames;
float[] delays;
Image[] images; // dirs come first
boolean rewind;
int loop;
String hotspot;
boolean movement;
public String getInfoLine() {
String extraInfo = "";
if(rewind) extraInfo += " rewind";
if(frames != 1) {
extraInfo += " loop(" + (loop==-1 ? "infinite" : loop) + ")";
}
if(hotspot != null) extraInfo += " hotspot('" + hotspot + "')";
if(movement) extraInfo += " movement";
if(extraInfo.equals("")) {
return String.format("state \"%s\", %d dir(s), %d frame(s)", name, dirs, frames);
} else {
return String.format("state \"%s\", %d dir(s), %d frame(s),%s", name, dirs, frames, extraInfo);
}
}
@Override public IconState clone() {
IconState is = new IconState(name, dirs, frames, images.clone(), delays==null ? null : delays.clone(), rewind, loop, hotspot, movement);
is.delays = delays != null ? delays.clone() : null;
is.rewind = rewind;
return is;
}
public IconState(String name, int dirs, int frames, Image[] images, float[] delays, boolean rewind, int loop, String hotspot, boolean movement) {
if(delays != null) {
if(Main.STRICT && delays.length != frames) {
throw new IllegalArgumentException("Delays and frames must be the same length!");
}
}
this.name = name;
this.dirs = dirs;
this.frames = frames;
this.images = images;
this.rewind = rewind;
this.loop = loop;
this.hotspot = hotspot;
this.delays = delays;
this.movement = movement;
}
void setDelays(float[] delays) {
this.delays = delays;
}
void setRewind(boolean b) {
rewind = b;
}
@Override public boolean equals(Object obj) {
if(obj == this) return true;
if(!(obj instanceof IconState)) return false;
IconState is = (IconState)obj;
if(!is.name.equals(name)) return false;
if(is.dirs != dirs) return false;
if(is.frames != frames) return false;
if(!Arrays.equals(images, is.images)) return false;
if(is.rewind != rewind) return false;
if(is.loop != loop) return false;
if(!Arrays.equals(delays, is.delays)) return false;
if(!(is.hotspot == null ? hotspot == null : is.hotspot.equals(hotspot))) return false;
if(is.movement != movement) return false;
return true;
}
public String infoStr() {
return "[" + frames + " frame(s), " + dirs + " dir(s)]";
}
public String getDescriptorFragment() {
String s = "";
String q = "\"";
String n = "\n";
s += "state = " + q + name + q + n;
s += "\tdirs = " + dirs + n;
s += "\tframes = " + frames + n;
if(delays != null) {
s += "\tdelay = " + delayArrayToString(delays) + n;
}
if(rewind) {
s += "\trewind = 1\n";
}
if(loop != -1) {
s += "\tloop = " + loop + n;
}
if(hotspot != null) {
s += "\thotspot = " + hotspot + n;
}
if(movement) {
s += "\tmovement = 1\n";
}
return s;
}
private static String delayArrayToString(float[] d) {
String s = "";
for(float f: d) {
s += ","+f;
}
return s.substring(1);
}
/**
* Dump the state to the given OutputStream in PNG format. Frames will be dumped along the X axis of the image, and directions will be dumped along the Y.
*/
public void dumpToPNG(OutputStream outS, int minDir, int maxDir, int minFrame, int maxFrame) {
int totalDirs = maxDir - minDir + 1;
int totalFrames = maxFrame - minFrame + 1;
int w = images[minDir + minFrame * this.dirs].w;
int h = images[minDir + minFrame * this.dirs].h;
if(Main.VERBOSITY > 0) System.out.println("Writing " + totalDirs + " dir(s), " + totalFrames + " frame(s), " + totalDirs*totalFrames + " image(s) total.");
ImageInfo ii = new ImageInfo(totalFrames * w, totalDirs * h, 8, true);
PngWriter out = new PngWriter(outS, ii);
out.setCompLevel(9);
Image[][] img = new Image[totalFrames][totalDirs];
{
for(int i=0; i<totalFrames; i++) {
for(int j=0; j<totalDirs; j++) {
img[i][j] = images[(minDir+j) + (minFrame+i) * this.dirs];
}
}
}
for(int imY=0; imY<totalDirs; imY++) {
for(int pxY=0; pxY<h; pxY++) {
ImageLineInt ili = new ImageLineInt(ii);
int[] buf = ili.getScanline();
for(int imX=0; imX<totalFrames; imX++) {
Image i = img[imX][imY];
for(int pxX=0; pxX<w; pxX++) {
RGBA c = i.getPixel(pxX, pxY);
buf[(imX*w + pxX)*4 ] = c.r;
buf[(imX*w + pxX)*4 + 1] = c.g;
buf[(imX*w + pxX)*4 + 2] = c.b;
buf[(imX*w + pxX)*4 + 3] = c.a;
}
}
out.writeRow(ili);
}
}
out.end();
}
public static IconState importFromPNG(DMI dmi, InputStream inS, String name, float[] delays, boolean rewind, int loop, String hotspot, boolean movement) throws DMIException {
int w = dmi.w;
int h = dmi.h;
PngReader in;
try {
in = new PngReader(inS);
} catch(PngjInputException pie) {
throw new DMIException("Bad file format!", pie);
}
int pxW = in.imgInfo.cols;
int pxH = in.imgInfo.rows;
int frames = pxW / w; //frames are read along the X axis, dirs along the Y, much like export.
int dirs = pxH / h;
// make sure the size is an integer multiple
if(frames * w != pxW || frames==0) throw new DMIException("Illegal image size!");
if(dirs * h != pxH || dirs==0) throw new DMIException("Illegal image size!");
int[][] px = new int[pxH][];
for(int i=0; i<pxH; i++) {
ImageLineInt ili = (ImageLineInt)in.readRow();
int[] sl = ili.getScanline();
px[i] = sl.clone();
}
int channelCount = in.imgInfo.alpha ? 4 : 3;
Image[] images = new Image[frames*dirs];
for(int imageY=0; imageY<dirs; imageY++) {
for(int imageX=0; imageX<frames; imageX++) {
RGBA[][] pixels = new RGBA[h][w];
for(int pixelY=0; pixelY<h; pixelY++) {
for(int pixelX=0; pixelX<w; pixelX++) {
int bY = imageY*h + pixelY;
int bX = imageX*channelCount*w + channelCount*pixelX;
pixels[pixelY][pixelX] = new RGBA(px[bY][bX ],
px[bY][bX + 1],
px[bY][bX + 2],
in.imgInfo.alpha ? px[bY][bX + 3] : 255);
}
}
images[_getIndex(imageY, imageX, dirs)] = new NonPalettedImage(w, h, pixels);
}
}
//public IconState(String name, int dirs, int frames, Image[] images, float[] delays, boolean rewind, int loop, String hotspot, boolean movement) {
return new IconState(name, dirs, frames, images, delays, rewind, loop, hotspot, movement);
}
//Converts a desired dir and frame to an index into the images array.
public int getIndex(int dir, int frame) {
return _getIndex(dir, frame, dirs);
}
private static int _getIndex(int dir, int frame, int totalDirs) {
return dir + frame*totalDirs;
}
public void insertDir(int dir, Image[] splice) {
int maxFrame = frames < splice.length? frames: splice.length;
for(int frameIdx = 0; frameIdx < maxFrame; frameIdx++) {
insertImage(dir, frameIdx, splice[frameIdx]);
}
}
public void insertFrame(int frame, Image[] splice) {
int maxDir = dirs < splice.length? dirs: splice.length;
for(int dirIdx = 0; dirIdx < maxDir; dirIdx++) {
insertImage(dirIdx, frame, splice[dirIdx]);
}
}
public void insertImage(int dir, int frame, Image splice) {
if(frame < 0 || frame >= frames)
throw new IllegalArgumentException("Provided frame is out of range: " + frame);
if(dir < 0 || dir >= dirs)
throw new IllegalArgumentException("Provided dir is out of range: " + dir);
images[getIndex(dir, frame)] = splice;
}
}

View File

@@ -1,126 +0,0 @@
package dmitool;
import java.util.HashMap;
import java.util.HashSet;
public class IconStateDiff {
static class ISAddress {
int dir;
int frame;
public ISAddress(int dir, int frame) {
this.dir = dir;
this.frame = frame;
}
public String infoStr(int maxDir, int maxFrame) {
if(maxDir == 1 && maxFrame == 1) {
return "";
} else if(maxDir == 1) {
return "{" + frame + "}";
} else if(maxFrame == 1) {
return "{" + Main.dirs[dir] + "}";
} else {
return "{" + Main.dirs[dir] + " " + frame + "}";
}
}
}
int oldFrameCount = 0;
int oldDirectionCount = 0;
boolean oldRewind = false;
int oldLoop = -1;
String oldHotspot = null;
int newFrameCount = 0;
int newDirectionCount = 0;
boolean newRewind = false;
int newLoop = -1;
String newHotspot = null;
IconState newState;
HashMap<ISAddress, Image> modifiedFrames = new HashMap<>();
HashMap<ISAddress, Image> newFrames = new HashMap<>();
HashSet<ISAddress> removedFrames = new HashSet<>();
public IconStateDiff(IconState base, IconState mod) {
int maxDir = Math.max(base.dirs, mod.dirs);
int maxFrame = Math.max(base.frames, mod.frames);
oldFrameCount = base.frames;
oldDirectionCount = base.dirs;
oldRewind = base.rewind;
oldLoop = base.loop;
oldHotspot = base.hotspot;
newFrameCount = mod.frames;
newDirectionCount = mod.dirs;
newRewind = mod.rewind;
newLoop = mod.loop;
newHotspot = mod.hotspot;
newState = mod;
Image baseI, modI;
for(int d=0; d<maxDir; d++) {
for(int f=0; f<maxFrame; f++) {
if(base.dirs > d && base.frames > f) {
baseI = base.images[f * base.dirs + d];
} else baseI = null;
if(mod.dirs > d && mod.frames > f) {
modI = mod.images[f * mod.dirs + d];
} else modI = null;
if(baseI == null && modI == null) continue;
if(baseI == null) newFrames.put(new ISAddress(d, f), modI);
else if(modI == null) removedFrames.add(new ISAddress(d, f));
else if(!baseI.equals(modI)) {
modifiedFrames.put(new ISAddress(d, f), modI);
}
}
}
}
@Override public String toString() {
String s = "";
String tmp;
if(newDirectionCount != oldDirectionCount)
s += " | dirs " + oldDirectionCount + "->" + newDirectionCount;
if(newFrameCount != oldFrameCount)
s += " | frames " + oldFrameCount + "->" + newFrameCount;
if(newRewind != oldRewind) {
s += " | rewind " + oldRewind + "->" + newRewind;
}
if(newLoop != oldLoop) {
s += " | loop " + oldLoop + "->" + newLoop;
}
if(newHotspot == null ? oldHotspot != null : !newHotspot.equals(oldHotspot)) {
s += " | hotspot " + oldHotspot + "->" + newHotspot;
}
if(!modifiedFrames.isEmpty()) {
int total_frames = Math.min(oldFrameCount, newFrameCount) * Math.min(oldDirectionCount, newDirectionCount);
tmp = "";
for(ISAddress isa: modifiedFrames.keySet()) {
String str = isa.infoStr(oldDirectionCount, oldFrameCount);
if(!"".equals(str)) {
tmp += ", " + str;
}
}
if(!"".equals(tmp)) {
s += " | modified " + modifiedFrames.size() + " of " + total_frames + ": " + tmp.substring(1);
} else {
s += " | modified " + modifiedFrames.size() + " of " + total_frames;
}
}
if("".equals(s))
return "No change";
return s.substring(3);
}
}

View File

@@ -1,32 +0,0 @@
package dmitool;
import java.io.IOException;
import java.io.OutputStream;
public abstract class Image {
int w, h;
abstract RGBA getPixel(int x, int y);
public Image(int w, int h) {
this.w = w;
this.h = h;
}
@Override public boolean equals(Object obj) {
if(obj == this) return true;
if(!(obj instanceof Image)) return false;
Image im = (Image) obj;
if(w != im.w || h != im.h) return false;
for(int i=0; i<w; i++) {
for(int j=0; j<h; j++) {
if(!getPixel(i, j).equals(im.getPixel(i, j))) return false;
}
}
return true;
}
}

View File

@@ -1,513 +0,0 @@
package dmitool;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Set;
public class Main {
public static int VERBOSITY = 0;
public static boolean STRICT = false;
public static final String VERSION = "v0.6 (7 Jan 2015)";
public static final String[] dirs = new String[] {
"S", "N", "E", "W", "SE", "SW", "NE", "NW"
};
public static final String helpStr =
"help\n" +
"\tthis text\n" +
"version\n" +
"\tprint version and exit\n" +
"verify [file]\n" +
"\tattempt to load the given file to check format\n" +
"info [file]\n" +
"\tprint information about [file], including a list of states\n" +
"diff [file1] [file2]\n" +
"\tdiff between [file1] and [file2]\n" +
"sort [file]\n" +
"\tsort the icon_states in [file] into ASCIIbetical order\n" +
"merge [base] [file1] [file2] [out]\n" +
"\tmerge [file1] and [file2]'s changes from a common ancestor [base], saving the result in [out]\n" +
"\tconflicts will be placed in [out].conflict.dmi\n" +
"extract [file] [state] [out] {args}\n"+
"\textract [state] from [file] in PNG format to [out]\n" +
"\targs specify direction and frame; input 'f' followed by a frame specifier, and/or 'd' followed by a direction specifier\n" +
"\tframe specifier can be a single number or number-number for a range\n" +
"\tdirection specifier can be a single direction, or direction-direction\n" +
"\tdirection can be 0-7 or S, N, E, W, SE, SW, NE, NW (non-case-sensitive)\n" +
"import [file] [state] [in] [options]\n" +
"\timport a PNG image from [in] into [file], with the name [state]\n" +
"\tinput should be in the same format given by the 'extract' command with no direction or frame arguments\n" +
"\t(i.e. frames should be on the x-axis, and directions on the y)\n" +
"\tpossible options:\n" +
"\t nodup | nd | n : if the state [state] already exists in [file], replace it instead of append\n" +
"\t rewind | rw | r : if there is more than one frame, the animation should be played forwards-backwards-forwards-[...]\n" +
"\t loop | lp | l : loop the animation infinitely; equivalent to \"loopn -1\"\n" +
"\t loopn N | lpn N | ln N : loop the animation N times; for infinite animations, use 'loop' or N = -1\n" +
"\t movement | move | mov | m : [state] should be marked as a movement state\n" +
"\t delays L | delay L | del L | d L : use the list L as a comma-separated list of delays (e.g. '1,1,2,2,1')\n" +
"\t hotspot H | hs H | h H : use H as the hotspot for this state\n" +
"\t direction D | dir D : replaces D with the image from [in], instead of the entire state. D can be 0-7 or S, N, E, etc. If the state does not already exist, this is ignored\n" +
"";
public static void main(String[] args) throws FileNotFoundException, IOException, DMIException {
Deque<String> argq = new ArrayDeque<>();
for(String s: args) {
argq.addLast(s);
}
if(argq.size() == 0) {
System.out.println("No command found; use 'help' for help");
return;
}
String switches = argq.peekFirst();
if(switches.startsWith("-")) {
for(char c: switches.substring(1).toCharArray()) {
switch(c) {
case 'v': VERBOSITY++; break;
case 'q': VERBOSITY--; break;
case 'S': STRICT = true; break;
}
}
argq.pollFirst();
}
String op = argq.pollFirst();
switch(op) {
case "diff": {
if(argq.size() < 2) {
System.out.println("Insufficient arguments for command!");
System.out.println(helpStr);
return;
}
String a = argq.pollFirst();
String b = argq.pollFirst();
if(VERBOSITY >= 0) System.out.println("Loading " + a);
DMI dmi = doDMILoad(a);
if(VERBOSITY >= 0) dmi.printInfo();
if(VERBOSITY >= 0) System.out.println("Loading " + b);
DMI dmi2 = doDMILoad(b);
if(VERBOSITY >= 0) dmi2.printInfo();
DMIDiff dmid = new DMIDiff(dmi, dmi2);
System.out.println(dmid);
break;
}
case "sort": {
if(argq.size() < 1) {
System.out.println("Insufficient arguments for command!");
System.out.println(helpStr);
return;
}
String f = argq.pollFirst();
if(VERBOSITY >= 0) System.out.println("Loading " + f);
DMI dmi = doDMILoad(f);
if(VERBOSITY >= 0) dmi.printInfo();
if(VERBOSITY >= 0) System.out.println("Saving " + f);
dmi.writeDMI(new FileOutputStream(f), true);
break;
}
case "merge": {
if(argq.size() < 4) {
System.out.println("Insufficient arguments for command!");
System.out.println(helpStr);
return;
}
String baseF = argq.pollFirst(),
aF = argq.pollFirst(),
bF = argq.pollFirst(),
mergedF = argq.pollFirst();
if(VERBOSITY >= 0) System.out.println("Loading " + baseF);
DMI base = doDMILoad(baseF);
if(VERBOSITY >= 0) base.printInfo();
if(VERBOSITY >= 0) System.out.println("Loading " + aF);
DMI aDMI = doDMILoad(aF);
if(VERBOSITY >= 0) aDMI.printInfo();
if(VERBOSITY >= 0) System.out.println("Loading " + bF);
DMI bDMI = doDMILoad(bF);
if(VERBOSITY >= 0) bDMI.printInfo();
DMIDiff aDiff = new DMIDiff(base, aDMI);
DMIDiff bDiff = new DMIDiff(base, bDMI);
DMIDiff mergedDiff = new DMIDiff();
DMI conflictDMI = new DMI(32, 32);
Set<String> cf = aDiff.mergeDiff(bDiff, conflictDMI, mergedDiff, aF, bF);
mergedDiff.applyToDMI(base);
base.writeDMI(new FileOutputStream(mergedF));
if(!cf.isEmpty()) {
if(VERBOSITY >= 0) for(String s: cf) {
System.out.println(s);
}
conflictDMI.writeDMI(new FileOutputStream(mergedF + ".conflict.dmi"), true);
System.out.println("Add/modify conflicts placed in '" + mergedF + ".conflict.dmi'");
System.exit(1); // Git expects non-zero on merge conflict
} else {
System.out.println("No conflicts");
System.exit(0);
}
break;
}
case "extract": {
if(argq.size() < 3) {
System.out.println("Insufficient arguments for command!");
System.out.println(helpStr);
return;
}
String file = argq.pollFirst(),
state = argq.pollFirst(),
outFile = argq.pollFirst();
DMI dmi = doDMILoad(file);
if(VERBOSITY >= 0) dmi.printInfo();
IconState is = dmi.getIconState(state);
if(is == null) {
System.out.println("icon_state '"+state+"' does not exist!");
return;
}
// minDir, Maxdir, minFrame, Maxframe
int mDir=0, Mdir=is.dirs-1;
int mFrame=0, Mframe=is.frames-1;
while(argq.size() > 1) {
String arg = argq.pollFirst();
switch(arg) {
case "d":
case "dir":
case "dirs":
case "direction":
case "directions":
String dString = argq.pollFirst();
if(dString.contains("-")) {
String[] splitD = dString.split("-");
if(splitD.length == 2) {
mDir = parseDir(splitD[0], is);
Mdir = parseDir(splitD[1], is);
} else {
System.out.println("Illegal dir string: '" + dString + "'!");
return;
}
} else {
mDir = parseDir(dString, is);
Mdir = mDir;
}
// Invalid value check, warnings are printed in parseDir()
if(mDir == -1 || Mdir == -1) return;
if(Mdir < mDir) {
System.out.println("Maximum dir greater than minimum dir!");
System.out.println("Textual direction order is S, N, E, W, SE, SW, NE, NW increasing 0 (S) to 7 (NW)");
return;
}
break;
case "f":
case "frame":
case "frames":
String fString = argq.pollFirst();
if(fString.contains("-")) {
String[] splitF = fString.split("-");
if(splitF.length == 2) {
mFrame = parseFrame(splitF[0], is);
Mframe = parseFrame(splitF[1], is);
} else {
System.out.println("Illegal frame string: '" + fString + "'!");
return;
}
} else {
mFrame = parseFrame(fString, is);
Mframe = mFrame;
}
// Invalid value check, warnings are printed in parseFrame()
if(mFrame == -1 || Mframe == -1) return;
if(Mframe < mFrame) {
System.out.println("Maximum frame greater than minimum frame!");
return;
}
break;
default:
System.out.println("Unknown argument '" + arg + "' detected, ignoring.");
}
}
if(!argq.isEmpty()) {
System.out.println("Extra argument '" + argq.pollFirst() + "' detected, ignoring.");
}
is.dumpToPNG(new FileOutputStream(outFile), mDir, Mdir, mFrame, Mframe);
break;
}
case "import": {
if(argq.size() < 3) {
System.out.println("Insufficient arguments for command!");
System.out.println(helpStr);
return;
}
String dmiFile = argq.pollFirst(),
stateName = argq.pollFirst(),
pngFile = argq.pollFirst();
boolean noDup = false;
boolean rewind = false;
int loop = 0;
boolean movement = false;
String hotspot = null;
float[] delays = null;
String replaceDir = null;
String replaceFrame = null;
while(!argq.isEmpty()) {
String s = argq.pollFirst();
switch(s.toLowerCase()) {
case "nodup":
case "nd":
case "n":
noDup = true;
break;
case "rewind":
case "rw":
case "r":
rewind = true;
break;
case "loop":
case "lp":
case "l":
loop = -1;
break;
case "loopn":
case "lpn":
case "ln":
if(!argq.isEmpty()) {
String loopTimes = argq.pollFirst();
try {
loop = Integer.parseInt(loopTimes);
} catch(NumberFormatException nfe) {
System.out.println("Illegal number '" + loopTimes + "' as argument to '" + s + "'!");
return;
}
} else {
System.out.println("Argument '" + s + "' requires a numeric argument following it!");
return;
}
break;
case "movement":
case "move":
case "mov":
case "m":
movement = true;
break;
case "delays":
case "delay":
case "del":
case "d":
if(!argq.isEmpty()) {
String delaysString = argq.pollFirst();
String[] delaysSplit = delaysString.split(",");
delays = new float[delaysSplit.length];
for(int i=0; i<delaysSplit.length; i++) {
try {
delays[i] = Integer.parseInt(delaysSplit[i]);
} catch(NumberFormatException nfe) {
System.out.println("Illegal number '" + delaysSplit[i] + "' as argument to '" + s + "'!");
return;
}
}
} else {
System.out.println("Argument '" + s + "' requires a list of delays (in the format 'a,b,c,d,[...]') following it!");
return;
}
break;
case "hotspot":
case "hs":
case "h":
if(!argq.isEmpty()) {
hotspot = argq.pollFirst();
} else {
System.out.println("Argument '" + s + "' requires a hotspot string following it!");
return;
}
break;
case "dir":
case "direction":
if(!argq.isEmpty()) {
replaceDir = argq.pollFirst();
} else {
System.out.println("Argument '" + s + "' requires a direction argument following it!");
return;
}
break;
case "f":
case "frame":
if(!argq.isEmpty()) {
replaceFrame = argq.pollFirst();
} else {
System.out.println("Argument '" + s + "' requires a frame argument following it!");
return;
}
break;
default:
System.out.println("Unknown import argument '" + s + "', ignoring.");
break;
}
}
if(VERBOSITY >= 0) System.out.println("Loading " + dmiFile);
DMI toImportTo = doDMILoad(dmiFile);
if(VERBOSITY >= 0) toImportTo.printInfo();
IconState is = IconState.importFromPNG(toImportTo, new FileInputStream(pngFile), stateName, delays, rewind, loop, hotspot, movement);
//image insertion
if(replaceDir != null || replaceFrame != null) {
IconState targetIs = toImportTo.getIconState(stateName);
if(targetIs == null) {
System.out.println("'direction' or 'frame' specified and no icon state '" + stateName + "' found, aborting!");
return;
}
if(is.images.length == 0) {
System.out.println("'direction' or 'frame' specified and imported is empty, aborting!");
return;
}
if(!noDup) targetIs = targetIs.clone();
int dirToReplace, frameToReplace;
if(replaceDir != null && replaceFrame != null) {
frameToReplace = parseFrame(replaceFrame, targetIs);
dirToReplace = parseDir(replaceDir, targetIs);
targetIs.insertImage(dirToReplace, frameToReplace, is.images[0]);
}
else if(replaceDir != null) {
dirToReplace = parseDir(replaceDir, targetIs);
targetIs.insertDir(dirToReplace, is.images);
}
else if(replaceFrame != null) {
frameToReplace = parseFrame(replaceFrame, targetIs);
targetIs.insertFrame(frameToReplace, is.images);
}
if(!noDup) toImportTo.addIconState(null, targetIs);
}
else {
if(noDup) {
if(!toImportTo.setIconState(is)) {
toImportTo.addIconState(null, is);
}
} else {
toImportTo.addIconState(null, is);
}
}
if(VERBOSITY >= 0) toImportTo.printInfo();
if(VERBOSITY >= 0) System.out.println("Saving " + dmiFile);
toImportTo.writeDMI(new FileOutputStream(dmiFile));
break;
}
case "verify": {
if(argq.size() < 1) {
System.out.println("Insufficient arguments for command!");
System.out.println(helpStr);
return;
}
String vF = argq.pollFirst();
if(VERBOSITY >= 0) System.out.println("Loading " + vF);
DMI v = doDMILoad(vF);
if(VERBOSITY >= 0) v.printInfo();
break;
}
case "info": {
if(argq.size() < 1) {
System.out.println("Insufficient arguments for command!");
System.out.println(helpStr);
return;
}
String infoFile = argq.pollFirst();
if(VERBOSITY >= 0) System.out.println("Loading " + infoFile);
DMI info = doDMILoad(infoFile);
info.printInfo();
info.printStateList();
break;
}
case "version":
System.out.println(VERSION);
return;
default:
System.out.println("Command '" + op + "' not found!");
case "help":
System.out.println(helpStr);
break;
}
}
static int parseDir(String s, IconState is) {
try {
int i = Integer.parseInt(s);
if(0 <= i && i < is.dirs) {
return i;
} else {
System.out.println("Direction not in valid range [0, "+(is.dirs-1)+"]!");
return -1;
}
} catch(NumberFormatException nfe) {
for(int q=0; q<dirs.length && q < is.dirs; q++) {
if(dirs[q].equalsIgnoreCase(s)) {
return q;
}
}
String dSummary = "";
for(int i=0; i<is.dirs; i++) {
dSummary += ", " + dirs[i];
}
dSummary = dSummary.substring(2);
System.out.println("Unknown or non-existent direction '" + s + "'!");
System.out.println("Valid range: [0, "+(is.dirs-1)+"], or " + dSummary);
return -1;
}
}
static int parseFrame(String s, IconState is) {
try {
int i = Integer.parseInt(s);
if(0 <= i && i < is.frames) {
return i;
} else {
System.out.println("Frame not in valid range [0, "+(is.frames-1)+"]!");
return -1;
}
} catch(NumberFormatException nfe) {
System.out.println("Failed to parse frame number: '" + s + "'!");
return -1;
}
}
static DMI doDMILoad(String file) {
try {
DMI dmi = new DMI(file);
return dmi;
} catch(DMIException dmie) {
System.out.println("Failed to load " + file + ": " + dmie.getMessage());
} catch(FileNotFoundException fnfe) {
System.out.println("File not found: " + file);
}
System.exit(3);
return null;
}
}

View File

@@ -1,14 +0,0 @@
package dmitool;
public class NonPalettedImage extends Image {
RGBA[][] pixels;
public NonPalettedImage(int w, int h, RGBA[][] pixels) {
super(w, h);
this.pixels = pixels;
}
RGBA getPixel(int x, int y) {
return pixels[y][x];
}
}

View File

@@ -1,27 +0,0 @@
package dmitool;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
public class PNGChunk {
int len;
int type;
byte[] b;
int crc;
public PNGChunk(DataInputStream in) throws IOException {
len = in.readInt();
type = in.readInt();
b = new byte[len];
in.read(b);
crc = in.readInt();
}
void write(DataOutputStream out) throws IOException {
out.writeInt(len);
out.writeInt(type);
out.write(b);
out.writeInt(crc);
}
}

View File

@@ -1,16 +0,0 @@
package dmitool;
public class PalettedImage extends Image {
int[][] pixels;
RGBA[] pal;
public PalettedImage(int w, int h, int[][] pixels, RGBA[] palette) {
super(w, h);
this.pixels = pixels;
this.pal = palette;
}
RGBA getPixel(int x, int y) {
return pal[pixels[y][x]];
}
}

View File

@@ -1,33 +0,0 @@
package dmitool;
public class RGBA {
int r, g, b, a;
public RGBA(int r, int g, int b, int a) {
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
@Override
public String toString() {
String s = Long.toString(toRGBA8888());
while(s.length() < 8)
s = "0" + s;
return "#" + s;
}
@Override public boolean equals(Object obj) {
if(obj == this) return true;
if(!(obj instanceof RGBA)) return false;
RGBA o = (RGBA) obj;
return r==o.r && g==o.g && b==o.b && a==o.a;
}
public long toRGBA8888() {
return (r<<24) | (g<<16) | (b<<8) | a;
}
}

0
tools/expand_filedir_paths.py Normal file → Executable file
View File

View File

@@ -5,7 +5,7 @@ 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 `install.sh` (Unix-like) as appropriate.
`Install.bat` (Windows) or `tools/hooks/install` (Unix-like) as appropriate.
Hooks expect a Unix-like environment on the backend. Usually this is handled
automatically by GUI tools like TortoiseGit and GitHub for Windows, but

2
tools/hooks/Uninstall.bat Executable file
View File

@@ -0,0 +1,2 @@
@call "%~dp0\..\bootstrap\python" -m hooks.install --uninstall %*
@pause

2
tools/hooks/dmi.merge Normal file → Executable file
View File

@@ -1,2 +1,2 @@
#!/bin/sh
exec tools/hooks/python.sh -m merge_driver_dmi "$@"
exec tools/bootstrap/python -m dmi.merge_driver "$@"

View File

@@ -1,16 +1,2 @@
@echo off
cd %~dp0
for %%f in (*.hook) do (
echo Installing hook: %%~nf
copy %%f ..\..\.git\hooks\%%~nf >nul
)
for %%f in (*.merge) do (
echo Installing merge driver: %%~nf
echo [merge "%%~nf"]^
driver = tools/hooks/%%f %%P %%O %%A %%B %%L >> ..\..\.git\config
)
echo Installing Python dependencies
python -m pip install -r ..\mapmerge2\requirements.txt
echo Done
pause
@call "%~dp0\..\bootstrap\python" -m hooks.install %*
@pause

101
tools/hooks/install.py Executable file
View File

@@ -0,0 +1,101 @@
#!/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))

22
tools/hooks/install.sh Normal file → Executable file
View File

@@ -1,20 +1,2 @@
#!/bin/bash
set -e
shopt -s nullglob
cd "$(dirname "$0")"
for f in *.hook; do
echo Installing hook: ${f%.hook}
cp $f ../../.git/hooks/${f%.hook}
done
for f in *.merge; do
echo Installing merge driver: ${f%.merge}
git config --replace-all merge.${f%.merge}.driver "tools/hooks/$f %P %O %A %B %L"
done
echo "Installing tgui hooks"
../../tgui/bin/tgui --install-git-hooks
echo "Installing Python dependencies"
./python.sh -m pip install -r ../mapmerge2/requirements.txt
echo "Done"
#!/bin/sh
exec "$(dirname "$0")/../bootstrap/python" -m hooks.install "$@"

169
tools/hooks/merge_frontend.py Executable file
View File

@@ -0,0 +1,169 @@
# 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

3
tools/hooks/pre-commit.hook Normal file → Executable file
View File

@@ -1,3 +1,2 @@
#!/bin/sh
# `sh` must be used here instead of `bash` to support GitHub Desktop.
exec tools/hooks/python.sh -m precommit
exec tools/bootstrap/python -m mapmerge2.precommit

39
tools/hooks/python.sh Normal file → Executable file
View File

@@ -1,32 +1,17 @@
#!/bin/sh
# `sh` must be used here instead of `bash` to support GitHub Desktop.
set -e
# Strip the "App Execution Aliases" from $PATH. Even if the user installed
# Python using the Windows Store on purpose, these aliases always generate
# "Permission denied" errors when sh.exe tries to invoke them.
PATH=$(echo "$PATH" | tr ":" "\n" | grep -v "AppData/Local/Microsoft/WindowsApps" | tr "\n" ":")
# Try to find a Python executable.
if command -v python3 >/dev/null 2>&1; then
PY=python3
elif command -v python >/dev/null 2>&1; then
PY=python
elif command -v py >/dev/null 2>&1; then
PY="py -3"
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 "Please install Python from https://www.python.org/downloads/"
echo "tools/hooks/python.sh is replaced by tools/bootstrap/python"
echo "Details: https://github.com/tgstation/tgstation/pull/55658"
exit 1
fi
# Deduce the path separator and add the mapmerge package to the search path.
PATHSEP=$($PY - <<'EOF'
import sys, os
if sys.version_info.major != 3 or sys.version_info.minor < 6:
sys.stderr.write("Python 3.6 or later is required, but you have:\n" + sys.version + "\n")
exit(1)
print(os.pathsep)
EOF
)
export PYTHONPATH=tools/mapmerge2/${PATHSEP}${PYTHONPATH}
exec $PY "$@"

0
tools/localhost-asset-webroot-server.py Normal file → Executable file
View File

4
tools/makeChangelog.bat Normal file → Executable file
View File

@@ -1,4 +1,4 @@
@echo off
rem Cheridan asked for this. - N3X
call python ss13_genchangelog.py ../html/changelog.html ../html/changelogs
pause
call "%~dp0\bootstrap\python" ss13_genchangelog.py ../html/changelog.html ../html/changelogs
pause

View File

@@ -15,11 +15,13 @@ contains the desired changes.
## Installation
To install Python dependencies, run `requirements-install.bat`, or run
`python -m pip install -r requirements.txt` directly. See the [Git hooks]
documentation to install the Git pre-commit hook which runs the map merger
automatically, or use `tools/mapmerge/Prepare Maps.bat` to save backups before
running `mapmerge.bat`.
To install the Git hooks, open the `tools/hooks/` folder and double-click
`Install.bat` (Linux users run `tools/hooks/install`).
To use Map Merge manually, such as when using a Git GUI which is incompatible
with some of the hooks, double-click the `.bat` files at the appropriate time.
A private copy of Python and any dependencies will be installed automatically.
For up-to-date installation and detailed troubleshooting instructions, visit
the [Map Merger] wiki article.

3
tools/mapmerge2/convert.py Normal file → Executable file
View File

@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import frontend
import dmm
from . import frontend, dmm
if __name__ == '__main__':
settings = frontend.read_settings()

2
tools/mapmerge2/dmm.py Normal file → Executable file
View File

@@ -545,4 +545,4 @@ def _parse(map_raw_text):
data = DMM(key_length, Coordinate(maxx, maxy, maxz))
data.dictionary = dictionary
data.grid = grid
return data
return data

4
tools/mapmerge2/dmm2tgm.bat Normal file → Executable file
View File

@@ -1,5 +1,5 @@
@echo off
set MAPROOT=../../_maps/
set MAPROOT=%~dp0/../../_maps/
set TGM=1
python convert.py
call "%~dp0\..\bootstrap\python" -m mapmerge2.convert %*
pause

4
tools/mapmerge2/mapmerge.bat Normal file → Executable file
View File

@@ -1,5 +1,5 @@
@echo off
set MAPROOT=../../_maps/
set MAPROOT=%~dp0/../../_maps/
set TGM=1
python mapmerge.py
call "%~dp0\..\bootstrap\python" -m mapmerge2.mapmerge %*
pause

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3
import frontend
import shutil
from dmm import *
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:

4
tools/mapmerge2/precommit.py Normal file → Executable file
View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3
import os
import pygit2
import dmm
from mapmerge import merge_map
from . import dmm
from .mapmerge import merge_map
def main(repo):
if repo.index.conflicts:

View File

@@ -1,3 +0,0 @@
@echo off
python -m pip install -r requirements.txt
pause

View File

@@ -1,3 +0,0 @@
pygit2==1.0.1
bidict==0.13.1
Pillow==7.1.0

4
tools/mapmerge2/tgm2dmm.bat Normal file → Executable file
View File

@@ -1,5 +1,5 @@
@echo off
set MAPROOT=../../_maps/
set MAPROOT=%~dp0/../../_maps/
set TGM=0
python convert.py
call "%~dp0\..\bootstrap\python" -m mapmerge2.convert %*
pause

0
tools/minibot/nudge.py Normal file → Executable file
View File

7
tools/requirements.txt Normal file
View File

@@ -0,0 +1,7 @@
pygit2==1.0.1
bidict==0.13.1
Pillow==7.2.0
# changelogs
PyYaml==5.3.1
beautifulsoup4==4.9.3

Some files were not shown because too many files have changed in this diff Show More