diff --git a/.gitignore b/.gitignore
index f89d046b..2bf14b32 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
# Godot 4+ specific ignores
.godot/
-.vscode/
\ No newline at end of file
+.vscode/
+build/**
\ No newline at end of file
diff --git a/Cirno.csproj b/Cirno.csproj
index 493468ac..91540dd0 100644
--- a/Cirno.csproj
+++ b/Cirno.csproj
@@ -4,6 +4,7 @@
true
+
\ No newline at end of file
diff --git a/CreateIcon.gd b/CreateIcon.gd
new file mode 100644
index 00000000..ff511c47
--- /dev/null
+++ b/CreateIcon.gd
@@ -0,0 +1,274 @@
+class_name CreateIcon
+extends SceneTree
+
+var error_callable: Callable
+
+
+func _init() -> void:
+ var arguments = OS.get_cmdline_args()
+ if arguments.size() != 4 and arguments.size() != 9:
+ print(
+ "Usage:\n godot -s CreateIcon.gd name ...\n",
+ "\n",
+ "Creates uncompressed windows ico file.\n",
+ "Add --headless to hide Godot console.\n",
+ "\n",
+ "Arguments:\n",
+ " godot path to Godot 4 beta2+ executable\n",
+ " name path to created icon\n",
+ " provide one or six files. If one provided it will be scaled for all\n",
+ " icon resolutions. Multiple files should be 16x16, 32x32, 48x48, 64x64,\n",
+ " 128x128\n and 256x256 pixels big."
+ )
+ quit()
+ return
+ var images := []
+ if arguments.size() == 9:
+ var names := [arguments[3], arguments[4], arguments[5], arguments[6], arguments[7], arguments[8]]
+ var check_names := {}
+ for name in names:
+ if check_names.has(name):
+ printerr("File ", name, " was added more than once")
+ return
+ check_names[name] = true
+ images = load_images(names)
+ else:
+ images = prepare_images(arguments[3])
+ if not images.is_empty():
+ save_icon(arguments[2], images)
+ quit()
+
+
+func load_images(paths: PackedStringArray) -> Array:
+ var images := []
+ for path in paths:
+ var image := Image.new()
+ var error = image.load(path)
+ if error:
+ print_error(str("Could not load image: ", path))
+ return []
+ image.convert(Image.FORMAT_RGBA8)
+ images.append(image)
+ images.sort_custom(sort_images_by_size)
+ var index := 0
+ for size in [16, 32, 48, 64, 128, 256]:
+ var image: Image = images[index]
+ if image.get_width() != size:
+ print_error(str("Image has incorrect width: ", image.get_width(), " expected: ", size))
+ return []
+ if image.get_height() != size:
+ print_error(str("Image has incorrect height: ", image.get_height(), " expected: ", size))
+ return []
+ index += 1
+ return images
+
+
+func prepare_images(path: String) -> Array:
+ var images := []
+ for size in [16, 32, 48, 64, 128, 256]:
+ var image := Image.new()
+ var error = image.load(path)
+ if error:
+ print_error(str("Could not load image: ", path))
+ return []
+ image.convert(Image.FORMAT_RGBA8)
+ image.resize(size, size)
+ images.append(image)
+ return images
+
+
+func save_icon(destination_path: String, images: Array) -> void:
+ var file = FileAccess.open(destination_path, FileAccess.WRITE)
+ if not file:
+ print_error(str("Could not open file for writing!\n", FileAccess.get_open_error()))
+ return
+ var icon_creator := IconCreator.new()
+ file.store_buffer(icon_creator.generate_icon(images))
+
+
+func print_error(error_message: String) -> void:
+ printerr(error_message)
+ if error_callable:
+ error_callable.call(error_message)
+
+
+static func sort_images_by_size(a: Image, b: Image) -> bool:
+ return a.get_width() < b.get_width()
+
+
+class IconCreator:
+ const ADLER_MOD := 65521
+ const ZLIB_BLOCK_SIZE := 16384
+ const CRC_TABLE_SIZE := 256
+ const ICON_ENTRY_SIZE := 16
+ var PNG_SIGNATURE := PackedByteArray([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0a])
+ var IHDR_SIGNATURE := PackedByteArray([0x49, 0x48, 0x44, 0x52])
+ var IDAT_SIGNATURE := PackedByteArray([0x49, 0x44, 0x41, 0x54])
+ var IEND_SIGNATURE := PackedByteArray([0x49, 0x45, 0x4e, 0x44])
+
+ var crc_table: Array
+
+
+ func _init() -> void:
+ crc_table = generate_crc_table()
+
+
+ func generate_icon(images: Array) -> PackedByteArray:
+ var result := PackedByteArray()
+ result.append_array(generate_icon_header(images.size()))
+ var offset := result.size() + images.size() * ICON_ENTRY_SIZE
+ var pngs := []
+ for image in images:
+ assert(image.get_format() == Image.FORMAT_RGBA8)
+ var png := generate_png(image)
+ pngs.append(png)
+ var icon_entry := generate_icon_entry(image, png.size(), offset)
+ result.append_array(icon_entry)
+ offset += png.size()
+ for png in pngs:
+ result.append_array(png)
+ return result
+
+
+ func generate_icon_header(size: int) -> PackedByteArray:
+ var result := PackedByteArray()
+ result.append_array(lsb_first(0x0, 2)) # reserved
+ result.append_array(lsb_first(0x1, 2)) # icon type
+ result.append_array(lsb_first(size, 2)) # image count
+ return result
+
+
+ func generate_icon_entry(image: Image, size: int, offset: int) -> PackedByteArray:
+ var result := PackedByteArray()
+ result.append(image.get_width()) # width
+ result.append(image.get_height()) # height
+ result.append(0x0) # size of color palette
+ result.append(0x0) # reserved
+ result.append_array(lsb_first(0, 2)) # no color planes
+ result.append_array(lsb_first(32, 2)) # bits per pixel
+ result.append_array(lsb_first(size)) # size of embedded png
+ result.append_array(lsb_first(offset))
+ return result
+
+
+ func generate_png(image: Image) -> PackedByteArray:
+ var result := PackedByteArray()
+ var header_chunk := generate_header_chunk(image.get_width(), image.get_height())
+ var data_chunk := generate_data_chunk(image)
+ var end_chunk := generate_end_chunk()
+ result.append_array(PNG_SIGNATURE)
+ result.append_array(generate_chunk(header_chunk))
+ result.append_array(generate_chunk(data_chunk))
+ result.append_array(generate_chunk(end_chunk))
+ return result
+
+
+ func generate_chunk(chunk: PackedByteArray) -> PackedByteArray:
+ var result := PackedByteArray()
+ result.append_array(msb_first(chunk.size() - 4))
+ result.append_array(chunk)
+ result.append_array(msb_first(crc(chunk)))
+ return result
+
+
+ func generate_header_chunk(width: int, height: int) -> PackedByteArray:
+ var result = PackedByteArray()
+ result.append_array(IHDR_SIGNATURE)
+ result.append_array(msb_first(width))
+ result.append_array(msb_first(height))
+ result.append(0x8) # bit depth
+ result.append(0x6) # color type 32bit RGBA
+ result.append(0x0) # compression method
+ result.append(0x0) # filter method
+ result.append(0x0) # interlace method
+ return result
+
+
+ func generate_data_chunk(image: Image) -> PackedByteArray:
+ @warning_ignore("shadowed_variable")
+ var filtered_pixels := filtered_pixels(image.get_width(), image.get_height(), image.get_data())
+ @warning_ignore("integer_division")
+ var zlib_block_count := filtered_pixels.size() / ZLIB_BLOCK_SIZE + (1 if filtered_pixels.size() % ZLIB_BLOCK_SIZE else 0)
+ var result := PackedByteArray()
+ result.append_array(IDAT_SIGNATURE)
+ result.append(0x78) # CMF
+ result.append(0x1) # FLG
+ for i in range(zlib_block_count):
+ var last_block := i == zlib_block_count - 1
+ result.append(0x1 if last_block else 0x0)
+ @warning_ignore("shadowed_variable")
+ var block_size := filtered_pixels.size() % ZLIB_BLOCK_SIZE if last_block else ZLIB_BLOCK_SIZE
+ result.append_array(block_size(block_size))
+ for b in range(block_size):
+ result.append(filtered_pixels[i * ZLIB_BLOCK_SIZE + b])
+ result.append_array(msb_first(adler(filtered_pixels)))
+ return result
+
+
+ func generate_end_chunk() -> PackedByteArray:
+ return IEND_SIGNATURE
+
+
+ func filtered_pixels(width: int, height: int, pixels: PackedByteArray) -> PackedByteArray:
+ var result = PackedByteArray()
+ for row in range(height):
+ result.append(0x0)
+ for column in range(width * 4):
+ result.append(pixels[row * width * 4 + column])
+ return result
+
+
+ func generate_crc_table() -> Array:
+ var result = []
+ var c: int
+ for n in range(CRC_TABLE_SIZE):
+ c = n
+ for _i in range(8):
+ if (c & 1) != 0:
+ c = 0xedb88320 ^ (c >> 1)
+ else:
+ c = c >> 1
+ result.append(c)
+ return result
+
+
+ func crc(bytes: PackedByteArray) -> int:
+ var c := 0xffffffff
+ for i in range(bytes.size()):
+ c = crc_table[(c ^ bytes[i]) & 0xff] ^ (c >> 8)
+ return c ^ 0xffffffff
+
+
+ func adler(bytes: PackedByteArray) -> int:
+ var a := 1
+ var b := 0
+ for byte in bytes:
+ a = (a + byte) % ADLER_MOD
+ b = (a + b) % ADLER_MOD
+ return b << 16 | a
+
+
+ func msb_first(i: int) -> PackedByteArray:
+ var result := PackedByteArray()
+ result.append((i >> 24) & 0xff)
+ result.append((i >> 16) & 0xff)
+ result.append((i >> 8) & 0xff)
+ result.append(i & 0xff)
+ return result
+
+
+ func lsb_first(i: int, size = 4) -> PackedByteArray:
+ var result := PackedByteArray()
+ for _s in range(size):
+ result.append(i & 0xff)
+ i = i >> 8
+ return result
+
+
+ func block_size(i: int) -> PackedByteArray:
+ var result := PackedByteArray()
+ result.append(i & 0xff)
+ result.append((i >> 8) & 0xff)
+ result.append((i & 0xff) ^ 0xff)
+ result.append(((i >> 8) & 0xff) ^ 0xff)
+ return result
diff --git a/CreateIcon.gd.uid b/CreateIcon.gd.uid
new file mode 100644
index 00000000..4f2ebb9a
--- /dev/null
+++ b/CreateIcon.gd.uid
@@ -0,0 +1 @@
+uid://bb0n1dj2k22gl
diff --git a/Export.ps1 b/Export.ps1
new file mode 100644
index 00000000..9788c52a
--- /dev/null
+++ b/Export.ps1
@@ -0,0 +1 @@
+F:\Apps\Godot_v4.4\Godot_v4.4-rc1_mono_win64.exe --headless --export-release "Windows Desktop" .\build\Cirno_No_Reason.exe
\ No newline at end of file
diff --git a/ReplaceIcon.gd b/ReplaceIcon.gd
new file mode 100644
index 00000000..3cfef3af
--- /dev/null
+++ b/ReplaceIcon.gd
@@ -0,0 +1,212 @@
+class_name ReplaceIcon
+extends SceneTree
+
+const ICON_SIZE := 359559
+
+var error_callable: Callable
+
+
+func _init() -> void:
+ var arguments = OS.get_cmdline_args()
+ if arguments.size() != 4:
+ print(
+ "Usage:\n",
+ " godot -s ReplaceIcon.gd icon name\n",
+ "\n",
+ "Replaces ico file in windows PE32+ executable.\n",
+ "Add --headless to hide Godot console.\n",
+ "\n",
+ "Arguments:\n",
+ " godot path to Godot 4 beta2+ executable\n",
+ " icon path to new icon\n",
+ " name path to modified PE32+ executable\n"
+ )
+ quit()
+ return
+ replace_icon(arguments[3], arguments[2])
+ quit()
+
+
+func replace_icon(executable_path: String, icon_path: String) -> void:
+ var icon_replacer := IconReplacer.new()
+
+ var images := get_images(icon_path)
+
+ var executable_file := FileAccess.open(executable_path, FileAccess.READ_WRITE)
+ if not executable_file:
+ print_error("Could not open executable file!")
+ return
+ var headers := executable_file.get_buffer(2048)
+ var resources_section_entry := icon_replacer.find_resources_section_entry(headers)
+ if not resources_section_entry:
+ return
+ if resources_section_entry.size_of_raw_data < 359559:
+ print_error("Could not find icons in executable. Wrong template?")
+ return
+
+ executable_file.seek(resources_section_entry.pointer_to_raw_data)
+ var resources := executable_file.get_buffer(resources_section_entry.size_of_raw_data)
+
+ resources = icon_replacer.replace_icons(resources, resources_section_entry.virtual_address, images)
+ if not resources.is_empty():
+ executable_file.seek(resources_section_entry.pointer_to_raw_data)
+ executable_file.store_buffer(resources)
+
+
+func get_images(icon_path: String) -> Dictionary:
+ var file := FileAccess.open(icon_path, FileAccess.READ)
+ if not file:
+ print_error(str("Could not open icon file!\n", FileAccess.get_open_error()))
+ return {}
+ return Icon.new(file.get_buffer(ICON_SIZE)).images
+
+
+func print_error(error_message: String) -> void:
+ printerr(error_message)
+ if error_callable:
+ error_callable.call(error_message)
+
+
+class IconReplacer:
+ enum ImageType {PE32 = 0x10b, PE32_PLUS = 0x20b}
+
+ const PE_HEADER_ADDRESS_OFFSET := 0x3c
+ const NUMBER_OF_SECTIONS_OFFSET := 0x6
+ const SIZE_OF_OPTIONAL_HEADER_OFFSET := 0x14
+ const MAGIC_OFFSET := 0x18
+ const COFF_HEADER_SIZE := 24
+ const SECTION_SIZE := 40
+ const SIZE_OF_RAW_DATA_OFFSET := 0x10
+ const POINTER_TO_RAW_DATA_OFFSET := 0x14
+ const DATA_ENTRY_SIZE := 16
+
+ var error_callable: Callable
+
+
+ func replace_icons(resources: PackedByteArray, rva_offset: int, images: Dictionary) -> PackedByteArray:
+ var data_entries := find_data_entries(resources)
+ for data_size in images.keys():
+ var icon_offset := find_icon_offset(data_entries, data_size, rva_offset)
+ if resources.slice(icon_offset + 1, icon_offset + 4).get_string_from_ascii() != "PNG":
+ print_error("Wrong icon type, PNG signature missing")
+ return PackedByteArray()
+ resources = IconReplacer.replace(resources, images[data_size], icon_offset)
+ return resources
+
+
+ func find_icon_offset(data_entries: Array, data_size: int, rva_offset: int) -> int:
+ for data_entry in data_entries:
+ if data_entry.size == data_size:
+ return data_entry.rva - rva_offset
+ return -1
+
+
+ func find_resources_section_entry(headers: PackedByteArray) -> SectionEntry:
+ var header_offset := IconReplacer.lsb_first(headers, PE_HEADER_ADDRESS_OFFSET, 2)
+ var image_type := IconReplacer.lsb_first(headers, header_offset + MAGIC_OFFSET, 2)
+ if not image_type == ImageType.PE32_PLUS:
+ print_error("Only PE32+ executables are handled.")
+ return null
+ var sections_size := IconReplacer.lsb_first(headers, header_offset + NUMBER_OF_SECTIONS_OFFSET, 2)
+ var size_of_optional_header := IconReplacer.lsb_first(headers, header_offset + SIZE_OF_OPTIONAL_HEADER_OFFSET, 2)
+ var sections_offset := header_offset + COFF_HEADER_SIZE + size_of_optional_header
+ for _i in range(sections_size):
+ var section_name = headers.slice(sections_offset, sections_offset + 8).get_string_from_ascii()
+ if section_name == ".rsrc":
+ return SectionEntry.new(headers.slice(sections_offset, sections_offset + SECTION_SIZE))
+ sections_offset += SECTION_SIZE
+ return null
+
+
+ func find_data_entries(resources: PackedByteArray) -> Array:
+ var result := []
+ parse_table(resources, 0, result)
+ return result
+
+
+ func parse_table(resources: PackedByteArray, offset: int, data_entries: Array) -> void:
+ var entry_count := IconReplacer.lsb_first(resources, offset + 14, 2)
+ offset += 16
+ for _i in range(entry_count):
+ parse_entry(resources, offset, data_entries)
+ offset += 8
+
+
+ func parse_entry(resources: PackedByteArray, offset: int, data_entries: Array) -> void:
+ var entry_offset := IconReplacer.lsb_first(resources, offset + 4)
+ if entry_offset & 0x80000000:
+ parse_table(resources, entry_offset & 0x7fffffff, data_entries)
+ else:
+ parse_data_entry(resources, entry_offset, data_entries)
+
+
+ func parse_data_entry(resources: PackedByteArray, offset: int, data_entries: Array) -> void:
+ data_entries.append(DataEntry.new(resources.slice(offset, offset + DATA_ENTRY_SIZE)))
+
+
+ func print_error(error_message: String) -> void:
+ printerr(error_message)
+ if error_callable:
+ error_callable.call(error_message)
+
+
+ static func lsb_first(bytes: PackedByteArray, offset: int, byte_count = 4) -> int:
+ var result := 0
+ for i in range(byte_count, 0, -1):
+ result = (result << 8) + bytes[offset + i - 1]
+ return result
+
+
+ static func replace(bytes: PackedByteArray, replacement: PackedByteArray, index: int) -> PackedByteArray:
+ for i in range(replacement.size()):
+ bytes.set(index + i, replacement[i])
+ return bytes
+
+
+class SectionEntry:
+ const VIRTUAL_ADDRESS_OFFSET := 0x0c
+ const SIZE_OF_RAW_DATA_OFFSET = 0x10
+ const POINTER_TO_RAW_DATA_OFFSET = 0x14
+
+ var virtual_address: int
+ var pointer_to_raw_data: int
+ var size_of_raw_data: int
+
+
+ func _init(bytes: PackedByteArray) -> void:
+ virtual_address = IconReplacer.lsb_first(bytes, VIRTUAL_ADDRESS_OFFSET)
+ size_of_raw_data = IconReplacer.lsb_first(bytes, SIZE_OF_RAW_DATA_OFFSET)
+ pointer_to_raw_data = IconReplacer.lsb_first(bytes, POINTER_TO_RAW_DATA_OFFSET)
+
+
+class DataEntry:
+ const RVA_OFFSET := 0
+ const SIZE_OFFSET := 4
+
+ var rva: int
+ var size: int
+
+
+ func _init(bytes: PackedByteArray) -> void:
+ rva = IconReplacer.lsb_first(bytes, RVA_OFFSET)
+ size = IconReplacer.lsb_first(bytes, SIZE_OFFSET)
+
+
+class Icon:
+ const IMAGE_COUNT_OFFSET := 0x4
+ const IMAGES_OFFSET := 0x6
+ const ICON_ENTRY_SIZE := 16
+ const SIZE_OFFSET := 0x8
+ const DATA_OFFSET := 0xc
+
+ var images := {}
+
+
+ func _init(bytes: PackedByteArray) -> void:
+ var image_count := IconReplacer.lsb_first(bytes, IMAGE_COUNT_OFFSET, 2)
+ var offset := IMAGES_OFFSET
+ for i in image_count:
+ var size := IconReplacer.lsb_first(bytes, offset + SIZE_OFFSET)
+ var data_offset := IconReplacer.lsb_first(bytes, offset + DATA_OFFSET)
+ images[size] = bytes.slice(data_offset, data_offset + size)
+ offset += ICON_ENTRY_SIZE
diff --git a/ReplaceIcon.gd.uid b/ReplaceIcon.gd.uid
new file mode 100644
index 00000000..fb1844db
--- /dev/null
+++ b/ReplaceIcon.gd.uid
@@ -0,0 +1 @@
+uid://cdbkovbleadx1
diff --git a/Sprites/Actors/Cirno.aseprite b/Sprites/Actors/Cirno.aseprite
index 8b940359..886aad11 100644
--- a/Sprites/Actors/Cirno.aseprite
+++ b/Sprites/Actors/Cirno.aseprite
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9bc3fdd9abed06ce91a678e656d33723778614600e2ae691e2403dfb9ccf5094
-size 4029
+oid sha256:25d9f61ebdb413dd604f8f13db73225ae033f7ba822e41c5ae419f4d5f3a608b
+size 4439
diff --git a/Sprites/Icon.ico b/Sprites/Icon.ico
new file mode 100644
index 00000000..9d108eef
Binary files /dev/null and b/Sprites/Icon.ico differ
diff --git a/addons/scene_palette/save_data/save_data.tres b/addons/scene_palette/save_data/save_data.tres
new file mode 100644
index 00000000..b30374ee
--- /dev/null
+++ b/addons/scene_palette/save_data/save_data.tres
@@ -0,0 +1,7 @@
+[gd_resource type="Resource" script_class="PalettePluginSaveData" load_steps=2 format=3 uid="uid://c6r8j36esb764"]
+
+[ext_resource type="Script" uid="uid://d2vskl7301jhr" path="res://addons/scene_palette/resources/rsc_pallete_plugin_save_data.gd" id="1_tuhxn"]
+
+[resource]
+script = ExtResource("1_tuhxn")
+favorites = {}
diff --git a/export_presets.cfg b/export_presets.cfg
index 5c9554ea..c6e4e99b 100644
--- a/export_presets.cfg
+++ b/export_presets.cfg
@@ -10,8 +10,10 @@ export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../builds/cirnoreason.exe"
+patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
+seed=0
encrypt_pck=false
encrypt_directory=false
script_export_mode=2
@@ -32,7 +34,7 @@ codesign/digest_algorithm=1
codesign/description=""
codesign/custom_options=PackedStringArray()
application/modify_resources=true
-application/icon=""
+application/icon="uid://dicat7qqa0a7o"
application/console_wrapper_icon=""
application/icon_interpolation=4
application/file_version=""
diff --git a/project.godot b/project.godot
index b98c82ba..22c17b3b 100644
--- a/project.godot
+++ b/project.godot
@@ -100,7 +100,7 @@ project/assembly_name="Cirno"
[editor_plugins]
-enabled=PackedStringArray("res://addons/DebugGUI/plugin.cfg", "res://addons/dialogic/plugin.cfg", "res://addons/smoothing/plugin.cfg")
+enabled=PackedStringArray("res://addons/DebugGUI/plugin.cfg", "res://addons/dialogic/plugin.cfg", "res://addons/scene_palette/plugin.cfg", "res://addons/smoothing/plugin.cfg")
[global_group]