Added formatter & linter

This commit is contained in:
Rob Kelly 2024-07-17 19:15:16 -06:00
parent 7b64dbd453
commit fdbe85a8ad
13 changed files with 444 additions and 0 deletions

49
.gdlintrc Normal file
View File

@ -0,0 +1,49 @@
class-definitions-order:
- tools
- classnames
- extends
- docstrings
- signals
- enums
- consts
- exports
- pubvars
- prvvars
- onreadypubvars
- onreadyprvvars
- staticvars
- others
class-load-variable-name: (([A-Z][a-z0-9]*)+|_?[a-z][a-z0-9]*(_[a-z0-9]+)*)
class-name: ([A-Z][a-z0-9]*)+
class-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)*
comparison-with-itself: null
constant-name: _?[A-Z][A-Z0-9]*(_[A-Z0-9]+)*
disable: []
duplicated-load: null
enum-element-name: '[A-Z][A-Z0-9]*(_[A-Z0-9]+)*'
enum-name: ([A-Z][a-z0-9]*)+
excluded_directories: !!set
.git: null
addons: null
expression-not-assigned: null
function-argument-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)*
function-arguments-number: 10
function-name: (_on_([A-Z][a-z0-9]*)+(_[a-z0-9]+)*|_?[a-z][a-z0-9]*(_[a-z0-9]+)*)
function-preload-variable-name: ([A-Z][a-z0-9]*)+
function-variable-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*'
load-constant-name: (([A-Z][a-z0-9]*)+|_?[A-Z][A-Z0-9]*(_[A-Z0-9]+)*)
loop-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)*
max-file-lines: 1000
max-line-length: 100
max-public-methods: 20
max-returns: 6
mixed-tabs-and-spaces: null
no-elif-return: null
no-else-return: null
private-method-call: null
signal-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*'
sub-class-name: _?([A-Z][a-z0-9]*)+
tab-characters: 1
trailing-whitespace: null
unnecessary-pass: null
unused-argument: null

31
.githooks/pre-commit Executable file
View File

@ -0,0 +1,31 @@
#!/bin/sh
GDFORMAT=gdformat
GDLINT=gdlint
VENV=.venv
PYTHON=python3
PIP=$VENV/bin/pip
if ! which $GDFORMAT &> /dev/null; then
if ! test -f $VENV/bin/$GDFORMAT; then
if ! which $PYTHON &> /dev/null; then
echo "Please install Python 3"
exit 1
fi
echo "gdscript-toolkit will be installed locally under $VENV."
echo "this only needs to be performed once."
echo "creating venv..."
$PYTHON -m venv $VENV
echo "installing gdscript-toolkit..."
$PIP install -r requirements.txt
fi
GDFORMAT=$VENV/bin/$GDFORMAT
GDLINT=$VENV/bin/$GDLINT
fi
set -x
$GDFORMAT --check .
$GDLINT .

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Ryan Haskell-Glatz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,70 @@
@tool
class_name FormatOnSave extends EditorPlugin
const SUCCESS: int = 0
const AUTO_RELOAD_SETTING: String = "text_editor/behavior/files/auto_reload_scripts_on_external_change"
var original_auto_reload_setting: bool
# LIFECYCLE EVENTS
func _enter_tree():
activate_auto_reload_setting()
resource_saved.connect(on_resource_saved)
func _exit_tree():
resource_saved.disconnect(on_resource_saved)
restore_original_auto_reload_setting()
# CALLED WHEN A SCRIPT IS SAVED
func on_resource_saved(resource: Resource):
if resource is Script:
var script: Script = resource
var current_script = get_editor_interface().get_script_editor().get_current_script()
var text_edit: CodeEdit = (
get_editor_interface().get_script_editor().get_current_editor().get_base_editor()
)
# Prevents other unsaved scripts from overwriting the active one
if current_script == script:
var filepath: String = ProjectSettings.globalize_path(resource.resource_path)
# Run gdformat
var exit_code = OS.execute("gdformat", [filepath])
# Replace source_code with formatted source_code
if exit_code == SUCCESS:
var formatted_source = FileAccess.get_file_as_string(resource.resource_path)
FormatOnSave.reload_script(text_edit, formatted_source)
# Workaround until this PR is merged:
# https://github.com/godotengine/godot/pull/83267
# Thanks, @KANAjetzt 💖
static func reload_script(text_edit: TextEdit, source_code: String) -> void:
var column := text_edit.get_caret_column()
var row := text_edit.get_caret_line()
var scroll_position_h := text_edit.get_h_scroll_bar().value
var scroll_position_v := text_edit.get_v_scroll_bar().value
text_edit.text = source_code
text_edit.set_caret_column(column)
text_edit.set_caret_line(row)
text_edit.scroll_horizontal = scroll_position_h
text_edit.scroll_vertical = scroll_position_v
text_edit.tag_saved_version()
# For this workaround to work, we need to disable the "Reload/Resave" pop-up
func activate_auto_reload_setting():
var settings := get_editor_interface().get_editor_settings()
original_auto_reload_setting = settings.get(AUTO_RELOAD_SETTING)
settings.set(AUTO_RELOAD_SETTING, true)
# If the plugin is disabled, let's attempt to restore the original editor setting
func restore_original_auto_reload_setting():
var settings := get_editor_interface().get_editor_settings()
settings.set(AUTO_RELOAD_SETTING, original_auto_reload_setting)

