aboutsummaryrefslogtreecommitdiff
path: root/pyjstest.py
blob: dc7e08d223a39ace94c064ec4e7a12f75a65229c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
#!/usr/bin/env python3
# File: pyjstest.py
# Location: https://bgstack15.ddns.net/git/pyjstest/
# Author: bgstack15
# SPDX-License-Identifier: GPL-3.0
# Startdate: 2022-06-29
# Title: Pyjstest
# Package: pyjstest
# Purpose: Display inputs of one connected gamepad
# History:
# Usage:
#    Adjust config.py and then run!
# Reference:
#    https://stackoverflow.com/a/50572903
#    https://pysdl2.readthedocs.io/en/0.9.11/tutorial/pong.html
# Improve:
#   display text of axes values? https://github.com/caerus706/Python3-pySDL2-examples/blob/master/text.py
# Documentation: See README.md
import ctypes, time, random
from sdl2 import *
import sdl2.ext, sys

from config import colors, CONFIG

class SoftwareRenderer(sdl2.ext.SoftwareSpriteRenderSystem):
   def __init__(self, window):
      super(SoftwareRenderer, self).__init__(window)
   def render(self, components):
      sdl2.ext.fill(self.surface, colors["BKGD"])
      super(SoftwareRenderer, self).render(components)

class MovementSystem(sdl2.ext.Applicator):
   def __init__(self, minx, miny, maxx, maxy):
      super(MovementSystem, self).__init__()
      self.componenttypes = Velocity, sdl2.ext.Sprite
      self.minx = minx
      self.miny = miny
      self.maxx = maxx
      self.maxy = maxy
   def process(self, world, componentsets):
      for velocity, sprite in componentsets:
         swidth, sheight = sprite.size
         sprite.x += velocity.vx
         sprite.y += velocity.vy
         sprite.x = max(self.minx, sprite.x)
         sprite.y = max(self.miny, sprite.y)
         pmaxx = sprite.x + swidth
         pmaxy = sprite.y + sheight
         if pmaxx > self.maxx:
            sprite.x = self.maxx - swidth
         if pmaxy > self.maxy:
            sprite.y = self.maxy - sheight

class Velocity(object):
   def __init__(self):
      super(Velocity, self).__init__()
      self.vx = 0
      self.vy = 0

class Rectangle(sdl2.ext.Entity):
   def __init__(self, world, sprite, posx=0, posy=0):
      self.world = world
      self.sprite = sprite
      self.sprite.position = posx, posy
      self.velocity = Velocity()

class TwoAxisBackground(sdl2.ext.Entity):
   def __init__(self, world, factory, color, posx=0, posy=0, sizex=100, sizey=100):
      self.world = world
      self.sprite = factory.from_color(color, size=(sizex, sizey))
      self.sprite.position = posx, posy

class Button(object):
   def __init__(self, world, factory, color_on, color_off, posx, posy, size, button, relx=0, rely=0, owner = None):
      self.world = world
      self.factory = factory
      self.color_on = color_on
      self.color_off = color_off
      self.posx = posx
      self.posy = posy
      self.size = size
      self.object = Rectangle(self.world, self.factory.from_color(self.color_off, size=(self.size, self.size)), self.posx, self.posy)
      self.status = False
      self.button = button
      self.type = "button"
      self.relx = relx
      self.rely = rely
      self.owner = owner # either None, or an Analog
   def on(self):
      if not self.status:
         self.object.sprite = self.factory.from_color(self.color_on, size=(self.size,self.size))
         if self.owner is None:
            self.object.sprite.position = self.posx+self.relx, self.posy+self.rely
         self.status = True
   def off(self):
      if self.status:
         self.object.sprite = self.factory.from_color(self.color_off, size=(self.size,self.size))
         if self.owner is None:
            self.object.sprite.position = self.posx+self.relx, self.posy+self.rely
         self.status = False

