A => .gitignore +2 -0
A => COPYRIGHT.txt +63 -0
@@ 1,63 @@
+# This is a sample COPYRIGHT.txt file for a Godot Engine project which uses the
+# Godot Universal Inclusion of Licenses Dilaog (GUILD). The first entry marked
+# "Format" contains a link to a page which gives details on how this file
+# should be formatted. The COPYRIGHT.txt file used by the Godot Engine is also
+# a good reference for the formatting of this file.
+#
+# If the Copyright File parameter in your LicenseDialog starts with "res://",
+# indicating that the file is stored as a resource in the game rather than in
+# the user's file system, you should take care to export this file with any
+# binaries you might compile for your game. If you have a copy of the
+# documentation, it will contain instructions on how you can do this. If you
+# do not have a copy of the documentation, you can find it at the Source link
+# below.
+#
+# You may include the licensing information contained herein (with
+# modifications where appropriate) as an attribution notice if you choose to
+# use GUILD in your game or other project; however, as it has been released
+# into the public domain, you are not required to do so.
+
+-----------------------------------------------------------------------
+
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: Generic Godot Engine License Dialog (GGELD)
+Upstream-Contact: Philip Pavlick <swashdev@pm.me>
+Source: https://git.sr.ht/~swashberry/godot-license-dialog
+
+Files: ./license_dialog.gd
+ ./license_dialog.tscn
+ ./project.godot
+ ./sample_project/sample_main_node.gd
+ ./sample_project/sample_main_node.tscn
+Comment: Generic Godot Engine License Dialog (GGELD)
+Copyright: 2021 Philip Pavlick
+License: Unlicense
+
+
+
+License: Unlicense
+ This is free and unencumbered software released into the public domain.
+ .
+ Anyone is free to copy, modify, publish, use, compile, sell, or
+ distribute this software, either in source code form or as a compiled
+ binary, for any purpose, commercial or non-commercial, and by any
+ means.
+ .
+ In jurisdictions that recognize copyright laws, the author or authors
+ of this software dedicate any and all copyright interest in the
+ software to the public domain. We make this dedication for the benefit
+ of the public at large and to the detriment of our heirs and
+ successors. We intend this dedication to be an overt act of
+ relinquishment in perpetuity of all present and future rights to this
+ software under copyright law.
+ .
+ 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 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.
+ .
+ For more information, please refer to <https://unlicense.org/>
+
A => UNLICENSE +24 -0
@@ 1,24 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+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 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.
+
+For more information, please refer to <https://unlicense.org>
A => license_dialog.gd +387 -0
@@ 1,387 @@
+tool
+extends WindowDialog
+# A dialog which displays a human-readable list of attribution notices.
+
+
+# Source: https://git.sr.ht/~swashberry/godot-license-dialog
+# Version 0.0.0
+
+
+# An enum used by the `_read_copyright_file` function to determine what kind of
+# line is being read at the time.
+enum { _FILE, _COPYRIGHT, _COMMENT, _LICENSE }
+
+# The name of the project which will be used. If left blank, this value will be
+# replaced with the name of the Godot Engine project.
+export(String) onready var project_name = "" setget set_project_name, \
+ get_project_name
+
+# The path to a file containing licensing information for the game.
+export(String, FILE, "*.txt") onready var copyright_file = \
+ "res://COPYRIGHT.txt" setget set_copyright_file, get_copyright_file
+
+# The text for the label which appears above the components list. The text
+# "GAME_NAME" will be replaced with the contents of `project_name`.
+export(String, MULTILINE) onready var label_text = "Clicking one of the " + \
+ "buttons below will display licensing information for individual " + \
+ "components of GAME_NAME and the engine it is built on, the Godot " + \
+ "Engine." setget set_label_text, get_label_text
+
+# A dictionary which will store licensing information for the game, parsed from
+# the game copyright file, and a corresponding TreeItem which will display this
+# information.
+var project_components: Dictionary = {}
+var project_components_tree: TreeItem
+
+# A dictionary which will store licensing information for the Godot Engine,
+# parsed from data collected from the engine itself, and a corresponding
+# TreeItem which will display this information.
+var _godot_components: Dictionary = {}
+var _godot_components_tree: TreeItem
+
+# A dictionary which will store the full text of the licensing gathered from
+# the above sources, and a TreeItem which will display this information.
+var _licenses: Dictionary = {}
+var _licenses_tree: TreeItem
+
+
+func _ready():
+ # Set the label text appropriately.
+ var game_name = project_name
+ if game_name == "":
+ game_name = ProjectSettings.get_setting( "application/config/name" )
+ $Label.set_text( label_text.replace( "GAME_NAME", game_name ) )
+ # Create the root for the component list tree.
+ var component_list = $ComponentList
+ var root = component_list.create_item()
+
+ # Populate the game components & licensing information from the copyright
+ # file.
+ _read_copyright_file()
+
+ if project_components.size() == 0:
+ push_warning( "Couldn't read any copyright data for this game!" )
+ else:
+ # Create a subtree for the project components list.
+ project_components_tree = component_list.create_item( root )
+ project_components_tree.set_text( 0, game_name )
+ project_components_tree.set_selectable( 0, false )
+ for component in project_components:
+ var component_item = component_list.create_item(
+ project_components_tree )
+ component_item.set_text( 0, component )
+
+ # Create a subtree for the Godot Engine components list.
+ _godot_components_tree = component_list.create_item( root )
+ _godot_components_tree.set_text( 0, "Godot Engine" )
+ _godot_components_tree.set_selectable( 0, false )
+
+ # Populate the Godot Engine components subtree.
+ var components: Array = Engine.get_copyright_info()
+ for component in components:
+ var component_item = component_list.create_item( _godot_components_tree
+ )
+ component_item.set_text( 0, component["name"] )
+ _godot_components[component["name"]] = component["parts"]
+
+ # The `_licenses` dictionary has already been populated by
+ # `_read_copyright_file` but still needs to be populated with licenses from
+ # the Godot Engine.
+ var license_info: Dictionary = Engine.get_license_info()
+ var keys = license_info.keys()
+ var key_count: int = keys.size()
+ for index in key_count:
+ _licenses[keys[index]] = license_info[keys[index]]
+
+ # Create a subtree for the licenses list.
+ _licenses_tree = component_list.create_item( root )
+ _licenses_tree.set_text( 0, "Licenses" )
+ _licenses_tree.set_selectable( 0, false )
+
+ # Populate the Licenses subtree.
+ keys = _licenses.keys()
+ # Sort the keys so that the licenses will be displayed in alphabetical
+ # order.
+ keys.sort()
+ key_count = keys.size()
+ for index in key_count:
+ var license_item = component_list.create_item( _licenses_tree )
+ license_item.set_text( 0, keys[index] )
+ license_item.set_selectable( 0, true )
+
+
+func set_project_name( new_name: String ):
+ project_name = new_name
+
+
+func get_project_name() -> String:
+ return project_name
+
+
+func set_copyright_file( file_path: String ):
+ copyright_file = file_path
+
+
+func get_copyright_file() -> String:
+ return copyright_file
+
+
+func set_label_text( text: String ):
+ label_text = text
+
+
+func get_label_text( replace_name: bool = false ) -> String:
+ if replace_name:
+ return label_text
+ else:
+ return label_text.replace( "GAME_NAME", project_name )
+
+
+func _on_ComponentList_item_selected():
+ var selected: TreeItem = $ComponentList.get_selected()
+ var parent: TreeItem = selected.get_parent()
+ var title: String = selected.get_text( 0 )
+ var parent_title: String = parent.get_text( 0 )
+
+ if parent_title == "Godot Engine":
+ _display_game_component_info( title, _godot_components[title] )
+ elif parent_title == "Licenses":
+ _display_license_info( title )
+ else:
+ _display_game_component_info( title, project_components[title] )
+
+
+func _display_game_component_info( var title: String, component: Array ):
+ var text: String = title
+
+ for part in component:
+ text += "\n\nFiles:"
+ for file in part["files"]:
+ text += "\n %s" % file
+ text += "\n"
+ for copyright in part["copyright"]:
+ text += "\nCopyright (c) %s" % copyright
+ text += "\nLicense: %s" % part["license"]
+
+ _popup_attribution_dialog( title, text )
+
+
+func _display_license_info( var key: String ):
+ _popup_attribution_dialog( key, _licenses[key] )
+
+
+func _popup_attribution_dialog( title: String, text: String ):
+ $AttributionDialog.set_title( title )
+ $AttributionDialog/TextBox.set_text( text )
+ $AttributionDialog/TextBox.scroll_vertical = 0
+ $AttributionDialog/TextBox.scroll_horizontal = 0
+ $AttributionDialog.popup_centered()
+
+
+
+# Reads in the copyright file given by `copyright_file` and parses it
+# to fill the `_game_copyright_info` and `_licenses` variables.
+func _read_copyright_file():
+ var f: File = File.new()
+ var err = f.open( copyright_file, File.READ )
+
+ if err != OK:
+ push_warning( "Couldn't find copyright file! Got error %d trying to open %s"
+ % [err, copyright_file] )
+ return
+
+ var file_paragraph: PoolStringArray = []
+ var comment_paragraph: PoolStringArray = []
+ var copyright_paragraph: PoolStringArray = []
+ var license_paragraph: PoolStringArray = []
+
+ var reading: int
+ var line_count: int = 0
+ var blank_line_count: int = 0
+ var got_first_file: bool = false
+ var reading_file_paragraph: bool = false
+
+ # Iterate through the copyright file one line at a time.
+ while not f.eof_reached():
+ line_count += 1
+ var line: String = f.get_line()
+
+ # Decide how to parse each line depending on its prefix.
+ if line.begins_with( "Files: " ):
+ blank_line_count = 0
+ got_first_file = true
+ reading_file_paragraph = true
+ reading = _FILE
+ line.erase( 0, 7 )
+ file_paragraph = [line.strip_edges()]
+ #print_debug( "Line %d: Started reading files w/ %s"
+ # % [line_count, line.strip_edges()] )
+ elif line.begins_with( "Comment: " ):
+ blank_line_count = 0
+ got_first_file = true
+ reading_file_paragraph = true
+ reading = _COMMENT
+ line.erase( 0, 9 )
+ comment_paragraph = [line.strip_edges()]
+ #print_debug( "Line %d: Started reading comments w/ %s"
+ # % [line_count, line.strip_edges()] )
+ elif line.begins_with( "Copyright: " ):
+ blank_line_count = 0
+ got_first_file = true
+ reading_file_paragraph = true
+ reading = _COPYRIGHT
+ line.erase( 0, 11 )
+ copyright_paragraph = [line.strip_edges()]
+ #print_debug( "Line %d: Started reading copyrights w/ %s"
+ # % [line_count, line.strip_edges()] )
+ elif line.begins_with( "License: " ):
+ blank_line_count = 0
+ got_first_file = true
+ reading_file_paragraph = true
+ reading = _LICENSE
+ line.erase( 0, 9 )
+ license_paragraph = [line.strip_edges()]
+ #print_debug( "Line %d: Started reading licenses w/ %s"
+ # % [line_count, line.strip_edges()] )
+ elif line.begins_with( " " ):
+ if not reading_file_paragraph:
+ push_warning( "Inappropriate indentation at line %d in %s"
+ % [line_count, copyright_file] )
+ else:
+ blank_line_count = 0
+ line = line.strip_edges()
+ match reading:
+ _FILE:
+ file_paragraph.append( line )
+ #print_debug( "Line %d: Reading file %s"
+ # % [line_count, line ] )
+ _COMMENT:
+ comment_paragraph.append( line )
+ #print_debug( "Line %d: Reading comment %s"
+ # % [line_count, line ] )
+ _COPYRIGHT:
+ copyright_paragraph.append( line )
+ #print_debug( "Line %d: Reading copyright %s"
+ # % [line_count, line ] )
+ _LICENSE:
+ license_paragraph.append( line )
+ #print_debug( "Line %d: Reading license %s"
+ # % [line_count, line ] )
+ _:
+ push_error( "Bad code detected! Invalid value %d for `reading`"
+ % reading )
+ elif line.strip_edges() == "":
+ # Only count blank lines after we start reading files.
+ if got_first_file:
+ blank_line_count += 1
+ # Three blank lines separate the file paragraphs from the license
+ # paragraphs, so if we count three blank lines we should break
+ # here.
+ if blank_line_count == 3:
+ break
+ # If there's only one blank line, assume we are terminating reading
+ # of a file paragraph.
+ elif blank_line_count == 1:
+ if file_paragraph.size() > 0 \
+ and comment_paragraph.size() > 0 \
+ and copyright_paragraph.size() > 0 \
+ and license_paragraph.size() > 0:
+ reading_file_paragraph = false
+ var full_paragraph: Dictionary = {}
+
+ full_paragraph["files"] = []
+ for file in file_paragraph:
+ full_paragraph["files"].append( file )
+
+ full_paragraph["copyright"] = []
+ for copyright in copyright_paragraph:
+ full_paragraph["copyright"].append( copyright )
+
+ var license_line_count: int = 0
+ var license = ""
+ for license_line in license_paragraph:
+ if license_line_count > 0:
+ license += "\n "
+ license += license_line
+ license_line_count += 1
+ full_paragraph["license"] = license
+
+ for comment in comment_paragraph:
+ if not project_components.has( comment ):
+ project_components[comment] = [full_paragraph]
+ else:
+ push_warning( "Duplicate component %s found at line %d in %s. Consider consolidating."
+ % [comment, line_count, copyright_file] )
+ project_components[comment].append( full_paragraph )
+ #print_debug( "Line %d: Finished reading file paragraph for %s"
+ # % [line_count, comment] )
+
+ file_paragraph = []
+ comment_paragraph = []
+ copyright_paragraph = []
+ license_paragraph = []
+ else:
+ push_warning( "Malformed file paragraph at line %d in %s"
+ % [line_count, copyright_file] )
+ push_warning( "NOTE: See this link for file format: %s"
+ % "https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/" )
+
+ # Having broken out of the loop, check if we aborted in the middle of a
+ # file paragraph.
+ if reading_file_paragraph:
+ push_warning( "Got to end of file paragraphs on line %d in %s while still reading file paragraph for %s!"
+ % [line_count, copyright_file, comment_paragraph[0]] )
+ push_warning( "NOTE: Three blank lines terminate reading file paragraphs!" )
+
+ # Having broken out of the loop, check if we've reached EOF already; if we
+ # have, the copyright file does not contain any licenses.
+ if f.eof_reached():
+ push_warning( "Reached EOF in %s before reading licenses!"
+ % copyright_file )
+ return
+
+ var short_license: String
+ var license: String = ""
+ var reading_license: bool = false
+
+ # Read the licenses:
+ while not f.eof_reached():
+ line_count += 1
+ var line: String = f.get_line()
+
+ if line.begins_with( "License: " ):
+ blank_line_count = 0
+ if reading_license:
+ push_warning( "Malformed license at line %d in %s! Reached next license before license finished!"
+ % [line_count, copyright_file] )
+ else:
+ line.erase( 0, 9 )
+ short_license = line.strip_edges()
+ license = ""
+ reading_license = true
+ #print_debug( "Line %d: Started reading license %s"
+ # % [line_count, short_license] )
+ elif line.begins_with( " " ):
+ blank_line_count = 0
+ if not reading_license:
+ push_warning( "Inappropriate indentation at line %d in %s"
+ % [line_count, copyright_file] )
+ else:
+ line = line.strip_edges()
+ # If the line only contains a dot, just add a newline.
+ if line != ".":
+ license += line
+ license += "\n"
+ elif line.strip_edges() == "":
+ #print_debug( "Found blank line at %d" % line_count )
+ blank_line_count += 1
+ if reading_license and blank_line_count == 1:
+ #print_debug( "Finished reading license %s" % short_license )
+ _licenses[short_license] = license
+ reading_license = false
+
+ # Having broken out of the loop, check if we aborted in the middle of
+ # reading a license.
+ if reading_license:
+ push_warning( "Reached EOF at line %d in %s in the middle of reading license for %s!"
+ % [line_count, copyright_file, short_license] )
A => license_dialog.tscn +66 -0
@@ 1,66 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://license_dialog.gd" type="Script" id=1]
+
+[node name="LicenseDialog" type="WindowDialog"]
+margin_right = 640.0
+margin_bottom = 460.0
+rect_min_size = Vector2( 350, 160 )
+window_title = "Licensing Information"
+resizable = true
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+label_text = "Clicking one of the buttons below will display licensing information for individual components of and the engine it is built on, the Godot Engine."
+
+[node name="Label" type="Label" parent="."]
+anchor_left = 0.01
+anchor_top = 0.01
+anchor_right = 0.99
+anchor_bottom = 0.1
+text = "Clicking one of the buttons below will display licensing information for individual components of Generic Godot Engine License Dialog and the engine it is built on, the Godot Engine."
+autowrap = true
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="ComponentList" type="Tree" parent="."]
+anchor_left = 0.01
+anchor_top = 0.11
+anchor_right = 0.99
+anchor_bottom = 0.99
+allow_reselect = true
+hide_root = true
+drop_mode_flags = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="AttributionDialog" type="AcceptDialog" parent="."]
+visible = true
+margin_right = 600.0
+margin_bottom = 400.0
+window_title = "This window title will be replaced with the title of an attribution notice."
+resizable = true
+dialog_autowrap = true
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="TextBox" type="TextEdit" parent="AttributionDialog"]
+anchor_left = 0.01
+anchor_top = 0.01
+anchor_right = 0.99
+anchor_bottom = 0.9
+margin_left = 2.0
+margin_top = 4.0
+margin_right = -2.0
+margin_bottom = 4.0
+custom_colors/font_color_readonly = Color( 1, 1, 1, 1 )
+text = "This text will be replaced with the text of an attribution notice when the window pops up."
+readonly = true
+__meta__ = {
+"_edit_use_anchors_": false
+}
+[connection signal="item_selected" from="ComponentList" to="." method="_on_ComponentList_item_selected"]