rgame 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +135 -1
- data/README.md +67 -65
- data/docs/api/README.md +86 -82
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -165
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1032 -265
- data/docs/api/drawing.md +160 -160
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +149 -50
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +226 -148
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +397 -244
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +186 -73
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +395 -146
- data/docs/api/ui.md +883 -54
- data/docs/api/values.md +216 -33
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/ext/rgame_core/app/app.c +33 -3
- data/ext/rgame_core/app/locale.c +67 -0
- data/ext/rgame_core/app/locale.h +28 -0
- data/ext/rgame_core/audio/audio.c +39 -2
- data/ext/rgame_core/example.rb +0 -49
- data/ext/rgame_core/extconf.rb +0 -125
- data/ext/rgame_core/include/rgame/core.h +38 -2
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +30 -7
- data/ext/rgame_core/ruby/core_ext.h +3 -0
- data/ext/rgame_core/ruby/locale_ext.c +44 -0
- data/ext/rgame_core/ruby/recording_ext.c +1 -1
- data/ext/rgame_core/ruby/renderer_ext.c +20 -20
- data/ext/rgame_util/extconf.rb +2 -20
- data/ext/rgame_util/route_search.c +305 -0
- data/ext/rgame_util/route_search.h +86 -0
- data/ext/rgame_util/route_search_ext.c +150 -0
- data/ext/rgame_util/solid_grid.c +58 -0
- data/ext/rgame_util/solid_grid.h +49 -0
- data/ext/rgame_util/solid_grid_ext.c +161 -0
- data/ext/rgame_util/tile_sweep.c +164 -0
- data/ext/rgame_util/tile_sweep.h +62 -0
- data/ext/rgame_util/tile_sweep_ext.c +155 -0
- data/ext/rgame_util/util_ext.c +3 -0
- data/ext/rgame_util/util_ext.h +15 -0
- data/lib/rgame/boot.rb +0 -10
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +6 -44
- data/lib/rgame/core/asset_manager.rb +13 -31
- data/lib/rgame/core/audio.rb +37 -16
- data/lib/rgame/core/font.rb +0 -3
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/renderer.rb +6 -63
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +2 -19
- data/lib/rgame/core/ui_atlas.rb +28 -13
- data/lib/rgame/core.rb +1 -8
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +1 -0
- data/lib/rgame/engine/audio_director.rb +36 -6
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +3 -3
- data/lib/rgame/engine/circle_collider.rb +4 -2
- data/lib/rgame/engine/collision_box.rb +26 -1
- data/lib/rgame/engine/collision_system.rb +110 -22
- data/lib/rgame/engine/component.rb +34 -10
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +27 -25
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +6 -5
- data/lib/rgame/engine/components/character_body.rb +20 -61
- data/lib/rgame/engine/components/circle_collider.rb +47 -11
- data/lib/rgame/engine/components/collision_world.rb +159 -31
- data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +123 -31
- data/lib/rgame/engine/components/player_controller.rb +5 -2
- data/lib/rgame/engine/components/pool.rb +1 -1
- data/lib/rgame/engine/components/screen_wrap.rb +33 -11
- data/lib/rgame/engine/components/sprite.rb +12 -6
- data/lib/rgame/engine/components/targeting.rb +9 -11
- data/lib/rgame/engine/components/thrust_controller.rb +1 -1
- data/lib/rgame/engine/components/tile_world.rb +41 -13
- data/lib/rgame/engine/components/timer.rb +1 -1
- data/lib/rgame/engine/components/velocity.rb +23 -7
- data/lib/rgame/engine/components/wander_controller.rb +6 -2
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +4 -6
- data/lib/rgame/engine/debug_overlay.rb +5 -11
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +276 -51
- data/lib/rgame/engine/input/action_mapper.rb +0 -26
- data/lib/rgame/engine/input/actions.rb +2 -8
- data/lib/rgame/engine/input/input_map.rb +39 -21
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +248 -133
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/players.rb +6 -13
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +4 -4
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +51 -19
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +2 -3
- data/lib/rgame/engine/tile_map_layer.rb +0 -2
- data/lib/rgame/engine/tileset.rb +2 -4
- data/lib/rgame/engine/timer.rb +2 -2
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +246 -71
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/viewports.rb +2 -5
- data/lib/rgame/engine/world_view.rb +5 -4
- data/lib/rgame/engine.rb +31 -25
- data/lib/rgame/game.rb +99 -27
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +20 -24
- data/lib/rgame/util/controls.rb +14 -28
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +0 -9
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +4 -14
- data/lib/rgame/util.rb +4 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +152 -11
- data/lib/rgame/engine/actor.rb +0 -53
- data/lib/rgame/engine/body.rb +0 -49
- data/lib/rgame/engine/cached_label.rb +0 -33
- data/lib/rgame/engine/input/player_controller.rb +0 -14
- data/lib/rgame/engine/matrix.rb +0 -32
- data/lib/rgame/engine/resettable.rb +0 -67
- data/lib/rgame/engine/tile_collision.rb +0 -78
- data/lib/rgame/engine/ui/menu_item.rb +0 -84
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'layer_boundary'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Game
|
|
8
|
+
# `RGame::Core` must never name `RGame::Engine`. The mirror of
|
|
9
|
+
# `NoCoreInEngineLayer`, and the more easily broken of the two.
|
|
10
|
+
#
|
|
11
|
+
# Engine is built *on top of* Core, so Core should not know it exists.
|
|
12
|
+
# Core owns windows, textures and sound devices; Engine owns scene
|
|
13
|
+
# concepts. A Core class that reaches upward makes the lower layer
|
|
14
|
+
# unusable without the higher one, and inverts a dependency that the whole
|
|
15
|
+
# three-layer split exists to keep pointing one way.
|
|
16
|
+
#
|
|
17
|
+
# Where Core genuinely needs something Engine has — a tile map's grid, say
|
|
18
|
+
# — it takes the object and calls it by method name, exactly as the engine
|
|
19
|
+
# layer does with a renderer. The one place allowed to name both sides is
|
|
20
|
+
# the glue class directly under `RGame`, which is what a glue class is
|
|
21
|
+
# for.
|
|
22
|
+
#
|
|
23
|
+
# Unlike its mirror, nothing catches this at runtime: loading Engine into
|
|
24
|
+
# a Core spec would work fine and the inversion would go unnoticed until
|
|
25
|
+
# someone tried to use Core on its own. This cop is the only guard.
|
|
26
|
+
#
|
|
27
|
+
# Both spellings are flagged — `RGame::Engine` and the bare `Engine` the
|
|
28
|
+
# layer still has before it is ported — because the interim is exactly
|
|
29
|
+
# when the mistake gets made.
|
|
30
|
+
#
|
|
31
|
+
# @example
|
|
32
|
+
# # bad — Core parsing a file format Engine owns
|
|
33
|
+
# def self.load(app, path)
|
|
34
|
+
# map = Engine::TileMap.parse(File.read(path))
|
|
35
|
+
# new(app, map)
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# # bad — a require, in either spelling
|
|
39
|
+
# require 'rgame/engine'
|
|
40
|
+
#
|
|
41
|
+
# # good — hand Core the parsed thing and call it by name
|
|
42
|
+
# def initialize(map, tiles)
|
|
43
|
+
# @columns = map.width
|
|
44
|
+
# end
|
|
45
|
+
class NoEngineInCoreLayer < RuboCop::Cop::Base
|
|
46
|
+
include LayerBoundary
|
|
47
|
+
|
|
48
|
+
MSG = 'RGame::Core must not name `%{name}`; take the object and call it ' \
|
|
49
|
+
'by method name, and let the glue layer wire the two together.'
|
|
50
|
+
MSG_REQUIRE = 'RGame::Core must not require `%{path}` — Engine is built on ' \
|
|
51
|
+
'top of Core, not the other way round.'
|
|
52
|
+
|
|
53
|
+
PREFIXES = [%w[RGame Engine], %w[Engine]].freeze
|
|
54
|
+
RESTRICTED_REQUIRE = %r{(\A|/)engine(/|\z)}
|
|
55
|
+
|
|
56
|
+
# @!method engine_require(node)
|
|
57
|
+
def_node_matcher :engine_require, <<~PATTERN
|
|
58
|
+
(send nil? {:require :require_relative} (str $_))
|
|
59
|
+
PATTERN
|
|
60
|
+
|
|
61
|
+
def on_send(node)
|
|
62
|
+
engine_require(node) do |path|
|
|
63
|
+
next unless RESTRICTED_REQUIRE.match?(path.to_s.delete_prefix('./'))
|
|
64
|
+
|
|
65
|
+
add_offense(node, message: format(MSG_REQUIRE, path: path))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def on_const(node)
|
|
70
|
+
return unless opens_namespace?(node, PREFIXES)
|
|
71
|
+
|
|
72
|
+
add_offense(node, message: format(MSG, name: matched_prefix(node).join('::')))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def matched_prefix(node)
|
|
78
|
+
path = const_path(node)
|
|
79
|
+
PREFIXES.find { |prefix| path.first(prefix.length) == prefix }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'hot_path'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Game
|
|
8
|
+
# Disallow string interpolation inside per-frame methods — the engine's lifecycle
|
|
9
|
+
# hooks (`update`/`control`/`draw`/`on_update`/`on_draw`/`on_control`) and any method
|
|
10
|
+
# tagged `# hot-path`.
|
|
11
|
+
#
|
|
12
|
+
# These run every frame (~60/s), and string interpolation builds a brand-new
|
|
13
|
+
# `String` each time it is evaluated (the `frozen_string_literal` pragma freezes
|
|
14
|
+
# *literals*, not interpolated results). So a `"Score: #{n}"` here is a per-frame
|
|
15
|
+
# allocation. Build the string once — on construction, or when the value changes
|
|
16
|
+
# (see `Engine::Text`) — and draw the cached copy.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# # bad
|
|
20
|
+
# def draw(renderer)
|
|
21
|
+
# renderer.text("Score: #{@score}", 10, 10)
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# # good (rebuilt only when @score changes)
|
|
25
|
+
# def draw(renderer)
|
|
26
|
+
# renderer.text(@score_label, 10, 10)
|
|
27
|
+
# end
|
|
28
|
+
class NoInterpolationInHotPath < RuboCop::Cop::Base
|
|
29
|
+
include HotPath
|
|
30
|
+
|
|
31
|
+
MSG = 'Avoid string interpolation in a per-frame method: it allocates a String ' \
|
|
32
|
+
'every frame. Build the string once (on change) and use the cached value.'
|
|
33
|
+
|
|
34
|
+
def on_def(node)
|
|
35
|
+
return unless hot_path_def?(node)
|
|
36
|
+
|
|
37
|
+
node.each_descendant(:dstr) do |dstr|
|
|
38
|
+
add_offense(dstr) if interpolated?(dstr)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def interpolated?(dstr)
|
|
45
|
+
dstr.children.any?(&:begin_type?)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Game
|
|
6
|
+
# Flag a String literal drawn or measured as a label: the first argument of
|
|
7
|
+
# `text` or `text_width`, on any receiver.
|
|
8
|
+
#
|
|
9
|
+
# Text a player reads belongs in a translation table, where `RGame::Game`
|
|
10
|
+
# loads it and a language switch reaches it. A literal at the call site is
|
|
11
|
+
# the one place no table can: it shows the same words in every language, and
|
|
12
|
+
# it is the form a reader copies out of an example. Build an `Engine::Text`
|
|
13
|
+
# from a key once, off the per-frame path, and pass it as it is.
|
|
14
|
+
#
|
|
15
|
+
# This sees the call site only. A constant or ivar holding a String passes,
|
|
16
|
+
# so a table of Strings chosen by state is not caught; whether a constant
|
|
17
|
+
# holds words is not a question syntax can answer.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# # bad
|
|
21
|
+
# renderer.text('Press Space to jump', 12, 12)
|
|
22
|
+
# renderer.text("Lives: #{lives}", 12, 34)
|
|
23
|
+
#
|
|
24
|
+
# # good
|
|
25
|
+
# @help = Engine::Text.new('help.jump') # in initialize
|
|
26
|
+
# renderer.text(@help, 12, 12)
|
|
27
|
+
class NoLiteralText < RuboCop::Cop::Base
|
|
28
|
+
MSG = 'Draw an Engine::Text built from a key, not a String literal: ' \
|
|
29
|
+
'text a player reads belongs in a translation table.'
|
|
30
|
+
|
|
31
|
+
RESTRICT_ON_SEND = %i[text text_width].freeze
|
|
32
|
+
|
|
33
|
+
def on_send(node)
|
|
34
|
+
label = node.first_argument
|
|
35
|
+
add_offense(label) if label&.type?(:str, :dstr)
|
|
36
|
+
end
|
|
37
|
+
alias on_csend on_send
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'hot_path'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Game
|
|
8
|
+
# Flag fresh Array / Range literals that are needless per-frame allocations.
|
|
9
|
+
#
|
|
10
|
+
# Two triggers:
|
|
11
|
+
# * Anywhere — a literal used as a method-call *receiver*: `[a, b].sum`,
|
|
12
|
+
# `(a..b).any? { ... }`. Each call allocates the collection just to reduce or
|
|
13
|
+
# iterate it; compare directly or loop with an index instead.
|
|
14
|
+
# * Inside a per-frame method (a lifecycle hook or a `# hot-path`-tagged helper) —
|
|
15
|
+
# *any* Array/Range literal, e.g. returning `[x, y, w, h]` for the caller to
|
|
16
|
+
# decompose. Expose the parts separately (see `Engine::AnimationSet` row/col/flip_x).
|
|
17
|
+
#
|
|
18
|
+
# Allowed: an empty `[]` (idiomatic mutable-state seed), a frozen `[...].freeze`
|
|
19
|
+
# (allocated once), a parallel-assignment RHS (`a, b = c, d`, which the VM does
|
|
20
|
+
# not allocate), and the four sends the VM rewrites to `opt_newarray_send`.
|
|
21
|
+
#
|
|
22
|
+
# That last one is not a concession, it is a correction. `[a, b].min` compiles to
|
|
23
|
+
# a single `opt_newarray_send` instruction that reads the operands off the stack
|
|
24
|
+
# and never builds an Array — measured at 0 allocations over 200,000 calls, and
|
|
25
|
+
# visible in `RubyVM::InstructionSequence.compile("[a, b].min").disasm`. Flagging
|
|
26
|
+
# it cost real clarity: `Style/MinMaxComparison` asks for exactly this form, so
|
|
27
|
+
# code was being written around a conflict that does not exist.
|
|
28
|
+
#
|
|
29
|
+
# The optimisation is narrow, which is why OPTIMISED_SENDS is a list rather than a
|
|
30
|
+
# guess. `sum`, `first`, `last` and everything else still emit a plain `newarray`,
|
|
31
|
+
# and even `min` falls back to one the moment it is given a block or an argument
|
|
32
|
+
# (`[a, b].min { ... }`, `[a, b].min(1)`). Ranges never had an Array to build.
|
|
33
|
+
class NoNeedlessAllocation < RuboCop::Cop::Base
|
|
34
|
+
include HotPath
|
|
35
|
+
|
|
36
|
+
MSG_RECEIVER = 'Needless %{kind} allocation: this literal is built every call ' \
|
|
37
|
+
'just to call `%{method}` on it — rewrite without the literal.'
|
|
38
|
+
|
|
39
|
+
OPTIMISED_SENDS = { min: 0, max: 0, hash: 0, include?: 1 }.freeze
|
|
40
|
+
MSG_HOT_PATH = 'Needless %{kind} allocation in a per-frame method: this literal ' \
|
|
41
|
+
'is built every frame — build it once or expose the parts directly.'
|
|
42
|
+
|
|
43
|
+
def on_array(node)
|
|
44
|
+
check(node, 'array')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def on_irange(node)
|
|
48
|
+
check(node, 'range')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def on_erange(node)
|
|
52
|
+
check(node, 'range')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def check(node, kind)
|
|
58
|
+
return if allowed?(node)
|
|
59
|
+
|
|
60
|
+
send = receiving_send(node)
|
|
61
|
+
if send
|
|
62
|
+
add_offense(node, message: format(MSG_RECEIVER, kind: kind, method: send.method_name))
|
|
63
|
+
elsif in_hot_path?(node)
|
|
64
|
+
add_offense(node, message: format(MSG_HOT_PATH, kind: kind))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def allowed?(node)
|
|
69
|
+
return true if node.array_type? && node.children.empty?
|
|
70
|
+
return true if frozen?(node)
|
|
71
|
+
return true if node.parent&.masgn_type?
|
|
72
|
+
return true if optimised_send?(node)
|
|
73
|
+
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def optimised_send?(node)
|
|
78
|
+
return false unless node.array_type?
|
|
79
|
+
return false if node.children.any?(&:splat_type?)
|
|
80
|
+
|
|
81
|
+
send = receiving_send(node)
|
|
82
|
+
return false if send.nil? || send.block_node
|
|
83
|
+
|
|
84
|
+
arity = OPTIMISED_SENDS[send.method_name]
|
|
85
|
+
!arity.nil? && send.arguments.size == arity
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def frozen?(node)
|
|
89
|
+
send = receiving_send(node)
|
|
90
|
+
send&.method?(:freeze)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def receiving_send(node)
|
|
94
|
+
receiver = node
|
|
95
|
+
parent = node.parent
|
|
96
|
+
if parent&.begin_type? && parent.children.one?
|
|
97
|
+
receiver = parent
|
|
98
|
+
parent = parent.parent
|
|
99
|
+
end
|
|
100
|
+
return unless parent&.send_type? && parent.receiver == receiver
|
|
101
|
+
|
|
102
|
+
parent
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def in_hot_path?(node)
|
|
106
|
+
enclosing = node.each_ancestor(:def).first
|
|
107
|
+
enclosing && hot_path_def?(enclosing)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# The Game/ cops' defaults, for any project that loads the plugin. Each cop's
|
|
2
|
+
# class comment, under cop/game/, says what it catches and why.
|
|
3
|
+
#
|
|
4
|
+
# Specs are test code: their draw stubs and fixture arrays are not a per-frame
|
|
5
|
+
# path the game ships, so the two allocation guards skip them. The draw-space
|
|
6
|
+
# and translation rules still apply there, because a spec that draws in the
|
|
7
|
+
# wrong place describes a game that does too.
|
|
8
|
+
#
|
|
9
|
+
# The two layer cops are off by default. Each needs to be told which files form
|
|
10
|
+
# a layer, and that is a fact about a project's layout rather than about rgame.
|
|
11
|
+
# A project from `rgame new` enables NoCoreInEngineLayer for its nodes/ and spec/.
|
|
12
|
+
|
|
13
|
+
Game/DrawInLocalSpace:
|
|
14
|
+
Description: A node's draw methods must not read its own position, which is already applied.
|
|
15
|
+
Enabled: true
|
|
16
|
+
|
|
17
|
+
Game/NoInterpolationInHotPath:
|
|
18
|
+
Description: No string interpolation in a per-frame method.
|
|
19
|
+
Enabled: true
|
|
20
|
+
Exclude:
|
|
21
|
+
- '**/spec/**/*'
|
|
22
|
+
|
|
23
|
+
Game/NoNeedlessAllocation:
|
|
24
|
+
Description: No throwaway Array or Range literal on a per-frame path.
|
|
25
|
+
Enabled: true
|
|
26
|
+
Exclude:
|
|
27
|
+
- '**/spec/**/*'
|
|
28
|
+
|
|
29
|
+
Game/NoLiteralText:
|
|
30
|
+
Description: Draw text from a translation key, not a String literal.
|
|
31
|
+
Enabled: true
|
|
32
|
+
|
|
33
|
+
Game/NoCoreInEngineLayer:
|
|
34
|
+
Description: Headless code must not name or require RGame::Core.
|
|
35
|
+
Enabled: false
|
|
36
|
+
|
|
37
|
+
Game/NoEngineInCoreLayer:
|
|
38
|
+
Description: RGame::Core must not name or require the engine layer.
|
|
39
|
+
Enabled: false
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../version'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Game
|
|
7
|
+
# Hands RuboCop the `Game/` cops' default configuration, in `default.yml`
|
|
8
|
+
# beside this file.
|
|
9
|
+
#
|
|
10
|
+
# A project loads it by path and class, from `.rubocop.yml`:
|
|
11
|
+
#
|
|
12
|
+
# plugins:
|
|
13
|
+
# - rgame/rubocop:
|
|
14
|
+
# plugin_class_name: RuboCop::Game::Plugin
|
|
15
|
+
#
|
|
16
|
+
# The bare gem name would make RuboCop `require 'rgame'` and load the whole
|
|
17
|
+
# engine, compiled extension included, just to lint. `lint_roller` comes
|
|
18
|
+
# with every RuboCop that supports plugins, so the gem needs no dependency.
|
|
19
|
+
#
|
|
20
|
+
# It lives under `RuboCop` rather than `RGame`, like every other RuboCop
|
|
21
|
+
# extension, because RuboCop's process is the only one that loads it.
|
|
22
|
+
class Plugin < LintRoller::Plugin
|
|
23
|
+
def about
|
|
24
|
+
LintRoller::About.new(
|
|
25
|
+
name: 'rgame',
|
|
26
|
+
version: RGame::VERSION,
|
|
27
|
+
homepage: 'https://github.com/psuessenb/rgame',
|
|
28
|
+
description: 'Per-frame allocation, draw-space, layering and translation rules for rgame games.'
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def supported?(context)
|
|
33
|
+
context.engine == :rubocop
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def rules(_context)
|
|
37
|
+
LintRoller::Rules.new(
|
|
38
|
+
type: :path,
|
|
39
|
+
config_format: :rubocop,
|
|
40
|
+
value: File.join(__dir__, 'default.yml')
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lint_roller'
|
|
4
|
+
|
|
5
|
+
require_relative 'rubocop/plugin'
|
|
6
|
+
require_relative 'rubocop/cop/game/draw_in_local_space'
|
|
7
|
+
require_relative 'rubocop/cop/game/no_core_in_engine_layer'
|
|
8
|
+
require_relative 'rubocop/cop/game/no_engine_in_core_layer'
|
|
9
|
+
require_relative 'rubocop/cop/game/no_interpolation_in_hot_path'
|
|
10
|
+
require_relative 'rubocop/cop/game/no_literal_text'
|
|
11
|
+
require_relative 'rubocop/cop/game/no_needless_allocation'
|
data/lib/rgame/util/color.rb
CHANGED
|
@@ -1,27 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# RGame::Util::Color is implemented in C — see ext/rgame_util/color_ext.c for
|
|
4
|
-
# the Ruby binding and ext/rgame_util/color.c for the packing arithmetic.
|
|
5
|
-
#
|
|
6
|
-
# Color = RGame::Util::Color
|
|
7
|
-
# Color.new(255, 128, 0) # r, g, b, a defaults to 255
|
|
8
|
-
# Color.rgba(255, 128, 0, 200) # the same thing, named
|
|
9
|
-
# Color.from_packed(0xFF8000FF) # 0xRRGGBBAA
|
|
10
|
-
# Color::WHITE # also BLACK and TRANSPARENT
|
|
11
|
-
#
|
|
12
|
-
# c.r; c.g; c.b; c.a # => Integer, 0..255
|
|
13
|
-
# c.packed # => Integer, 0xRRGGBBAA
|
|
14
|
-
#
|
|
15
|
-
# Instances are frozen and compare by value, so a Color works as a Hash key and
|
|
16
|
-
# can be shared without anyone tinting it out from under its other holders.
|
|
17
|
-
#
|
|
18
|
-
# Every draw call accepts a colour as nil (white), [r, g, b], [r, g, b, a] or a
|
|
19
|
-
# Color; `Color.coerce` is the single place that conversion happens.
|
|
20
|
-
#
|
|
21
|
-
# A colour is a *value* — no window, no GPU handle — so it lives in Util rather
|
|
22
|
-
# than Core. That is what lets the engine layer hold one as an attribute; see
|
|
23
|
-
# CLAUDE.md, "Value objects go in Util; only handle-owners go in Core".
|
|
24
|
-
#
|
|
25
|
-
# It loads from lib/rgame/util_ext.so, which `make ext-util` copies out of
|
|
26
|
-
# ext/rgame_util/.
|
|
27
3
|
require 'rgame/util_ext'
|
|
4
|
+
|
|
5
|
+
module RGame
|
|
6
|
+
module Util
|
|
7
|
+
class Color
|
|
8
|
+
RED = new(255, 0, 0)
|
|
9
|
+
GREEN = new(0, 255, 0)
|
|
10
|
+
BLUE = new(0, 0, 255)
|
|
11
|
+
YELLOW = new(255, 255, 0)
|
|
12
|
+
CYAN = new(0, 255, 255)
|
|
13
|
+
MAGENTA = new(255, 0, 255)
|
|
14
|
+
ORANGE = new(255, 165, 0)
|
|
15
|
+
PURPLE = new(128, 0, 128)
|
|
16
|
+
BROWN = new(139, 69, 19)
|
|
17
|
+
PINK = new(255, 192, 203)
|
|
18
|
+
GRAY = new(128, 128, 128)
|
|
19
|
+
LIGHT_GRAY = new(192, 192, 192)
|
|
20
|
+
DARK_GRAY = new(64, 64, 64)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/rgame/util/controls.rb
CHANGED
|
@@ -26,17 +26,6 @@ module RGame
|
|
|
26
26
|
# by _Static_assert at compile time, and spec/rgame/util/controls_spec.rb
|
|
27
27
|
# parses that header and compares every value here against it.
|
|
28
28
|
module Controls
|
|
29
|
-
# --- Keyboard. Values are SDL scancodes, which name a physical *position*
|
|
30
|
-
# rather than a letter: KEY_A is the key marked A on a QWERTY board and Q
|
|
31
|
-
# on AZERTY. A game rebinding controls shows the player what their layout
|
|
32
|
-
# calls it; the engine only ever compares numbers. ---
|
|
33
|
-
#
|
|
34
|
-
# The set is what a Western keyboard can be relied on to have. No numpad
|
|
35
|
-
# (most laptops have none), no GUI/Windows/Command key, no print-screen
|
|
36
|
-
# cluster, and nothing whose position depends on the layout.
|
|
37
|
-
|
|
38
|
-
# Letters. Scancodes are physical *positions*, so KEY_A is the key marked A on
|
|
39
|
-
# a QWERTY board and Q on AZERTY.
|
|
40
29
|
KEY_A = 4
|
|
41
30
|
KEY_B = 5
|
|
42
31
|
KEY_C = 6
|
|
@@ -64,7 +53,6 @@ module RGame
|
|
|
64
53
|
KEY_Y = 28
|
|
65
54
|
KEY_Z = 29
|
|
66
55
|
|
|
67
|
-
# Digits along the top row.
|
|
68
56
|
KEY_1 = 30
|
|
69
57
|
KEY_2 = 31
|
|
70
58
|
KEY_3 = 32
|
|
@@ -76,14 +64,12 @@ module RGame
|
|
|
76
64
|
KEY_9 = 38
|
|
77
65
|
KEY_0 = 39
|
|
78
66
|
|
|
79
|
-
# Editing and whitespace.
|
|
80
67
|
KEY_RETURN = 40
|
|
81
68
|
KEY_ESCAPE = 41
|
|
82
69
|
KEY_BACKSPACE = 42
|
|
83
70
|
KEY_TAB = 43
|
|
84
71
|
KEY_SPACE = 44
|
|
85
72
|
|
|
86
|
-
# Punctuation, by position on a US board.
|
|
87
73
|
KEY_MINUS = 45
|
|
88
74
|
KEY_EQUALS = 46
|
|
89
75
|
KEY_LEFTBRACKET = 47
|
|
@@ -96,7 +82,6 @@ module RGame
|
|
|
96
82
|
KEY_PERIOD = 55
|
|
97
83
|
KEY_SLASH = 56
|
|
98
84
|
|
|
99
|
-
# Function row and caps lock.
|
|
100
85
|
KEY_CAPSLOCK = 57
|
|
101
86
|
KEY_F1 = 58
|
|
102
87
|
KEY_F2 = 59
|
|
@@ -111,7 +96,6 @@ module RGame
|
|
|
111
96
|
KEY_F11 = 68
|
|
112
97
|
KEY_F12 = 69
|
|
113
98
|
|
|
114
|
-
# The navigation cluster.
|
|
115
99
|
KEY_INSERT = 73
|
|
116
100
|
KEY_HOME = 74
|
|
117
101
|
KEY_PAGEUP = 75
|
|
@@ -119,14 +103,11 @@ module RGame
|
|
|
119
103
|
KEY_END = 77
|
|
120
104
|
KEY_PAGEDOWN = 78
|
|
121
105
|
|
|
122
|
-
# Arrows.
|
|
123
106
|
KEY_RIGHT = 79
|
|
124
107
|
KEY_LEFT = 80
|
|
125
108
|
KEY_DOWN = 81
|
|
126
109
|
KEY_UP = 82
|
|
127
110
|
|
|
128
|
-
# Modifiers. No GUI key: that is Windows on a PC and Command on a Mac, which
|
|
129
|
-
# is exactly the platform-specific territory this list stays out of.
|
|
130
111
|
KEY_LCTRL = 224
|
|
131
112
|
KEY_LSHIFT = 225
|
|
132
113
|
KEY_LALT = 226
|
|
@@ -134,10 +115,8 @@ module RGame
|
|
|
134
115
|
KEY_RSHIFT = 229
|
|
135
116
|
KEY_RALT = 230
|
|
136
117
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
# and TOUCHPAD are hardware the id space describes but most pads do not
|
|
140
|
-
# have, and read as never pressed on one that does not. ---
|
|
118
|
+
BUTTON_GAMEPAD_FIRST = 0x1000
|
|
119
|
+
|
|
141
120
|
PAD_A = 4096
|
|
142
121
|
PAD_B = 4097
|
|
143
122
|
PAD_X = 4098
|
|
@@ -160,9 +139,6 @@ module RGame
|
|
|
160
139
|
PAD_PADDLE4 = 4115
|
|
161
140
|
PAD_TOUCHPAD = 4116
|
|
162
141
|
|
|
163
|
-
# --- Analog axes. Their own small space: they are float-valued and read
|
|
164
|
-
# through a different call, so folding them into the button space would
|
|
165
|
-
# only invite asking for an axis as if it were a button. ---
|
|
166
142
|
AXIS_LEFT_X = 0
|
|
167
143
|
AXIS_LEFT_Y = 1
|
|
168
144
|
AXIS_RIGHT_X = 2
|
|
@@ -170,14 +146,24 @@ module RGame
|
|
|
170
146
|
AXIS_TRIGGER_LEFT = 4
|
|
171
147
|
AXIS_TRIGGER_RIGHT = 5
|
|
172
148
|
|
|
173
|
-
# --- Devices. The keyboard is 0 so single-player code can leave it out;
|
|
174
|
-
# gamepads follow, one per player slot. ---
|
|
175
149
|
KEYBOARD = 0
|
|
176
150
|
GAMEPAD_FIRST = 1
|
|
177
151
|
MAX_GAMEPADS = 4
|
|
178
152
|
|
|
179
153
|
# The device id for a player slot: gamepad(0) is the first controller.
|
|
180
154
|
def self.gamepad(slot) = GAMEPAD_FIRST + slot
|
|
155
|
+
|
|
156
|
+
# Is this *device* a controller rather than the keyboard? What a game asks
|
|
157
|
+
# when the answer changes what it shows the player — the prompt for an
|
|
158
|
+
# action is a key cap or a face button depending on what they last used.
|
|
159
|
+
def self.gamepad?(device) = device >= GAMEPAD_FIRST
|
|
160
|
+
|
|
161
|
+
# Is this *button* one a controller has rather than one a keyboard has?
|
|
162
|
+
# The mirror of the question above, on the other id space, and the pair is
|
|
163
|
+
# what lets a caller keep the two apart: an action bound to both lists its
|
|
164
|
+
# keys and its pad buttons together, and only a device can say which half
|
|
165
|
+
# of that list applies right now.
|
|
166
|
+
def self.pad_button?(id) = id >= BUTTON_GAMEPAD_FIRST
|
|
181
167
|
end
|
|
182
168
|
end
|
|
183
169
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rgame/util_ext'
|
|
4
|
+
|
|
5
|
+
module RGame
|
|
6
|
+
module Util
|
|
7
|
+
# Connected regions and A* routes over a SolidGrid, in C. The search reads its grid and
|
|
8
|
+
# never writes it, and keeps it alive; any number of searches may read one grid.
|
|
9
|
+
#
|
|
10
|
+
# The grid is 8-connected with octile costs (1 straight, √2 diagonal), and a diagonal
|
|
11
|
+
# step is allowed only when both orthogonal cells beside it are open, so a route never
|
|
12
|
+
# cuts the corner of a solid cell. Routes are cells, `[[col, row], ...]`, start and goal
|
|
13
|
+
# included — or nil when either end is solid, outside the grid, or in another region.
|
|
14
|
+
#
|
|
15
|
+
# Regions are labelled on the first ask after the grid's #revision moves, so a goal in
|
|
16
|
+
# another region is a lookup rather than a search that exhausts the map, and it stays
|
|
17
|
+
# right when the grid changes. Everything else a search keeps between queries is reused,
|
|
18
|
+
# so a repeated query allocates nothing but its result. One search is therefore not safe
|
|
19
|
+
# to use from two threads at once; two searches over one grid are.
|
|
20
|
+
#
|
|
21
|
+
# grid = RGame::Util::SolidGrid.build(8, 3) { |col, row| col == 3 && row < 2 }
|
|
22
|
+
# search = RGame::Util::RouteSearch.new(grid)
|
|
23
|
+
# search.find(0, 0, 7, 0) # => [[0, 0], [1, 1], [2, 2], [3, 2], [4, 2], [5, 1], [6, 0], [7, 0]]
|
|
24
|
+
class RouteSearch
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|