class Dpad(object):
   """ Represent simple UDLR position of an analog stick. One Dpad object per direction per axis excluding off. """
   def __init__(self, world, factory, color_on, color_off, posx, posy, size, axis, direction):
      self.world = world
      self.factory = factory
      self.color_on = color_on
      self.color_off = color_off
      self.posx = posx
      self.posy = posy
      self.size = size
      self.type = "dpad"
      self.axis = axis
      self.direction = direction
      self.object = Rectangle(self.world, self.factory.from_color(self.color_off, size=(self.size,self.size)), self.posx, self.posy)
      self.status = False
   def positive(self, axis):
      #print(f"Dpad axis {axis} moved positive.")
      self.on() if "positive" == self.direction else self.off()
   def negative(self, axis):
      #print(f"Dpad axis {axis} moved negative.")
      self.on() if "negative" == self.direction else self.off()
   def middle(self, axis):
      self.off()
   def on(self):
      if not self.status:
         #self.object = Rectangle(self.world, self.factory.from_color(self.color_on, size=(self.size,self.size)), self.posx, self.posy)
         self.object.sprite = self.factory.from_color(self.color_on, size=(self.size,self.size))
         self.object.sprite.position = self.posx, self.posy
         self.status = True
   def off(self):
      if self.status:
         #self.object = Rectangle(self.world, self.factory.from_color(self.color_off, size=(self.size,self.size)), self.posx, self.posy)
         self.object.sprite = self.factory.from_color(self.color_off, size=(self.size,self.size))
         self.object.sprite.position = self.posx, self.posy
         self.status = False
   def react(self, config, axis, value):
      #print(f"Dpad axis {axis} value {value}")
      if value >= config["dpad_axes"]["min"][axis] and value < config["dpad_axes"]["mid_min"][axis]:
         self.negative(axis)
      elif value >= config["dpad_axes"]["mid_min"][axis] and value <= config["dpad_axes"]["mid_max"][axis]:
         self.middle(axis)
      elif value > config["dpad_axes"]["mid_max"][axis] and value <= config["dpad_axes"]["max"][axis]:
         self.positive(axis)
      else:
         print(f"Warning! For Dpad {self} axis {axis}, value {value} is undefined behavior.")

class Analog(object):
   def __init__(self, world, factory, color_on, color_off, centerx, centery, indicator_size, size, axes, orientations, buttons):
      self.world = world
      self.factory = factory
      self.color_on = color_on
      self.color_off = color_off
      self.posx = centerx
      self.posy = centery
      self.size = size
      self.type = "analog"
      self.axes = axes
      self.indicator_size = indicator_size
      self.orientations = orientations
      self.buttons_in = buttons # a list of ["color_on", "color_off", button_id]
      self.ulx = int(self.posx-(size/2))
      self.uly = int(self.posy-(size/2))
      self.lrx = int(self.posx+(size/2))
      self.lry = int(self.posy+(size/2))
      self.background = TwoAxisBackground(self.world, self.factory, self.color_off, self.ulx, self.uly, size, size)
      self.indicator = Rectangle(self.world, self.factory.from_color(self.color_on, size=(self.indicator_size,self.indicator_size)), self.posx, self.posy)
      #self.movement = MovementSystem(self.ulx, self.uly, self.lrx, self.lry)
      #self.world.add_system(self.movement)
      self.buttons = []
      for b in buttons:
         self.buttons.append(Button(self.world,self.factory,colors[b[0]],colors[b[1]],self.posx,self.posy,b[4],b[5],b[2],b[3],self))
   def react(self, config, axis, value):
      #print(f"Updating with {axis}, {value}")
      _max = config["dpad_axes"]["max"][axis]
      _min = config["dpad_axes"]["min"][axis]
      percent = (value - _min) / (_max - _min) * 100
      current_position = self.indicator.sprite.position # (x, y)
      newx = current_position[0]
      newy = current_position[1]
      # X is axes[0]
      if axis == self.axes[0]:
         newx = int((self.ulx+(self.size*percent/100))-(self.indicator_size/2))
      elif axis == self.axes[1]:
         newy = int((self.uly+(self.size*percent/100))-(self.indicator_size/2))
      self.indicator.sprite.position = newx, newy
      for b in self.buttons:
         b.object.sprite.position = (newx+b.relx), (newy+b.rely)
      return newx, newy