View File

@ -0,0 +1,6 @@
[plugin]
name="Format on Save"
description="Runs `gdformat` on save to automatically format your GD script as you code."
author="Ryan Haskell-Glatz"
version="1.2.0"
script="format_on_save.gd"

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Rob Kelly <contact@robkel.ly>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,6 @@
[plugin]
name="gdLint Plugin"
description="Static code analysis with `gdlint`"
author="Rob Kelly"
version="1.0.0"
script="run_linter.gd"

View File

@ -0,0 +1,33 @@
@tool
class_name GDLintPlugin extends EditorPlugin
# If you've installed gdlint in a venv, you may want to overwrite this
const GDLINT: String = "gdlint"
func _enter_tree() -> void:
assert(not OS.execute(GDLINT, ["-h"]), "Could not find gdLint binary at {0}".format([GDLINT]))
resource_saved.connect(on_save)
func _exit_tree() -> void:
resource_saved.disconnect(on_save)
func on_save(resource: Resource) -> void:
# Run linting when a script resource is saved
if resource is Script:
var script: Script = resource
var filepath: String = ProjectSettings.globalize_path(resource.resource_path)
var script_editor = EditorInterface.get_script_editor()
var code_editor: CodeEdit = (
script_editor.get_current_editor().get_base_editor()
if script_editor.get_current_script() == script
else null
)
var gdlint_output: Array[String] = []
var error: int = OS.execute(GDLINT, [filepath], gdlint_output, true)
if error:
push_warning("gdLint:\n" + gdlint_output[0])

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 popcar2
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,16 @@
# Godot Tilemap Collision Baker
![showcase](./showcase.gif)
**Note: This script is for Godot 4, it may need some tinkering to work on Godot 3.x**
---
Godot Tilemap Baker is a tool to easily pre-bake collisions on square tilemaps. This can be useful for many reasons, the biggest one being that using the [default tileset collision can cause issues with rigidbodies](https://github.com/godotengine/godot/issues/72372) because objects tend to get stuck in-between tiles. TilemapBaker was built with this in mind, so floors and ceilings are always one smooth rectangle collider. It should also ^theoretically be more optimized because you end up with way less colliders.
## How to use
Simply attach the `TilemapCollisionBaker` script to an empty StaticBody2D, then point to your Tilemap in the inspector, and hit "Run Script". This is going to wrap your tileset with large box colliders, to be added as children to this node.
You can also select a specific tile layer to bake collisions for, which can be useful if you have slopes or water on separate layers for example.
Don't forget to remove your collisions from the tileset (if you ever had any) when using this tool.
Loved the project? [Consider buying me a cup of Ko-Fi!](https://ko-fi.com/popcar2)

View File

@ -0,0 +1,168 @@
@tool
extends StaticBody2D
## This script pre-bakes collisions for square tilemaps, therefore optimizing code
## and getting rid of weird physics bugs!
##
## How it works TLDR:
## This script finds the position of every tile on the layer you've selected from the top left
## It then goes to the right until it reaches an edge, then created a rectange CollisionShape2D
## and places it in the correct position and size. It keeps doing this until it reaches the end.
## For further optimizations, it combines different rows of CollisionShapes if
## they are the same size.
## Your TileMap Node
@export var tilemap_nodepath: NodePath
## The tilemap layer to bake collisions on.
## You can bake for multiple layers by disabling delete_children_on_run and running multiple times.
@export var target_tiles_layer: int = 0
## Whether or not you want the children of this node to be deleted on run or not.
## Be careful with this!
@export var delete_children_on_run: bool = true
## A fake button to run the code. Bakes collisions and adds colliders as children to this node!
@export var run_script: bool = false : set = run_code
func run_code(_fake_bool = null):
var tile_map: TileMap = get_node(tilemap_nodepath)
if tile_map == null:
print("Hey, you forgot to set your Tilemap Nodepath.")
return
if delete_children_on_run:
delete_children()
var tile_size = tile_map.tile_set.tile_size
var tilemap_locations = tile_map.get_used_cells(target_tiles_layer)
if tilemap_locations.size() == 0:
print("Hey, this tilemap is empty (did you choose the correct layer?)")
return
# I use .pop_back() to go through the array, so I sort them from bottom right to top left.
tilemap_locations.sort_custom(sortVectorsByY)
var last_loc: Vector2i = Vector2i(-99999, -99999)
var size: Vector2i = Vector2i(1, 1)
var xMarginStart = 0
print("Starting first pass (Creating initial colliders)...")
var first_colliders_arr = []
## First pass: add horizontal rect colliders starting from the top left
while true:
var temp_loc = tilemap_locations.pop_back()
if temp_loc == null:
# Add the last collider and break out of loop
var newXPos = (xMarginStart + abs(last_loc.x - xMarginStart) / 2.0 + 0.5) * tile_size.x
@warning_ignore("integer_division")
var newYPos = last_loc.y * tile_size.y - (-tile_size.y / 2)
first_colliders_arr.append(createCollisionShape(Vector2i(newXPos, newYPos), size, tile_size))
print("Finished calculating first pass!")
break
if last_loc == Vector2i(-99999, -99999):
last_loc = temp_loc
xMarginStart = temp_loc.x
continue
if last_loc.y == temp_loc.y and abs(last_loc.x - temp_loc.x) == 1:
size += Vector2i(1,0)
else:
var newXPos = (xMarginStart + abs(last_loc.x - xMarginStart) / 2.0 + 0.5) * tile_size.x
@warning_ignore("integer_division")
var newYPos = last_loc.y * tile_size.y - (-tile_size.y / 2)
first_colliders_arr.append(createCollisionShape(Vector2i(newXPos, newYPos), size, tile_size))
size = Vector2i(1, 1)
xMarginStart = temp_loc.x
#print("New row placed at (%s, %s)" % [newXPos, newYPos])
last_loc = temp_loc
## Sort collider nodes for use in second pass
first_colliders_arr.sort_custom(sortNodesByX)
var last_collider_pos: Vector2 = Vector2(-99999, -99999)
var last_collider
var colliders_to_merge = 1 # Used to count how many colliders will merge
var second_colliders_arr = []
print("Starting second pass (Merging colliders)...")
## Second pass: Merge colliders that are on top of eachother and are the same size
while true:
var temp_collider = first_colliders_arr.pop_back()
if temp_collider == null:
# Add final merged collider and break
last_collider.shape.size.y = tile_size.y * colliders_to_merge
last_collider.position.y -= (colliders_to_merge / 2.0 - 0.5) * tile_size.y
second_colliders_arr.append(last_collider)
print("Finished baking tilemap collisions!")
break
if last_collider_pos == Vector2(-99999, -99999):
last_collider_pos = temp_collider.position
last_collider = temp_collider
continue
var tile_y_distance = abs(temp_collider.position.y - last_collider_pos.y) / tile_size.y
if last_collider_pos.x == temp_collider.position.x and tile_y_distance == 1:
#print("Adding 1 to the merge")
colliders_to_merge += 1
last_collider_pos = temp_collider.position
else:
#print("Merging %s colliders" % colliders_to_merge)
last_collider_pos = temp_collider.position
last_collider.shape.size.y = tile_size.y * colliders_to_merge
last_collider.position.y -= (colliders_to_merge / 2.0 - 0.5) * tile_size.y
second_colliders_arr.append(last_collider)
colliders_to_merge = 1
last_collider = temp_collider
## Adds all colliders as children to this node
for collider in second_colliders_arr:
add_child(collider, true)
collider.owner = get_tree().edited_scene_root
## Move this node's position to cover the tilemap
position = tile_map.position
func createCollisionShape(pos, size, tile_size) -> CollisionShape2D:
var collisionShape = CollisionShape2D.new()
var rectangleShape = RectangleShape2D.new()
rectangleShape.size = size * tile_size
collisionShape.set_shape(rectangleShape)
collisionShape.position = pos
return collisionShape
func delete_children():
for child in get_children():
child.queue_free()
## Sorts array of vectors in ascending order with respect to Y
func sortVectorsByY(a, b):
if a.y > b.y:
return true
if a.y == b.y:
if a.x > b.x:
return true
return false
## Sorts array of nodes in ascending order with respects to position
func sortNodesByX(a, b):
if a.position.x > b.position.x:
return true
if a.position.x == b.position.x:
if a.position.y > b.position.y:
return true
return false

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
gdtoolkit>=4.2.2,<4.3
setuptools>=69.5.1,<69.6