diff options
Diffstat (limited to 'dragon')
40 files changed, 2283 insertions, 1195 deletions
diff --git a/dragon/api.rb b/dragon/api.rb new file mode 100644 index 0000000..8ac1bae --- /dev/null +++ b/dragon/api.rb @@ -0,0 +1,634 @@ +# coding: utf-8 +# Copyright 2019 DragonRuby LLC +# MIT License +# api.rb has been released under MIT (*only this file*). + +module GTK + class Api + def initialize + end + + def get_api_autocomplete args, req + html = <<-S +<html> + <head> + <meta charset="UTF-8"/> + <title>DragonRuby Game Toolkit Documentation</title> + <style> + pre { + border: solid 1px silver; + padding: 10px; + font-size: 14px; + white-space: pre-wrap; + white-space: -moz-pre-wrap; + white-space: -pre-wrap; + white-space: -o-pre-wrap; + word-wrap: break-word; + } + </style> + </head> + <body> + <script> + async function submitForm() { + const result = await fetch("/dragon/autocomplete/", { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ index: document.getElementById("index").value, + text: document.getElementById("text").value }), + }); + document.getElementById("autocomplete-results").innerHTML = await result.text(); + } + </script> + <form> + <div>index</div> + <input name="index" id="index" type="text" value="27" /> + <div>code</div> + <textarea name="text" id="text" rows="30" cols="80">def tick args + args.state. +end</textarea> + <br/> + <input type="button" value="Get Suggestions" onclick="submitForm();" /> + <span id="success-notification"></span> + </form> + <pre id="autocomplete-results"> + </pre> + + #{links} + </body> +</html> +S + + req.respond 200, + html, + { 'Content-Type' => 'text/html' } + end + + def post_api_autocomplete args, req + json = ($gtk.parse_json req.body) + index = json["index"].to_i + text = json["text"] + suggestions = args.gtk.suggest_autocompletion index: index, text: text + list_as_string = suggestions.join("\n") + req.respond 200, list_as_string, { 'Content-Type' => 'text/plain' } + end + + define_method :links do + <<-S + <ul> + <li><a href="/">Home</a></li> + <li><a href="/docs.html">Docs</a></li> + <li><a href="/dragon/control_panel/">Control Panel</a></li> + <li><a href="/dragon/eval/">Console</a></li> + <li><a href="/dragon/log/">Logs</a></li> + <li><a href="/dragon/puts/">Puts</a></li> + <li><a href="/dragon/code/">Code</a></li> + </ul> +S + end + + def get_index args, req + req.respond 200, <<-S, { 'Content-Type' => 'text/html' } +<html> + <head> + <meta charset="UTF-8"/> + <title>DragonRuby Game Toolkit Documentation</title> + </head> + <body> + #{links} + </body> +</html> +S + end + + def source_code_links args + links = args.gtk.reload_list_history.keys.map do |f| + "<li><a href=\"/dragon/code/edit/?file=#{f}\">#{f}</a></li>" + end + <<-S +<ul> + #{links.join("\n")} +</ul> +S + end + + def get_api_code args, req + view = <<-S +<html> + <head> + <meta charset="UTF-8"/> + <title>DragonRuby Game Toolkit Documentation</title> + </head> + <body> + #{source_code_links args} + + #{links} + </body> +</html> +S + req.respond 200, + view, + { 'Content-Type' => 'text/html' } + end + + def code_edit_view args, file + view = <<-S +<html> + <head> + <meta charset="UTF-8"/> + <title>DragonRuby Game Toolkit Documentation</title> + </head> + <body> + <script> + async function submitForm() { + const result = await fetch("/dragon/code/update/?file=#{file}", { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: document.getElementById("code").value }), + }); + document.getElementById("success-notification").innerHTML = "update successful"; + setTimeout(function() { document.getElementById("success-notification").innerHTML = ""; }, 3000); + } + </script> + <form> + <div><code>#{file}:</code></div> + <textarea name="code" id="code" rows="30" cols="80">#{args.gtk.read_file file}</textarea> + <br/> + <input type="button" value="Update" onclick="submitForm();" /> + <span id="success-notification"></span> + </form> + #{source_code_links args} + + #{links} + </body> +</html> +S + end + + def get_api_code_edit args, req + file = req.uri.split('?').last.gsub("file=", "") + view = code_edit_view args, file + req.respond 200, + view, + { 'Content-Type' => 'text/html' } + end + + def post_api_code_update args, req + file = req.uri.split('?').last.gsub("file=", "") + code = ($gtk.parse_json req.body)["code"] + args.gtk.write_file file, code + view = code_edit_view args, file + req.respond 200, + view, + { 'Content-Type' => 'text/html' } + end + + def get_api_boot args, req + req.respond 200, + args.gtk.read_file("tmp/src_backup/boot.txt"), + { 'Content-Type' => 'text/plain' } + end + + def get_api_trace args, req + req.respond 200, + args.gtk.read_file("logs/trace.txt"), + { 'Content-Type' => 'text/plain' } + end + + def get_api_log args, req + req.respond 200, + args.gtk.read_file("logs/log.txt"), + { 'Content-Type' => 'text/plain' } + end + + def post_api_log args, req + Log.log req.body + + req.respond 200, + "ok", + { 'Content-Type' => 'text/plain' } + end + + def get_api_puts args, req + req.respond 200, + args.gtk.read_file("logs/puts.txt"), + { 'Content-Type' => 'text/plain' } + end + + def get_api_changes args, req + req.respond 200, + args.gtk.read_file("tmp/src_backup/src_backup_changes.txt"), + { 'Content-Type' => 'text/plain' } + end + + def get_favicon_ico args, req + @favicon ||= args.gtk.read_file('docs/favicon.ico') + req.respond 200, @favicon, { "Content-Type" => 'image/x-icon' } + end + + def get_src_backup args, req + file_name = req.uri.gsub("/dragon/", "") + req.respond 200, + args.gtk.read_file("tmp/src_backup/#{file_name}"), + { 'Content-Type' => 'text/plain' } + end + + def get_not_found args, req + puts("METHOD: #{req.method}"); + puts("URI: #{req.uri}"); + puts("HEADERS:"); + req.headers.each { |k,v| puts(" #{k}: #{v}") } + req.respond 404, "not found: #{req.uri}", { } + end + + def get_docs_html args, req + req.respond 200, + args.gtk.read_file("docs/docs.html"), + { 'Content-Type' => 'text/html' } + end + + def get_docs_css args, req + req.respond 200, + args.gtk.read_file("docs/docs.css"), + { 'Content-Type' => 'text/css' } + end + + def get_docs_search_gif args, req + req.respond 200, + args.gtk.read_file("docs/docs_search.gif"), + { 'Content-Type' => 'image/gif' } + end + + def get_src_backup_index_html args, req + req.respond 200, + args.gtk.read_file("/tmp/src_backup/src_backup_index.html"), + { 'Content-Type' => 'text/html' } + end + + def get_src_backup_index_txt args, req + req.respond 200, + args.gtk.read_file("/tmp/src_backup/src_backup_index.txt"), + { 'Content-Type' => 'text/txt' } + end + + def get_src_backup_css args, req + req.respond 200, + args.gtk.read_file("/tmp/src_backup/src_backup.css"), + { 'Content-Type' => 'text/css' } + end + + def get_src_backup_changes_html args, req + req.respond 200, + args.gtk.read_file("/tmp/src_backup/src_backup_changes.html"), + { 'Content-Type' => 'text/html' } + end + + def get_src_backup_changes_txt args, req + req.respond 200, + args.gtk.read_file("/tmp/src_backup/src_backup_changes.txt"), + { 'Content-Type' => 'text/txt' } + end + + def get_api_eval args, req + eval_view = <<-S +<html lang="en"> + <head><title>Eval</title></head> + <style> + pre { + border: solid 1px silver; + padding: 10px; + font-size: 14px; + white-space: pre-wrap; + white-space: -moz-pre-wrap; + white-space: -pre-wrap; + white-space: -o-pre-wrap; + word-wrap: break-word; + } + </style> + <body> + <script> + async function submitForm() { + const result = await fetch("/dragon/eval/", { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ code: document.getElementById("code").value }), + }); + document.getElementById("eval-result").innerHTML = await result.text(); + } + </script> + <form> + <textarea name="code" id="code" rows="10" cols="80"># write your code here and set $result.\n$result = $gtk.args.state</textarea> + <br/> + <input type="button" onclick="submitForm();" value="submit" /> + </form> + <pre>curl -H "Content-Type: application/json" --data '{ "code": "$result = $args.state" }' -X POST http://localhost:9001/dragon/eval/</pre> + <div>Eval Result:</div> + <pre id="eval-result"></pre> + #{links} + </body> +</html> +S + req.respond 200, + eval_view, + { 'Content-Type' => 'text/html' } + end + + def post_api_eval args, req + if json? req + code = ($gtk.parse_json req.body)["code"] + code = code.gsub("$result", "$eval_result") + Object.new.instance_eval do + begin + Kernel.eval code + rescue Exception => e + $eval_result = e + end + end + end + + req.respond 200, + "#{$eval_result || $eval_results || "nil"}", + { 'Content-Type' => 'text/plain' } + + $eval_result = nil + $eval_results = nil + end + + def api_css_string + + end + + def get_api_console args, req + html = console_view "# write your code here and set $result.\n$result = $gtk.args.state" + req.respond 200, + html, + { 'Content-Type' => 'text/html' } + end + + def control_panel_view + <<-S +<html lang="en"> + <head><title>console</title></head> + <body> + <script> + async function submitForm(url) { + const result = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({}), + }); + document.getElementById("success-notification").innerHTML = "successful"; + setTimeout(function() { document.getElementById("success-notification").innerHTML = ""; }, 3000); + } + </script> + <form> + <input type="button" value="Show Console" onclick="submitForm('/dragon/show_console/')" /> + </form> + <form> + <input type="button" value="Reset Game" onclick="submitForm('/dragon/reset/');" /> + </form> + <form> + <input type="button" value="Record Gameplay" onclick="submitForm('/dragon/record/');" /> + </form> + <form> + <input type="button" value="Stop Recording" onclick="submitForm('/dragon/record_stop/');" /> + </form> + <form> + <input type="button" value="Replay Recording" onclick="submitForm('/dragon/replay/');" /> + </form> + <div id="success-notification"></div> + #{links} + </body> +</html> +S + end + + def get_api_control_panel args, req + req.respond 200, + control_panel_view, + { 'Content-Type' => 'text/html' } + end + + def json? req + req.headers.find { |k, v| k == "Content-Type" && (v.include? "application/json") } + end + + def post_api_reset args, req + $gtk.reset if json? req + req.respond 200, + control_panel_view, + { 'Content-Type' => 'text/html' } + end + + def post_api_record args, req + $recording.start 100 if json? req + req.respond 200, + control_panel_view, + { 'Content-Type' => 'text/html' } + end + + def post_api_record_stop args, req + $recording.stop 'replay.txt' if json? req + req.respond 200, + control_panel_view, + { 'Content-Type' => 'text/html' } + end + + def post_api_replay args, req + $replay.start 'replay.txt' if json? req + req.respond 200, + control_panel_view, + { 'Content-Type' => 'text/html' } + end + + def post_api_show_console args, req + $gtk.console.show if json? req + req.respond 200, + control_panel_view, + { 'Content-Type' => 'text/html' } + end + + def tick args + args.inputs.http_requests.each do |req| + match_candidate = { method: req.method.downcase.to_sym, + uri: req.uri, + uri_without_query_string: (req.uri.split '?').first, + query_string: (req.uri.split '?').last, + has_query_string: !!(req.uri.split '?').last, + has_api_prefix: (req.uri.start_with? "/dragon"), + end_with_rb: (req.uri.end_with? ".rb"), + has_file_extension: file_extensions.find { |f| req.uri.include? f }, + has_trailing_slash: (req.uri.split('?').first.end_with? "/") } + + if !match_candidate[:has_file_extension] + if !match_candidate[:has_trailing_slash] + match_candidate[:uri] = match_candidate[:uri_without_query_string] + "/" + if match_candidate[:query_string] + match_candidate[:uri] += "?#{match_candidate[:query_string]}" + end + end + end + + context = { args: args, req: req, match_candidate: match_candidate } + + process! context: context, routes: routes + end + end + + def url_decode args, string + args.fn.gsub string, + '+', " ", + '%27', "'", + '%22', '"', + '%0D%0A', "\n", + '%3D', "=", + '%3B', ";", + '%7C', "|", + '%28', "(", + '%29', ")", + '%7B', "{", + '%7D', "}", + '%2C', ",", + '%3A', ":", + '%5B', "[", + '%5D', "]", + '%23', "#", + '%21', "!", + '%3C', "<", + '%3E', ">", + '%2B', "+", + '%2F', "/", + '%40', "@", + '%3F', "?", + '%26', "&", + '%24', "$", + '%5C', "\\", + '%60', "`", + '%7E', "~", + '%C2%B2', "²", + '%5E', "^", + '%C2%BA', "º", + '%C2%A7', "§", + '%20', " ", + '%0A', "\n", + '%25', "%", + '%2A', "*" + end + + def file_extensions + [".html", ".css", ".gif", ".txt", ".ico", ".rb"] + end + + def routes + [{ match_criteria: { method: :get, uri: "/" }, + handler: :get_index }, + { match_criteria: { method: :get, uri: "/dragon/" }, + handler: :get_index }, + + { match_criteria: { method: :get, uri: "/dragon/boot/" }, + handler: :get_api_boot }, + { match_criteria: { method: :get, uri: "/dragon/trace/" }, + handler: :get_api_trace }, + { match_criteria: { method: :get, uri: "/dragon/puts/" }, + handler: :get_api_puts }, + { match_criteria: { method: :get, uri: "/dragon/log/" }, + handler: :get_api_log }, + { match_criteria: { method: :post, uri: "/dragon/log/" }, + handler: :post_api_log }, + { match_criteria: { method: :get, uri: "/dragon/changes/" }, + handler: :get_api_changes }, + { match_criteria: { method: :get, uri: "/dragon/eval/" }, + handler: :get_api_eval }, + { match_criteria: { method: :post, uri: "/dragon/eval/" }, + handler: :post_api_eval }, + { match_criteria: { method: :get, uri: "/dragon/console/" }, + handler: :get_api_console }, + { match_criteria: { method: :post, uri: "/dragon/console/" }, + handler: :post_api_console }, + { match_criteria: { method: :get, uri: "/dragon/control_panel/" }, + handler: :get_api_control_panel }, + { match_criteria: { method: :post, uri: "/dragon/reset/" }, + handler: :post_api_reset }, + { match_criteria: { method: :post, uri: "/dragon/record/" }, + handler: :post_api_record }, + { match_criteria: { method: :post, uri: "/dragon/record_stop/" }, + handler: :post_api_record_stop }, + { match_criteria: { method: :post, uri: "/dragon/replay/" }, + handler: :post_api_replay }, + { match_criteria: { method: :post, uri: "/dragon/show_console/" }, + handler: :post_api_show_console }, + { match_criteria: { method: :get, uri: "/dragon/code/" }, + handler: :get_api_code }, + { match_criteria: { method: :get, uri: "/dragon/autocomplete/" }, + handler: :get_api_autocomplete }, + { match_criteria: { method: :post, uri: "/dragon/autocomplete/" }, + handler: :post_api_autocomplete }, + { match_criteria: { method: :get, uri_without_query_string: "/dragon/code/edit/", has_query_string: true }, + handler: :get_api_code_edit }, + { match_criteria: { method: :post, uri_without_query_string: "/dragon/code/update/", has_query_string: true }, + handler: :post_api_code_update }, + + + { match_criteria: { method: :get, uri: "/docs.html" }, + handler: :get_docs_html }, + { match_criteria: { method: :get, uri_without_query_string: "/docs.css" }, + handler: :get_docs_css }, + { match_criteria: { method: :get, uri: "/docs_search.gif" }, + handler: :get_docs_search_gif }, + + { match_criteria: { method: :get, uri: "/src_backup_index.html" }, + handler: :get_src_backup_index_html }, + + { match_criteria: { method: :get, uri: "/src_backup_index.txt" }, + handler: :get_src_backup_index_txt }, + + { match_criteria: { method: :get, uri: "/src_backup_changes.html" }, + handler: :get_src_backup_changes_html }, + + { match_criteria: { method: :get, uri: "/src_backup_changes.txt" }, + handler: :get_src_backup_changes_txt }, + + { match_criteria: { method: :get, uri: "/src_backup.css" }, + handler: :get_src_backup_css }, + + { match_criteria: { method: :get, uri: "/favicon.ico" }, + handler: :get_favicon_ico }, + + { match_criteria: { method: :get, end_with_rb: true }, + handler: :get_src_backup }, + + { match_criteria: { method: :get, end_with_rb: true }, + handler: :get_src_backup } + + ] + end + + def process! opts + routes = opts[:routes] + context = opts[:context] + routes.each do |route| + match_found = (process_single! route: route, context: context) + return if match_found + end + end + + def process_single! opts + match_criteria = opts[:route][:match_criteria] + m = opts[:route][:handler] + args = opts[:context][:args] + req = opts[:context][:req] + match_candidate = opts[:context][:match_candidate] + match_criteria.each do |k, v| + return false if match_candidate[k] != v + end + + begin + send m, args, req + rescue Exception => e + req.respond 200, + "#{e}\n#{e.__backtrace_to_org__}", + { 'Content-Type' => 'text/plain' } + end + return true + end + end +end diff --git a/dragon/args.rb b/dragon/args.rb index 2b11008..d13d867 100644 --- a/dragon/args.rb +++ b/dragon/args.rb @@ -10,66 +10,32 @@ module GTK class Args include ArgsDeprecated include Serialize - - # Contains information related to input devices and input events. - # - # @return [Inputs] + attr_accessor :cvars attr_accessor :inputs - - # Contains the means to interact with output devices such as the screen. - # - # @return [Outputs] attr_accessor :outputs - - # Contains the means to interact with the audio mixer. - # - # @return [Hash] attr_accessor :audio - - # Contains display size information to assist in positioning things on the screen. - # - # @return [Grid] attr_accessor :grid - - # Provides access to game play recording facilities within Game Toolkit. - # - # @return [Recording] attr_accessor :recording - - # Gives you access to geometry related functions. - # - # @return [Geometry] attr_accessor :geometry - attr_accessor :fn - - # This is where you'll put state associated with your video game. - # - # @return [OpenEntity] attr_accessor :state - - # Gives you access to the top level DragonRuby runtime. - # - # @return [Runtime] + attr_accessor :temp_state attr_accessor :runtime alias_method :gtk, :runtime - attr_accessor :passes - attr_accessor :wizards - attr_accessor :layout - attr_accessor :easing - attr_accessor :string def initialize runtime, recording @inputs = Inputs.new @outputs = Outputs.new args: self + @cvars = {} @audio = {} @passes = [] @state = OpenEntity.new + @temp_state = OpenEntity.new @state.tick_count = -1 @runtime = runtime @recording = recording @@ -99,11 +65,12 @@ module GTK def serialize { - state: state.as_hash, - inputs: inputs.serialize, - passes: passes.serialize, - outputs: outputs.serialize, - grid: grid.serialize + state: state.as_hash, + temp_state: temp_state.as_hash, + inputs: inputs.serialize, + passes: passes.serialize, + outputs: outputs.serialize, + grid: grid.serialize } end @@ -235,5 +202,22 @@ module GTK def autocomplete_methods [:inputs, :outputs, :gtk, :state, :geometry, :audio, :grid, :layout, :fn] end + + def method_missing name, *args, &block + if (args.length <= 1) && (@state.as_hash.key? name) + raise <<-S +* ERROR - :#{name} method missing on ~#{self.class.name}~. +The method + :#{name} +with args + #{args} +doesn't exist on #{inspect}. +** POSSIBLE SOLUTION - ~args.state.#{name}~ exists. +Did you forget ~.state~ before ~.#{name}~? +S + end + + super + end end end diff --git a/dragon/assert.rb b/dragon/assert.rb index bc21add..974846a 100644 --- a/dragon/assert.rb +++ b/dragon/assert.rb @@ -24,10 +24,10 @@ To add an assertion open up this class and write: class Assert def custom_assertion actual, expected, message = nil - # this tell Game Toolkit that an assertion was performed (so that the test isn't marked inconclusive). + # this tells Game Toolkit that an assertion was performed (so that the test isn't marked inconclusive). @assertion_performed = true - # perform your custom logic here and rais an exception to denote a failure. + # perform your custom logic here and raise an exception to denote a failure. raise "Some Error. #{message}." end @@ -37,14 +37,14 @@ end attr :assertion_performed =begin -Us this if you are throwing your own exceptions and you want to mark the tests as ran (so that it wont be marked as inconclusive). +Use this if you are throwing your own exceptions and you want to mark the tests as ran (so that it wont be marked as inconclusive). =end def ok! @assertion_performed = true end =begin -Assert if a value is a thruthy value. All assert method take an optional final parameter that is the message to display to the user. +Assert if a value is a truthy value. All assert methods take an optional final parameter that is the message to display to the user. @example @@ -98,7 +98,7 @@ end @assertion_performed = true if actual != expected actual_string = "#{actual}#{actual.nil? ? " (nil) " : " " }".strip - message = "actual:\n#{actual_string}\n\ndid not equal\n\nexpected:\n#{expected}.\n#{message}" + message = "actual:\n#{actual_string}\n\ndid not equal\n\nexpected:\n#{expected}\n#{message}" raise message end nil @@ -108,7 +108,7 @@ end @assertion_performed = true if actual == expected actual_string = "#{actual}#{actual.nil? ? " (nil) " : " " }".strip - message = "actual:\n#{actual_string}\n\nequaled\n\nexpected:\n#{expected}.\n#{message}" + message = "actual:\n#{actual_string}\n\nequaled\n\nexpected:\n#{expected}\n#{message}" raise message end nil diff --git a/dragon/attr_gtk.rb b/dragon/attr_gtk.rb index 2f2ccc5..e53e1b9 100644 --- a/dragon/attr_gtk.rb +++ b/dragon/attr_gtk.rb @@ -19,6 +19,10 @@ module AttrGTK args.state end + def temp_state + args.temp_state + end + def inputs args.inputs end @@ -46,4 +50,12 @@ module AttrGTK def layout args.layout end + + def new_entity entity_type, init_hash = nil, &block + args.state.new_entity entity_type, init_hash, &block + end + + def new_entity_strict entity_type, init_hash = nil, &block + args.state.new_entity_strict entity_type, init_hash, &block + end end diff --git a/dragon/attr_sprite.rb b/dragon/attr_sprite.rb index 69ddd8c..754429d 100644 --- a/dragon/attr_sprite.rb +++ b/dragon/attr_sprite.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # attr_sprite.rb has been released under MIT (*only this file*). @@ -37,7 +38,7 @@ module AttrSprite attr_accessor :x, :y, :w, :h, :path, :angle, :a, :r, :g, :b, :tile_x, :tile_y, :tile_w, :tile_h, :flip_horizontally, :flip_vertically, :angle_anchor_x, :angle_anchor_y, :id, - :source_x, :source_y, :source_w, :source_h + :source_x, :source_y, :source_w, :source_h, :blendmode_enum def primitive_marker :sprite diff --git a/dragon/autocomplete.rb b/dragon/autocomplete.rb index 490d6c9..ce29d0a 100644 --- a/dragon/autocomplete.rb +++ b/dragon/autocomplete.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # autocomplete.rb has been released under MIT (*only this file*). @@ -24,7 +25,7 @@ module GTK sub_index = index - previous_line[:sum] word = (cursor_line[:line][0..sub_index - 1]).strip token = (word.split " ")[-1] - dots = (token.split ".") + dots = (token.split ".").flat_map { |s| s.split "[" }.flat_map { |s| s.split "]" }.flat_map { |s| s.split "(" }.flat_map { |s| s.split ")" } dot = dots[-1] end @@ -45,6 +46,10 @@ module GTK ignores ||= [] ignores = [ignores].flatten keys = keys.map { |k| k.to_s } + keys = keys.reject { |k| k.include? '"' } + .reject { |k| k.start_with? "'" } + .reject { |k| k.include? "," } + .reject { |k| k.start_with? "#" } others = ["def", "end"] + [ :entity_keys_by_ref, :entity_name, @@ -102,6 +107,10 @@ module GTK return autocomplete_filter_methods lookup_result.call if lookup_result + if dot[0].upcase == dot[0] && (Object.const_defined? dot.to_sym) + return (Object.const_get dot.to_sym).autocomplete_methods + end + start_collecting = false dots_after_state = dots.find_all do |s| if s == "state" @@ -117,10 +126,16 @@ module GTK target = target.as_hash[k.to_sym] if target.respond_to? :as_hash end - return autocomplete_filter_methods target.as_hash.keys + if target.respond_to? :as_hash + return autocomplete_filter_methods target.as_hash.keys + else + return autocomplete_filter_methods target.autocomplete_methods + end end + text = text.each_line.reject { |l| l.strip.start_with? "#" }.join "\n" + text = text.each_line.map { |l| l.split("#").first }.join "\n" text.gsub!("[", " ") text.gsub!("]", " ") text.gsub!("(", " ") diff --git a/dragon/benchmark.rb b/dragon/benchmark.rb new file mode 100644 index 0000000..38ea991 --- /dev/null +++ b/dragon/benchmark.rb @@ -0,0 +1,111 @@ +# coding: utf-8 +# Copyright 2019 DragonRuby LLC +# MIT License +# benchmark.rb has been released under MIT (*only this file*). + +module GTK + class Runtime + module Benchmark + def benchmark_single iterations, name, proc + log <<-S +** Invoking :#{name}... +S + idx = 0 + r = nil + time_start = Time.now + while idx < iterations + r = proc.call + idx += 1 + end + result = (Time.now - time_start).round 3 + + { name: name, + time: result, + time_ms: (result * 1000).to_i } + end + + def benchmark opts = {} + iterations = opts.iterations + + log <<-S +* BENCHMARK: Started +** Caller: #{(caller || []).first} +** Iterations: #{iterations} +S + procs = opts.find_all { |k, v| v.respond_to? :call } + + times = procs.map do |(name, proc)| + benchmark_single iterations, name, proc + end.sort_by { |r| r.time } + + first_place = times.first + second_place = times.second || first_place + + times = times.map do |candidate| + average_time = first_place.time + .add(candidate.time) + .abs + .fdiv(2) + + difference_percentage = 0 + if average_time == 0 + difference_percentage = 0 + else + difference_percentage = first_place.time + .subtract(candidate.time) + .abs + .fdiv(average_time) + .imult(100) + end + + difference_time = ((first_place.time - candidate.time) * 1000).round + candidate.merge(difference_percentage: difference_percentage, + difference_time: difference_time) + end + + too_small_to_measure = false + if (first_place.time + second_place.time) == 0 + too_small_to_measure = true + difference_percentage = 0 + log <<-S +* BENCHMARK: Average time for experiments were too small. Increase the number of iterations. +S + else + difference_percentage = (((first_place.time - second_place.time).abs.fdiv((first_place.time + second_place.time).abs.fdiv(2))) * 100).round + end + + difference_time = first_place.time.-(second_place.time).*(1000).abs.round + + r = { + iterations: iterations, + first_place: first_place, + second_place: second_place, + difference_time: difference_time, + difference_percentage: difference_percentage, + times: times, + too_small_to_measure: too_small_to_measure + } + + log_message = [] + only_one_result = first_place.name == second_place.name + + if only_one_result + log <<-S +* BENCHMARK: #{r.first_place.name} completed in #{r.first_place.time_ms}ms." +S + else + log <<-S +* BENCHMARK: #{r.message} +** Fastest: #{r.first_place.name.inspect} +** Second: #{r.second_place.name.inspect} +** Margin: #{r.difference_percentage}% (#{r.difference_time.abs}ms) #{r.first_place.time_ms}ms vs #{r.second_place.time_ms}ms. +** Times: +#{r.times.map { |t| "*** #{t.name}: #{t.time_ms}ms (#{t.difference_percentage}% #{t.difference_time.abs}ms)." }.join("\n")} +S + end + + r + end + end + end +end diff --git a/dragon/console.rb b/dragon/console.rb index c30c311..6d9733d 100644 --- a/dragon/console.rb +++ b/dragon/console.rb @@ -1,26 +1,29 @@ +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # console.rb has been released under MIT (*only this file*). # Contributors outside of DragonRuby who also hold Copyright: # - Kevin Fischer: https://github.com/kfischer-okarin -# - Austin Meyer: https://github.com/Niyy module GTK class Console include ConsoleDeprecated - attr_accessor :show_reason, :log, :logo, :background_color, - :text_color, :animation_duration, + attr_accessor :show_reason, :log, :logo, + :animation_duration, :max_log_lines, :max_history, :log, - :last_command_errored, :last_command, :error_color, :shown_at, - :header_color, :archived_log, :last_log_lines, :last_log_lines_count, + :last_command_errored, :last_command, :shown_at, + :archived_log, :last_log_lines, :last_log_lines_count, :suppress_left_arrow_behavior, :command_set_at, :toast_ids, :bottom, - :font_style, :menu + :font_style, :menu, + :background_color, :spam_color, :text_color, :warn_color, + :error_color, :header_color, :code_color, :comment_color, + :debug_color, :unfiltered_color def initialize - @font_style = FontStyle.new(font: 'font.ttf', size_enum: -1, line_height: 1.1) + @font_style = FontStyle.new(font: 'font.ttf', size_enum: -1.5, line_height: 1.1) @menu = Menu.new self @disabled = false @log_offset = 0 @@ -35,13 +38,22 @@ module GTK @command_history_index = -1 @nonhistory_input = '' @logo = 'console-logo.png' - @history_fname = 'console_history.txt' + @history_fname = 'logs/console_history.txt' @background_color = Color.new [0, 0, 0, 224] - @text_color = Color.new [255, 255, 255] - @error_color = Color.new [200, 50, 50] @header_color = Color.new [100, 200, 220] + @code_color = Color.new [210, 168, 255] + @comment_color = Color.new [0, 200, 100] @animation_duration = 1.seconds @shown_at = -1 + + # these are the colors for text at various log levels. + @spam_color = Color.new [160, 160, 160] + @debug_color = Color.new [0, 255, 0] + @text_color = Color.new [255, 255, 255] + @warn_color = Color.new [255, 255, 0] + @error_color = Color.new [200, 50, 50] + @unfiltered_color = Color.new [0, 255, 255] + load_history end @@ -107,7 +119,13 @@ module GTK nil end - def add_text obj + def add_text obj, loglevel=-1 + # loglevel is one of the values of LogLevel in logging.h, or -1 to say "we don't care, colorize it with your special string parsing magic" + loglevel = -1 if loglevel < 0 + loglevel = 5 if loglevel > 5 # 5 == unfiltered (it's 0x7FFFFFFE in C, clamp it down) + loglevel = 2 if (loglevel == -1) && obj.start_with?('!c!') # oh well + colorstr = (loglevel != -1) ? "!c!#{loglevel}" : nil + @last_log_lines_count ||= 1 @log_invocation_count += 1 @@ -116,12 +134,18 @@ module GTK log_lines = [] str.each_line do |s| - s.wrapped_lines(self.console_text_width).each do |l| - log_lines << l + if colorstr.nil? + s.wrapped_lines(self.console_text_width).each do |l| + log_lines << l + end + else + s.wrapped_lines(self.console_text_width).each do |l| + log_lines << "#{colorstr}#{l}" + end end end - if log_lines == @last_log_lines + if log_lines == @last_log_lines && log_lines.length != 0 @last_log_lines_count += 1 new_log_line_with_count = @last_log_lines.last + " (#{@last_log_lines_count})" if log_lines.length > 1 @@ -314,6 +338,8 @@ S if cmd == 'quit' || cmd == ':wq' || cmd == ':q!' || cmd == ':q' || cmd == ':wqa' $gtk.request_quit + elsif cmd.start_with? ':' + send ((cmd.gsub '-', '_').gsub ':', '') else puts "-> #{cmd}" begin @@ -322,13 +348,19 @@ S if $results.nil? puts "=> nil" elsif $results == :console_silent_eval + # do nothing since the console is silent else puts "=> #{$results}" end @last_command_errored = false rescue Exception => e try_search_docs e - puts "* EXCEPTION: #{e}" + # if an exception is thrown and the bactrace includes something helpful, then show it + if (e.backtrace || []).first && (e.backtrace.first.include? "(eval)") + puts "* EXCEPTION: #{e}" + else + puts "* EXCEPTION: #{e}\n#{e.__backtrace_to_org__}" + end end end end @@ -388,10 +420,12 @@ S def mouse_wheel_scroll args @inertia ||= 0 - if args.inputs.mouse.wheel && args.inputs.mouse.wheel.y > 0 - @inertia = 1 - elsif args.inputs.mouse.wheel && args.inputs.mouse.wheel.y < 0 - @inertia = -1 + if args.inputs.mouse.wheel + if args.inputs.mouse.wheel.y > 0 + @inertia = 1 + elsif args.inputs.mouse.wheel.y < 0 + @inertia = -1 + end end if args.inputs.mouse.click @@ -400,13 +434,11 @@ S return if @inertia == 0 - if @inertia != 0 - @inertia = (@inertia * 0.7) - if @inertia > 0 - @log_offset -= 1 - elsif @inertia < 0 - @log_offset += 1 - end + @inertia = (@inertia * 0.7) + if @inertia > 0 + @log_offset += 1 + elsif @inertia < 0 + @log_offset -= 1 end if @inertia.abs < 0.01 @@ -424,6 +456,7 @@ S if console_toggle_key_down? args args.inputs.text.clear toggle + args.inputs.keyboard.clear if !@visible end return unless visible? @@ -435,7 +468,16 @@ S @log_offset = 0 if @log_offset < 0 if args.inputs.keyboard.key_down.enter - eval_the_set_command + if slide_progress > 0.5 + # in the event of an exception, the console window pops up + # and is pre-filled with $gtk.reset. + # there is an annoying scenario where the exception could be thrown + # by pressing enter (while playing the game). if you press enter again + # quickly, then the game is reset which closes the console. + # so enter in the console is only evaluated if the slide_progress + # is atleast half way down the page. + eval_the_set_command + end elsif args.inputs.keyboard.key_down.v if args.inputs.keyboard.key_down.control || args.inputs.keyboard.key_down.meta prompt << $gtk.ffi_misc.getclipboard @@ -512,7 +554,7 @@ S def write_line(args, left, y, str, archived: false) color = color_for_log_entry(str) color = color.mult_alpha(0.5) if archived - + str = str[4..-1] if str.start_with?('!c!') # chop off loglevel color args.outputs.reserved << font_style.label(x: left.shift_right(10), y: y, text: str, color: color) end @@ -555,6 +597,11 @@ S end render_log_offset args + + args.outputs.reserved << { x: 10.from_right, y: @bottom + 10, + text: "Press CTRL+g or ESCAPE to clear the prompt.", + vertical_alignment_enum: 0, + alignment_enum: 2, r: 80, g: 80, b: 80 }.label! end def render_log_offset args @@ -577,7 +624,7 @@ S end def include_subdued_markers? text - include_any_words? text, subdued_markers + (text.start_with? "* INFO: ") && (include_any_words? text, subdued_markers) end def include_any_words? text, words @@ -723,8 +770,34 @@ S (log_entry.start_with? "**** ") end - def color_for_log_entry(log_entry) - if include_row_marker? log_entry + def code? log_entry + (just_symbol? log_entry) || (codeblock_marker? log_entry) + end + + def just_symbol? log_entry + scrubbed = log_entry.gsub("*", "").strip + (scrubbed.start_with? ":") && (!scrubbed.include? " ") && (!scrubbed.include? "=>") + end + + def code_comment? log_entry + return true if log_entry.strip.start_with?("# ") + return false + end + + def codeblock_marker? log_entry + return true if log_entry.strip.start_with?("#+begin_src") + return true if log_entry.strip.start_with?("#+end_src") + return false + end + + def color_for_plain_text log_entry + log_entry = log_entry[4..-1] if log_entry.start_with? "!c!" + + if code? log_entry + @code_color + elsif code_comment? log_entry + @comment_color + elsif include_row_marker? log_entry @text_color elsif include_error_marker? log_entry @error_color @@ -739,6 +812,29 @@ S end end + def color_for_log_entry(log_entry) + if log_entry.start_with?('!c!') # loglevel color specified. + return case log_entry[3..3].to_i + when 0 # spam + @spam_color + when 1 # debug + @debug_color + #when 2 # info (caught by the `else` block.) + # @text_color + when 3 # warn + @warn_color + when 4 # error + @error_color + when 5 # unfiltered + @unfiltered_color + else + color_for_plain_text log_entry + end + end + + return color_for_plain_text log_entry + end + def prompt @prompt ||= Prompt.new(font_style: font_style, text_color: @text_color, console_text_width: console_text_width) end diff --git a/dragon/console_color.rb b/dragon/console_color.rb index f5b164d..1b19c22 100644 --- a/dragon/console_color.rb +++ b/dragon/console_color.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # console_color.rb has been released under MIT (*only this file*). @@ -22,6 +23,10 @@ module GTK @color end + def to_s + "GTK::Console::Color #{to_h}" + end + def to_h { r: @color[0], g: @color[1], b: @color[2], a: @color[3] } end diff --git a/dragon/console_font_style.rb b/dragon/console_font_style.rb index 8efab4f..0bb7cc4 100644 --- a/dragon/console_font_style.rb +++ b/dragon/console_font_style.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # console_font_style.rb has been released under MIT (*only this file*). @@ -33,7 +34,7 @@ module GTK size_enum: size_enum, alignment_enum: alignment_enum, **color.to_h, - }.label + }.label! end end end diff --git a/dragon/console_menu.rb b/dragon/console_menu.rb index d10ceeb..0f21614 100644 --- a/dragon/console_menu.rb +++ b/dragon/console_menu.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # console_menu.rb has been released under MIT (*only this file*). @@ -51,7 +52,7 @@ module GTK def itch_wizard_clicked @console.scroll_to_bottom - $wizards.itch.start + $wizards.itch.restart end def docs_clicked @@ -76,6 +77,7 @@ module GTK @buttons = [ (button id: :record, row: 0, col: 9, text: "record gameplay", method: :record_clicked), (button id: :replay, row: 0, col: 10, text: "start replay", method: :replay_clicked), + *custom_buttons ] elsif @menu_shown == :hidden @buttons = [ @@ -134,8 +136,8 @@ module GTK method: method }.let do |entity| primitives = [] - primitives << entity[:rect].merge(a: 164).solid - primitives << entity[:rect].merge(r: 255, g: 255, b: 255).border + primitives << entity[:rect].solid!(a: 164) + primitives << entity[:rect].border!(r: 255, g: 255, b: 255) primitives << text.wrapped_lines(5) .map_with_index do |l, i| [ diff --git a/dragon/console_prompt.rb b/dragon/console_prompt.rb index b97bdba..8c02de0 100644 --- a/dragon/console_prompt.rb +++ b/dragon/console_prompt.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # console_prompt.rb has been released under MIT (*only this file*). @@ -71,14 +72,14 @@ module GTK return if @cursor_position.zero? new_pos = @cursor_position - 1 - (is_word_boundary? @current_input_str[new_pos]) ? + (is_word_boundary? @current_input_str[new_pos]) ? (new_pos -= 1 until !(is_word_boundary? @current_input_str[new_pos - 1]) || new_pos.zero?): (new_pos -= 1 until (is_word_boundary? @current_input_str[new_pos - 1]) || new_pos.zero?) @cursor_position = new_pos update_cursor_position_px end - + def move_cursor_right @cursor_position += 1 if @cursor_position < current_input_str.length update_cursor_position_px @@ -91,7 +92,7 @@ module GTK (is_word_boundary? @current_input_str[new_pos]) ? (new_pos += 1 until !(is_word_boundary? @current_input_str[new_pos]) || (new_pos.equal? str_len)): (new_pos += 1 until (is_word_boundary? @current_input_str[new_pos]) || (new_pos.equal? str_len)) - + @cursor_position = new_pos update_cursor_position_px end @@ -157,11 +158,11 @@ S # partition the original list of items into a string to be printed items.each_slice(columns).each_with_index do |cells, i| - pretty_print_row_seperator string_width, cell_width, column_width, columns + pretty_print_row_separator string_width, cell_width, column_width, columns pretty_print_row cells, string_width, cell_width, column_width, columns end - pretty_print_row_seperator string_width, cell_width, column_width, columns + pretty_print_row_separator string_width, cell_width, column_width, columns end end @@ -174,17 +175,17 @@ S "#{" " * (string_width.length - c.length) } #{c} |" end.join - # remove seperators between empty values + # remove separators between empty values formated_row = formated_row.gsub(" | ", " ") puts formated_row end - def pretty_print_row_seperator string_width, cell_width, column_width, columns + def pretty_print_row_separator string_width, cell_width, column_width, columns # this is a joint: +-------- column_joint = "+#{"-" * cell_width}" - # multiple joints create a row seperator: +----+----+ + # multiple joints create a row separator: +----+----+ puts (column_joint * columns) + "+" end @@ -193,12 +194,12 @@ S args.outputs.reserved << (@cursor_color.to_h.merge x: x + @cursor_position_px + 0.5, y: y + 5, x2: x + @cursor_position_px + 0.5, - y2: y + @font_style.letter_size.y + 5) + y2: y + @font_style.letter_size.y + 4) args.outputs.reserved << (@cursor_color.to_h.merge x: x + @cursor_position_px + 1, y: y + 5, x2: x + @cursor_position_px + 1, - y2: y + @font_style.letter_size.y + 5) + y2: y + @font_style.letter_size.y + 4) # debugging rectangle for string # args.outputs.reserved << (@cursor_color.to_h.merge x: x, diff --git a/dragon/controller_config.rb b/dragon/controller_config.rb deleted file mode 100644 index 091fa1e..0000000 --- a/dragon/controller_config.rb +++ /dev/null @@ -1,396 +0,0 @@ -# Copyright 2019 DragonRuby LLC -# MIT License -# controller_config.rb has been released under MIT (*only this file*). - -# !!! FIXME: add console command to forget custom binding(s) -# !!! FIXME: add console command to forget replace existing binding(s) -# !!! FIXME: add console command go into play_around mode to make sure controller isn't wonky. - -module GTK - class ControllerConfig - def initialize runtime - @runtime = runtime - @raw_joysticks = {} # things that aren't game controllers to try to configure. - @target = nil - @animation_duration = (1.5).seconds - @toggled_at = 0 - @fading = 0 - @current_part = 0 - @part_alpha = 0 - @part_alpha_increment = 10 - @joystick_state = {} - @playing_around = false - @used_bindings = {} - @bindings = [] - @parts = [ - [ 919, 282, 'A button', 'a' ], - [ 960, 323, 'B button', 'b' ], - [ 878, 323, 'X button', 'x' ], - [ 919, 365, 'Y button', 'y' ], - [ 433, 246, 'left stick left', '-leftx' ], - [ 497, 246, 'left stick right', '+leftx' ], - [ 466, 283, 'left stick up', '-lefty' ], - [ 466, 218, 'left stick down', '+lefty' ], - [ 466, 246, 'left stick button', 'leftstick' ], - [ 741, 246, 'right stick left', '-rightx' ], - [ 802, 246, 'right stick right', '+rightx' ], - [ 773, 283, 'right stick up', '-righty' ], - [ 773, 218, 'right stick down', '+righty' ], - [ 772, 246, 'right stick button', 'rightstick' ], - [ 263, 465, 'left shoulder button', 'leftshoulder' ], - [ 263, 503, 'left trigger', 'lefttrigger' ], - [ 977, 465, 'right shoulder button', 'rightshoulder' ], - [ 977, 503, 'right trigger', 'righttrigger' ], - [ 318, 365, 'D-pad up', 'dpup' ], - [ 360, 322, 'D-pad right', 'dpright' ], - [ 318, 280, 'D-pad down', 'dpdown' ], - [ 275, 322, 'D-pad left', 'dpleft' ], - [ 570, 402, 'select/back button', 'back'], - [ 619, 448, 'guide/home button', 'guide' ], - [ 669, 402, 'start button', 'start' ], - ] - end - - def rawjoystick_connected jid, joystickname, guid - return if jid < 0 - @raw_joysticks[jid] = { name: joystickname, guid: guid } - end - - def rawjoystick_disconnected jid - return if jid < 0 - if @raw_joysticks[jid] != nil - @raw_joysticks.delete(jid) - @runtime.ffi_misc.close_raw_joystick(jid) - # Fade out the config screen if we were literally configuring this controller right now. - if [email protected]? && @target[0] == jid - @target[0] = nil - @toggled_at = Kernel.global_tick_count - @fading = -1 - end - end - end - - def build_binding_string - bindingstr = '' - skip = false - - for i in [email protected] - if skip ; skip = false ; next ; end - - binding = @bindings[i] - next if binding.nil? - - part = @parts[i][3] - - # clean up string: - # if axis uses -a0 for negative and +a0 for positive, just make it "leftx:a0" instead of "-leftx:-a0,+leftx:+a0" - # if axis uses +a0 for negative and -a0 for positive, just make it "leftx:a0~" instead of "-leftx:+a0,+leftx:-a0" - if part == '-leftx' || part == '-lefty' || part == '-rightx' || part == '-righty' - nextbinding = @bindings[i+1] - if binding.start_with?('-a') && nextbinding.start_with?('+a') && binding[2..-1] == nextbinding[2..-1] - skip = true - part = part[1..-1] - binding = binding[1..-1] - elsif binding.start_with?('+a') && nextbinding.start_with?('-a') && binding[2..-1] == nextbinding[2..-1] - skip = true - part = part[1..-1] - binding = "#{binding[1..-1]}~" - end - end - - bindingstr += "#{!bindingstr.empty? ? ',' : ''}#{part}:#{binding}" - end - - details = @target[1] - - # !!! FIXME: no String.delete in mRuby?!?! Maybe so when upgrading. - #name = details[:name].delete(',') - # !!! FIXME: ...no regexp either... :/ - #name = details[:name].gsub(/,/, ' ') # !!! FIXME: will SDL let you escape these instead? - unescaped = details[:name] - name = '' - for i in 0..unescaped.length-1 - ch = unescaped[i] - name += (ch == ',') ? ' ' : ch - end - return "#{details[:guid]},#{name},platform:#{@runtime.platform},#{bindingstr}" - end - - def move_to_different_part part - if !@joystick_state[:axes].nil? - @joystick_state[:axes].each { |i| i[:farthestval] = i[:startingval] if !i.nil? } - end - @current_part = part - end - - def previous_part - if @current_part > 0 - # remove the binding that we previous had here so it can be reused. - bindstr = @bindings[@current_part - 1] - @bindings[@current_part - 1] = nil - @used_bindings[bindstr] = nil - move_to_different_part @current_part - 1 - end - end - - def next_part - if @current_part < (@parts.length - 1) - move_to_different_part @current_part + 1 - else - @playing_around = true - end - end - - def set_binding bindstr - return false if !@used_bindings[bindstr].nil? - @used_bindings[bindstr] = @current_part - @bindings[@current_part] = bindstr - return true - end - - # Called when a lowlevel joystick moves an axis. - def rawjoystick_axis jid, axis, value - return if @target.nil? || jid != @target[0] || @fading != 0 # skip if not currently considering this joystick. - - @joystick_state[:axes] ||= [] - @joystick_state[:axes][axis] ||= { - moving: false, - startingval: 0, - currentval: 0, - farthestval: 0 - } - - # this is the logic from SDL's controllermap.c, more or less, since this is hard to get right from scratch. - state = @joystick_state[:axes][axis] - state[:currentval] = value - if !state[:moving] - state[:moving] = true - state[:startingval] = value - state[:farthestval] = value - end - - current_distance = (value - state[:startingval]).abs - farthest_distance = (state[:farthestval] - state[:startingval]).abs - if current_distance > farthest_distance - state[:farthestval] = value - farthest_distance = (state[:farthestval] - state[:startingval]).abs - end - - # If we've gone out far enough and started to come back, let's bind this axis - if (farthest_distance >= 16000) && (current_distance <= 10000) - next_part if set_binding("#{(state[:farthestval] < 0) ? '-' : '+'}a#{axis}") - end - end - - # Called when a lowlevel joystick moves a hat. - def rawjoystick_hat jid, hat, value - return if @target.nil? || jid != @target[0] || @fading != 0 # skip if not currently considering this joystick. - - @joystick_state[:hats] ||= [] - @joystick_state[:hats][hat] = value - - return if value == 0 # 0 == centered, skip it - next_part if set_binding("h#{hat}.#{value}") - end - - # Called when a lowlevel joystick moves a button. - def rawjoystick_button jid, button, pressed - return if @target.nil? || jid != @target[0] || @fading != 0 # skip if not currently considering this joystick. - - @joystick_state[:buttons] ||= [] - @joystick_state[:buttons][button] = pressed - - return if !pressed - next_part if set_binding("b#{button}") - end - - def calc_fading - if @fading == 0 - return 255 - elsif @fading > 0 # fading in - percent = @toggled_at.global_ease(@animation_duration, :flip, :quint, :flip) - if percent >= 1.0 - percent = 1.0 - @fading = 0 - end - else # fading out - percent = @toggled_at.global_ease(@animation_duration, :flip, :quint) - if percent <= 0.0 - percent = 0.0 - @fading = 0 - end - end - - return (percent * 255.0).to_i - end - - def render_basics args, msg, fade=255 - joystickname = @target[1][:name] - args.outputs.primitives << [0, 0, GAME_WIDTH, GAME_HEIGHT, 255, 255, 255, fade].solid - args.outputs.primitives << [0, 0, GAME_WIDTH, GAME_HEIGHT, 'dragonruby-controller.png', 0, fade, 255, 255, 255].sprite - args.outputs.primitives << [GAME_WIDTH / 2, 700, joystickname, 2, 1, 0, 0, 0, fade].label - args.outputs.primitives << [GAME_WIDTH / 2, 650, msg, 0, 1, 0, 0, 0, 255].label if !msg.empty? - end - - def render_part_highlight args, part, alpha=255 - partsize = 41 - args.outputs.primitives << [part[0], part[1], partsize, partsize, 255, 0, 0, alpha].border - args.outputs.primitives << [part[0]-1, part[1]-1, partsize+2, partsize+2, 255, 0, 0, alpha].border - args.outputs.primitives << [part[0]-2, part[1]-2, partsize+4, partsize+4, 255, 0, 0, alpha].border - end - - def choose_target - if @target.nil? - while !@raw_joysticks.empty? - t = @raw_joysticks.shift # see if there's a joystick waiting on us. - next if t[0] < 0 # just in case. - next if t[1][:guid].nil? # did we already handle this guid? Dump it. - @target = t - break - end - return false if @target.nil? # nothing to configure at the moment. - @toggled_at = Kernel.global_tick_count - @fading = 1 - @current_part = 0 - @part_alpha = 0 - @part_alpha_increment = 10 - @joystick_state = {} - @used_bindings = {} - @playing_around = false - @bindings = [] - end - return true - end - - def render_part_highlight_from_bindstr args, bindstr, alpha=255 - partidx = @used_bindings[bindstr] - return if partidx.nil? - render_part_highlight args, @parts[partidx], alpha - end - - def play_around args - return false if !@playing_around - - if args.inputs.keyboard.key_down.escape - @current_part = 0 - @part_alpha = 0 - @part_alpha_increment = 10 - @used_bindings = {} - @playing_around = false - @bindings = [] - elsif args.inputs.keyboard.key_down.space - jid = @target[0] - bindingstr = build_binding_string - #puts("new controller binding: '#{bindingstr}'") - @runtime.ffi_misc.add_controller_config bindingstr - @runtime.ffi_misc.convert_rawjoystick_to_controller jid - @target[0] = -1 # Conversion closes the raw joystick. - - # Handle any other pending joysticks that have the same GUID (so if you plug in four of the same model, we're already done!) - guid = @target[1][:guid] - @raw_joysticks.each { |jid, details| - if details[:guid] == guid - @runtime.ffi_misc.convert_rawjoystick_to_controller jid - details[:guid] = nil - end - } - - # Done with this guy. - @playing_around = false - @toggled_at = Kernel.global_tick_count - @fading = -1 - return false - end - - render_basics args, 'Now play around with the controller, and make sure it feels right!' - args.outputs.primitives << [GAME_WIDTH / 2, 90, '[ESCAPE]: Reconfigure, [SPACE]: Save this configuration', 0, 1, 0, 0, 0, 255].label - - axes = @joystick_state[:axes] - if !axes.nil? - for i in 0..axes.length-1 - next if axes[i].nil? - value = axes[i][:currentval] - next if value.nil? || (value.abs < 16000) - render_part_highlight_from_bindstr args, "#{value < 0 ? '-' : '+'}a#{i}" - end - end - - hats = @joystick_state[:hats] - if !hats.nil? - for i in 0..hats.length-1 - value = hats[i] - next if value.nil? || (value == 0) - render_part_highlight_from_bindstr args, "h#{i}.#{value}" - end - end - - buttons = @joystick_state[:buttons] - if !buttons.nil? - for i in 0..buttons.length-1 - value = buttons[i] - next if value.nil? || !value - render_part_highlight_from_bindstr args, "b#{i}" - end - end - - return true - end - - def should_tick? - return true if @play_around - return true if @target - return false - end - - def tick args - return true if play_around args - return false if !choose_target - - jid = @target[0] - - if @fading == 0 - # Cancel config? - if args.inputs.keyboard.key_down.escape - # !!! FIXME: prompt to ignore this joystick forever or just this run - @toggled_at = Kernel.global_tick_count - @fading = -1 - end - end - - if @fading == 0 - if args.inputs.keyboard.key_down.backspace - previous_part - elsif args.inputs.keyboard.key_down.space - next_part - end - end - - fade = calc_fading - if (@fading < 0) && (fade == 0) - @runtime.ffi_misc.close_raw_joystick(jid) if jid >= 0 - @target = nil # done with this controller - return false - end - - render_basics args, (@fading >= 0) ? "We don't recognize this controller, so tell us about it!" : '', fade - - return true if fade < 255 # all done for now - - part = @parts[@current_part] - args.outputs.primitives << [GAME_WIDTH / 2, 575, "Please press the #{part[2]}.", 0, 1, 0, 0, 0, 255].label - render_part_highlight args, part, @part_alpha - args.outputs.primitives << [GAME_WIDTH / 2, 90, '[ESCAPE]: Ignore controller, [BACKSPACE]: Go back one button, [SPACE]: Skip this button', 0, 1, 0, 0, 0, 255].label - - @part_alpha += @part_alpha_increment - if (@part_alpha_increment > 0) && (@part_alpha >= 255) - @part_alpha = 255 - @part_alpha_increment = -10 - elsif (@part_alpha_increment < 0) && (@part_alpha <= 0) - @part_alpha = 0 - @part_alpha_increment = 10 - end - - return true - end - end -end diff --git a/dragon/directional_input_helper_methods.rb b/dragon/directional_input_helper_methods.rb index a587212..0a15c70 100644 --- a/dragon/directional_input_helper_methods.rb +++ b/dragon/directional_input_helper_methods.rb @@ -13,7 +13,7 @@ module GTK error_message = <<-S * ERROR -The GTK::DirectionalKeys module should only be included in objects that respond to the following api heirarchy: +The GTK::DirectionalKeys module should only be included in objects that respond to the following api hierarchy: - (#{ directional_methods.join("|") }) - key_held.(#{ directional_methods.join("|") }) @@ -70,6 +70,12 @@ S end end + def directional_angle + return nil unless directional_vector + + Math.atan2(up_down, left_right).to_degrees + end + def method_missing m, *args # combine the key with ctrl_ if m.to_s.start_with?("ctrl_") diff --git a/dragon/docs.rb b/dragon/docs.rb index cc80d7e..17a6e9a 100644 --- a/dragon/docs.rb +++ b/dragon/docs.rb @@ -46,7 +46,7 @@ module DocsOrganizer unsorted.each do |k| puts <<-S * WARNING: #{klass.name} is not included in DocsOrganizer::class_sort_order. Please place this -module in it's correct topilogical order. +module in its correct topological order. S end @@ -253,13 +253,14 @@ S parse_log = [] html_start_to_toc_start = <<-S -<html> +<html lang="en"> <head> + <meta charset="utf-8"> <title>DragonRuby Game Toolkit Documentation</title> <link href="docs.css?ver=#{Time.now.to_i}" rel="stylesheet" type="text/css" media="all"> </head> <body> - <div id='toc'> + <div id='table-of-contents'> S html_toc_end_to_content_start = <<-S </div> @@ -342,6 +343,11 @@ S __docs_append_true_line__ true_lines, current_true_line, parse_log __docs_append_true_line__ true_lines, l, parse_log current_true_line = "" + elsif l.start_with? "***** " + parse_log << "- Header detected." + __docs_append_true_line__ true_lines, current_true_line, parse_log + __docs_append_true_line__ true_lines, l, parse_log + current_true_line = "" else current_true_line += l.rstrip + " " end @@ -373,6 +379,9 @@ S text = text.gsub("]", "-") text = text.gsub(":", "-") text = text.gsub(" ", "-") + text = text.gsub(".", "-") + text = text.gsub(",", "-") + text = text.gsub("?", "-") text end @@ -434,6 +443,15 @@ S link_id = text_to_id.call l # toc_html += "<ul><ul><ul><li><a href='##{link_id}'>#{formatted_html}</a></li></ul></ul></ul>" content_html += "<h4>#{__docs_line_to_html__ l, parse_log}</h4>\n" + elsif l.start_with? "**** " + parse_log << "- H5 detected." + content_html += close_list_if_needed.call inside_ul, inside_ol + inside_ol = false + inside_ul = false + formatted_html = __docs_line_to_html__ l, parse_log + link_id = text_to_id.call l + # toc_html += "<ul><ul><ul><li><a href='##{link_id}'>#{formatted_html}</a></li></ul></ul></ul>" + content_html += "<h5>#{__docs_line_to_html__ l, parse_log}</h5>\n" elsif l.strip.length == 0 && !inside_pre # do nothing elsif l.start_with? "#+begin_src" diff --git a/dragon/draw.rb b/dragon/draw.rb index 128624d..2963315 100644 --- a/dragon/draw.rb +++ b/dragon/draw.rb @@ -1,4 +1,4 @@ -# Contributors outside of DragonRuby who also hold Copyright: Nick Sandberg +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # draw.rb has been released under MIT (*only this file*). @@ -6,197 +6,45 @@ module GTK class Runtime module Draw - - def execute_draw_order pass - # Don't change this draw order unless you understand - # the implications. - render_solids pass - render_static_solids pass - render_sprites pass - render_static_sprites pass - render_primitives pass - render_static_primitives pass - render_labels pass - render_static_labels pass - render_lines pass - render_static_lines pass - render_borders pass - render_static_borders pass - end - def primitives pass if $top_level.respond_to? :primitives_override return $top_level.tick_render @args, pass end - execute_draw_order pass - - if !$gtk.production - # pass.debug.each { |r| draw_primitive r } - idx = 0 - length = pass.debug.length - while idx < length - draw_primitive (pass.debug.at idx) - idx += 1 - end - - # pass.static_debug.each { |r| draw_primitive r } - idx = 0 - length = pass.static_debug.length - while idx < length - draw_primitive (pass.static_debug.at idx) - idx += 1 - end - end + fn.each_send pass.solids, self, :draw_solid + fn.each_send pass.static_solids, self, :draw_solid + fn.each_send pass.sprites, self, :draw_sprite + fn.each_send pass.static_sprites, self, :draw_sprite + fn.each_send pass.primitives, self, :draw_primitive + fn.each_send pass.static_primitives, self, :draw_primitive + fn.each_send pass.labels, self, :draw_label + fn.each_send pass.static_labels, self, :draw_label + fn.each_send pass.lines, self, :draw_line + fn.each_send pass.static_lines, self, :draw_line + fn.each_send pass.borders, self, :draw_border + fn.each_send pass.static_borders, self, :draw_border - # pass.reserved.each { |r| draw_primitive r } - idx = 0 - length = pass.reserved.length - while idx < length - draw_primitive (pass.reserved.at idx) - idx += 1 + if !self.production + fn.each_send pass.debug, self, :draw_primitive + fn.each_send pass.static_debug, self, :draw_primitive end - # pass.static_reserved.each { |r| draw_primitive r } - idx = 0 - length = pass.static_reserved.length - while idx < length - draw_primitive (pass.static_reserved.at idx) - idx += 1 - end + fn.each_send pass.reserved, self, :draw_primitive + fn.each_send pass.static_reserved, self, :draw_primitive rescue Exception => e pause! pretty_print_exception_and_export! e end - - def render_solids pass - # pass.solids.each { |s| draw_solid s } - # while loops are faster than each with block - idx = 0 - length = pass.solids.length - while idx < pass.solids.length - draw_solid (pass.solids.at idx) # accessing an array using .value instead of [] is faster - idx += 1 - end - end - - def render_static_solids pass - # pass.static_solids.each { |s| draw_solid s } - idx = 0 - length = pass.static_solids.length - while idx < length - draw_solid (pass.static_solids.at idx) - idx += 1 - end - end - - def render_sprites pass - # pass.sprites.each { |s| draw_sprite s } - idx = 0 - length = pass.sprites.length - while idx < length - draw_sprite (pass.sprites.at idx) - idx += 1 - end - end - - def render_static_sprites pass - # pass.static_sprites.each { |s| draw_sprite s } - idx = 0 - length = pass.static_sprites.length - while idx < length - draw_sprite (pass.static_sprites.at idx) - idx += 1 - end - end - - def render_primitives pass - # pass.primitives.each { |p| draw_primitive p } - idx = 0 - length = pass.primitives.length - while idx < length - draw_primitive (pass.primitives.at idx) - idx += 1 - end - end - - def render_static_primitives pass - # pass.static_primitives.each { |p| draw_primitive p } - idx = 0 - length = pass.static_primitives.length - while idx < length - draw_primitive (pass.static_primitives.at idx) - idx += 1 - end - end - - def render_labels pass - # pass.labels.each { |l| draw_label l } - idx = 0 - length = pass.labels.length - while idx < length - draw_label (pass.labels.at idx) - idx += 1 - end - end - - def render_static_labels pass - # pass.static_labels.each { |l| draw_label l } - idx = 0 - length = pass.static_labels.length - while idx < length - draw_label (pass.static_labels.at idx) - idx += 1 - end - end - - def render_lines pass - # pass.lines.each { |l| draw_line l } - idx = 0 - length = pass.lines.length - while idx < length - draw_line (pass.lines.at idx) - idx += 1 - end - end - - def render_static_lines pass - # pass.static_lines.each { |l| draw_line l } - idx = 0 - length = pass.static_lines.length - while idx < pass.static_lines.length - draw_line (pass.static_lines.at idx) - idx += 1 - end - end - - def render_borders pass - # pass.borders.each { |b| draw_border b } - idx = 0 - length = pass.borders.length - while idx < length - draw_border (pass.borders.at idx) - idx += 1 - end - end - - def render_static_borders pass - # pass.static_borders.each { |b| draw_border b } - idx = 0 - length = pass.static_borders.length - while idx < length - draw_border (pass.static_borders.at idx) - idx += 1 - end - end - def draw_solid s return unless s if s.respond_to? :draw_override s.draw_override @ffi_draw else - @ffi_draw.draw_solid s.x, s.y, s.w, s.h, s.r, s.g, s.b, s.a + s = s.as_hash if s.is_a? OpenEntity + @ffi_draw.draw_solid_2 s.x, s.y, s.w, s.h, + s.r, s.g, s.b, s.a, + (s.blendmode_enum || 1) end rescue Exception => e raise_conversion_for_rendering_failed s, e, :solid @@ -207,14 +55,16 @@ module GTK if s.respond_to? :draw_override s.draw_override @ffi_draw else - @ffi_draw.draw_sprite_3 s.x, s.y, s.w, s.h, - s.path.s_or_default, + s = s.as_hash if s.is_a? OpenEntity + @ffi_draw.draw_sprite_4 s.x, s.y, s.w, s.h, + (s.path || '').to_s, s.angle, s.a, s.r, s.g, s.b, s.tile_x, s.tile_y, s.tile_w, s.tile_h, !!s.flip_horizontally, !!s.flip_vertically, s.angle_anchor_x, s.angle_anchor_y, - s.source_x, s.source_y, s.source_w, s.source_h + s.source_x, s.source_y, s.source_w, s.source_h, + (s.blendmode_enum || 1) end rescue Exception => e raise_conversion_for_rendering_failed s, e, :sprite @@ -225,7 +75,8 @@ module GTK if s.respond_to? :draw_override s.draw_override @ffi_draw else - @ffi_draw.draw_screenshot s.path.s_or_default, + s = s.as_hash if s.is_a? OpenEntity + @ffi_draw.draw_screenshot (s.path || '').to_s, s.x, s.y, s.w, s.h, s.angle, s.a, s.r, s.g, s.b, @@ -243,10 +94,14 @@ module GTK if l.respond_to? :draw_override l.draw_override @ffi_draw else - @ffi_draw.draw_label l.x, l.y, l.text.s_or_default, - l.size_enum, l.alignment_enum, - l.r, l.g, l.b, l.a, - l.font.s_or_default(nil) + l = l.as_hash if l.is_a? OpenEntity + @ffi_draw.draw_label_3 l.x, l.y, + (l.text || '').to_s, + l.size_enum, l.alignment_enum, + l.r, l.g, l.b, l.a, + l.font, + (l.vertical_alignment_enum || 2), + (l.blendmode_enum || 1) end rescue Exception => e raise_conversion_for_rendering_failed l, e, :label @@ -257,7 +112,22 @@ module GTK if l.respond_to? :draw_override l.draw_override @ffi_draw else - @ffi_draw.draw_line l.x, l.y, l.x2, l.y2, l.r, l.g, l.b, l.a + l = l.as_hash if l.is_a? OpenEntity + if l.x2 + @ffi_draw.draw_line_2 l.x, l.y, l.x2, l.y2, + l.r, l.g, l.b, l.a, + (l.blendmode_enum || 1) + else + w = l.w || 0 + w = 1 if w == 0 + h = l.h || 0 + h = 1 if h == 0 + @ffi_draw.draw_line_2 l.x, l.y, + l.x + w - 1, + l.y + h - 1, + l.r, l.g, l.b, l.a, + (l.blendmode_enum || 1) + end end rescue Exception => e raise_conversion_for_rendering_failed l, e, :line @@ -268,7 +138,10 @@ module GTK if s.respond_to? :draw_override s.draw_override @ffi_draw else - @ffi_draw.draw_border s.x, s.y, s.w, s.h, s.r, s.g, s.b, s.a + s = s.as_hash if s.is_a? OpenEntity + @ffi_draw.draw_border_2 s.x, s.y, s.w, s.h, + s.r, s.g, s.b, s.a, + (s.blendmode_enum || 1) end rescue Exception => e raise_conversion_for_rendering_failed s, e, :border @@ -288,7 +161,6 @@ module GTK pause! pretty_print_exception_and_export! e end - end end end diff --git a/dragon/easing.rb b/dragon/easing.rb index ef8ca0c..8b41fa6 100644 --- a/dragon/easing.rb +++ b/dragon/easing.rb @@ -9,8 +9,8 @@ module GTK ease_extended start_tick, current_tick, start_tick + duration, - (initial_value *definitions), - (final_value *definitions), + initial_value(*definitions), + final_value(*definitions), *definitions end diff --git a/dragon/framerate.rb b/dragon/framerate.rb index 2550443..478b340 100644 --- a/dragon/framerate.rb +++ b/dragon/framerate.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # framerate.rb has been released under MIT (*only this file*). @@ -33,13 +34,8 @@ module GTK if @tick_speed_count > 60 * 2 if framerate_below_threshold? @last_framerate = current_framerate - if [email protected]? - if !@framerate_important_notification_happened - log_important framerate_warning_message - else - log framerate_warning_message - end - @framerate_important_notification_happened = true + if [email protected]? && [email protected]_replaying? + log framerate_warning_message end end diff --git a/dragon/framerate_diagnostics.rb b/dragon/framerate_diagnostics.rb index 4586472..6985011 100644 --- a/dragon/framerate_diagnostics.rb +++ b/dragon/framerate_diagnostics.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # framerate_diagnostics.rb has been released under MIT (*only this file*). @@ -115,7 +116,7 @@ If this warning is getting annoying put the following in your tick method: def framerate_diagnostics_primitives [ - { x: 0, y: 93.from_top, w: 500, h: 93, a: 128 }.solid, + { x: 0, y: 93.from_top, w: 500, h: 93, a: 128 }.solid!, { x: 5, y: 5.from_top, @@ -124,7 +125,7 @@ If this warning is getting annoying put the following in your tick method: g: 255, b: 255, size_enum: -2 - }.label, + }.label!, { x: 5, y: 20.from_top, @@ -133,7 +134,7 @@ If this warning is getting annoying put the following in your tick method: g: 255, b: 255, size_enum: -2 - }.label, + }.label!, { x: 5, y: 35.from_top, @@ -142,7 +143,7 @@ If this warning is getting annoying put the following in your tick method: g: 255, b: 255, size_enum: -2 - }.label, + }.label!, { x: 5, y: 50.from_top, @@ -151,7 +152,7 @@ If this warning is getting annoying put the following in your tick method: g: 255, b: 255, size_enum: -2 - }.label, + }.label!, { x: 5, y: 65.from_top, @@ -160,7 +161,7 @@ If this warning is getting annoying put the following in your tick method: g: 255, b: 255, size_enum: -2 - }.label, + }.label!, ] end diff --git a/dragon/geometry.rb b/dragon/geometry.rb index 9385022..d7c8ffc 100644 --- a/dragon/geometry.rb +++ b/dragon/geometry.rb @@ -5,6 +5,22 @@ module GTK module Geometry + def self.rotate_point point, angle, around = nil + s = Math.sin angle.to_radians + c = Math.cos angle.to_radians + px = point.x + py = point.y + cx = 0 + cy = 0 + if around + cx = around.x + cy = around.y + end + + point.merge(x: ((px - cx) * c - (py - cy) * s) + cx, + y: ((px - cx) * s + (py - cy) * c) + cy) + end + # Returns f(t) for a cubic Bezier curve. def self.cubic_bezier t, a, b, c, d s = 1 - t @@ -86,7 +102,7 @@ module GTK rescue Exception => e raise e, <<-S * ERROR: -center_inside_rect for self #{self} and other_rect #{other_rect}. Failed with exception #{e}. +center_inside_rect for self #{self} and other_rect #{other_rect}.\n#{e}. S end @@ -103,7 +119,7 @@ S rescue Exception => e raise e, <<-S * ERROR: -center_inside_rect_x for self #{self} and other_rect #{other_rect}. Failed with exception #{e}. +center_inside_rect_x for self #{self} and other_rect #{other_rect}.\n#{e}. S end @@ -120,7 +136,7 @@ S rescue Exception => e raise e, <<-S * ERROR: -center_inside_rect_y for self #{self} and other_rect #{other_rect}. Failed with exception #{e}. +center_inside_rect_y for self #{self} and other_rect #{other_rect}.\n#{e}. S end @@ -129,7 +145,7 @@ S end - # Returns a primitive that is anchored/repositioned based off its retangle. + # Returns a primitive that is anchored/repositioned based off its rectangle. # @gtk def anchor_rect anchor_x, anchor_y current_w = self.w @@ -172,8 +188,16 @@ S end # @gtk - def self.line_y_intercept line - line.y - line_slope(line) * line.x + def self.line_y_intercept line, replace_infinity: nil + line.y - line_slope(line, replace_infinity: replace_infinity) * line.x + rescue Exception => e +raise <<-S +* ERROR: ~Geometry::line_y_intercept~ +The following exception was thrown for line: #{line} +#{e} + +Consider passing in ~replace_infinity: VALUE~ to handle for vertical lines. +S end # @gtk @@ -249,14 +273,22 @@ S end # @gtk - def self.line_intersect line_one, line_two - m1 = line_slope(line_one) - m2 = line_slope(line_two) - b1 = line_y_intercept(line_one) - b2 = line_y_intercept(line_two) + def self.line_intersect line_one, line_two, replace_infinity: nil + m1 = line_slope(line_one, replace_infinity: replace_infinity) + m2 = line_slope(line_two, replace_infinity: replace_infinity) + b1 = line_y_intercept(line_one, replace_infinity: replace_infinity) + b2 = line_y_intercept(line_two, replace_infinity: replace_infinity) x = (b1 - b2) / (m2 - m1) y = (-b2.fdiv(m2) + b1.fdiv(m1)).fdiv(1.fdiv(m1) - 1.fdiv(m2)) [x, y] + rescue Exception => e +raise <<-S +* ERROR: ~Geometry::line_intersect~ +The following exception was thrown for line_one: #{line_one}, line_two: #{line_two} +#{e} + +Consider passing in ~replace_infinity: VALUE~ to handle for vertical lines. +S end def self.contract_intersect_rect? @@ -265,10 +297,10 @@ S # @gtk def self.intersect_rect? rect_one, rect_two, tolerance = 0.1 - return false if rect_one.right - tolerance < rect_two.left + tolerance - return false if rect_one.left + tolerance > rect_two.right - tolerance - return false if rect_one.top - tolerance < rect_two.bottom + tolerance - return false if rect_one.bottom + tolerance > rect_two.top - tolerance + return false if ((rect_one.x + rect_one.w) - tolerance) < (rect_two.x + tolerance) + return false if (rect_one.x + tolerance) > ((rect_two.x + rect_two.w) - tolerance) + return false if ((rect_one.y + rect_one.h) - tolerance) < (rect_two.y + tolerance) + return false if (rect_one.y + tolerance) > ((rect_two.y + rect_two.h) - tolerance) return true rescue Exception => e context_help_rect_one = (rect_one.__help_contract_implementation contract_intersect_rect?)[:not_implemented_methods] @@ -296,6 +328,7 @@ S - rect_one: #{rect_one} - rect_two: #{rect_two} #{context_help} +\n#{e} S end @@ -306,14 +339,14 @@ S y = y.shift_down(size * anchor_y) [x, y, size, size] rescue Exception => e - raise e, ":to_square failed for size: #{size} x: #{x} y: #{y} anchor_x: #{anchor_x} anchor_y: #{anchor_y}." + raise e, ":to_square failed for size: #{size} x: #{x} y: #{y} anchor_x: #{anchor_x} anchor_y: #{anchor_y}.\n#{e}" end # @gtk def self.distance point_one, point_two Math.sqrt((point_two.x - point_one.x)**2 + (point_two.y - point_one.y)**2) rescue Exception => e - raise e, ":distance failed for point_one: #{point_one} point_two #{point_two}." + raise e, ":distance failed for point_one: #{point_one} point_two #{point_two}.\n#{e}" end # @gtk @@ -322,31 +355,34 @@ S d_x = end_point.x - start_point.x Math::PI.+(Math.atan2(d_y, d_x)).to_degrees rescue Exception => e - raise e, ":angle_from failed for start_point: #{start_point} end_point: #{end_point}." + raise e, ":angle_from failed for start_point: #{start_point} end_point: #{end_point}.\n#{e}" end # @gtk def self.angle_to start_point, end_point angle_from end_point, start_point rescue Exception => e - raise e, ":angle_to failed for start_point: #{start_point} end_point: #{end_point}." + raise e, ":angle_to failed for start_point: #{start_point} end_point: #{end_point}.\n#{e}" end # @gtk def self.point_inside_circle? point, circle_center_point, radius (point.x - circle_center_point.x) ** 2 + (point.y - circle_center_point.y) ** 2 < radius ** 2 rescue Exception => e - raise e, ":point_inside_circle? failed for point: #{point} circle_center_point: #{circle_center_point} radius: #{radius}" + raise e, ":point_inside_circle? failed for point: #{point} circle_center_point: #{circle_center_point} radius: #{radius}.\n#{e}" end # @gtk def self.inside_rect? inner_rect, outer_rect, tolerance = 0.0 + return nil if !inner_rect + return nil if !outer_rect + inner_rect.x + tolerance >= outer_rect.x - tolerance && - inner_rect.right - tolerance <= outer_rect.right + tolerance && + (inner_rect.x + inner_rect.w) - tolerance <= (outer_rect.x + outer_rect.w) + tolerance && inner_rect.y + tolerance >= outer_rect.y - tolerance && - inner_rect.top - tolerance <= outer_rect.top + tolerance + (inner_rect.y + inner_rect.h) - tolerance <= (outer_rect.y + outer_rect.h) + tolerance rescue Exception => e - raise e, ":inside_rect? failed for inner_rect: #{inner_rect} outer_rect: #{outer_rect}." + raise e, ":inside_rect? failed for inner_rect: #{inner_rect} outer_rect: #{outer_rect}.\n#{e}" end # @gtk @@ -381,7 +417,7 @@ S return rect end rescue Exception => e - raise e, ":scale_rect_extended failed for rect: #{rect} percentage_x: #{percentage_x} percentage_y: #{percentage_y} anchors_x: #{anchor_x} anchor_y: #{anchor_y}." + raise e, ":scale_rect_extended failed for rect: #{rect} percentage_x: #{percentage_x} percentage_y: #{percentage_y} anchors_x: #{anchor_x} anchor_y: #{anchor_y}.\n#{e}" end # @gtk @@ -395,7 +431,21 @@ S anchor_x: anchor_x, anchor_y: anchor_y rescue Exception => e - raise e, ":scale_rect failed for rect: #{rect} percentage: #{percentage} anchors [#{anchor_x} (x), #{anchor_y} (y)]." + raise e, ":scale_rect failed for rect: #{rect} percentage: #{percentage} anchors [#{anchor_x} (x), #{anchor_y} (y)].\n#{e}" + end + + def self.rect_to_line rect + l = rect.to_hash.line + l.merge(x2: l.x + l.w - 1, + y2: l.y + l.h) + end + + def self.rect_center_point rect + { x: rect.x + rect.w.half, y: rect.y + rect.h.half } + end + + def rect_center_point + Geometry.rect_center_point self end end # module Geometry end # module GTK diff --git a/dragon/grid.rb b/dragon/grid.rb index dd36df7..2cfa04e 100644 --- a/dragon/grid.rb +++ b/dragon/grid.rb @@ -213,5 +213,13 @@ module GTK def bottom_right [@right, @bottom].point end + + def x + 0 + end + + def y + 0 + end end end diff --git a/dragon/help.rb b/dragon/help.rb deleted file mode 100644 index aee60d8..0000000 --- a/dragon/help.rb +++ /dev/null @@ -1,72 +0,0 @@ -# coding: utf-8 -# Copyright 2019 DragonRuby LLC -# MIT License -# help.rb has been released under MIT (*only this file*). - -module GTK - class Help - def self.primitive_contract primitive_name - if primitive_name == :label - label_contract - elsif primitive_name == :solid - solid_border_contract - elsif primitive_name == :border - solid_border_contract - elsif primitive_name == :sprite - sprite_contract - else - help_text = "No contract found for primitive #{primitive_name}. The supported primitives are :label, :solid, :border, :sprite." - end - end - - def self.label_contract - <<-S -* :label (if :primitive_marker returns :label) -** :x, :y, :text -** :size_enum -default: 0 -negative value means smaller text -positive value means larger text -** :alignment_enum default: 0 -default: 0 -0: left aligned, 1: center aligned, 2: right aligned -** :r, :g, :b, :a -default: 0's for rgb and 255 for a -** :font -default nil -path to ttf file -S - end - - def self.solid_border_contract - <<-S -* :solid, :border (if :primitive_marker returns :solid or :border) -** :x, :y, :w, :h, :r, :g, :b, :a -S - end - - def self.label_contract - <<-S -* :line (if :primitive_marker returns :line) -** :x, :y, :x2, :y2, :r, :g, :b, :a -S - end - - def self.sprite_contract - <<-S -* :sprite (if :primitive_marker returns :sprite) -** :x, :y, :w, :h -** :angle, :angle_anchor_x, :angle_anchor_y -default for angle: 0 (0 to 360 degress) -default for angle_anchor_(x|y): 0 (decimal value between 0 and 1.0, 0.5 means center) -** :r, :g, :b, :a -** :tile_x, :tile_y -default: 0, x, y offset for sprite to crop at -** :tile_w, :tile_h -default: -1, width and height of crop (-1 means full width and height) -** :flip_horizontally, :flip_vertically -default: falsey value -S - end - end -end diff --git a/dragon/hotload.rb b/dragon/hotload.rb index 1cb062a..1196e2b 100644 --- a/dragon/hotload.rb +++ b/dragon/hotload.rb @@ -10,7 +10,14 @@ module GTK def hotload_init @hotload_if_needed = 0 @mailbox_if_needed = 0 + + # schema for file_mtimes + # { FILE_PATH: { current: (Time as Fixnum), + # last: (Time as Fixnum) }, + # FILE_PATH: { current: (Time as Fixnum), + # last: (Time as Fixnum) } } @file_mtimes = { } + @suppress_mailbox = true files_to_reload.each { |f| init_mtimes f } init_mtimes 'app/mailbox.rb' @@ -48,6 +55,7 @@ module GTK 'dragon/symbol.rb', 'dragon/numeric_deprecated.rb', 'dragon/numeric.rb', + 'dragon/hash_deprecated.rb', 'dragon/hash.rb', 'dragon/outputs_deprecated.rb', 'dragon/array_docs.rb', @@ -80,6 +88,7 @@ module GTK 'dragon/trace.rb', 'dragon/readme_docs.rb', 'dragon/hotload_client.rb', + 'dragon/wizards.rb', 'dragon/ios_wizard.rb', 'dragon/itch_wizard.rb', ] + core_files_to_reload + @required_files @@ -98,10 +107,8 @@ module GTK end def init_mtimes file - current_key = "current_#{file}".to_sym - last_key = "last_#{file}".to_sym - @file_mtimes[current_key] ||= @ffi_file.mtime(file) - @file_mtimes[last_key] ||= @ffi_file.mtime(file) + @file_mtimes[file] ||= { current: @ffi_file.mtime(file), + last: @ffi_file.mtime(file) } end def hotload_source_files @@ -133,25 +140,36 @@ module GTK end def hotload_if_needed + return if Kernel.tick_count < 0 hotload_source_files check_mailbox end def on_load_succeeded file - @rcb_sender.files_reloaded << file - @rcb_sender.reloaded_files << file + self.files_reloaded << file + self.reloaded_files << file Trace.untrace_classes! end + def reset_all_mtimes + @file_mtimes.each do |file, _| + @file_mtimes[file].current = @ffi_file.mtime(file) + @file_mtimes[file].last = @file_mtimes[file].current + end + + files_to_reload.each do |file, _| + @file_mtimes[file] ||= {} + @file_mtimes[file].current = @ffi_file.mtime(file) + @file_mtimes[file].last = @file_mtimes[file].current + end + end + def reload_if_needed file, force = false - current_key = "current_#{file}".to_sym - last_key = "last_#{file}".to_sym - @file_mtimes[current_key] ||= nil - @file_mtimes[last_key] ||= nil - @file_mtimes[current_key] = @ffi_file.mtime(file) - return if !force && @file_mtimes[last_key] == @file_mtimes[current_key] + @file_mtimes[file] ||= { current: @ffi_file.mtime(file), last: @ffi_file.mtime(file) } + @file_mtimes[file].current = @ffi_file.mtime(file) + return if !force && @file_mtimes[file].current == @file_mtimes[file].last on_load_succeeded file if reload_ruby_file file - @file_mtimes[last_key] = @file_mtimes[current_key] + @file_mtimes[file].last = @file_mtimes[file].current end end end diff --git a/dragon/inputs.rb b/dragon/inputs.rb index 0ae4f86..8be97f9 100644 --- a/dragon/inputs.rb +++ b/dragon/inputs.rb @@ -191,6 +191,30 @@ module GTK } end + def self.method_to_key_hash + return @method_to_key_hash if @method_to_key_hash + @method_to_key_hash = {} + string_representation_overrides ||= { + backspace: '\b' + } + char_to_method_hash.each do |k, v| + v.each do |vi| + t = { char_or_raw_key: k } + + if k.is_a? Numeric + t[:raw_key] = k + t[:string_representation] = "raw_key == #{k}" + else + t[:char] = k + t[:string_representation] = "\"#{k.strip}\"" + end + + @method_to_key_hash[vi] = t + end + end + @method_to_key_hash + end + def self.char_to_method char, int = nil methods = char_to_method_hash[char] || char_to_method_hash[int] methods ? methods.dup : [char.to_sym || int] @@ -289,7 +313,8 @@ module GTK value = Kernel.tick_count if value collection.each do |m| - self.instance_variable_set("@#{m.to_s}".to_sym, value) + m_to_s = m.to_s + self.instance_variable_set("@#{m_to_s}".to_sym, value) if m_to_s.strip.length > 0 rescue Exception => e raise e, <<-S * ERROR: @@ -305,24 +330,32 @@ S end def method_missing m, *args - begin - define_singleton_method(m) do - r = self.instance_variable_get("@#{m.without_ending_bang}".to_sym) - clear_key m - return r + if KeyboardKeys.method_to_key_hash[m.without_ending_bang] + begin + define_singleton_method(m) do + r = self.instance_variable_get("@#{m.without_ending_bang}".to_sym) + clear_key m + return r + end + + return self.send m + rescue Exception => e + log_important "#{e}" end - - return self.send m - rescue Exception => e - log_important "#{e}" end + did_you_mean = KeyboardKeys.method_to_key_hash.find_all do |k, v| + k.to_s[0..1] == m.to_s[0..1] + end.map {|k, v| ":#{k} (#{v[:string_representation]})" } + did_you_mean_string = "" + did_you_mean_string = ". Did you mean #{did_you_mean.join ", "}?" + raise <<-S * ERROR: -There is no member on the keyboard called #{m}. Here is a to_s representation of what's available: - -#{KeyboardKeys.char_to_method_hash.map { |k, v| "[#{k} => #{v.join(",")}]" }.join(" ")} +#{KeyboardKeys.method_to_key_hash.map { |k, v| "** :#{k} #{v.string_representation}" }.join("\n")} +There is no key on the keyboard called :#{m}#{did_you_mean_string}. +Full list of available keys =:points_up:=. S end @@ -706,6 +739,10 @@ module GTK (controller_one && controller_one.directional_vector) end + def directional_angle + keyboard.directional_angle || (controller_one && controller_one.directional_angle) + end + # Returns a signal indicating right (`1`), left (`-1`), or neither ('0'). # # @return [Integer] diff --git a/dragon/ios_wizard.rb b/dragon/ios_wizard.rb index 0d48108..c040dea 100644 --- a/dragon/ios_wizard.rb +++ b/dragon/ios_wizard.rb @@ -1,16 +1,11 @@ +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # ios_wizard.rb has been released under MIT (*only this file*). -class WizardException < Exception - attr_accessor :console_primitives +# Contributors outside of DragonRuby who also hold Copyright: Michał Dudziński - def initialize *console_primitives - @console_primitives = console_primitives - end -end - -class IOSWizard +class IOSWizard < Wizard def initialize @doctor_executed_at = 0 end @@ -23,23 +18,46 @@ class IOSWizard @steps ||= [] end - def steps_development_build + def prerequisite_steps [ :check_for_xcode, :check_for_brew, :check_for_certs, - :check_for_device, - :check_for_dev_profile, + ] + end + + def app_metadata_retrieval_steps + [ :determine_team_identifier, :determine_app_name, :determine_app_id, - :blow_away_temp, + ] + end + + def steps_development_build + [ + *prerequisite_steps, + + :check_for_device, + :check_for_dev_profile, + + *app_metadata_retrieval_steps, + :determine_devcert, + + :clear_tmp_directory, :stage_app, + :development_write_info_plist, + :write_entitlements_plist, :compile_icons, - :create_payload_directory, + :clear_payload_directory, + + :create_payload_directory_dev, + + :create_payload, :code_sign_payload, + :create_ipa, :deploy ] @@ -47,20 +65,28 @@ class IOSWizard def steps_production_build [ - :check_for_xcode, - :check_for_brew, - :check_for_certs, + *prerequisite_steps, + :check_for_distribution_profile, - :determine_team_identifier, - :determine_app_name, - :determine_app_id, - :blow_away_temp, + :determine_app_version, + + *app_metadata_retrieval_steps, + :determine_prodcert, + + :clear_tmp_directory, :stage_app, + :production_write_info_plist, + :write_entitlements_plist, :compile_icons, - :create_payload_directory, + :clear_payload_directory, + + :create_payload_directory_prod, + + :create_payload, :code_sign_payload, + :create_ipa, :print_publish_help ] @@ -96,6 +122,8 @@ class IOSWizard @steps = steps_development_build @steps = steps_production_build if @production_build @certificate_name = nil + @app_version = opts[:version] + @app_version = "1.0" if @opts[:env] == :dev && !@app_version init_wizard_status log_info "Starting iOS Wizard so we can deploy to your device." @start_at = Kernel.global_tick_count @@ -117,8 +145,10 @@ class IOSWizard log "=" * $console.console_text_width else log_error e.to_s + log e.__backtrace_to_org__ end + init_wizard_status $console.set_command "$wizards.ios.start env: :#{@opts[:env]}" end @@ -225,13 +255,70 @@ class IOSWizard return "profiles/development.mobileprovision" end + def ios_metadata_template + <<-S +# ios_metadata.txt is used by the Pro version of DragonRuby Game Toolkit to create iOS apps. +# Information about the Pro version can be found at: http://dragonruby.org/toolkit/game#purchase + +# teamid needs to be set to your assigned Team Id which can be found at https://developer.apple.com/account/#/membership/ +teamid= +# appid needs to be set to your application identifier which can be found at https://developer.apple.com/account/resources/identifiers/list +appid= +# appname is the name you want to show up underneath the app icon on the device. Keep it under 10 characters. +appname= +# devcert is the certificate to use for development/deploying to your local device +devcert= +# prodcert is the certificate to use for distribution to the app store +prodcert= +S + end + + def ios_metadata + contents = $gtk.read_file 'metadata/ios_metadata.txt' + + if !contents + $gtk.write_file 'metadata/ios_metadata.txt', ios_metadata_template + contents = $gtk.read_file 'metadata/ios_metadata.txt' + end + + kvps = contents.each_line + .reject { |l| l.strip.length == 0 || (l.strip.start_with? "#") } + .map do |l| + key, value = l.split("=") + [key.strip.to_sym, value.strip] + end.flatten + Hash[*kvps] + end + + def game_metadata + contents = $gtk.read_file 'metadata/game_metadata.txt' + + kvps = contents.each_line + .reject { |l| l.strip.length == 0 || (l.strip.start_with? "#") } + .map do |l| + key, value = l.split("=") + [key.strip.to_sym, value.strip] + end.flatten + Hash[*kvps] + end + + def raise_ios_metadata_required + raise WizardException.new( + "* mygame/metadata/ios_metadata.txt needs to be filled out.", + "You need to update metadata/ios_metadata.txt with a valid teamid, appname, appid, devcert, and prodcert.", + "Instructions for where the values should come from are within metadata/ios_metadata.txt." + ) + end + def determine_team_identifier - @team_name = (team_identifier_from_provisioning_profile @opts[:env]) - log_info "Team Identifer is: #{@team_name}" + @team_id = (ios_metadata.teamid || "") + raise_ios_metadata_required if @team_id.strip.length == 0 + log_info "Team Identifer is: #{@team_id}" end def determine_app_name - @app_name = (provisioning_profile_xml @opts[:env])[:children].first[:children].first[:children][1][:children].first[:data] + @app_name = (ios_metadata.appname || "") + raise_ios_metadata_required if @app_name.strip.length == 0 log_info "App name is: #{@app_name}." end @@ -255,37 +342,22 @@ class IOSWizard $gtk.parse_xml scrubbed end - def app_id_from_provisioning_profile environment - application_identifier_index = (provisioning_profile_xml environment)[:children][0][:children][0][:children][13][:children][0][:children][0][:data] - (provisioning_profile_xml environment)[:children][0][:children][0][:children][13][:children].each.with_index do |node, i| - if node[:children] && node[:children][0] && node[:children][0][:data] == "application-identifier" - application_identifier_index = i - break - end - end - - app_id_with_team_identifier = (provisioning_profile_xml environment)[:children].first[:children].first[:children][13][:children][application_identifier_index + 1][:children].first[:data] - team_identifer = team_identifier_from_provisioning_profile environment - app_id_with_team_identifier.gsub "#{team_identifer}.", "" + def determine_app_id + @app_id = ios_metadata.appid + raise_ios_metadata_required if @app_id.strip.length == 0 + log_info "App Identifier is set to: #{@app_id}" end - def team_identifier_from_provisioning_profile environment - team_identifer_index = (provisioning_profile_xml environment)[:children][0][:children][0][:children][13][:children][0][:children][0][:data] - - (provisioning_profile_xml environment)[:children][0][:children][0][:children][13][:children].each.with_index do |node, i| - if node[:children] && node[:children][0] && node[:children][0][:data] == "com.apple.developer.team-identifier" - team_identifer_index = i - break - end - end - - (provisioning_profile_xml environment)[:children].first[:children].first[:children][13][:children][team_identifer_index + 1][:children].first[:data] + def determine_devcert + @certificate_name = ios_metadata.devcert + raise_ios_metadata_required if @certificate_name.strip.length == 0 + log_info "Dev Certificate is set to: #{@certificate_name}" end - def determine_app_id - @app_id = app_id_from_provisioning_profile @opts[:env] - - log_info "App Identifier is set to : #{@app_id}" + def determine_prodcert + @certificate_name = ios_metadata.prodcert + raise_ios_metadata_required if @certificate_name.strip.length == 0 + log_info "Production (Distribution) Certificate is set to: #{@certificate_name}" end def set_app_name name @@ -303,16 +375,10 @@ class IOSWizard end end - def blow_away_temp + def clear_tmp_directory sh "rm -rf #{tmp_directory}" end - def stage_app - log_info "Staging." - sh "mkdir -p #{tmp_directory}" - sh "cp -R #{relative_path}/dragonruby-ios.app \"#{tmp_directory}/#{@app_name}.app\"" - end - def set_app_id id log_info = "App Id set to: #{id}" @app_id = id @@ -343,34 +409,13 @@ class IOSWizard def check_for_certs log_info "Attempting to find certificates on your computer." - if !cli_app_exist?(security_cli_app) - raise WizardException.new( - "* It doesn't look like you have #{security_cli_app}.", - "** 1. Open Disk Utility and run First Aid.", - { w: 700, h: 148, path: get_reserved_sprite("disk-utility.png") }, - ) - end - - if valid_certs.length == 0 - raise WizardException.new( - "* It doesn't look like you have any valid certs installed.", - "** 1. Open Xcode.", - "** 2. Log into your developer account. Xcode -> Preferences -> Accounts.", - { w: 700, h: 98, path: get_reserved_sprite("login-xcode.png") }, - "** 3. After loggin in, select Manage Certificates...", - { w: 700, h: 115, path: get_reserved_sprite("manage-certificates.png") }, - "** 4. Add a certificate for Apple Development.", - { w: 700, h: 217, path: get_reserved_sprite("add-cert.png") }, - ) - raise "You do not have any Apple development certs on this computer." - end - if @production_build - @certificate_name = valid_certs.find_all { |f| f[:name].include? "Distribution" }.first[:name] + @certificate_name = ios_metadata[:prodcert] else - @certificate_name = valid_certs.find_all { |f| f[:name].include? "Development" }.first[:name] + @certificate_name = ios_metadata[:devcert] end - log_info "I will be using Certificate: '#{@certificate_name}'." + + log_info "I will be using certificate: '#{@certificate_name}'." end def idevice_id_cli_app @@ -385,24 +430,6 @@ class IOSWizard "xcodebuild" end - def valid_certs - certs = sh("#{security_cli_app} -q find-identity -p codesigning -v").each_line.map do |l| - if l.include?(")") && !l.include?("Developer ID") && (l.include?("Development") || l.include?("Distribution")) - l.strip - else - nil - end - end.reject_nil.map do |l| - number, id, name = l.split(' ', 3) - name = name.gsub("\"", "") if name - { - number: 1, - id: id, - name: name - } - end - end - def connected_devices sh("idevice_id -l").strip.each_line.map do |l| l.strip @@ -444,7 +471,8 @@ XML log_info "Creating Entitlements.plist" - $gtk.write_file_root "tmp/ios/Entitlements.plist", entitlement_plist_string.gsub(":app_id", "#{@team_name}.#{@app_id}").strip + $gtk.write_file_root "tmp/ios/Entitlements.plist", entitlement_plist_string.gsub(":app_id", "#{@team_id}.#{@app_id}").strip + $gtk.write_file_root "tmp/ios/Entitlements.txt", entitlement_plist_string.gsub(":app_id", "#{@team_id}.#{@app_id}").strip sh "/usr/bin/plutil -convert binary1 \"#{tmp_directory}/Entitlements.plist\"" sh "/usr/bin/plutil -convert xml1 \"#{tmp_directory}/Entitlements.plist\"" @@ -482,15 +510,15 @@ XML <key>CFBundleExecutable</key> <string>:app_name</string> <key>CFBundleInfoDictionaryVersion</key> - <string>6.0</string> + <string>:app_version</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> - <string>5.6</string> + <string>:app_version</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleVersion</key> - <string>5.6</string> + <string>:app_version</string> <key>CFBundleIcons</key> <dict> <key>CFBundlePrimaryIcon</key> @@ -639,13 +667,13 @@ XML <key>CFBundleIdentifier</key> <string>:app_id</string> <key>CFBundleInfoDictionaryVersion</key> - <string>6.0</string> + <string>:app_version</string> <key>CFBundleName</key> <string>:app_name</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> - <string>5.2</string> + <string>:app_version</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleSupportedPlatforms</key> @@ -653,7 +681,7 @@ XML <string>iPhoneOS</string> </array> <key>CFBundleVersion</key> - <string>5.2</string> + <string>:app_version</string> <key>DTCompiler</key> <string>com.apple.compilers.llvm.clang.1_0</string> <key>DTPlatformBuild</key> @@ -739,6 +767,7 @@ XML info_plist_string.gsub!(":app_id", @app_id) $gtk.write_file_root "tmp/ios/#{@app_name}.app/Info.plist", info_plist_string.strip + $gtk.write_file_root "tmp/ios/Info.txt", info_plist_string.strip @info_plist_written = true end @@ -792,13 +821,13 @@ XML <key>CFBundleIdentifier</key> <string>:app_id</string> <key>CFBundleInfoDictionaryVersion</key> - <string>6.0</string> + <string>:app_version</string> <key>CFBundleName</key> <string>:app_name</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> - <string>5.2</string> + <string>:app_version</string> <key>CFBundleSignature</key> <string>????</string> <key>CFBundleSupportedPlatforms</key> @@ -806,7 +835,7 @@ XML <string>iPhoneOS</string> </array> <key>CFBundleVersion</key> - <string>5.2</string> + <string>:app_version</string> <key>DTCompiler</key> <string>com.apple.compilers.llvm.clang.1_0</string> <key>DTPlatformBuild</key> @@ -890,8 +919,10 @@ XML info_plist_string.gsub!(":app_name", @app_name) info_plist_string.gsub!(":app_id", @app_id) + info_plist_string.gsub!(":app_version", @app_version) $gtk.write_file_root "tmp/ios/#{@app_name}.app/Info.plist", info_plist_string.strip + $gtk.write_file_root "tmp/ios/Info.txt", info_plist_string.strip @info_plist_written = true end @@ -913,28 +944,56 @@ XML "#{relative_path}/#{$gtk.cli_arguments[:dragonruby]}" end - def write_ip_address + def embed_mobileprovision + sh %Q[cp #{@provisioning_profile_path} "#{app_path}/embedded.mobileprovision"] + sh %Q[/usr/bin/plutil -convert binary1 "#{app_path}/Info.plist"] + end + + def clear_payload_directory + sh %Q[rm "#{@app_name}".ipa] + sh %Q[rm -rf "#{app_path}/app"] + sh %Q[rm -rf "#{app_path}/sounds"] + sh %Q[rm -rf "#{app_path}/sprites"] + sh %Q[rm -rf "#{app_path}/data"] + sh %Q[rm -rf "#{app_path}/fonts"] + end + + def stage_app + log_info "Staging." + sh "mkdir -p #{tmp_directory}" + sh "cp -R #{relative_path}/dragonruby-ios.app \"#{tmp_directory}/#{@app_name}.app\"" + sh %Q[cp -r "#{root_folder}/app/" "#{app_path}/app/"] + sh %Q[cp -r "#{root_folder}/sounds/" "#{app_path}/sounds/"] + sh %Q[cp -r "#{root_folder}/sprites/" "#{app_path}/sprites/"] + sh %Q[cp -r "#{root_folder}/data/" "#{app_path}/data/"] + sh %Q[cp -r "#{root_folder}/fonts/" "#{app_path}/fonts/"] + end + + def create_payload + sh %Q[mkdir -p #{tmp_directory}/ipa_root/Payload] + sh %Q[cp -r "#{app_path}" "#{tmp_directory}/ipa_root/Payload"] + sh %Q[chmod -R 755 "#{tmp_directory}/ipa_root/Payload"] + end + + def create_payload_directory_dev + # write dev machine's ip address for hotloading $gtk.write_file "app/server_ip_address.txt", $gtk.ffi_misc.get_local_ip_address.strip + + embed_mobileprovision + clear_payload_directory + stage_app end - def create_payload_directory - sh "cp #{@provisioning_profile_path} \"#{app_path}/embedded.mobileprovision\"" - sh "/usr/bin/plutil -convert binary1 \"#{app_path}/Info.plist\"" - write_ip_address - sh "rm \"#{@app_name}\".ipa" - sh "rm -rf \"#{app_path}/app\"" - sh "rm -rf \"#{app_path}/sounds\"" - sh "rm -rf \"#{app_path}/sprites\"" - sh "rm -rf \"#{app_path}/data\"" - sh "rm -rf \"#{app_path}/fonts\"" - sh "cp -r \"#{root_folder}/app/\" \"#{app_path}/app/\"" - sh "cp -r \"#{root_folder}/sounds/\" \"#{app_path}/sounds/\"" - sh "cp -r \"#{root_folder}/sprites/\" \"#{app_path}/sprites/\"" - sh "cp -r \"#{root_folder}/data/\" \"#{app_path}/data/\"" - sh "cp -r \"#{root_folder}/fonts/\" \"#{app_path}/fonts/\"" - sh "mkdir -p #{tmp_directory}/ipa_root/Payload" - sh "cp -r \"#{app_path}\" \"#{tmp_directory}/ipa_root/Payload\"" - sh "chmod -R 755 \"#{tmp_directory}/ipa_root/Payload\"" + def create_payload_directory_prod + # production builds does not hotload ip address + sh %Q[rm "#{root_folder}/app/server_ip_address.txt"] + + embed_mobileprovision + stage_app + + # production build marker + sh %Q[mkdir -p "#{app_path}/metadata/"] + sh %Q[touch metadata/DRAGONRUBY_PRODUCTION_BUILD] end def create_ipa @@ -967,10 +1026,22 @@ SCRIPT end def print_publish_help - log_info "Go to https://appstoreconnect.apple.com/apps and create an App if you haven't already done so." - log_info "Go to https://appleid.apple.com and create a 'Application Specific Password'." - log_info "To upload your app, Download Transporter from the App Store https://apps.apple.com/us/app/transporter/id1450874784?mt=12." - log_info "Your app is located at ./tmp/ios/#{@app_name}.ipa" + has_transporter = (sh "ls /Applications/Transporter.app").include? "Contents" + if !has_transporter + $gtk.openurl "https://apps.apple.com/us/app/transporter/id1450874784?mt=12" + $console.set_command "$wizards.ios.start env: :#{@opts[:env]}, version: \"#{@opts[:version]}\"" + raise WizardException.new( + "* To upload your app, Download Transporter from the App Store https://apps.apple.com/us/app/transporter/id1450874784?mt=12." + ) + else + sh "mkdir ./tmp/ios/intermediary_artifacts" + sh "mv \"#{tmp_directory}/#{@app_name}.app\" #{tmp_directory}/intermediary_artifacts/" + sh "mv \"#{tmp_directory}/do_zip.sh\" #{tmp_directory}/intermediary_artifacts" + sh "mv \"#{tmp_directory}/Entitlements.plist\" #{tmp_directory}/intermediary_artifacts" + sh "mv \"#{tmp_directory}/ipa_root\" #{tmp_directory}/intermediary_artifacts/" + sh "open /Applications/Transporter.app" + sh "open ./tmp/ios/" + end end def compile_icons @@ -991,4 +1062,24 @@ S sh "cp -r \"#{root_folder}/native/\" \"#{app_path}/native/\"" sh "CODESIGN_ALLOCATE=\"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/codesign_allocate\" /usr/bin/codesign -f -s \"#{@certificate_name}\" --entitlements #{tmp_directory}/Entitlements.plist \"#{tmp_directory}/#{@app_name}.app/native/ios-device/ext.dylib\"" end + + def set_version version + @app_version = version + start env: @opts[:env], version: version + end + + def app_version + log_info "Attempting to retrieve App Version from metadata/ios_metadata.txt." + ios_version_number = (ios_metadata.version || "").strip + if ios_version_number.length == 0 + log_info "Not found. Attempting to retrieve App Version from metadata/game_metadata.txt." + ios_version_number = (game_metadata.version || "").strip + end + ios_version_number + end + + def determine_app_version + @app_version = app_version + return if @app_version + end end diff --git a/dragon/itch_wizard.rb b/dragon/itch_wizard.rb index d87d07c..e976dc9 100644 --- a/dragon/itch_wizard.rb +++ b/dragon/itch_wizard.rb @@ -1,39 +1,16 @@ +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # itch_wizard.rb has been released under MIT (*only this file*). -class ItchWizard +class ItchWizard < Wizard def steps [ :check_metadata, - :deploy + :deploy, ] end - def metadata_file_path - "metadata/game_metadata.txt" - end - - def get_metadata - metadata = $gtk.read_file metadata_file_path - - if !metadata - write_blank_metadata - metadata = $gtk.read_file metadata_file_path - end - - dev_id, dev_title, game_id, game_title, version, icon = *metadata.each_line.to_a - - { - dev_id: dev_id.strip, - dev_title: dev_title.strip, - game_id: game_id.strip, - game_title: game_title.strip, - version: version.strip, - icon: icon.strip - } - end - def write_blank_metadata $gtk.write_file metadata_file_path, <<-S.strip #devid=myname @@ -51,7 +28,7 @@ S write_blank_metadata end - if metadata_text.each_line.to_a.length != 6 + if metadata_text.strip.each_line.to_a.length < 6 write_blank_metadata end @@ -63,70 +40,67 @@ S if metadata[:dev_id].start_with?("#") || !@dev_id log "* PROMPT: Please provide your username for Itch." - $console.set_command "$wizards.itch.set_dev_id \"your-itch-username\"" + $console.set_command "$wizards.itch.set_dev_id \"#{metadata[:dev_id]}\"" return :need_dev_id end if metadata[:dev_title].start_with?("#") || !@dev_title log "* PROMPT: Please provide developer's/company's name that you want displayed." - $console.set_command "$wizards.itch.set_dev_title \"Your Name\"" + $console.set_command "$wizards.itch.set_dev_title \"#{metadata[:dev_title]}\"" return :need_dev_title end if metadata[:game_id].start_with?("#") || !@game_id log "* PROMPT: Please provide the id for you game. This is the id you specified when you set up a new game page on Itch." - $console.set_command "$wizards.itch.set_game_id \"your-game-id\"" + $console.set_command "$wizards.itch.set_game_id \"#{metadata[:game_id]}\"" return :need_game_id end if metadata[:game_title].start_with?("#") || !@game_title log "* PROMPT: Please provide the display name for your game. (This can include spaces)" - $console.set_command "$wizards.itch.set_game_title \"Your Game\"" + $console.set_command "$wizards.itch.set_game_title \"#{metadata[:game_title]}\"" return :need_game_title end if metadata[:version].start_with?("#") || !@version log "* PROMPT: Please provide the version for your game." - $console.set_command "$wizards.itch.set_version \"1.0\"" + $console.set_command "$wizards.itch.set_version \"#{metadata[:version]}\"" return :need_version end if metadata[:icon].start_with?("#") || !@icon log "* PROMPT: Please provide icon path for your game." - $console.set_command "$wizards.itch.set_icon \"icon.png\"" + $console.set_command "$wizards.itch.set_icon \"#{metadata[:icon]}\"" return :need_icon end + puts "here!! success!!!" + return :success end def set_dev_id value @dev_id = value - write_metadata start end def set_dev_title value @dev_title = value - write_metadata start end def set_game_id value @game_id = value - write_metadata start end def set_game_title value @game_title = value - write_metadata start end def set_version value @version = value - write_metadata start end @@ -169,7 +143,7 @@ S end if @icon - text += "icon=metadata/#{@icon}\n" + text += "icon=#{@icon}\n" else text += "#icon=metadata/icon.png\n" end @@ -187,10 +161,25 @@ S def deploy log_info "* Running dragonruby-publish: #{package_command}" - results = $gtk.exec package_command + $gtk.openurl "http://itch.io/dashboard" if $gtk.platform == "Mac OS X" + if $gtk.platform? :mac + $gtk.exec "rm -rf ./builds" + end + results = $gtk.exec "#{package_command} --only-package" + puts File.expand_path("./builds") + log "#+begin_src" log results log "#+end_src" + + if $gtk.platform? :mac + $gtk.exec "open ./builds/" + elsif $gtk.platform? :windows + $gtk.exec "powershell \"ii .\"" + end + + $gtk.openurl "https://itch.io/dashboard" + :success end @@ -201,7 +190,7 @@ S steps.each do |m| begin log_info "Running Itch Wizard Step: ~$wizards.itch.#{m}~" - result = (send m) || :success if @wizard_status[m][:result] != :success + result = (send m) || :success @wizard_status[m][:result] = result if result != :success log_info "Exiting wizard. :#{result}" diff --git a/dragon/keys.rb b/dragon/keys.rb index c931aff..c49d98a 100644 --- a/dragon/keys.rb +++ b/dragon/keys.rb @@ -14,7 +14,7 @@ module GTK :l1, :r1, :l2, :r2, :l3, :r3, - :start, :select, + :start, :select, :home, :directional_up, :directional_down, :directional_left, :directional_right ].freeze @@ -22,6 +22,22 @@ module GTK attr label end + def back + @select + end + + def back= value + @select = value + end + + def guide + @home + end + + def guide= value + @home = value + end + # Activate a key. # # @return [void] diff --git a/dragon/layout.rb b/dragon/layout.rb index 1b0f8d0..18ca8c7 100644 --- a/dragon/layout.rb +++ b/dragon/layout.rb @@ -1,3 +1,4 @@ +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # layout.rb has been released under MIT (*only this file*). @@ -276,10 +277,18 @@ module GTK device.grid_area.row_count end + def row_max_index + row_count - 1 + end + def col_count device.grid_area.col_count end + def col_max_index + col_count - 1 + end + def gutter_height device.grid_area.gutter end @@ -302,18 +311,124 @@ module GTK def rect_defaults { - row: nil, - col: nil, - h: 1, - w: 1, - dx: 0, - dy: 0, - rect: :control_rect + row: nil, + col: nil, + h: 1, + w: 1, + dx: 0, + dx_ratio: 1, + dy: 0, + dy_ratio: 1, + dh_ratio: 1, + dw_ratio: 1, + merge: nil, + rect: :control_rect } end - def rect opts + def row n + (rect row: n, col: 0, w: 0, h: 0).x + end + + def row_from_bottom n + (rect row: row_count - n, col: 0, w: 0, h: 0).x + end + + def col n + (rect row: 0, col: n, w: 0, h: 0).y + end + + def col_from_right n + (rect row: 0, col: col_max_index - n, w: 0, h: 0).y + end + + def w n + (rect row: 0, col: 0, w: n, h: 1).w + end + + def h n + (rect row: 0, col: 0, w: 1, h: n).h + end + + def rect_group opts + group = opts.group + r = opts.row || 0 + r = row_max_index - opts.row_from_bottom if opts.row_from_bottom + c = opts.col || 0 + c = col_max_index - opts.col_from_right if opts.col_from_right + drow = opts.drow || 0 + dcol = opts.dcol || 0 + w = opts.w || 0 + h = opts.h || 0 + merge = opts[:merge] + + running_row = r + running_col = c + + running_col = calc_col_offset(opts.col_offset) if opts.col_offset + running_row = calc_row_offset(opts.row_offset) if opts.row_offset + + group.map do |i| + group_layout_opts = i.layout || {} + group_layout_opts = group_layout_opts.merge row: running_row, + col: running_col, + merge: merge, + w: w, h: h + result = (rect group_layout_opts).merge i + + if (i.is_a? Hash) && (i.primitive_marker == :label) + if i.alignment_enum == 1 + result.x += result.w.half + elsif i.alignment_enum == 2 + result.x += result.w + end + end + + running_row += drow + running_col += dcol + result + end + end + + def calc_row_offset opts = {} + count = opts[:count] || opts[:length] || 0 + h = opts.h || 1 + (row_count - (count * h)) / 2.0 + end + + def calc_col_offset opts = {} + count = opts[:count] || opts[:length] || 0 + w = opts.w || 1 + (col_count - (count * w)) / 2.0 + end + + def point opts = {} + opts.w = 1 + opts.h = 1 + opts.row ||= 0 + opts.col ||= 0 + r = rect opts + r.x += r.w * opts.col_anchor if opts.col_anchor + r.y += r.h * opts.row_anchor if opts.row_anchor + r + end + + def rect *all_opts + if all_opts.length == 1 + opts = all_opts.first + else + opts = {} + all_opts.each do |o| + opts.merge! o + end + end + + opts.row = row_max_index - opts.row_from_bottom if opts.row_from_bottom + opts.col = col_max_index - opts.col_from_right if opts.col_from_right opts = rect_defaults.merge opts + opts.row ||= 0 + opts.col ||= 0 + result = send opts[:rect] if opts[:row] && opts[:col] && opts[:w] && opts[:h] col = rect_col opts[:col], opts[:w] @@ -321,7 +436,9 @@ module GTK result = control_rect.merge x: col.x, y: row.y, w: col.w, - h: row.h + h: row.h, + center_x: col.center_x, + center_y: row.center_y elsif opts[:row] && !opts[:col] result = rect_row opts[:row], opts[:h] elsif !opts[:row] && opts[:col] @@ -359,13 +476,21 @@ module GTK result[:h] += device.grid_area.gutter * 2 end - result[:x] += opts[:dx] if opts[:dx] - result[:y] += opts[:dy] if opts[:dy] - result[:w] += opts[:dw] if opts[:dw] - result[:h] += opts[:dh] if opts[:dh] + result[:x] += opts[:dx] if opts[:dx] + result[:x] *= opts[:dx_ratio] if opts[:dx_ratio] + result[:y] += opts[:dy] if opts[:dy] + result[:y] *= opts[:dy_ratio] if opts[:dy_ratio] + result[:w] += opts[:dw] if opts[:dw] + result[:w] *= opts[:dw_ratio] if opts[:dw_ratio] + result[:h] += opts[:dh] if opts[:dh] + result[:h] *= opts[:dh_ratio] if opts[:dh_ratio] + result.merge! opts[:merge] if opts[:merge] result[:row] = opts[:row] result[:col] = opts[:col] + result[:h] = result[:h].clamp 0 + result[:w] = result[:w].clamp 0 + if $gtk.args.grid.name == :center result[:x] -= 640 result[:y] -= 360 @@ -400,7 +525,7 @@ module GTK row_y = device.h - row_y - row_h - result = control_rect.merge y: row_y, h: row_h + result = control_rect.merge y: row_y, h: row_h, center_y: (row_y + row_h.half) @rect_cache[:row][index][h] = result @rect_cache[:row][index][h] end @@ -423,7 +548,7 @@ module GTK col_w = col_w.to_i col_w -= 1 if col_w.odd? - result = control_rect.merge x: col_x, w: col_w + result = control_rect.merge x: col_x, w: col_w, center_x: (col_x + col_w.half) @rect_cache[:col][index][w] = result @rect_cache[:col][index][w] end @@ -474,6 +599,26 @@ module GTK @device end + def debug_primitives opts = {} + @primitives ||= col_count.map_with_index do |col| + row_count.map_with_index do |row| + cell = rect row: row, col: col + center = Geometry.rect_center_point cell + [ + cell.merge(opts).border, + cell.merge(opts) + .label!(x: center.x, + y: center.y, + text: "#{row},#{col}", + size_enum: -3, + vertical_alignment_enum: 1, + alignment_enum: 1) + ] + end + end + @primitives + end + def serialize { device: @device.serialize, @@ -487,5 +632,12 @@ module GTK def to_s serialize.to_s end + + def reset + @primitives = nil + @rect_cache ||= {} + @rect_cache.clear + end + end end diff --git a/dragon/log.rb b/dragon/log.rb index 7bf09fd..2e39217 100644 --- a/dragon/log.rb +++ b/dragon/log.rb @@ -129,6 +129,11 @@ module GTK self.puts message end + def self.reset + @once = {} + nil + end + def self.puts_once *ids, message id = "#{ids}" @once ||= {} diff --git a/dragon/mouse_docs.rb b/dragon/mouse_docs.rb index ef4b923..523d5ce 100644 --- a/dragon/mouse_docs.rb +++ b/dragon/mouse_docs.rb @@ -46,12 +46,12 @@ The ~GTK::MousePoint~ has the following properties. - ~x~: Integer representing the mouse's x. - ~y~: Integer representing the mouse's y. - ~point~: Array with the ~x~ and ~y~ values. -- ~w~: Width of the point that always returns ~0~ (included so that it can seemlessly work with ~GTK::Geometry~ functions). -- ~h~: Height of the point that always returns ~0~ (included so that it can seemlessly work with ~GTK::Geometry~ functions). -- ~left~: This value is the same as ~x~ (included so that it can seemlessly work with ~GTK::Geometry~ functions). -- ~right~: This value is the same as ~x~ (included so that it can seemlessly work with ~GTK::Geometry~ functions). -- ~top~: This value is the same as ~y~ (included so that it can seemlessly work with ~GTK::Geometry~ functions). -- ~bottom~: This value is the same as ~y~ (included so that it can seemlessly work with ~GTK::Geometry~ functions). +- ~w~: Width of the point that always returns ~0~ (included so that it can seamlessly work with ~GTK::Geometry~ functions). +- ~h~: Height of the point that always returns ~0~ (included so that it can seamlessly work with ~GTK::Geometry~ functions). +- ~left~: This value is the same as ~x~ (included so that it can seamlessly work with ~GTK::Geometry~ functions). +- ~right~: This value is the same as ~x~ (included so that it can seamlessly work with ~GTK::Geometry~ functions). +- ~top~: This value is the same as ~y~ (included so that it can seamlessly work with ~GTK::Geometry~ functions). +- ~bottom~: This value is the same as ~y~ (included so that it can seamlessly work with ~GTK::Geometry~ functions). - ~created_at~: The tick (~args.state.tick_count~) that this structure was created. - ~global_created_at~: The global tick (~Kernel.global_tick_count~) that this structure was created. diff --git a/dragon/numeric.rb b/dragon/numeric.rb index 93bacae..52ddf61 100644 --- a/dragon/numeric.rb +++ b/dragon/numeric.rb @@ -13,6 +13,35 @@ class Numeric alias_method :lte, :<= alias_method :__original_eq_eq__, :== unless Numeric.instance_methods.include? :__original_eq_eq__ + def to_layout_row opts = {} + $layout.rect(row: self, + col: opts.col || 0, + w: opts.w || 0, + h: opts.h || 0).y + end + + def to_layout_col opts = {} + $layout.rect(row: 0, + col: self, + w: opts.w || 0, + h: opts.h || 0).x + end + + def to_layout_w + $layout.rect(row: 0, col: 0, w: self, h: 1).w + end + + def to_layout_h + $layout.rect(row: 0, col: 0, w: 1, h: self).h + end + + def to_layout_row_from_bottom opts = {} + ($layout.row_max_index - self).to_layout_row opts + end + + def to_layout_col_from_right opts = {} + ($layout.col_max_index - self).to_layout_col opts + end # Converts a numeric value representing seconds into frames. # @@ -28,10 +57,26 @@ class Numeric self / 2.0 end + def third + self / 3.0 + end + + def quarter + self / 4.0 + end + def to_byte clamp(0, 255).to_i end + def clamp *opts + min = (opts.at 0) + max = (opts.at 1) + return min if min && self < min + return max if max && self > max + return self + end + def clamp_wrap min, max max, min = min, max if min > max return self if self >= min && self <= max @@ -256,7 +301,7 @@ S self * Math::PI.fdiv(180) end - # Converts a number representing an angle in radians to degress. + # Converts a number representing an angle in radians to degrees. # # @gtk def to_degrees @@ -273,21 +318,21 @@ S GTK::Geometry.to_square(self, x, y, anchor_x, anchor_y) end - # Returns a normal vector for a number that represents an angle in degress. + # Returns a normal vector for a number that represents an angle in degrees. # # @gtk def vector max_value = 1 [vector_x(max_value), vector_y(max_value)] end - # Returns the y component of a normal vector for a number that represents an angle in degress. + # Returns the y component of a normal vector for a number that represents an angle in degrees. # # @gtk def vector_y max_value = 1 max_value * Math.sin(self.to_radians) end - # Returns the x component of a normal vector for a number that represents an angle in degress. + # Returns the x component of a normal vector for a number that represents an angle in degrees. # # @gtk def vector_x max_value = 1 @@ -314,6 +359,18 @@ S (self % n) == 0 end + def multiply n + self * n + end + + def fmult n + self * n.to_f + end + + def imult n + (self * n).to_i + end + def mult n self * n end @@ -415,32 +472,6 @@ S (0..self).to_a end - def >= other - return false if !other - return gte other - end - - def > other - return false if !other - return gt other - end - - def <= other - return false if !other - return lte other - end - - def < other - return false if !other - return gt other - end - - def == other - return true if __original_eq_eq__ other - return __original_eq_eq__ other.entity_id if other.is_a? OpenEntity - return false - end - # @gtk def map unless block_given? @@ -526,34 +557,6 @@ The object above is not a Numeric. S end - def - other - return nil unless other - super - rescue Exception => e - __raise_arithmetic_exception__ other, :-, e - end - - def + other - return nil unless other - super - rescue Exception => e - __raise_arithmetic_exception__ other, :+, e - end - - def * other - return nil unless other - super - rescue Exception => e - __raise_arithmetic_exception__ other, :*, e - end - - def / other - return nil unless other - super - rescue Exception => e - __raise_arithmetic_exception__ other, :/, e - end - def serialize self end @@ -579,6 +582,10 @@ S def self.clamp n, min, max n.clamp min, max end + + def mid? l, r + (between? l, r) || (between? r, l) + end end class Fixnum @@ -604,40 +611,6 @@ class Fixnum return !even? end - def + other - return nil unless other - super - rescue Exception => e - __raise_arithmetic_exception__ other, :+, e - end - - def * other - return nil unless other - super - rescue Exception => e - __raise_arithmetic_exception__ other, :*, e - end - - def / other - return nil unless other - super - rescue Exception => e - __raise_arithmetic_exception__ other, :/, e - end - - def - other - return nil unless other - super - rescue Exception => e - __raise_arithmetic_exception__ other, :-, e - end - - def == other - return true if __original_eq_eq__ other - return __original_eq_eq__ other.entity_id if other.is_a? GTK::OpenEntity - return false - end - # Returns `-1` if the number is less than `0`. `+1` if the number # is greater than `0`. Returns `0` if the number is equal to `0`. # @@ -693,34 +666,6 @@ class Float alias_method :__original_multiply__, :* unless Float.instance_methods.include? :__original_multiply__ alias_method :__original_divide__, :- unless Float.instance_methods.include? :__original_divide__ - def - other - return nil unless other - super - rescue Exception => e - __raise_arithmetic_exception__ other, :-, e - end - - def + other - return nil unless other - super - rescue Exception => e - __raise_arithmetic_exception__ other, :+, e - end - - def * other - return nil unless other - super - rescue Exception => e - __raise_arithmetic_exception__ other, :*, e - end - - def / other - return nil unless other - super - rescue Exception => e - __raise_arithmetic_exception__ other, :/, e - end - def serialize self end @@ -762,4 +707,8 @@ class Integer def nan? false end + + def center other + (self - other).abs.fdiv(2) + end end diff --git a/dragon/outputs_docs.rb b/dragon/outputs_docs.rb index cd4a20c..db15989 100644 --- a/dragon/outputs_docs.rb +++ b/dragon/outputs_docs.rb @@ -80,7 +80,7 @@ wide and 90 pixels tall. ** Rendering a solid using an Array with colors and alpha -The value for the color and alpha is an number between ~0~ and ~255~. The +The value for the color and alpha is a number between ~0~ and ~255~. The alpha property is optional and will be set to ~255~ if not specified. Creates a green solid rectangle with an opacity of 50%. @@ -116,7 +116,7 @@ be provided in any order. ** Rendering a solid using a Class You can also create a class with solid/border properties and render it as a primitive. -ALL properties must on the class. *Additionally*, a method called ~primitive_marker~ +ALL properties must be on the class. *Additionally*, a method called ~primitive_marker~ must be defined on the class. Here is an example: @@ -150,7 +150,7 @@ Here is an example: S end - + def docs_sprites <<-S * DOCS: ~GTK::Outputs#sprites~ @@ -171,7 +171,7 @@ wide and 90 pixels tall. ** Rendering a sprite using an Array with colors and alpha -The value for the color and alpha is an number between ~0~ and ~255~. The +The value for the color and alpha is a number between ~0~ and ~255~. The alpha property is optional and will be set to ~255~ if not specified. Creates a green circle sprite with an opacity of 50%. @@ -209,7 +209,7 @@ be provided in any order. ** Rendering a solid using a Class You can also create a class with solid/border properties and render it as a primitive. -ALL properties must on the class. *Additionally*, a method called ~primitive_marker~ +ALL properties must be on the class. *Additionally*, a method called ~primitive_marker~ must be defined on the class. Here is an example: diff --git a/dragon/readme_docs.rb b/dragon/readme_docs.rb index 020f856..541c3ce 100644 --- a/dragon/readme_docs.rb +++ b/dragon/readme_docs.rb @@ -36,8 +36,6 @@ to get fancy you can provide a ~lambda~ to filter documentation: #+begin_src docs_search { |entry| (entry.include? "Array") && (!entry.include? "Enumerable") } #+end_src - -[[docs_search.gif]] S end @@ -67,20 +65,25 @@ Reply with: I am a Dragon Rider. #+end_quote -* Watch Some Intro Videos +* Intro Videos + +Here are some videos to help you get the lay of the land. -Each video is only 20 minutes and all of them will fit into a lunch -break. So please watch them: +** Quick Api Tour 1. Beginner Introduction to DragonRuby Game Toolkit: [[https://youtu.be/ixw7TJhU08E]] -2. Intermediate Introduction to Ruby Syntax: [[https://youtu.be/HG-XRZ5Ppgc]] -3. Intermediate Introduction to Arrays in Ruby: [[https://youtu.be/N72sEYFRqfo]] -The second and third videos are not required if you are proficient -with Ruby, but *definitely* watch the first one. +** If You Are Completely New to Ruby and Programming + +1. Intermediate Introduction to Ruby Syntax: [[https://youtu.be/HG-XRZ5Ppgc]] +2. Intermediate Introduction to Arrays in Ruby: [[https://youtu.be/N72sEYFRqfo]] +3. You may also want to try this free course provided at [[http://dragonruby.school]]. + +** If You Have Game Dev Experience -You may also want to try this free course provided at -[[http://dragonruby.school]]. +1. Building Tetris - Part 1: [[https://youtu.be/xZMwRSbC4rY]] +2. Building Tetris - Part 2: [[https://youtu.be/C3LLzDUDgz4]] +3. Low Res Game Jam Tutorial: [[https://youtu.be/pCI90ukKCME]] * Getting Started Tutorial @@ -430,7 +433,7 @@ Console type: ~$wizards.ios.start~ and you will be guided through the deployment process. To deploy to Android, you need to have an Android emulator/device, and -a environment that is able to run Android SDK. ~dragonruby-publish~ +an environment that is able to run Android SDK. ~dragonruby-publish~ will create an APK for you. From there, you can sign the APK and install it to your device. The signing and installation procedure varies from OS to OS. Here's an example of what the command might look @@ -453,7 +456,7 @@ The following tenants of DragonRuby are what set us apart from other game engines. Given that Game Toolkit is a relatively new engine, there are definitely features that are missing. So having a big check list of "all the cool things" is not this engine's forte. This is -compensated with a strong commitment to the following principals. +compensated with a strong commitment to the following principles. ** Challenge The Status Quo @@ -482,10 +485,10 @@ overwhelms beginners who are new to the engine or programming in general. DragonRuby's philosophy is to provide multiple options across the "make it fast" vs "make it right" spectrum, with incremental/intuitive transitions between the options provided. A concrete example of this philosophy -would be render primitives: the spectrum of options allows renderable constructs take -the form of tuples/arrays (easy to pickup, simple, and fast to code/prototype with), +would be render primitives: the spectrum of options allows renderable constructs that +take the form of tuples/arrays (easy to pickup, simple, and fast to code/prototype with), hashes (a little more work, but gives you the ability to add additional properties), -open and string entities (more work than hashes, but yields cleaner apis), +open and strict entities (more work than hashes, but yields cleaner apis), and finally - if you really need full power/flexibility in rendering - classes (which take the most amount of code and programming knowledge to create). @@ -656,11 +659,15 @@ You can represent a sprite as a ~Hash~: flip_vertically: false, flip_horizontally: false, angle_anchor_x: 0.5, - angle_anchor_y: 1.0 + angle_anchor_y: 1.0, + blendmode_enum: 1 } end #+end_src +The ~blendmode_enum~ value can be set to ~0~ (no blending), ~1~ (alpha blending), +~2~ (additive blending), ~3~ (modulo blending), ~4~ (multiply blending). + You can represent a sprite as an ~object~: #+begin_src ruby @@ -670,7 +677,7 @@ You can represent a sprite as an ~object~: :source_x, :source_y, :source_w, :source_h, :tile_x, :tile_y, :tile_w, :tile_h, :flip_horizontally, :flip_vertically, - :angle_anchor_x, :angle_anchor_y + :angle_anchor_x, :angle_anchor_y, :blendmode_enum def primitive_marker :sprite @@ -760,16 +767,17 @@ You can add additional metadata about your game within a label, which requires y #+begin_src def tick args args.outputs.labels << { - x: 200, - y: 550, - text: "dragonruby", - size_enum: 2, - alignment_enum: 1, - r: 155, - g: 50, - b: 50, - a: 255, - font: "fonts/manaspc.ttf", + x: 200, + y: 550, + text: "dragonruby", + size_enum: 2, + alignment_enum: 1, + r: 155, + g: 50, + b: 50, + a: 255, + font: "fonts/manaspc.ttf", + vertical_alignment_enum: 0, # 0 is bottom, 1 is middle, 2 is top # You can add any properties you like (this will be ignored/won't cause errors) game_data_one: "Something", game_data_two: { @@ -801,6 +809,21 @@ You can get the render size of any string using ~args.gtk.calcstringbox~. ] end #+end_src + +** Rendering Labels With New Line Characters And Wrapping + +You can use a strategy like the following to create multiple labels from a String. + +#+begin_src ruby + def tick args + long_string = "Lorem ipsum dolor sit amet, consectetur adipiscing elitteger dolor velit, ultricies vitae libero vel, aliquam imperdiet enim." + max_character_length = 30 + long_strings_split = args.string.wrapped_lines long_string, max_character_length + args.outputs.labels << long_strings_split.map_with_index do |s, i| + { x: 10, y: 600 - (i * 20), text: s } + end + end +#+end_src S end @@ -1031,7 +1054,7 @@ The elevator pitch is: DragonRuby is a Multilevel Cross-platform Runtime. The "multiple levels" within the runtime allows us to target platforms no other Ruby can target: PC, Mac, Linux, Raspberry Pi, WASM, iOS, Android, Nintendo -Switch, PS4, Xbox, and Scadia. +Switch, PS4, Xbox, and Stadia. **** What does Multilevel Cross-platform mean? @@ -1109,7 +1132,7 @@ file called ~repl.rb~ and put it in ~mygame/app/repl.rb~: - If you use the `repl` method, the code will be executed and the DragonRuby Console will automatically open so you can see the results (on Mac and Linux, the results will also be printed to the terminal). -- All ~puts~ statements will also be saved to ~logs/log.txt~. So if you want to stay in your editor and not look at the terminal, or the DragonRuby Console, you can ~tail~ this file. +- All ~puts~ statements will also be saved to ~logs/puts.txt~. So if you want to stay in your editor and not look at the terminal, or the DragonRuby Console, you can ~tail~ this file. 4. To ignore code in ~repl.rb~, instead of commenting it out, prefix ~repl~ with the letter ~x~ and it'll be ignored. @@ -1145,7 +1168,7 @@ challenges in creating something that is compatible. You can use DragonRuby's replay capabilities to troubleshoot: 1. DragonRuby is hot loaded which gives you a very fast feedback loop (if the game throws an exception, it's because of the code you just added). -2. Use ~./dragonruby mygame --record~ to create a game play recording that you can use to find the exception (you can replay a recoding by executing ~./dragonruby mygame --replay last_replay.txt~ or through the DragonRuby Console using ~$gtk.recording.start_replay "last_replay.txt"~. +2. Use ~./dragonruby mygame --record~ to create a game play recording that you can use to find the exception (you can replay a recording by executing ~./dragonruby mygame --replay last_replay.txt~ or through the DragonRuby Console using ~$gtk.recording.start_replay "last_replay.txt"~. 3. DragonRuby also ships with a unit testing facility. You can invoke the following command to run a test: ~./dragonruby . --eval some_ruby_file.rb --no-tick~. 4. Get into the habit of adding debugging facilities within the game itself. You can add drawing primitives to ~args.outputs.debug~ that will render on top of your game but will be ignored in a production release. 5. Debugging something that runs at 60fps is (imo) not that helpful. The exception you are seeing could have been because of a change that occurred many frames ago. @@ -1158,7 +1181,7 @@ Let's check the official source for the answer to this question: isrubydead.com: [[https://isrubydead.com/]]. On a more serious note, Ruby's _quantity_ levels aren't what they used -to be. And that's totally fine. Every one chases the new and shiny. +to be. And that's totally fine. Everyone chases the new and shiny. What really matters is _quality/maturity_. Here is the latest (StackOverflow Survey sorted by highest paid developers)[https://insights.stackoverflow.com/survey/2019#top-paying-technologies]. @@ -1232,13 +1255,11 @@ questions asked. *** But still, you should offer a free version. So I can try it out and see if I like it. -You can try our [web-based sandbox environment](). But it won't do the -runtime justice. Or just come to our [Slack]() or [Discord]() channel -and ask questions. We'd be happy to have a one on one video chat with -you and show off all the cool stuff we're doing. +You can try our web-based sandbox environment at [[http://fiddle.dragonruby.org]]. But it won't do the +runtime justice. Or just come to our Discord Channel at [[http://discord.dragonruby.org]] and ask questions. +We'd be happy to have a one on one video chat with you and show off all the cool stuff we're doing. -Seriously just buy it. Get a refund if you don't like it. We make it -stupid easy to do so. +Seriously just buy it. Get a refund if you don't like it. We make it stupid easy to do so. *** I still think you should do a free version. Think of all people who would give it a shot. diff --git a/dragon/recording.rb b/dragon/recording.rb new file mode 100644 index 0000000..72d8da9 --- /dev/null +++ b/dragon/recording.rb @@ -0,0 +1,260 @@ +# coding: utf-8 +# Copyright 2019 DragonRuby LLC +# MIT License +# recording.rb has been released under MIT (*only this file*). + +module GTK + # FIXME: Gross + # @gtk + class Replay + # @gtk + def self.start file_name = nil + $recording.start_replay file_name + end + + # @gtk + def self.stop + $recording.stop_replay + end + end + + # @gtk + class Recording + def initialize runtime + @runtime = runtime + @tick_count = 0 + @global_input_order = 1 + end + + def tick + @tick_count += 1 + end + + def start_recording seed_number = nil + if !seed_number + log <<-S +* ERROR: +To start recording, you must provide an integer value to +seed random number generation. +S + $console.set_command "$recording.start SEED_NUMBER" + return + end + + if @is_recording + log <<-S +* ERROR: +You are already recording, first cancel (or stop) the current recording. +S + $console.set_command "$recording.cancel" + return + end + + if @is_replaying + log <<-S +* ERROR: +You are currently replaying a recording, first stop the replay. +S + return + end + + log_info <<-S +Recording has begun with RNG seed value set to #{seed_number}. +To stop recording use stop_recording(filename). +The recording will stop without saving a file if a filename is nil. +S + + $console.set_command "$recording.stop 'replay.txt'" + @runtime.__reset__ + @seed_number = seed_number + @runtime.set_rng seed_number + + @tick_count = 0 + @global_input_order = 1 + @is_recording = true + @input_history = [] + @runtime.notify! "Recording started. When completed, open the console to save it using $recording.stop FILE_NAME (or cancel).", 300 + end + + # @gtk + def start seed_number = nil + start_recording seed_number + end + + def is_replaying? + @is_replaying + end + + def is_recording? + @is_recording + end + + # @gtk + def stop file_name = nil + stop_recording file_name + end + + # @gtk + def cancel + stop_recording_core + @runtime.notify! "Recording cancelled." + end + + def stop_recording file_name = nil + if !file_name + log <<-S +* ERROR: +To please specify a file name when calling: +$recording.stop FILE_NAME + +If you do NOT want to save the recording, call: +$recording.cancel +S + $console.set_command "$recording.stop 'replay.txt'" + return + end + + if !@is_recording + log_info "You are not currently recording. Use start_recording(seed_number) to start recording." + $console.set_command "$recording.start" + return + end + + if file_name + text = "replay_version 2.0\n" + text << "stopped_at #{@tick_count}\n" + text << "seed #{@seed_number}\n" + text << "recorded_at #{Time.now.to_s}\n" + @input_history.each do |items| + text << "#{items}\n" + end + @runtime.write_file file_name, text + @runtime.write_file 'last_replay.txt', text + log_info "The recording has been saved successfully at #{file_name}. You can use start_replay(\"#{file_name}\") to replay the recording." + end + + $console.set_command "$replay.start '#{file_name}'" + stop_recording_core + @runtime.notify! "Recording saved to #{file_name}. To replay it: $replay.start \"#{file_name}\"." + log_info "You can run the replay later on startup using: ./dragonruby mygame --replay #{@replay_file_name}" + nil + end + + def stop_recording_core + @is_recording = false + @input_history = nil + @last_history = nil + @runtime.__reset__ + end + + def start_replay file_name = nil + if !file_name + log <<-S +* ERROR: +Please provide a file name to $recording.start. +S + $console.set_command "$replay.start 'replay.txt'" + return + end + + text = @runtime.read_file file_name + return false unless text + + if text.each_line.first.strip != "replay_version 2.0" + raise "The replay file #{file_name} is not compatible with this version of DragonRuby Game Toolkit. Please recreate the replay (sorry)." + end + + @replay_file_name = file_name + + $replay_data = { input_history: { } } + text.each_line do |l| + if l.strip.length == 0 + next + elsif l.start_with? 'replay_version' + next + elsif l.start_with? 'seed' + $replay_data[:seed] = l.split(' ').last.to_i + elsif l.start_with? 'stopped_at' + $replay_data[:stopped_at] = l.split(' ').last.to_i + elsif l.start_with? 'recorded_at' + $replay_data[:recorded_at] = l.split(' ')[1..-1].join(' ') + elsif l.start_with? '[' + name, value_1, value_2, value_count, id, tick_count = l.strip.gsub('[', '').gsub(']', '').split(',') + $replay_data[:input_history][tick_count.to_i] ||= [] + $replay_data[:input_history][tick_count.to_i] << { + id: id.to_i, + name: name.gsub(':', '').to_sym, + value_1: value_1.to_f, + value_2: value_2.to_f, + value_count: value_count.to_i + } + else + raise "Replay data seems corrupt. I don't know how to parse #{l}." + end + end + + $replay_data[:input_history].keys.each do |key| + $replay_data[:input_history][key] = $replay_data[:input_history][key].sort_by {|input| input[:id]} + end + + @runtime.__reset__ + @runtime.set_rng $replay_data[:seed] + @tick_count = 0 + @is_replaying = true + log_info "Replay has been started." + @runtime.notify! "Replay started [#{@replay_file_name}]." + end + + def stop_replay notification_message = "Replay has been stopped." + if !is_replaying? + log <<-S +* ERROR: +No replay is currently running. Call $replay.start FILE_NAME to start a replay. +S + + $console.set_command "$replay.start 'replay.txt'" + return + end + log_info notification_message + @is_replaying = false + $replay_data = nil + @tick_count = 0 + @global_input_order = 1 + $console.set_command_silent "$replay.start '#{@replay_file_name}'" + @runtime.__reset__ + @runtime.notify! notification_message + end + + def record_input_history name, value_1, value_2, value_count, clear_cache = false + return if @is_replaying + return unless @is_recording + @input_history << [name, value_1, value_2, value_count, @global_input_order, @tick_count] + @global_input_order += 1 + end + + def stage_replay_values + return unless @is_replaying + return unless $replay_data + + if $replay_data[:stopped_at] <= @tick_count + stop_replay "Replay completed [#{@replay_file_name}]. To rerun, bring up the Console and press enter." + return + end + + inputs_this_tick = $replay_data[:input_history][@tick_count] + + if @tick_count.zmod? 60 + log_info "Replay ends in #{($replay_data[:stopped_at] - @tick_count).idiv 60} second(s)." + end + + return unless inputs_this_tick + inputs_this_tick.each do |v| + args = [] + args << v[:value_1] if v[:value_count] >= 1 + args << v[:value_2] if v[:value_count] >= 2 + args << :replay + $gtk.send v[:name], *args + end + end + end +end diff --git a/dragon/remote_hotload_client.rb b/dragon/remote_hotload_client.rb index 87693cd..ce8f175 100644 --- a/dragon/remote_hotload_client.rb +++ b/dragon/remote_hotload_client.rb @@ -47,7 +47,7 @@ module GTK def local_state @local_state ||= OpenEntity.new @local_state.hotload_client ||= @local_state.new_entity(:hotload_client, - notes: "This enitity is used by DragonRuby Game Toolkit to provide you hotloading on remote machines.", + notes: "This entity is used by DragonRuby Game Toolkit to provide you hotloading on remote machines.", changes: { }, changes_queue: [], reloaded_files_times: []) diff --git a/dragon/runtime_docs.rb b/dragon/runtime_docs.rb index 90addff..1f4a509 100644 --- a/dragon/runtime_docs.rb +++ b/dragon/runtime_docs.rb @@ -12,7 +12,8 @@ module RuntimeDocs :docs_api_summary, :docs_reset, :docs_calcstringbox, - :docs_write_file + :docs_write_file, + :docs_benchmark ] end @@ -119,18 +120,18 @@ The properties ~args.inputs.mouse.(click|down|previous_click|up)~ each return ~n that has an ~x~, ~y~ properties along with helper functions to determine collision: ~inside_rect?~, ~inside_circle~. *** ~.controller_one~, ~.controller_two~ Represents controllers connected to the usb ports. -**** ~.up +**** ~.up~ Returns ~true~ if ~up~ is pressed or held on the directional or left analog. -**** ~.down +**** ~.down~ Returns ~true~ if ~down~ is pressed or held on the directional or left analog. -**** ~.left +**** ~.left~ Returns ~true~ if ~left~ is pressed or held on the directional or left analog. -**** ~.right +**** ~.right~ Returns ~true~ if ~right~ is pressed or held on the directional or left analog. -**** ~.left_right +**** ~.left_right~ Returns ~-1~ (left), ~0~ (neutral), or ~+1~ (right) depending on results of ~args.inputs.controller_(one|two).left~ and ~args.inputs.controller_(one|two).right~. -**** ~.up_down -Returns ~-1~ (down), ~0~ (neutral), or ~+1~ (up) depending on results of ~args.inputs.controller_(one|two).down~ and ~args.inputs.controller_(one|two).up~. +**** ~.up_down~ +Returns ~-1~ (down), ~0~ (neutral), or ~+1~ (up) depending on results of ~args.inputs.controller_(one|two).up~ and ~args.inputs.controller_(one|two).down~. **** ~.(left_analog_x_raw|right_analog_x_raw)~ Returns the raw integer value for the analog's horizontal movement (~-32,000 to +32,000~). **** ~.left_analog_y_raw|right_analog_y_raw)~ @@ -139,13 +140,13 @@ Returns the raw integer value for the analog's vertical movement (~-32,000 to +3 Returns a number between ~-1~ and ~1~ which represents the percentage the analog is moved horizontally as a ratio of the maximum horizontal movement. **** ~.left_analog_y_perc|right_analog_y_perc)~ Returns a number between ~-1~ and ~1~ which represents the percentage the analog is moved vertically as a ratio of the maximum vertical movement. -**** ~.directional_up)~ +**** ~.directional_up~ Returns ~true~ if ~up~ is pressed or held on the directional. -**** ~.directional_down)~ +**** ~.directional_down~ Returns ~true~ if ~down~ is pressed or held on the directional. -**** ~.directional_left)~ +**** ~.directional_left~ Returns ~true~ if ~left~ is pressed or held on the directional. -**** ~.directional_right)~ +**** ~.directional_right~ Returns ~true~ if ~right~ is pressed or held on the directional. **** ~.(a|b|x|y|l1|r1|l2|r2|l3|r3|start|select)~ Returns ~true~ if the specific button is pressed or held. @@ -170,7 +171,7 @@ Returns ~true~ if ~right~ or ~d~ is pressed or held on the keyboard. **** ~.left_right~ Returns ~-1~ (left), ~0~ (neutral), or ~+1~ (right) depending on results of ~args.inputs.keyboard.left~ and ~args.inputs.keyboard.right~. **** ~.up_down~ -Returns ~-1~ (down), ~0~ (neutral), or ~+1~ (up) depending on results of ~args.inputs.keyboard.down~ and ~args.inputs.keyboard.up~. +Returns ~-1~ (left), ~0~ (neutral), or ~+1~ (right) depending on results of ~args.inputs.keyboard.up~ and ~args.inputs.keyboard.up~. **** keyboard properties The following properties represent keys on the keyboard and are available on ~args.inputs.keyboard.KEY~, ~args.inputs.keyboard.key_down.KEY~, ~args.inputs.keyboard.key_held.KEY~, and ~args.inputs.keyboard.key_up.KEY~: - ~alt~ @@ -301,6 +302,14 @@ Send a Primitive to this collection to render an unfilled rectangle to the scree Send any Primitive to this collection which represents things you render to the screen for debugging purposes. Primitives in this collection will not be rendered in a production release of your game. ** ~args.geometry~ This property contains geometric functions. Functions can be invoked via ~args.geometry.FUNCTION~. + +Here are some general notes with regards to the arguments these geometric functions accept. + +1. ~Rectangles~ can be represented as an ~Array~ with four (or more) values ~[x, y, w, h]~, as a ~Hash~ ~{ x:, y:, w:, h: }~ or an object that responds to ~x~, ~y~, ~w~, and ~h~. +2. ~Points~ can be represent as an ~Array~ with two (or more) values ~[x, y]~, as a ~Hash~ ~{ x:, y:}~ or an object that responds to ~x~, and ~y~. +3. ~Lines~ can be represented as an ~Array~ with four (or more) values ~[x, y, x2, y2]~, as a ~Hash~ ~{ x:, y:, x2:, y2: }~ or an object that responds to ~x~, ~y~, ~x2~, and ~y2~. +4. ~Angles~ are represented as degrees (not radians). + *** ~.inside_rect? rect_1, rect_2~ Returns ~true~ if ~rect_1~ is inside ~rect_2~. *** ~.intersect_rect? rect_2, rect_2~ @@ -477,6 +486,8 @@ Invalids the texture cache of a sprite. Shows the mouse cursor. *** ~.hide_cursor~ Hides the mouse cursor. +*** ~.set_cursor path, dx, dy~ +Sets the system cursor to a sprite ~path~ with an offset of ~dx~ and ~dy~. *** ~.cursor_shown?~ Returns ~true~ if the mouse cursor is shown. *** ~.set_window_fullscreen enabled~ @@ -530,6 +541,39 @@ is currently in the file. Use ~GTK::Runtime#append_file~ to append to the file a #+end_src S end + + def docs_benchmark +<<-S +* DOCS: ~GTK::Runtime#benchmark~ +You can use this function to compare the relative performance of methods. + +#+begin_src ruby + def tick args + # press r to run benchmark + if args.inputs.keyboard.key_down.r + args.gtk.console.show + args.gtk.benchmark iterations: 1000, # number of iterations + # label for experiment + using_numeric_map: -> () { + # experiment body + v = 100.map do |i| + i * 100 + end + }, + # label for experiment + using_numeric_times: -> () { + # experiment body + v = [] + 100.times do |i| + v << i * 100 + end + } + end + end +#+end_src +S + end + end class GTK::Runtime diff --git a/dragon/string.rb b/dragon/string.rb index 62c151a..153ba27 100644 --- a/dragon/string.rb +++ b/dragon/string.rb @@ -29,6 +29,30 @@ S end end + def char_byte + return nil if self.length == 0 + c = self.each_char.first.bytes + c = c.first if c.is_a? Enumerable + c + end + + def insert_character_at index, char + t = each_char.to_a + t = (t.insert index, char) + t.join + end + + def excluding_character_at index + t = each_char.to_a + t.delete_at index + t.join + end + + def excluding_last_character + return "" if self.length <= 1 + return excluding_character_at(self.length - 1) + end + def end_with_bang? self[-1] == "!" end diff --git a/dragon/tests.rb b/dragon/tests.rb index 7cbba09..ad1a780 100644 --- a/dragon/tests.rb +++ b/dragon/tests.rb @@ -132,7 +132,7 @@ S log "#{self.failed.length} test(s) failed." self.failed.each do |h| log "**** Test name: :#{h[:m]}" - log "#{h[:e].to_s.gsub("* ERROR:", "").strip}" + log "#{h[:e].to_s.gsub("* ERROR:", "").strip}\n#{h[:e].__backtrace_to_org__}" end end end diff --git a/dragon/tweetcart.rb b/dragon/tweetcart.rb new file mode 100644 index 0000000..67e950e --- /dev/null +++ b/dragon/tweetcart.rb @@ -0,0 +1,102 @@ +# coding: utf-8 +# Copyright 2019 DragonRuby LLC +# MIT License +# tweetcart.rb has been released under MIT (*only this file*). + +def $top_level.TICK &block + $top_level.define_method(:tick) do |args| + args.outputs[:scene].w = 160 + args.outputs[:scene].h = 90 + args.outputs[:scene].background_color = [0, 0, 0, 0] + block.call args + args.outputs.sprites << { x: 0, y: 0, w: 1280, h: 720, path: :scene } + end + + def $top_level.bg! *rgb + r,g,b = rgb + r ||= 255 + g ||= r + b ||= g + $args.outputs.background_color = [r, g, b] + end + + def $top_level.slds + $args.outputs[:scene].sprites + end + + def $top_level.slds! *os + if (os.first.is_a? Numeric) + sld!(*os) + else + os.each { |o| sld!(*o) } + end + end + + def $top_level.sld! *params + x, y, w, h, r, g, b, a = nil + if params.length == 2 + x, y = params + elsif params.length == 3 && (params.last.is_a? Array) + x = params[0] + y = params[1] + r, g, b, a = params[2] + r ||= 255 + g ||= r + b ||= g + a ||= 255 + elsif params.length == 4 + x, y, w, h = params + elsif params.length == 5 && (params.last.is_a? Array) + x = params[0] + y = params[1] + w = params[2] + h = params[3] + r,g,b,a = params[4] + r ||= 255 + g ||= r + b ||= g + a ||= 255 + elsif params.length >= 7 + x, y, w, h, r, g, b = params + else + raise "I don't know how to render #{params} with reasonable defaults." + end + + w ||= 1 + h ||= 1 + r ||= 255 + g ||= 255 + b ||= 255 + a ||= 255 + + slds << { x: x, y: y, + w: w, h: h, + r: r, g: g, b: b, a: a, + path: :pixel } + end +end + +=begin +wht = [255] * 3 +red = [255, 0, 0] +blu = [0, 130, 255] +purp = [150, 80, 255] + +TICK { + bg! 0 + + slds << [0, 0, 3, 3, 0, 255, 0, 255] + + sld! 10, 10 + sld! 20, 20, 3, 2 + sld! 30, 30, 2, 2, red + sld! 35, 35, blu + + slds! 40, 40 + + slds! [50, 50], + [60, 60, purp], + [70, 70, 10, 10, wht], + [80, 80, 4, 4, 255, 0, 255] +} +=end diff --git a/dragon/wizards.rb b/dragon/wizards.rb index 3501b80..b0c7ca0 100644 --- a/dragon/wizards.rb +++ b/dragon/wizards.rb @@ -1,7 +1,42 @@ +# coding: utf-8 # Copyright 2019 DragonRuby LLC # MIT License # wizards.rb has been released under MIT (*only this file*). +class Wizard + def metadata_file_path + "metadata/game_metadata.txt" + end + + def get_metadata + metadata = $gtk.read_file metadata_file_path + + if !metadata + write_blank_metadata + metadata = $gtk.read_file metadata_file_path + end + + dev_id, dev_title, game_id, game_title, version, icon = *metadata.each_line.to_a + + { + dev_id: dev_id.strip.gsub("#", "").gsub("devid=", ""), + dev_title: dev_title.strip.gsub("#", "").gsub("devtitle=", ""), + game_id: game_id.strip.gsub("#", "").gsub("gameid=", ""), + game_title: game_title.strip.gsub("#", "").gsub("gametitle=", ""), + version: version.strip.gsub("#", "").gsub("version=", ""), + icon: icon.strip.gsub("#", "").gsub("icon=", "") + } + end +end + +class WizardException < Exception + attr_accessor :console_primitives + + def initialize *console_primitives + @console_primitives = console_primitives + end +end + module GTK class Wizards attr_accessor :ios, :itch |