class Hat(object):
   def __init__(self, world, factory, color_on, color_off, posx, posy, size, hat, off, up, right, down, left):
      self.world = world
      self.factory = factory
      self.color_on = color_on
      self.color_off = color_off
      self.hat = hat
      self.basex = posx
      self.basey = posy
      self.size = size
      self.type = "hat"
      # center, up, right, down, left
      self.posx = [0, 0, 20, 0, -20]
      self.posy = [0, -20, 0, 20, 0]
      self.box = []
      for i in range(0,5):
         self.box.append(Rectangle(self.world, self.factory.from_color(self.color_off, size=(self.size,self.size)), self.basex+self.posx[i], self.basey+self.posy[i]))
      # array of binary values that will describe the current hat position
      self.binary = [off, up, right, down, left]
   def react(self, value):
      print(f"Hat {self.hat} value {value}")
      possible = [0,1,2,3,4]
      on = []
      if value == self.binary[0]:
         on.append(0)
      for r in range(1,5): # 1 to 4
         if value & self.binary[r]:
            on.append(r)
      # evaluate all the options
      off = possible
      for option in on:
         off.remove(option)
      #print(f"Hat options on: {on}")
      #print(f"Hat options off: {off}")
      x = 0
      while x < 5:
         self.box = []
         if x in on:
            self.box.append(Rectangle(self.world, self.factory.from_color(self.color_on, size=(self.size,self.size)), self.basex+self.posx[x], self.basey+self.posy[x]))
         else:
            self.box.append(Rectangle(self.world, self.factory.from_color(self.color_off, size=(self.size,self.size)), self.basex+self.posx[x], self.basey+self.posy[x]))
         x = x + 1

# color_on, color_off, x,y, size, axis, direction
#["GRAY", "DARK_GRAY", 60, 40, 20, 2, 1],
class Trigger(object):
   """ Direction is not actually implemented. """
   def __init__(self, world, factory, renderer, color_on, color_off, color_bkgd, x1, y1, x2, y2, size, axis, direction):
      self.world = world
      self.factory = factory
      self.color_on = color_on
      self.color_off = color_off
      self.color_bkgd = color_bkgd
      self.axis = axis
      self.x1 = x1
      self.y1 = y1
      self.x2 = x2
      self.y2 = y2
      self.boxx1 = min(x1,x2)
      self.boxy1 = min(y1,y2)
      self.boxx2 = max(x1,x2)
      self.boxy2 = max(y1,y2)
      self.linex1 = x1 - self.boxx1
      self.liney1 = y1 - self.boxy1
      self.linex2 = x2 - self.boxx1
      self.liney2 = y2 - self.boxy1
      self.type = "trigger"
      self.indicator_size = size
      # This line makes rectangle the minimum size so the drawn line actually displays.
      self.rectangle_size = max((self.boxx2-self.boxx1),1),max((self.boxy2-self.boxy1),1)
      print(f"Drawing rectangle, {self.color_bkgd}, size ({self.rectangle_size}), starting ({self.boxx1},{self.boxy1})")
      self.background = Rectangle(self.world, self.factory.from_color(self.color_bkgd, size=self.rectangle_size), self.boxx1, self.boxy1)

      print(f"Drawing line color {self.color_off}, from ({self.linex1}, {self.liney1}) to ({self.linex2},{self.liney2})")
      sdl2.ext.draw.line(self.background.sprite, self.color_off, (self.linex1, self.liney1, self.linex2, self.liney2))

      self.indicator = Rectangle(self.world, self.factory.from_color(self.color_on, size=(self.indicator_size,self.indicator_size)), self.x1, self.y1)
   def react(self, config, axis, value):
      """ This one-axis trigger will move in X,Y dimensions based on the line drawn. """
      print(f"Axis {axis} value {value}")
      _max = config["dpad_axes"]["max"][axis]
      _min = config["dpad_axes"]["min"][axis]
      percent = (value - _min) / (_max - _min) * 100
      current_position = self.indicator.sprite.position # (x, y)
      newx = int((self.x2 - self.x1)*percent/100-(self.indicator_size/2) + self.x1) +1
      newy = int((self.y2 - self.y1)*percent/100-(self.indicator_size/2) + self.y1) +1
      self.indicator.sprite.position = newx, newy

