").append(m.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,e||[a.responseText,b,a])}),this},m.expr.filters.animated=function(a){return m.grep(m.timers,function(b){return a===b.elem}).length};var cd=a.document.documentElement;function dd(a){return m.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}m.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=m.css(a,"position"),l=m(a),n={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=m.css(a,"top"),i=m.css(a,"left"),j=("absolute"===k||"fixed"===k)&&m.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),m.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(n.top=b.top-h.top+g),null!=b.left&&(n.left=b.left-h.left+e),"using"in b?b.using.call(a,n):l.css(n)}},m.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){m.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,m.contains(b,e)?(typeof e.getBoundingClientRect!==K&&(d=e.getBoundingClientRect()),c=dd(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===m.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),m.nodeName(a[0],"html")||(c=a.offset()),c.top+=m.css(a[0],"borderTopWidth",!0),c.left+=m.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-m.css(d,"marginTop",!0),left:b.left-c.left-m.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||cd;while(a&&!m.nodeName(a,"html")&&"static"===m.css(a,"position"))a=a.offsetParent;return a||cd})}}),m.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);m.fn[a]=function(d){return V(this,function(a,d,e){var f=dd(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?m(f).scrollLeft():e,c?e:m(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),m.each(["top","left"],function(a,b){m.cssHooks[b]=Lb(k.pixelPosition,function(a,c){return c?(c=Jb(a,b),Hb.test(c)?m(a).position()[b]+"px":c):void 0})}),m.each({Height:"height",Width:"width"},function(a,b){m.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){m.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return V(this,function(b,c,d){var e;return m.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?m.css(b,c,g):m.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),m.fn.size=function(){return this.length},m.fn.andSelf=m.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return m});var ed=a.jQuery,fd=a.$;return m.noConflict=function(b){return a.$===m&&(a.$=fd),b&&a.jQuery===m&&(a.jQuery=ed),m},typeof b===K&&(a.jQuery=a.$=m),m});
\ No newline at end of file
diff --git a/tools/LinuxOneShot/SetupProgram/PreCompile.sh b/tools/LinuxOneShot/SetupProgram/PreCompile.sh
index ba8859699b..328c2a3aef 100755
--- a/tools/LinuxOneShot/SetupProgram/PreCompile.sh
+++ b/tools/LinuxOneShot/SetupProgram/PreCompile.sh
@@ -34,12 +34,12 @@ if ! { [ -x "$has_git" ] && [ -x "$has_grep" ] && [ -f "/usr/lib/i386-linux-gnu/
if ! [ -x "$has_sudo" ]; then
dpkg --add-architecture i386
apt-get update
- apt-get install -y git libssl-dev:i386 grep mysql-client
+ apt-get install -y git libssl-dev:i386 grep mysql-client lib32z1 pkg-config libssl-dev
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 apt-get install -y git libssl-dev:i386 grep mysql-client lib32z1 pkg-config libssl-dev
sudo rm -rf /var/lib/apt/lists/*
fi
fi
@@ -58,10 +58,17 @@ 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"
+env PKG_CONFIG_ALLOW_CROSS=1 ~/.cargo/bin/cargo build --release --target=i686-unknown-linux-gnu
+mv target/i686-unknown-linux-gnu/release/librust_g.so "$1/librust_g.so"
cd ..
+# compile tgui
+echo "Compiling tgui..."
+cd "$1"
+chmod +x tools/bootstrap/node # Workaround for https://github.com/tgstation/tgstation-server/issues/1167
+env TG_BOOTSTRAP_CACHE="$original_dir" TG_BOOTSTRAP_NODE_LINUX=1 TG_BUILD_TGS_MODE=1 tools/bootstrap/node tools/build/build.js
+cd "$original_dir"
+
if [ ! -d "../GameStaticFiles/config" ]; then
echo "Creating initial config..."
cp -r "$1/config" "../GameStaticFiles/config"
@@ -76,3 +83,10 @@ if [ "$DATABASE_EXISTS" != "ss13_db" ]; then
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
+
+# compile tgui
+echo "Compiling tgui..."
+cd "$1"
+chmod +x tools/bootstrap/node # Workaround for https://github.com/tgstation/tgstation-server/issues/1167
+chmod +x tgui/bin/tgui
+env TG_BOOTSTRAP_CACHE="$original_dir" TG_BOOTSTRAP_NODE_LINUX=1 TG_BUILD_TGS_MODE=1 tools/bootstrap/node tools/build/build.js
\ No newline at end of file
diff --git a/tools/LinuxOneShot/SetupProgram/SetupProgram.csproj b/tools/LinuxOneShot/SetupProgram/SetupProgram.csproj
index a52352cf6e..294d40cb17 100644
--- a/tools/LinuxOneShot/SetupProgram/SetupProgram.csproj
+++ b/tools/LinuxOneShot/SetupProgram/SetupProgram.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/tools/LinuxOneShot/TGS_Config/appsettings.Production.json b/tools/LinuxOneShot/TGS_Config/appsettings.Production.json
index c963584b17..13d4911afc 100644
--- a/tools/LinuxOneShot/TGS_Config/appsettings.Production.json
+++ b/tools/LinuxOneShot/TGS_Config/appsettings.Production.json
@@ -18,12 +18,5 @@
"Enable": true,
"AllowAnyOrigin": false,
"AllowedOrigins": null
- },
- "Kestrel": {
- "EndPoints": {
- "Http": {
- "Url": "http://0.0.0.0:80"
- }
- }
}
}
diff --git a/tools/UpdatePaths/__main__.py b/tools/UpdatePaths/__main__.py
index ba2da1ea04..23b7b328b4 100644
--- a/tools/UpdatePaths/__main__.py
+++ b/tools/UpdatePaths/__main__.py
@@ -140,7 +140,7 @@ def update_map(map_filepath, updates, verbose=False):
dmm_data = DMM.from_file(map_filepath)
for update_string in updates:
update_path(dmm_data, update_string, verbose)
- dmm_data.to_file(map_filepath, True)
+ dmm_data.to_file(map_filepath)
def update_all_maps(map_directory, updates, verbose=False):
diff --git a/tools/WebhookProcessor/github_webhook_processor.php b/tools/WebhookProcessor/github_webhook_processor.php
index 035d777feb..b4609c3a03 100644
--- a/tools/WebhookProcessor/github_webhook_processor.php
+++ b/tools/WebhookProcessor/github_webhook_processor.php
@@ -262,8 +262,6 @@ function tag_pr($payload, $opened) {
$tags[] = $tag;
check_tag_and_replace($payload, '[dnm]', 'Do Not Merge', $tags);
- if(!check_tag_and_replace($payload, '[wip]', 'Work In Progress', $tags) && check_tag_and_replace($payload, '[ready]', 'Work In Progress', $remove))
- $tags[] = 'Needs Review';
return array($tags, $remove);
}
@@ -287,81 +285,11 @@ function get_reviews($payload){
return json_decode(github_apisend($payload['pull_request']['url'] . '/reviews'), true);
}
-function check_ready_for_review($payload, $labels = null, $remove = array()){
- $r4rlabel = 'Needs Review';
- $labels_which_should_not_be_ready = array('Do Not Merge', 'Work In Progress', 'Merge Conflict');
- $has_label_already = false;
- $should_not_have_label = false;
- if($labels == null)
- $labels = get_labels($payload);
- $returned = array($labels, $remove);
- //if the label is already there we may need to remove it
- foreach($labels as $L){
- if(in_array($L, $labels_which_should_not_be_ready))
- $should_not_have_label = true;
- if($L == $r4rlabel)
- $has_label_already = true;
- }
-
- if($has_label_already && $should_not_have_label){
- $remove[] = $r4rlabel;
- return $returned;
- }
-
- //find all reviews to see if changes were requested at some point
- $reviews = get_reviews($payload);
-
- $reviews_ids_with_changes_requested = array();
- $dismissed_an_approved_review = false;
-
- foreach($reviews as $R)
- if(is_maintainer($payload, $R['user']['login'])){
- $lower_state = strtolower($R['state']);
- if($lower_state == 'changes_requested')
- $reviews_ids_with_changes_requested[] = $R['id'];
- else if ($lower_state == 'approved'){
- dismiss_review($payload, $R['id'], 'Out of date review');
- $dismissed_an_approved_review = true;
- }
- }
-
- if(!$dismissed_an_approved_review && count($reviews_ids_with_changes_requested) == 0){
- if($has_label_already)
- $remove[] = $r4rlabel;
- return $returned; //no need to be here
- }
-
- if(count($reviews_ids_with_changes_requested) > 0){
- //now get the review comments for the offending reviews
-
- $review_comments = json_decode(github_apisend($payload['pull_request']['review_comments_url']), true);
-
- foreach($review_comments as $C){
- //make sure they are part of an offending review
- if(!in_array($C['pull_request_review_id'], $reviews_ids_with_changes_requested))
- continue;
-
- //review comments which are outdated have a null position
- if($C['position'] !== null){
- if($has_label_already)
- $remove[] = $r4rlabel;
- return $returned; //no need to tag
- }
- }
- }
-
- //finally, add it if necessary
- if(!$has_label_already){
- $labels[] = $r4rlabel;
- }
- return $returned;
-}
-
function check_dismiss_changelog_review($payload){
- global $require_changelog;
+ global $require_changelogs;
global $no_changelog;
- if(!$require_changelog)
+ if(!$require_changelogs)
return;
if(!$no_changelog)
@@ -406,8 +334,6 @@ function handle_pr($payload) {
check_dismiss_changelog_review($payload);
case 'synchronize':
list($labels, $remove) = tag_pr($payload, false);
- if($payload['action'] == 'synchronize')
- list($labels, $remove) = check_ready_for_review($payload, $labels, $remove);
set_labels($payload, $labels, $remove);
return;
case 'reopened':
@@ -647,15 +573,16 @@ function get_pr_code_friendliness($payload, $oldbalance = null){
'Refactor' => 10,
'Code Improvement' => 2,
'Grammar and Formatting' => 1,
+ 'Quality of Life' => 1,
'Priority: High' => 15,
'Priority: CRITICAL' => 20,
'Unit Tests' => 6,
'Logging' => 1,
'Feedback' => 2,
'Performance' => 12,
+ 'Atomic' => 2,
'Feature' => -10,
'Balance/Rebalance' => -8,
- 'Tweak' => -2,
'Sound' => 1,
'Sprites' => 1,
'GBP: Reset' => $startingPRBalance - $oldbalance,
@@ -676,8 +603,7 @@ function get_pr_code_friendliness($payload, $oldbalance = null){
}
}
- $affecting = abs($maxNegative) >= $maxPositive ? $maxNegative : $maxPositive;
- return $affecting;
+ return $maxNegative + $maxPositive;
}
function is_maintainer($payload, $author){
@@ -819,6 +745,8 @@ function checkchangelog($payload, $compile = true) {
}
if (!strlen($firstword)) {
+ if (count($currentchangelogblock) <= 0)
+ continue;
$currentchangelogblock[count($currentchangelogblock)-1]['body'] .= "\n";
continue;
}
@@ -840,12 +768,10 @@ function checkchangelog($payload, $compile = true) {
$currentchangelogblock[] = array('type' => 'bugfix', 'body' => $item);
}
break;
- case 'rsctweak':
- case 'tweaks':
- case 'tweak':
- if($item != 'tweaked a few things') {
- $tags[] = 'Tweak';
- $currentchangelogblock[] = array('type' => 'tweak', 'body' => $item);
+ case 'qol':
+ if($item != 'made something easier to use') {
+ $tags[] = 'Quality of Life';
+ $currentchangelogblock[] = array('type' => 'qol', 'body' => $item);
}
break;
case 'soundadd':
diff --git a/tools/deploy.sh b/tools/deploy.sh
index 1729ca493c..d09b606a86 100755
--- a/tools/deploy.sh
+++ b/tools/deploy.sh
@@ -32,4 +32,6 @@ cp -r strings/* $1/strings/
#find $1/_maps -name "*.dm" -type f -delete
#dlls on windows
-cp *.dll $1/ || true
+if [ "$(uname -o)" = "Msys" ]; then
+ cp ./*.dll $1/
+fi
diff --git a/tools/dmi/test.py b/tools/dmi/test.py
index 057e47c0a3..09629927d8 100755
--- a/tools/dmi/test.py
+++ b/tools/dmi/test.py
@@ -19,7 +19,7 @@ def _self_test():
raise
count += 1
- print(f"Successfully parsed {count} dmi files")
+ print(f"{os.path.relpath(__file__)}: successfully parsed {count} .dmi files")
def _usage():
diff --git a/tools/hooks/README.md b/tools/hooks/README.md
index 058ef04d61..c710590397 100644
--- a/tools/hooks/README.md
+++ b/tools/hooks/README.md
@@ -1,41 +1,36 @@
# Git Integration Hooks
-This folder contains installable scripts for [Git hooks] and [merge drivers].
+This folder contains installable scripts for Git [hooks] and [merge drivers].
Use of these hooks and drivers is optional and they must be installed
explicitly before they take effect.
To install the current set of hooks, or update if new hooks are added, run
-`Install.bat` (Windows) or `tools/hooks/install` (Unix-like) as appropriate.
+`Install.bat` (Windows) or `tools/hooks/install.sh` (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
-[Git for Windows] is an option if you prefer to use a CLI even on Windows.
+If your Git GUI does not support a given hook, there is usually a `.bat` file
+or other script you can run instead - see the links below for details.
-## Current Hooks
+## Hooks
-* **Pre-commit**: Runs [mapmerge2] on changed maps, if any.
-* **DMI merger**: Attempts to [fix icon conflicts] when performing a git merge.
- If it succeeds, the file is marked merged. If it fails, it logs what states
- are still in conflict and adds them to the .dmi file, where the desired
- resolution can be chosen.
+* **Pre-commit**: Runs [mapmerge2] to reduce the diff on any changed maps.
+* **DMI merger**: Attempts to [fix icon conflicts] when performing a Git merge.
+* **DMM merger**: Attempts to [fix map conflicts] when performing a Git merge.
## Adding New Hooks
-New [Git hooks] may be added by creating a file named `
.hook` in
+New Git [hooks] may be added by creating a file named `.hook` in
this directory. Git determines what hooks are available and what their names
-are. The install script copies the `.hook` file into `.git/hooks`, so editing
-the `.hook` file will require a reinstall.
+are.
-New [merge drivers] may be added by adding a shell script named `.merge`
+New Git [merge drivers] may be added by adding a shell script named `.merge`
and updating `.gitattributes` in the root of the repository to include the line
-`*. merge=`. The install script will set up the merge driver to point
-to the `.merge` file directly, and editing it will not require a reinstall.
+`*. merge=`.
-`tools/hooks/python.sh` may be used as a trampoline to ensure that the correct
-version of Python is found.
+Adding or removing hooks or merge drivers requires running the install script
+again, but modifying them does not. See existing `.hook` and `.merge` files for examples.
-[Git hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
+[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
[merge drivers]: https://git-scm.com/docs/gitattributes#_performing_a_three_way_merge
-[Git for Windows]: https://gitforwindows.org/
[mapmerge2]: ../mapmerge2/README.md
-[fix icon conflicts]: ../mapmerge2/merge_driver_dmi.py
+[fix icon conflicts]: https://tgstation13.org/wiki/Resolving_icon_conflicts
+[fix map conflicts]: https://tgstation13.org/wiki/Map_Merger
diff --git a/tools/hooks/dmm.merge b/tools/hooks/dmm.merge
new file mode 100644
index 0000000000..64fa16b502
--- /dev/null
+++ b/tools/hooks/dmm.merge
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec tools/bootstrap/python -m mapmerge2.merge_driver "$@"
diff --git a/tools/mapmerge2/I Forgot To Map Merge.bat b/tools/mapmerge2/I Forgot To Map Merge.bat
new file mode 100644
index 0000000000..952b9fe255
--- /dev/null
+++ b/tools/mapmerge2/I Forgot To Map Merge.bat
@@ -0,0 +1,7 @@
+@echo off
+echo Installing hooks for next time...
+call "%~dp0\..\bootstrap\python.bat" -m hooks.install
+echo.
+echo Fixing things up...
+call "%~dp0\..\bootstrap\python.bat" -m mapmerge2.fixup
+pause
diff --git a/tools/mapmerge2/Prepare Maps.bat b/tools/mapmerge2/Prepare Maps.bat
deleted file mode 100644
index 6955e9c877..0000000000
--- a/tools/mapmerge2/Prepare Maps.bat
+++ /dev/null
@@ -1,12 +0,0 @@
-@echo off
-cd ../../_maps/
-
-for /R %%f in (*.dmm) do copy "%%f" "%%f.backup"
-
-cls
-echo All dmm files in _maps directories have been backed up
-echo Now you can make your changes...
-echo ---
-echo Remember to run mapmerge.bat just before you commit your changes!
-echo ---
-pause
diff --git a/tools/mapmerge2/README.md b/tools/mapmerge2/README.md
index cbe4c30c4e..71164bdb71 100644
--- a/tools/mapmerge2/README.md
+++ b/tools/mapmerge2/README.md
@@ -1,8 +1,27 @@
-# Map Merge 2
+# Map Merger
-**Map Merge 2** is an improvement over previous map merging scripts, with
-better merge-conflict prevention, multi-Z support, and automatic handling of
-key overflow. For up-to-date tips and tricks, also visit the [Map Merger] wiki article.
+The **Map Merger** is a collection of scripts that keep this repository's maps
+in a format which is easier to track in Git and less likely to cause merge
+conflicts. When merge conflicts do occur, it can sometimes resolve them.
+
+For detailed troubleshooting instructions and other tips, visit the
+[Map Merger] wiki article.
+
+## Installation
+
+To install the [Git hooks], open the `tools/hooks/` folder and double-click
+`Install.bat`. Linux users run `tools/hooks/install.sh`.
+
+## Manual Use
+
+If using a Git GUI which is not compatible with the hooks:
+
+* Before committing, double-click `Run Before Committing.bat`
+* When a merge has map conflicts, double-click `Resolve Map Conflicts.bat`
+
+The console will show whether the operation succeeded.
+
+For more details, see the [Map Merger] wiki article.
## What Map Merging Is
@@ -13,18 +32,8 @@ version of the map while maintaining all the actual changes. It requires an old
version of the map to use as a reference and a new version of the map which
contains the desired changes.
-## Installation
-
-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.
+Map Merge 2 adds multi-Z support, automatic handling of key overflow, better
+merge conflict prevention, and a real merge conflict resolver.
## Code Structure
diff --git a/tools/mapmerge2/Resolve Map Conflicts.bat b/tools/mapmerge2/Resolve Map Conflicts.bat
new file mode 100644
index 0000000000..cd4b95834b
--- /dev/null
+++ b/tools/mapmerge2/Resolve Map Conflicts.bat
@@ -0,0 +1,2 @@
+@call "%~dp0\..\bootstrap\python.bat" -m mapmerge2.merge_driver --posthoc %*
+@pause
diff --git a/tools/mapmerge2/Run Before Committing.bat b/tools/mapmerge2/Run Before Committing.bat
new file mode 100644
index 0000000000..e989edb7b5
--- /dev/null
+++ b/tools/mapmerge2/Run Before Committing.bat
@@ -0,0 +1,2 @@
+@call "%~dp0\..\bootstrap\python" -m mapmerge2.precommit --use-workdir %*
+@pause
diff --git a/tools/mapmerge2/dmm.py b/tools/mapmerge2/dmm.py
index f66f2b8edf..2d87cfc172 100755
--- a/tools/mapmerge2/dmm.py
+++ b/tools/mapmerge2/dmm.py
@@ -22,20 +22,19 @@ class DMM:
@staticmethod
def from_file(fname):
- # stream the file rather than forcing all its contents to memory
with open(fname, 'r', encoding=ENCODING) as f:
- return _parse(iter(lambda: f.read(1), ''))
+ return _parse(f.read())
@staticmethod
def from_bytes(bytes):
return _parse(bytes.decode(ENCODING))
- def to_file(self, fname, tgm = True):
+ def to_file(self, fname, *, tgm = True):
self._presave_checks()
with open(fname, 'w', newline='\n', encoding=ENCODING) as f:
(save_tgm if tgm else save_dmm)(self, f)
- def to_bytes(self, tgm = True):
+ def to_bytes(self, *, tgm = True):
self._presave_checks()
bio = io.BytesIO()
with io.TextIOWrapper(bio, newline='\n', encoding=ENCODING) as f:
@@ -43,20 +42,29 @@ class DMM:
f.flush()
return bio.getvalue()
+ def get_or_generate_key(self, tile):
+ try:
+ return self.dictionary.inv[tile]
+ except KeyError:
+ key = self.generate_new_key()
+ self.dictionary[key] = tile
+ return key
+
+ def get_tile(self, coord):
+ return self.dictionary[self.grid[coord]]
+
+ def set_tile(self, coord, tile):
+ tile = tuple(tile)
+ self.grid[coord] = self.get_or_generate_key(tile)
+
def generate_new_key(self):
free_keys = self._ensure_free_keys(1)
+ max_key = max_key_for(self.key_length)
# choose one of the free keys at random
- key = 0
- while free_keys:
- if key not in self.dictionary:
- # this construction is used to avoid needing to construct the
- # full set in order to random.choice() from it
- if random.random() < 1 / free_keys:
- return key
- free_keys -= 1
- key += 1
-
- raise RuntimeError("ran out of keys, this shouldn't happen")
+ key = random.randint(0, max_key - 1)
+ while key in self.dictionary:
+ key = random.randint(0, max_key - 1)
+ return key
def overwrite_key(self, key, fixed, bad_keys):
try:
@@ -125,6 +133,9 @@ class DMM:
for x in range(1, self.size.x + 1):
yield (y, x)
+ def __repr__(self):
+ return f"DMM(size={self.size}, key_length={self.key_length}, dictionary_size={len(self.dictionary)})"
+
# ----------
# key handling
@@ -227,10 +238,8 @@ def is_bad_atom_ordering(key, atoms):
print(f"Warning: key '{key}' is missing either a turf or area")
return can_fix
-def fix_atom_ordering(atoms):
- movables = []
- turfs = []
- areas = []
+def split_atom_groups(atoms):
+ movables, turfs, areas = [], [], []
for each in atoms:
if each.startswith('/turf'):
turfs.append(each)
@@ -238,6 +247,10 @@ def fix_atom_ordering(atoms):
areas.append(each)
else:
movables.append(each)
+ return movables, turfs, areas
+
+def fix_atom_ordering(atoms):
+ movables, turfs, areas = split_atom_groups(atoms)
movables.extend(turfs)
movables.extend(areas)
return movables
@@ -287,7 +300,7 @@ def save_tgm(dmm, output):
output.write("\n")
for x in range(1, max_x + 1):
output.write(f"({x},{1},{z}) = {{\"\n")
- for y in range(1, max_y + 1):
+ for y in range(max_y, 0, -1):
output.write(f"{num_to_key(dmm.grid[x, y, z], dmm.key_length)}\n")
output.write("\"}\n")
@@ -309,7 +322,7 @@ def save_dmm(dmm, output):
for z in range(1, max_z + 1):
output.write(f"(1,1,{z}) = {{\"\n")
- for y in range(1, max_y + 1):
+ for y in range(max_y, 0, -1):
for x in range(1, max_x + 1):
try:
output.write(num_to_key(dmm.grid[x, y, z], dmm.key_length))
@@ -542,7 +555,17 @@ def _parse(map_raw_text):
if curr_y > maxy:
maxy = curr_y
+ if not grid:
+ # Usually caused by unbalanced quotes.
+ max_key = num_to_key(max(dictionary.keys()), key_length, True)
+ raise ValueError(f"dmm failed to parse, check for a syntax error near or after key {max_key!r}")
+
+ # Convert from raw .dmm coordinates to DM/BYOND coordinates by flipping Y
+ grid2 = dict()
+ for (x, y, z), tile in grid.items():
+ grid2[x, maxy + 1 - y, z] = tile
+
data = DMM(key_length, Coordinate(maxx, maxy, maxz))
data.dictionary = dictionary
- data.grid = grid
+ data.grid = grid2
return data
diff --git a/tools/mapmerge2/dmm2tgm.bat b/tools/mapmerge2/dmm2tgm.bat
deleted file mode 100755
index 76084281a1..0000000000
--- a/tools/mapmerge2/dmm2tgm.bat
+++ /dev/null
@@ -1,5 +0,0 @@
-@echo off
-set MAPROOT=%~dp0/../../_maps/
-set TGM=1
-call "%~dp0\..\bootstrap\python" -m mapmerge2.convert %*
-pause
diff --git a/tools/mapmerge2/dmm_test.py b/tools/mapmerge2/dmm_test.py
new file mode 100644
index 0000000000..02f5383f2a
--- /dev/null
+++ b/tools/mapmerge2/dmm_test.py
@@ -0,0 +1,39 @@
+import os
+import sys
+from .dmm import *
+
+
+def _self_test():
+ # test: can we load every DMM in the tree
+ count = 0
+ for dirpath, dirnames, filenames in os.walk('.'):
+ if '.git' in dirnames:
+ dirnames.remove('.git')
+ for filename in filenames:
+ if filename.endswith('.dmm'):
+ fullpath = os.path.join(dirpath, filename)
+ try:
+ DMM.from_file(fullpath)
+ except Exception:
+ print('Failed on:', fullpath)
+ raise
+ count += 1
+
+ print(f"{os.path.relpath(__file__)}: successfully parsed {count} .dmm files")
+
+
+def _usage():
+ print(f"Usage:")
+ print(f" tools{os.sep}bootstrap{os.sep}python -m {__spec__.name}")
+ exit(1)
+
+
+def _main():
+ if len(sys.argv) == 1:
+ return _self_test()
+
+ return _usage()
+
+
+if __name__ == '__main__':
+ _main()
diff --git a/tools/mapmerge2/fixup.py b/tools/mapmerge2/fixup.py
new file mode 100644
index 0000000000..f3133d1d73
--- /dev/null
+++ b/tools/mapmerge2/fixup.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python3
+import os
+import pygit2
+from . import dmm
+from .mapmerge import merge_map
+
+
+STATUS_INDEX = (pygit2.GIT_STATUS_INDEX_NEW
+ | pygit2.GIT_STATUS_INDEX_MODIFIED
+ | pygit2.GIT_STATUS_INDEX_DELETED
+ | pygit2.GIT_STATUS_INDEX_RENAMED
+ | pygit2.GIT_STATUS_INDEX_TYPECHANGE
+)
+STATUS_WT = (pygit2.GIT_STATUS_WT_NEW
+ | pygit2.GIT_STATUS_WT_MODIFIED
+ | pygit2.GIT_STATUS_WT_DELETED
+ | pygit2.GIT_STATUS_WT_RENAMED
+ | pygit2.GIT_STATUS_WT_TYPECHANGE
+)
+ABBREV_LEN = 12
+TGM_HEADER = dmm.TGM_HEADER.encode(dmm.ENCODING)
+
+
+def walk_tree(tree, *, _prefix=''):
+ for child in tree:
+ if isinstance(child, pygit2.Tree):
+ yield from walk_tree(child, _prefix=f'{_prefix}{child.name}/')
+ else:
+ yield f'{_prefix}{child.name}', child
+
+
+def insert_into_tree(repo, tree_builder, path, blob_oid):
+ try:
+ first, rest = path.split('/', 1)
+ except ValueError:
+ tree_builder.insert(path, blob_oid, pygit2.GIT_FILEMODE_BLOB)
+ else:
+ inner = repo.TreeBuilder(tree_builder.get(first))
+ insert_into_tree(repo, inner, rest, blob_oid)
+ tree_builder.insert(first, inner.write(), pygit2.GIT_FILEMODE_TREE)
+
+
+def main(repo):
+ if repo.index.conflicts:
+ print("You need to resolve merge conflicts first.")
+ return 1
+
+ # Ensure the index is clean.
+ for path, status in repo.status().items():
+ if status & pygit2.GIT_STATUS_IGNORED:
+ continue
+ if status & STATUS_INDEX:
+ print("You have changes staged for commit. Commit them or unstage them first.")
+ print("If you are about to commit maps for the first time, run `Run Before Committing.bat`.")
+ return 1
+ if path.endswith(".dmm") and (status & STATUS_WT):
+ print("You have modified maps. Commit them first.")
+ print("If you are about to commit maps for the first time, run `Run Before Committing.bat`.")
+ return 1
+
+ # Read the HEAD commit.
+ head_commit = repo[repo.head.target]
+ head_files = {}
+ for path, blob in walk_tree(head_commit.tree):
+ if path.endswith(".dmm"):
+ data = blob.read_raw()
+ if not data.startswith(TGM_HEADER):
+ head_files[path] = dmm.DMM.from_bytes(data)
+
+ if not head_files:
+ print("All committed maps appear to be in the correct format.")
+ print("If you are about to commit maps for the first time, run `Run Before Committing.bat`.")
+ return 1
+
+ # Work backwards to find a base for each map, converting as found.
+ converted = {}
+ if len(head_commit.parents) != 1:
+ print("Unable to automatically fix anything because HEAD is a merge commit.")
+ return 1
+ commit_message_lines = []
+ working_commit = head_commit.parents[0]
+ while len(converted) < len(head_files):
+ for path in head_files.keys() - converted.keys():
+ try:
+ blob = working_commit.tree[path]
+ except KeyError:
+ commit_message_lines.append(f"{'new':{ABBREV_LEN}}: {path}")
+ print(f"Converting new map: {path}")
+ converted[path] = head_files[path]
+ else:
+ data = blob.read_raw()
+ if data.startswith(TGM_HEADER):
+ str_id = str(working_commit.id)[:ABBREV_LEN]
+ commit_message_lines.append(f"{str_id}: {path}")
+ print(f"Converting map: {path}")
+ converted[path] = merge_map(head_files[path], dmm.DMM.from_bytes(data))
+ if len(working_commit.parents) != 1:
+ print("A merge commit was encountered before good versions of these maps were found:")
+ print("\n".join(f" {x}" for x in head_files.keys() - base_files.keys()))
+ return 1
+ working_commit = working_commit.parents[0]
+
+ # Okay, do the actual work.
+ tree_builder = repo.TreeBuilder(head_commit.tree)
+ for path, merged_map in converted.items():
+ blob_oid = repo.create_blob(merged_map.to_bytes())
+ insert_into_tree(repo, tree_builder, path, blob_oid)
+ repo.index.add(pygit2.IndexEntry(path, blob_oid, repo.index[path].mode))
+ merged_map.to_file(os.path.join(repo.workdir, path))
+
+ # Save the index.
+ repo.index.write()
+
+ # Commit the index to the current branch.
+ signature = pygit2.Signature(repo.config['user.name'], repo.config['user.email'])
+ joined = "\n".join(commit_message_lines)
+ repo.create_commit(
+ repo.head.name,
+ signature, # author
+ signature, # committer
+ f'Convert maps to TGM\n\n{joined}\n\nAutomatically commited by: {os.path.relpath(__file__, repo.workdir)}',
+ tree_builder.write(),
+ [head_commit.id],
+ )
+
+ # Success.
+ print("Successfully committed a fixup. Push as needed.")
+ return 0
+
+
+if __name__ == '__main__':
+ exit(main(pygit2.Repository(pygit2.discover_repository(os.getcwd()))))
diff --git a/tools/mapmerge2/mapmerge.bat b/tools/mapmerge2/mapmerge.bat
deleted file mode 100755
index 0b3dc2cf31..0000000000
--- a/tools/mapmerge2/mapmerge.bat
+++ /dev/null
@@ -1,5 +0,0 @@
-@echo off
-set MAPROOT=%~dp0/../../_maps/
-set TGM=1
-call "%~dp0\..\bootstrap\python" -m mapmerge2.mapmerge %*
-pause
diff --git a/tools/mapmerge2/mapmerge.py b/tools/mapmerge2/mapmerge.py
index 44d01ce348..d9cdea24cf 100644
--- a/tools/mapmerge2/mapmerge.py
+++ b/tools/mapmerge2/mapmerge.py
@@ -66,7 +66,7 @@ def merge_map(new_map, old_map, delete_unused=False):
# step two: delete unused keys
if unused_keys:
- print(f"Notice: Trimming {len(unused_keys)} unused dictionary keys.")
+ #print(f"Notice: Trimming {len(unused_keys)} unused dictionary keys.")
for key in unused_keys:
del merged.dictionary[key]
diff --git a/tools/mapmerge2/merge_driver.py b/tools/mapmerge2/merge_driver.py
new file mode 100644
index 0000000000..39bc1f723e
--- /dev/null
+++ b/tools/mapmerge2/merge_driver.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+import sys
+import collections
+from . import dmm, mapmerge
+from hooks.merge_frontend import MergeDriver
+
+
+debug_stats = collections.defaultdict(int)
+
+
+def select(base, left, right, *, debug=None):
+ if left == right:
+ # whether or not it's in the base, both sides agree
+ if debug:
+ debug_stats[f"select {debug} both"] += 1
+ return left
+ elif base == left:
+ # base == left, but right is different: accept right
+ if debug:
+ debug_stats[f"select {debug} right"] += 1
+ return right
+ elif base == right:
+ # base == right, but left is different: accept left
+ if debug:
+ debug_stats[f"select {debug} left"] += 1
+ return left
+ else:
+ # all three versions are different
+ if debug:
+ debug_stats[f"select {debug} fail"] += 1
+ return None
+
+
+def three_way_merge(base, left, right):
+ if base.size != left.size or base.size != right.size:
+ print("Dimensions have changed:")
+ print(f" Base: {base.size}")
+ print(f" Ours: {left.size}")
+ print(f" Theirs: {right.size}")
+ return True, None
+
+ trouble = False
+ merged = dmm.DMM(base.key_length, base.size)
+ merged.dictionary = base.dictionary.copy()
+
+ for (z, y, x) in base.coords_zyx:
+ coord = x, y, z
+ base_tile = base.get_tile(coord)
+ left_tile = left.get_tile(coord)
+ right_tile = right.get_tile(coord)
+
+ # try to merge the whole tiles
+ whole_tile_merge = select(base_tile, left_tile, right_tile, debug='tile')
+ if whole_tile_merge is not None:
+ merged.set_tile(coord, whole_tile_merge)
+ continue
+
+ # try to merge each group independently (movables, turfs, areas)
+ base_movables, base_turfs, base_areas = dmm.split_atom_groups(base_tile)
+ left_movables, left_turfs, left_areas = dmm.split_atom_groups(left_tile)
+ right_movables, right_turfs, right_areas = dmm.split_atom_groups(right_tile)
+
+ merged_movables = select(base_movables, left_movables, right_movables, debug='movable')
+ merged_turfs = select(base_turfs, left_turfs, right_turfs, debug='turf')
+ merged_areas = select(base_areas, left_areas, right_areas, debug='area')
+
+ if merged_movables is not None and merged_turfs is not None and merged_areas is not None:
+ merged.set_tile(coord, merged_movables + merged_turfs + merged_areas)
+ continue
+
+ # TODO: more advanced strategies?
+
+ # fall back to requiring manual conflict resolution
+ trouble = True
+ print(f" C: Both sides touch the tile at {coord}")
+
+ if merged_movables is None:
+ obj_name = "---Merge conflict marker---"
+ merged_movables = left_movables + [f'/obj{{name = "{obj_name}"}}'] + right_movables
+ print(f" Left and right movable groups are split by an `/obj` named \"{obj_name}\"")
+ if merged_turfs is None:
+ merged_turfs = left_turfs
+ print(f" Saving turf: {', '.join(left_turfs)}")
+ print(f" Alternative: {', '.join(right_turfs)}")
+ print(f" Original: {', '.join(base_turfs)}")
+ if merged_areas is None:
+ merged_areas = left_areas
+ print(f" Saving area: {', '.join(left_areas)}")
+ print(f" Alternative: {', '.join(right_areas)}")
+ print(f" Original: {', '.join(base_areas)}")
+
+ merged.set_tile(coord, merged_movables + merged_turfs + merged_areas)
+
+ merged = mapmerge.merge_map(merged, base)
+ return trouble, merged
+
+
+class DmmDriver(MergeDriver):
+ driver_id = 'dmm'
+
+ def merge(self, base, left, right):
+ map_base = dmm.DMM.from_bytes(base.read())
+ map_left = dmm.DMM.from_bytes(left.read())
+ map_right = dmm.DMM.from_bytes(right.read())
+ trouble, merge_result = three_way_merge(map_base, map_left, map_right)
+ return not trouble, merge_result
+
+ def to_file(self, outfile, merge_result):
+ outfile.write(merge_result.to_bytes())
+
+ def post_announce(self, success, merge_result):
+ if not success:
+ print("!!! Manual merge required!")
+ if merge_result:
+ print(" A best-effort merge was performed. You must edit the map and confirm")
+ print(" that all coordinates mentioned above are as desired.")
+ else:
+ print(" The map was totally unable to be merged; you must start with one version")
+ print(" or the other and manually resolve the conflict. Information about the")
+ print(" conflicting tiles is listed above.")
+
+
+if __name__ == '__main__':
+ exit(DmmDriver().main())
diff --git a/tools/mapmerge2/precommit.py b/tools/mapmerge2/precommit.py
index 7350c40329..3d22917f92 100755
--- a/tools/mapmerge2/precommit.py
+++ b/tools/mapmerge2/precommit.py
@@ -1,10 +1,12 @@
#!/usr/bin/env python3
import os
+import sys
import pygit2
from . import dmm
from .mapmerge import merge_map
-def main(repo):
+
+def main(repo, *, use_workdir=False):
if repo.index.conflicts:
print("You need to resolve merge conflicts first.")
return 1
@@ -16,12 +18,21 @@ def main(repo):
except KeyError:
pass
+ target_statuses = pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_INDEX_NEW
+ skip_to_file_statuses = pygit2.GIT_STATUS_WT_DELETED | pygit2.GIT_STATUS_WT_MODIFIED
+ if use_workdir:
+ target_statuses |= pygit2.GIT_STATUS_WT_MODIFIED | pygit2.GIT_STATUS_WT_NEW
+ skip_to_file_statuses &= ~pygit2.GIT_STATUS_WT_MODIFIED
+
changed = 0
for path, status in repo.status().items():
- if path.endswith(".dmm") and (status & (pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_INDEX_NEW)):
+ if path.endswith(".dmm") and (status & target_statuses):
# read the index
index_entry = repo.index[path]
- index_map = dmm.DMM.from_bytes(repo[index_entry.id].read_raw())
+ if use_workdir:
+ index_map = dmm.DMM.from_file(os.path.join(repo.workdir, path))
+ else:
+ index_map = dmm.DMM.from_bytes(repo[index_entry.id].read_raw())
try:
head_blob = repo[repo[repo.head.target].tree[path].id]
@@ -32,7 +43,7 @@ def main(repo):
merged_map = index_map
else:
# Entry in HEAD, merge the index over it
- print(f"Merging map: {path}", flush=True)
+ print(f"Converting map: {path}", flush=True)
assert not (status & pygit2.GIT_STATUS_INDEX_NEW)
head_map = dmm.DMM.from_bytes(head_blob.read_raw())
merged_map = merge_map(index_map, head_map)
@@ -43,15 +54,16 @@ def main(repo):
changed += 1
# write to the working directory if that's clean
- if status & (pygit2.GIT_STATUS_WT_DELETED | pygit2.GIT_STATUS_WT_MODIFIED):
+ if status & skip_to_file_statuses:
print(f"Warning: {path} has unindexed changes, not overwriting them")
else:
merged_map.to_file(os.path.join(repo.workdir, path))
if changed:
repo.index.write()
- print(f"Merged {changed} maps.")
return 0
+
if __name__ == '__main__':
- exit(main(pygit2.Repository(pygit2.discover_repository(os.getcwd()))))
+ repo = pygit2.Repository(pygit2.discover_repository(os.getcwd()))
+ exit(main(repo, use_workdir='--use-workdir' in sys.argv))
diff --git a/tools/mapmerge2/tgm2dmm.bat b/tools/mapmerge2/tgm2dmm.bat
deleted file mode 100755
index b446002bcb..0000000000
--- a/tools/mapmerge2/tgm2dmm.bat
+++ /dev/null
@@ -1,5 +0,0 @@
-@echo off
-set MAPROOT=%~dp0/../../_maps/
-set TGM=0
-call "%~dp0\..\bootstrap\python" -m mapmerge2.convert %*
-pause