rgame 0.1.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 +228 -0
- data/README.md +142 -243
- data/docs/api/README.md +141 -90
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -166
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1045 -254
- data/docs/api/drawing.md +187 -119
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +165 -45
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +344 -85
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +503 -123
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +197 -64
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +400 -147
- data/docs/api/ui.md +927 -0
- data/docs/api/values.md +234 -19
- 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/README.md +6 -5
- data/ext/rgame_core/app/app.c +215 -11
- 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 +113 -2
- data/ext/rgame_core/example.rb +18 -56
- data/ext/rgame_core/extconf.rb +16 -113
- data/ext/rgame_core/graphics/canvas.c +45 -4
- data/ext/rgame_core/graphics/canvas.h +65 -10
- data/ext/rgame_core/graphics/clip.c +22 -13
- data/ext/rgame_core/include/rgame/core.h +151 -5
- data/ext/rgame_core/input/gamepad.c +57 -3
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +46 -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 +42 -19
- data/ext/rgame_util/color_ext.c +12 -3
- 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 -42
- 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/input.rb +35 -41
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/recording.rb +3 -1
- data/lib/rgame/core/renderer.rb +75 -84
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +77 -65
- 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 +55 -10
- 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 +35 -1
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +31 -23
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +45 -0
- data/lib/rgame/engine/components/character_body.rb +21 -41
- 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 +22 -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 +70 -29
- 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 +45 -0
- data/lib/rgame/engine/debug_overlay.rb +23 -18
- 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 +76 -22
- data/lib/rgame/engine/input/actions.rb +63 -12
- data/lib/rgame/engine/input/input_map.rb +196 -0
- data/lib/rgame/engine/layout.rb +82 -0
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +364 -80
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/player.rb +69 -0
- data/lib/rgame/engine/player_layer.rb +70 -0
- data/lib/rgame/engine/players.rb +205 -0
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +29 -7
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +53 -8
- 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 +82 -0
- 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 +290 -0
- 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/view.rb +76 -0
- data/lib/rgame/engine/viewports.rb +171 -0
- data/lib/rgame/engine/world_view.rb +71 -0
- data/lib/rgame/engine.rb +43 -25
- data/lib/rgame/game.rb +164 -22
- 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 +106 -44
- 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 +123 -0
- data/lib/rgame/util.rb +5 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +176 -20
- 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/camera_view.rb +0 -28
- 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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Game
|
|
6
|
+
# A node's draw methods must not read the node's own position — in either
|
|
7
|
+
# space, because both are already applied by the time they run.
|
|
8
|
+
#
|
|
9
|
+
# `Node2D#draw` pushes the node's transform onto the renderer and runs
|
|
10
|
+
# everything below it inside that, so within `on_draw` the origin *is* the
|
|
11
|
+
# node, turned the way the node is turned. Drawing is done at (0, 0), or at
|
|
12
|
+
# an offset from it that means something to the node itself.
|
|
13
|
+
#
|
|
14
|
+
# Both spellings of "where am I" are therefore wrong there, and differ only
|
|
15
|
+
# in how far off they put the result:
|
|
16
|
+
#
|
|
17
|
+
# - `world_x` is where the node sits on the map, which every ancestor's
|
|
18
|
+
# transform — the camera's included — has already contributed to. Passing
|
|
19
|
+
# it applies the whole chain twice.
|
|
20
|
+
# - `x` is the node's offset inside its parent, which its own transform has
|
|
21
|
+
# already applied. Passing it offsets by that much a second time.
|
|
22
|
+
#
|
|
23
|
+
# Both are silent: they draw, they just draw in the wrong place, and only
|
|
24
|
+
# once something is nested under an offset or rotated parent — which is
|
|
25
|
+
# exactly the case a spec at the origin cannot see.
|
|
26
|
+
#
|
|
27
|
+
# A **component** drawing for its node is on this path too, and asks the
|
|
28
|
+
# node by name: `node.world_x` for a cull test is a different object's
|
|
29
|
+
# coordinate and is not flagged. Only a bare, receiverless read of the
|
|
30
|
+
# node's own transform is.
|
|
31
|
+
#
|
|
32
|
+
# `width` and `height` are not positions and are ordinary here — a node
|
|
33
|
+
# drawing its own box says `renderer.rect(0, 0, width, height)`.
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# # bad — the traversal has already placed the renderer on this node
|
|
37
|
+
# def on_draw(renderer, _view)
|
|
38
|
+
# renderer.rect(world_x, world_y, width, height)
|
|
39
|
+
# end
|
|
40
|
+
#
|
|
41
|
+
# # bad — same mistake, a smaller distance
|
|
42
|
+
# def on_draw(renderer, _view)
|
|
43
|
+
# renderer.rect(x, y, width, height)
|
|
44
|
+
# end
|
|
45
|
+
#
|
|
46
|
+
# # good — its own origin
|
|
47
|
+
# def on_draw(renderer, _view)
|
|
48
|
+
# renderer.rect(0, 0, width, height)
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# # good — a component culling against the camera, in world space, by name
|
|
52
|
+
# def draw(renderer, view)
|
|
53
|
+
# return if culled?(view, node.world_x, node.world_y, node.width, node.height)
|
|
54
|
+
#
|
|
55
|
+
# renderer.image(@id, 0, 0)
|
|
56
|
+
# end
|
|
57
|
+
class DrawInLocalSpace < RuboCop::Cop::Base
|
|
58
|
+
MSG = 'Draw in local space: `%{name}` is already applied by the time ' \
|
|
59
|
+
'`%{method}` runs, so passing it places this %{distance} a second ' \
|
|
60
|
+
'time. Draw at your own origin (0, 0), or an offset from it.'
|
|
61
|
+
|
|
62
|
+
METHODS = %i[draw on_draw _draw_content draw_children].freeze
|
|
63
|
+
|
|
64
|
+
RELATIVE = %i[x y angle].freeze
|
|
65
|
+
WORLD = %i[world_x world_y world_angle].freeze
|
|
66
|
+
IVARS = { :@rel_x => :x, :@rel_y => :y, :@rel_angle => :angle,
|
|
67
|
+
:@world_x => :world_x, :@world_y => :world_y,
|
|
68
|
+
:@world_angle => :world_angle }.freeze
|
|
69
|
+
|
|
70
|
+
def on_def(node)
|
|
71
|
+
return unless METHODS.include?(node.method_name)
|
|
72
|
+
|
|
73
|
+
node.each_descendant(:ivar, :send) do |read|
|
|
74
|
+
name = own_transform_read(read)
|
|
75
|
+
next unless name
|
|
76
|
+
|
|
77
|
+
add_offense(read, message: format(MSG, name: name, method: node.method_name,
|
|
78
|
+
distance: distance_for(name)))
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def own_transform_read(node)
|
|
85
|
+
if node.ivar_type?
|
|
86
|
+
IVARS[node.children.first]
|
|
87
|
+
elsif self_read?(node)
|
|
88
|
+
name = node.method_name
|
|
89
|
+
name if RELATIVE.include?(name) || WORLD.include?(name)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self_read?(node)
|
|
94
|
+
node.send_type? && node.receiver.nil? && node.arguments.empty? && !node.block_literal?
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def distance_for(name)
|
|
98
|
+
WORLD.include?(name) ? 'on the map' : 'inside its parent'
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Game
|
|
6
|
+
# Shared definition of the per-frame "hot path" for the allocation-guard cops.
|
|
7
|
+
#
|
|
8
|
+
# The hot path is the set of methods that run every frame: the engine's six
|
|
9
|
+
# lifecycle hooks, plus any method an author opts into with a `# hot-path` magic
|
|
10
|
+
# comment on the line directly above its `def`. The opt-in covers per-frame
|
|
11
|
+
# *helpers* the lifecycle methods call, where an allocation is just as costly but
|
|
12
|
+
# the method name alone can't reveal it.
|
|
13
|
+
module HotPath
|
|
14
|
+
METHODS = %i[update control draw draw_children _draw_content
|
|
15
|
+
on_update on_draw on_control].freeze
|
|
16
|
+
|
|
17
|
+
# True for a `def` that runs per frame: a lifecycle method by name, or one tagged
|
|
18
|
+
# `# hot-path` on the line directly above it.
|
|
19
|
+
def hot_path_def?(node)
|
|
20
|
+
return false unless node.def_type?
|
|
21
|
+
|
|
22
|
+
METHODS.include?(node.method_name) || hot_path_tagged?(node)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def hot_path_tagged?(node)
|
|
28
|
+
line_above = node.source_range.first_line - 1
|
|
29
|
+
processed_source.comments.any? do |comment|
|
|
30
|
+
comment.location.line == line_above && comment.text.match?(/\A#\s*hot-path\b/)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Game
|
|
6
|
+
# Shared by the two cops that police a layer boundary —
|
|
7
|
+
# `NoCoreInEngineLayer` and `NoEngineInCoreLayer`. They are mirror images:
|
|
8
|
+
# each forbids one namespace inside the other's files, and the only thing
|
|
9
|
+
# that differs is which namespace and which message.
|
|
10
|
+
#
|
|
11
|
+
# Kept in one place because the interesting part is not the comparison but
|
|
12
|
+
# the *walk*: `RGame::Core::Renderer` is three nested `const` nodes, and a
|
|
13
|
+
# naive check reports the same reference three times. Getting that right
|
|
14
|
+
# once beats getting it right twice and then fixing only one of them.
|
|
15
|
+
module LayerBoundary
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def const_path(node)
|
|
19
|
+
names = []
|
|
20
|
+
current = node
|
|
21
|
+
while current&.const_type?
|
|
22
|
+
names.unshift(current.short_name.to_s)
|
|
23
|
+
current = current.namespace
|
|
24
|
+
end
|
|
25
|
+
names
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def opens_namespace?(node, prefixes)
|
|
29
|
+
return false unless under?(node, prefixes)
|
|
30
|
+
|
|
31
|
+
!(node.parent&.const_type? && under?(node.parent, prefixes))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def under?(node, prefixes)
|
|
35
|
+
return false unless node.const_type?
|
|
36
|
+
|
|
37
|
+
path = const_path(node)
|
|
38
|
+
prefixes.any? { |prefix| path.first(prefix.length) == prefix }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'layer_boundary'
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module Cop
|
|
7
|
+
module Game
|
|
8
|
+
# The engine layer must never name `RGame::Core`.
|
|
9
|
+
#
|
|
10
|
+
# `RGame::Engine` holds the game concepts — scene tree, signals, sprites,
|
|
11
|
+
# tile maps — and its whole value is that it can be specified with no
|
|
12
|
+
# window, no GPU and no clock. It reaches the platform only through
|
|
13
|
+
# objects handed to it: a node's `draw` receives a `renderer` and calls
|
|
14
|
+
# methods on it by name, never storing it and never asking its class.
|
|
15
|
+
#
|
|
16
|
+
# Engine specs enforce this at runtime by simply never loading
|
|
17
|
+
# `rgame/core`, so a stray reference raises `NameError`. That only catches
|
|
18
|
+
# code a test run actually executes, though — this cop covers the
|
|
19
|
+
# branches it doesn't reach.
|
|
20
|
+
#
|
|
21
|
+
# A game has the same line: its nodes and specs run headless, and only its
|
|
22
|
+
# glue class loads the window. So `require 'rgame/game'` is refused too,
|
|
23
|
+
# since it loads everything `rgame/core` does.
|
|
24
|
+
#
|
|
25
|
+
# The short spelling `Core::Image` is flagged only inside `module RGame`,
|
|
26
|
+
# the one place it resolves to `RGame::Core`. Anywhere else it names the
|
|
27
|
+
# project's own `Core`.
|
|
28
|
+
#
|
|
29
|
+
# `RGame::Util` is fine anywhere: those are shareable value types with no
|
|
30
|
+
# OS handle behind them, which is exactly why they live in Util.
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# # bad — names the class
|
|
34
|
+
# def draw(renderer)
|
|
35
|
+
# RGame::Core::Renderer.new
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# # bad — the same thing, resolved through the enclosing RGame
|
|
39
|
+
# module RGame
|
|
40
|
+
# Core::Image.new(app, path)
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# # bad — a require pulls SDL into the process
|
|
44
|
+
# require 'rgame/core'
|
|
45
|
+
#
|
|
46
|
+
# # good — duck-typed against whatever it is handed
|
|
47
|
+
# def draw(renderer, _view)
|
|
48
|
+
# renderer.sprite(:hero, 0, 0, 0, 0)
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# # good — Util types may be held as attributes
|
|
52
|
+
# @grid = RGame::Util::Tensor.new(w, h, d)
|
|
53
|
+
class NoCoreInEngineLayer < RuboCop::Cop::Base
|
|
54
|
+
include LayerBoundary
|
|
55
|
+
|
|
56
|
+
MSG = 'Headless code must not name `RGame::Core`; receive the object ' \
|
|
57
|
+
'and call it by method name instead.'
|
|
58
|
+
MSG_REQUIRE = 'Headless code must not require `%{path}` — that loads ' \
|
|
59
|
+
'SDL/OpenGL and breaks headless specs.'
|
|
60
|
+
|
|
61
|
+
QUALIFIED = [%w[RGame Core]].freeze
|
|
62
|
+
WITHIN_RGAME = [%w[RGame Core], %w[Core]].freeze
|
|
63
|
+
RESTRICTED_REQUIRE = %r{\Argame/(core|game)(/|\z)|\Argame/core_ext\z}
|
|
64
|
+
|
|
65
|
+
# `require "rgame/core"` and friends.
|
|
66
|
+
# @!method core_require(node)
|
|
67
|
+
def_node_matcher :core_require, <<~PATTERN
|
|
68
|
+
(send nil? {:require :require_relative} (str $_))
|
|
69
|
+
PATTERN
|
|
70
|
+
|
|
71
|
+
def on_send(node)
|
|
72
|
+
core_require(node) do |path|
|
|
73
|
+
next unless RESTRICTED_REQUIRE.match?(path.to_s.delete_prefix('./'))
|
|
74
|
+
|
|
75
|
+
add_offense(node, message: format(MSG_REQUIRE, path: path))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Any constant path under RGame::Core. The walk, and reporting one
|
|
80
|
+
# offence per written reference rather than one per path segment, is in
|
|
81
|
+
# LayerBoundary — shared with this cop's mirror.
|
|
82
|
+
def on_const(node)
|
|
83
|
+
add_offense(node) if opens_namespace?(node, prefixes_for(node))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def prefixes_for(node)
|
|
89
|
+
within_rgame?(node) ? WITHIN_RGAME : QUALIFIED
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def within_rgame?(node)
|
|
93
|
+
node.each_ancestor(:module, :class).any? do |scope|
|
|
94
|
+
const_path(scope.identifier).first == 'RGame'
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -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'
|