diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..8ad74f7
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Normalize EOL for all files that Git considers text files.
+* text=auto eol=lf
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0af181c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+# Godot 4+ specific ignores
+.godot/
+/android/
diff --git a/BSON Examples/dunk.gd b/BSON Examples/dunk.gd
new file mode 100644
index 0000000..4323709
--- /dev/null
+++ b/BSON Examples/dunk.gd
@@ -0,0 +1,34 @@
+extends Control
+
+
+# These are set in dunk.tscn in the root node properties.
+@export var JSONEditor: CodeEdit
+@export var OutputLabel: Label
+@export var CopyPopup: PopupPanel
+
+
+func _on_button_pressed() -> void:
+ var n_json = JSON.parse_string(JSONEditor.text) # Could be Dictionary or null
+ if !n_json: return # JSON parse failed
+
+ var b_json := BSON.to_bson(n_json)
+ var d_json := BSON.from_bson(b_json)
+
+ OutputLabel.text = ("SERIALIZED BSON:\n"
+ + str(b_json)
+ + "\nDESERIALIZED JSON:\n"
+ + str(d_json))
+
+func _on_copy_pressed() -> void:
+ DisplayServer.clipboard_set(OutputLabel.text)
+ CopyPopup.show()
+
+ var timer := Timer.new()
+ timer.autostart = true
+ timer.one_shot = true
+ timer.wait_time = 1.5
+ add_child(timer)
+
+ await timer.timeout
+ timer.queue_free()
+ CopyPopup.hide()
diff --git a/BSON Examples/dunk.tscn b/BSON Examples/dunk.tscn
new file mode 100644
index 0000000..3531d6b
--- /dev/null
+++ b/BSON Examples/dunk.tscn
@@ -0,0 +1,131 @@
+[gd_scene load_steps=2 format=3 uid="uid://ckdfb5ggwslbk"]
+
+[ext_resource type="Script" path="res://BSON Examples/dunk.gd" id="1_p38ab"]
+
+[node name="Dunk" type="Control" node_paths=PackedStringArray("JSONEditor", "OutputLabel", "CopyPopup")]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("1_p38ab")
+JSONEditor = NodePath("Panel/JSONEdit")
+OutputLabel = NodePath("Panel/Scroll/Output")
+CopyPopup = NodePath("CopyPopup")
+
+[node name="Panel" type="Panel" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 119.0
+offset_top = 67.0
+offset_right = -119.0
+offset_bottom = -67.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="JSONEdit" type="CodeEdit" parent="Panel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 86.0
+offset_top = 14.0
+offset_right = -86.0
+offset_bottom = -277.0
+grow_horizontal = 2
+grow_vertical = 2
+text = "{
+ \"username\": \"johndoe\",
+ \"firstname\": \"John\",
+ \"lastname\": \"Doe\",
+ \"email\": {
+ \"adress\": \"john-doe@example.com\",
+ \"verified\": true
+ },
+ \"age\": 32,
+ \"phone\": {
+ \"number\": \"0123456789\",
+ \"verified\": false
+ },
+ \"assets\": [
+ \"foo\",
+ \"bar\",
+ 42,
+ true,
+ false,
+ 3.14159265,
+ \"baz\"
+ ]
+}"
+middle_mouse_paste_enabled = false
+minimap_draw = true
+gutters_draw_line_numbers = true
+indent_automatic = true
+auto_brace_completion_enabled = true
+auto_brace_completion_highlight_matching = true
+
+[node name="Dunk" type="Button" parent="Panel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 364.0
+offset_top = 243.0
+offset_right = -364.0
+offset_bottom = -228.0
+grow_horizontal = 2
+grow_vertical = 2
+text = "Run"
+
+[node name="Copy" type="Button" parent="Panel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 364.0
+offset_top = 291.0
+offset_right = -364.0
+offset_bottom = -180.0
+grow_horizontal = 2
+grow_vertical = 2
+text = "Copy Output
+"
+
+[node name="Scroll" type="ScrollContainer" parent="Panel"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 83.0
+offset_top = 346.0
+offset_right = -83.0
+offset_bottom = -11.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="Output" type="Label" parent="Panel/Scroll"]
+layout_mode = 2
+size_flags_horizontal = 3
+text = "This example will \"dunk\" your JSON into BSON and then convert it back to JSON.
+Due to a quirk in the way Godot parses JSON, all integers will be parsed as floats.
+This may be a bug in Godot."
+horizontal_alignment = 1
+autowrap_mode = 3
+
+[node name="CopyPopup" type="PopupPanel" parent="."]
+transparent_bg = true
+position = Vector2i(492, 586)
+size = Vector2i(164, 31)
+
+[node name="Label" type="Label" parent="CopyPopup"]
+offset_left = 4.0
+offset_top = 4.0
+offset_right = 160.0
+offset_bottom = 27.0
+text = "Copied to clipboard!"
+
+[connection signal="pressed" from="Panel/Dunk" to="." method="_on_button_pressed"]
+[connection signal="pressed" from="Panel/Copy" to="." method="_on_copy_pressed"]
diff --git a/README.md b/README.md
index bb1b64a..8f771f0 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,17 @@
-# godot-bson
+# BSON for the Godot Engine
+This is a simple BSON serializer and deserializer written in GDScript that is originally designed to be compatible with [JSON for Modern C++](https://json.nlohmann.me/)'s BSON components, but it can be used with any other BSON tool.
-A BSON serializer/deserializer for the Godot Engine
\ No newline at end of file
+From [bsonspec.org](https://bsonspec.org/):
+BSON, short for Binary [JSON](http://json.org), is a binary-encoded serialization of JSON-like documents. Like JSON, BSON supports the embedding of documents and arrays within other documents and arrays.
+
+This plugin is useful for server/client communication, interacting with MongoDB, reducing JSON file sizes, etc.
+
+After enabling this plugin in your Godot project settings, you can access the BSON object with:
+```
+BSON.to_bson(Dictionary)
+```
+and
+```
+BSON.from_bson(PackedByteArray)
+```
+You can also test out this plugin with `/BSON Examples/dunk.tscn`. This example will take your JSON, serialize it to BSON, then deserialize it back to JSON.
diff --git a/addons/bson/bson.gd b/addons/bson/bson.gd
new file mode 100644
index 0000000..47ba328
--- /dev/null
+++ b/addons/bson/bson.gd
@@ -0,0 +1,269 @@
+# COPYRIGHT 2025 Colormatic Studios and contributors.
+# This file is the BSON serializer for the Godot Engine,
+# published under the MIT license. https://opensource.org/license/MIT
+
+class_name BSON
+
+
+static func to_bson(data: Dictionary) -> PackedByteArray:
+ var document := dictionary_to_bytes(data)
+ document.append(0x00)
+ var buffer := PackedByteArray()
+ buffer.append_array(int32_to_bytes(document.size() + 4))
+ buffer.append_array(document)
+ return buffer
+
+static func get_byte_type(value: Variant) -> int:
+ match typeof(value):
+ TYPE_STRING:
+ return 0x02
+ TYPE_INT:
+ if abs(value as int) < 2147483647: # 32 bit signed integer limit
+ return 0x10
+ else:
+ return 0x12
+ TYPE_FLOAT:
+ return 0x01
+ TYPE_ARRAY:
+ return 0x04
+ TYPE_DICTIONARY:
+ return 0x03
+ TYPE_BOOL:
+ return 0x08
+ _:
+ push_error("BSON serialization error: Unsupported type: ", typeof(value))
+ return 0x00
+
+static func int16_to_bytes(value: int) -> PackedByteArray:
+ var buffer := PackedByteArray()
+ buffer.resize(2)
+ buffer.encode_s16(0, value)
+ return buffer
+
+static func int32_to_bytes(value: int) -> PackedByteArray:
+ var buffer := PackedByteArray()
+ buffer.resize(4)
+ buffer.encode_s32(0, value)
+ return buffer
+
+static func int64_to_bytes(value: int) -> PackedByteArray:
+ var buffer := PackedByteArray()
+ buffer.resize(8)
+ buffer.encode_s64(0, value)
+ return buffer
+
+static func float_to_bytes(value: float) -> PackedByteArray:
+ var buffer := PackedByteArray()
+ buffer.resize(4)
+ buffer.encode_float(0, value)
+ return buffer
+
+static func double_to_bytes(value: float) -> PackedByteArray:
+ var buffer := PackedByteArray()
+ buffer.resize(8)
+ buffer.encode_double(0, value)
+ return buffer
+
+static func dictionary_to_bytes(dict: Dictionary) -> PackedByteArray:
+ var buffer := PackedByteArray()
+
+ for key: String in dict.keys():
+ buffer.append(get_byte_type(dict[key]))
+ var key_string_bytes := key.to_utf8_buffer()
+ buffer.append_array(key_string_bytes)
+ buffer.append(0x00)
+ buffer.append_array(serialize_variant(dict[key]))
+
+ return buffer
+
+static func array_to_bytes(array: Array[Variant]) -> PackedByteArray:
+ var buffer := PackedByteArray()
+
+ for index: int in range(array.size()):
+ buffer.append(get_byte_type(array[index]))
+ # For whatever reason, BSON wants array indexes to be strings. This makes no sense.
+ var s_index := str(index)
+ buffer.append_array(s_index.to_utf8_buffer())
+ buffer.append(0x00)
+
+ buffer.append_array(serialize_variant(array[index]))
+
+ return buffer
+
+static func serialize_variant(data: Variant) -> PackedByteArray:
+ var buffer := PackedByteArray()
+ match typeof(data):
+ TYPE_DICTIONARY:
+ var document := dictionary_to_bytes(data as Dictionary)
+ buffer.append_array(int32_to_bytes(document.size()))
+ buffer.append_array(document)
+ buffer.append(0x00)
+ TYPE_ARRAY:
+ var b_array := array_to_bytes(data as Array[Variant])
+ buffer.append_array(int32_to_bytes(b_array.size()))
+ buffer.append_array(b_array)
+ buffer.append(0x00)
+ TYPE_STRING:
+ var str_as_bytes := (data as String).to_utf8_buffer()
+ buffer.append_array(int32_to_bytes(str_as_bytes.size() + 1))
+ buffer.append_array(str_as_bytes)
+ buffer.append(0x00)
+ TYPE_INT:
+ if abs(data as int) < 2147483647: # 32 bit signed integer limit
+ buffer.append_array(int32_to_bytes(data as int))
+ else:
+ buffer.append_array(int64_to_bytes(data as int))
+ TYPE_FLOAT:
+ buffer.append_array(double_to_bytes(data as float))
+ TYPE_BOOL:
+ buffer.append((data as bool) if 0x01 else 0x00)
+ _:
+ buffer.append(0x00)
+
+ return buffer
+
+
+static func from_bson(data: PackedByteArray) -> Dictionary:
+ return Deserializer.new(data).read_dictionary()
+
+
+class Deserializer:
+ var buffer: PackedByteArray
+ var read_position := 0
+
+ func _init(buffer: PackedByteArray):
+ self.buffer = buffer
+
+ func get_int8() -> int:
+ var value := buffer[read_position]
+ read_position += 1
+ return value
+
+ func get_int16() -> int:
+ var value := buffer.decode_s16(read_position)
+ read_position += 2
+ return value
+
+ func get_int32() -> int:
+ var value := buffer.decode_s32(read_position)
+ read_position += 4
+ return value
+
+ func get_int64() -> int:
+ var value := buffer.decode_s64(read_position)
+ read_position += 8
+ return value
+
+ func get_float() -> float:
+ var value := buffer.decode_float(read_position)
+ read_position += 4
+ return value
+
+ func get_double() -> float:
+ var value := buffer.decode_double(read_position)
+ read_position += 8
+ return value
+
+ func get_string() -> String:
+ var expected_size = get_int32()
+ var s_value: String
+ var iter := 0
+ while true:
+ iter += 1
+ var b_char := get_int8()
+ if b_char == 0x00: break
+ s_value += char(b_char)
+ if expected_size != iter: # Check if the string is terminated with 0x00
+ push_error("BSON deserialization error: String was the wrong size."
+ + " Position: "
+ + str(read_position - iter)
+ + ", stated size: "
+ + str(expected_size)
+ + ", actual size: "
+ + str(iter))
+ return s_value
+
+ func get_bool() -> bool:
+ return (get_int8() == 1)
+
+ func read_dictionary() -> Dictionary:
+ var object = {}
+
+ var expected_size := get_int32()
+
+ var iter := 0
+ while true:
+ iter += 1
+ var type := get_int8()
+ if type == 0x00: break
+
+ var key := ""
+
+ while true:
+ var k_char := get_int8()
+ if k_char == 0x00: break
+ key += char(k_char)
+
+ match type:
+ 0x02: object[key] = get_string()
+ 0x10: object[key] = get_int32()
+ 0x12: object[key] = get_int64()
+ 0x01: object[key] = get_double()
+ 0x08: object[key] = get_bool()
+ 0x04: object[key] = read_array()
+ 0x03: object[key] = read_dictionary()
+ _:
+ push_error("BSON deserialization error: Unsupported type "
+ + str(type)
+ + " at byte "
+ + str(read_position - 1))
+
+ if iter > expected_size:
+ push_warning("BSON deserialization warning: Dictionary is the wrong length."
+ + " Expected dictionary length: "
+ + str(expected_size)
+ + ", Actual dictionary length: "
+ + str(iter))
+ return object
+
+ func read_array() -> Array:
+ var array: Array
+
+ var expected_size := get_int32()
+
+ var iter := 0
+ while true:
+ iter += 1
+ var type := get_int8()
+ if type == 0x00: break
+
+ var key: String
+
+ while true:
+ var k_char := get_int8()
+ if k_char == 0x00: break
+ key += char(k_char)
+
+ # IMPORTANT: Since the array is being deserialized sequentially, we can
+ # use the Array.append() function. It would be better to set the index
+ # directly, but that is not possible. (It *could* cause null holes)
+ # The BSON specification unfortunately allows for null holes, but
+ # this deserializer will just remove any gaps. This could cause an
+ # index desynchronization between two programs communicating with BSON,
+ # but that means the other program has a buggy serializer.
+ match type:
+ 0x02: array.append(get_string())
+ 0x10: array.append(get_int32())
+ 0x12: array.append(get_int64())
+ 0x01: array.append(get_double())
+ 0x08: array.append(get_bool())
+ 0x04: array.append(read_array())
+ 0x03: array.append(read_dictionary())
+ _: push_error("BSON deserialization error: Unsupported type: " + str(type))
+ if iter > expected_size:
+ push_warning("BSON deserialization warning: Array is the wrong length."
+ + " Expected array length: "
+ + str(expected_size)
+ + ", Actual array length: "
+ + str(iter))
+ return array
diff --git a/addons/bson/plugin.cfg b/addons/bson/plugin.cfg
new file mode 100644
index 0000000..ae2b3c5
--- /dev/null
+++ b/addons/bson/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="BSON"
+description="A BSON class to serialize and deserialize BSON in GDScript."
+author="Colormatic Studios"
+version=""
+script="plugin.gd"
diff --git a/addons/bson/plugin.gd b/addons/bson/plugin.gd
new file mode 100644
index 0000000..b7c36d0
--- /dev/null
+++ b/addons/bson/plugin.gd
@@ -0,0 +1,20 @@
+@tool
+extends EditorPlugin
+
+
+const AUTOLOAD_NAME = "BSON"
+
+
+#func _enter_tree() -> void:
+ # Initialization of the plugin goes here.
+ #pass
+
+#func _exit_tree() -> void:
+ # Clean-up of the plugin goes here.
+ #pass
+
+func _enable_plugin() -> void:
+ add_autoload_singleton(AUTOLOAD_NAME, "res://addons/bson/bson.gd")
+
+func _disable_plugin() -> void:
+ remove_autoload_singleton(AUTOLOAD_NAME)
diff --git a/icon-full.svg b/icon-full.svg
new file mode 100644
index 0000000..b8e6a18
--- /dev/null
+++ b/icon-full.svg
@@ -0,0 +1,89 @@
+
+
+
+
diff --git a/icon-full.svg.import b/icon-full.svg.import
new file mode 100644
index 0000000..66378a2
--- /dev/null
+++ b/icon-full.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c6htrl16c0eay"
+path="res://.godot/imported/icon-full.svg-e72bf6bf218ce1384c33716fd0ad1d25.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon-full.svg"
+dest_files=["res://.godot/imported/icon-full.svg-e72bf6bf218ce1384c33716fd0ad1d25.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/icon.svg b/icon.svg
new file mode 100644
index 0000000..7382e60
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1,78 @@
+
+
+
+
diff --git a/icon.svg.import b/icon.svg.import
new file mode 100644
index 0000000..45d6014
--- /dev/null
+++ b/icon.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://0iy4oa1unjrt"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/normal_map_invert_y=false
+process/hdr_as_srgb=false
+process/hdr_clamp_exposure=false
+process/size_limit=0
+detect_3d/compress_to=1
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/project.godot b/project.godot
new file mode 100644
index 0000000..1ffbe97
--- /dev/null
+++ b/project.godot
@@ -0,0 +1,20 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="BSON for Godot"
+run/main_scene="res://BSON Examples/dunk.tscn"
+config/features=PackedStringArray("4.3", "Forward Plus")
+config/icon="res://icon.svg"
+
+[editor_plugins]
+
+enabled=PackedStringArray("res://addons/bson/plugin.cfg")