def find_joystick_config_by_alias(config, name):
   """
   Given CONFIG and a joystick name, see if that joystick name shows up as either a ruleset or alias for a ruleset, and then return the proper ruleset name.
   """
   if name in config:
      print(f"DEBUG (fjcba): Found name {name} in config.")
      return name
   else:
      for c in config:
         if "aliases" in config[c]:
            if name in config[c]["aliases"]:
               print(f"DEBUG (fjcba): Found name {c} that matches alias {name}.")
               return c
   print(f"DEBUG (fjcba): oops! Found nothing that matches {name}.")
   return None

def run():
   world = sdl2.ext.World()
   gamepads = []
   #joystick = Joystick()
   SDL_Init(SDL_INIT_JOYSTICK)
   sdl2.ext.init()
   joystick = ""
   # load resources/ directory
   #RESOURCES = sdl2.ext.Resources(__file__,"resources")
   factory = sdl2.ext.SpriteFactory(sdl2.ext.SOFTWARE)
   #sprite = factory.from_image(RESOURCES.get_path("gamepad.png"))

   input_array = []
   #if "buttons" in CONFIG[joystick]:
   #   for b in CONFIG[joystick]["buttons"]:
   #      input_array.append(button(world,factory,colors[b[0]],colors[b[1]],b[2],b[3],b[4]))
   #if "dpads" in CONFIG[joystick]:
   #   for b in CONFIG[joystick]["dpads"]:
   #      input_array.append(Dpad(world,factory,colors[b[0]],colors[b[1]],b[2],b[3],b[4],b[5]))
   #if "hats" in CONFIG[joystick]:
   #   for b in CONFIG[joystick]["hats"]:
   #      input_array.append(Hat(world, factory, colors[b[0]], colors[b[1]], b[2], b[3], b[4], b[5],b[6],b[7],b[8],b[9]))
   #window = sdl2.ext.Window("Pyjoy", size=CONFIG[joystick]["window"])
   #window = sdl2.ext.Window("Pyjoy", size=(100,100), flags = SDL_WINDOW_RESIZABLE)
   window = sdl2.ext.Window("Pyjoy", size=(100,100))
   window.show()

   spriterenderer = SoftwareRenderer(window)
   world.add_system(spriterenderer)
   running = True
   status = {}
   while running:
      event = SDL_Event()
      while SDL_PollEvent(ctypes.byref(event)) != 0:
         if event.type == SDL_JOYDEVICEADDED:
            #self.device = SDL_JoystickOpen(event.jdevice.which)
            joystick = SDL_JoystickName(SDL_JoystickOpen(event.jdevice.which)).decode('UTF-8')
            print(f"Device: {joystick}")
            config_name = find_joystick_config_by_alias(CONFIG,joystick)
            window.title = config_name
            if config_name:
               joystick = config_name
               print(f"Working with controller type {config_name}")
               #window.size = CONFIG[joystick]["window"]
               SDL_SetWindowSize(window.window,CONFIG[joystick]["window"][0],CONFIG[joystick]["window"][1])
               window.refresh()
               #window.get_surface()
               # Use a temporary list, to avoid the "Set size changed during iteration."
               temp_list = []
               for i in world.entities:
                  temp_list.append(i)
               for i in temp_list:
                  i.delete()
               world.remove_system(spriterenderer)
               spriterenderer = SoftwareRenderer(window)
               world.add_system(spriterenderer)
               input_array = []
               if "buttons" in CONFIG[joystick]:
                  for b in CONFIG[joystick]["buttons"]:
                     input_array.append(Button(world,factory,colors[b[0]],colors[b[1]],b[2],b[3],b[4],b[5]))
               if "dpads" in CONFIG[joystick]:
                  for b in CONFIG[joystick]["dpads"]:
                     input_array.append(Dpad(world,factory,colors[b[0]],colors[b[1]],b[2],b[3],b[4],b[5],b[6]))
               if "hats" in CONFIG[joystick]:
                  for b in CONFIG[joystick]["hats"]:
                     input_array.append(Hat(world, factory, colors[b[0]], colors[b[1]], b[2], b[3], b[4], b[5],b[6],b[7],b[8],b[9],b[10]))
               if "analogs" in CONFIG[joystick]:
                  for b in CONFIG[joystick]["analogs"]:
                     a = Analog(world, factory, colors[b[0]], colors[b[1]], b[2], b[3], b[4], b[5], b[6], b[7], b[8])
                     input_array.append(a)
                     for b in a.buttons:
                        input_array.append(b)
               if "triggers" in CONFIG[joystick]:
                  for b in CONFIG[joystick]["triggers"]:
                     input_array.append(Trigger(world, factory, spriterenderer, colors[b[0]], colors[b[1]], colors[b[2]], b[3], b[4], b[5], b[6], b[7], b[8], b[9]))
               #spriterenderer = SoftwareRenderer(window)
               #world.add_system(spriterenderer)
            #return 0
         elif event.type == SDL_JOYAXISMOTION:
            #self.axis[event.jaxis.axis] = event.jaxis.value
            #print(event.jaxis.axis,event.jaxis.value)
            status[event.jaxis.axis] = event.jaxis.value
            axis = event.jaxis.axis
            value = event.jaxis.value
            for q in input_array:
               if "dpad" == q.type and q.axis == axis:
                  q.react(CONFIG[joystick], axis, value)
               if "analog" == q.type and axis in q.axes:
                  response = q.react(CONFIG[joystick], axis, value)
                  #print(f"Coord of indicator: {response}")
               if "trigger" == q.type and axis == q.axis:
                  q.react(CONFIG[joystick], axis, value)
         elif event.type == SDL_JOYHATMOTION:
            #print(f"hat {event.jhat.hat},{event.jhat.value}")
            for q in input_array:
               if "hat" == q.type and q.hat == event.jhat.hat:
                  q.react(event.jhat.value)
         elif event.type == SDL_JOYBUTTONDOWN:
            #self.button[event.jbutton.button] = True
            print(f"{event.jbutton.button} down")
            b = event.jbutton.button
            for q in input_array:
               if "button" == q.type and b == q.button:
                  q.on()
         elif event.type == SDL_JOYBUTTONUP:
            #self.button[event.jbutton.button] = False
            print(f"{event.jbutton.button} up")
            b = event.jbutton.button
            for q in input_array:
               if "button" == q.type and b == q.button:
                  q.off()
         elif event.type == sdl2.SDL_QUIT:
            window.close()
            print("quitting...")
            running = False
            break
         else:
            # file /usr/lib/python3/dist-packages/sdl2/events.py
            # 1536 = SDL_JOYAXISMOTION but gets thrown separately too?
            # 1024 = SDL_MOUSEMOTION
            #if event.type not in [1536, 1538, 1024]:
            print(f"Unknown event {event.type}")
      result = "\r"
      for s in sorted(status):
         result += f"{s}: {status[s]:06} "
      print(result, end=" ")
      world.process()
   return 0

if __name__ == "__main__":
   running_main = True
   while running_main:
      if -1 == run():
         running_main = False
   sys.exit(0)
bgstack15