Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
Tags

persistent state of contraption widgets?

A topic by Louisono created Dec 23, 2023 Views: 146 Replies: 5
Viewing posts 1 to 3
(+1)

I'm making a contraption to help with making sprites. It consists of:

  • a "sprite" canvas, storing the picture for the sprite
  • a "hitbox" canvas storing position and size of the sprite's hitbox
  • a button to copy a part of the background of the card the contraption is on
  • an invisible slider with internal function (keeps track of whether the interacting user is setting the top left corner or bottom right corner of the hitbox)

I'm running into the following issue: I copy the background of a card into an instance of my contraption and set the hitbox by dragging and/or clicking on the larger "sprite" canvas. After saving and quitting decker, upon re-opening the deck my sprite (the picture part) is still there, however the hitbox is reset to prototype value... Any idea why the size & pos of this second canvas does not persist? For reference here's the contraption:

{contraption:sprite_maker}
size:[140,140]
resizable:1
margin:[0,0,0,27]
description:"make a sprite"
script:"sprite_maker.0"
attributes:{"name":[],"label":[],"type":[]}
{widgets}
sprite:{"type":"canvas","size":[134,107],"pos":[4,4],"locked":1,"script":"sprite_maker.1","show":"transparent","image":"%%IMG2AIYAawD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8AOg==","scale":1}
hitbox:{"type":"canvas","size":[22,36],"pos":[57,32],"locked":1,"script":"sprite_maker.2","show":"transparent","draggable":1,"scale":1}
copy:{"type":"button","size":[109,20],"pos":[10,116],"script":"sprite_maker.3","text":"copy background"}
click_field:{"type":"slider","size":[100,25],"pos":[125,113],"show":"none","interval":[0,1]}
{script:sprite_maker.0}
me.show:"transparent"
on get_sprite do
  sprite.copy[(0, 0) sprite.size]
end
on get_hitbox do
  offset:hitbox.pos
  size:hitbox.size
  ("offset", "size") dict (list offset), (list size)
end
{end}
{script:sprite_maker.1}
on click pos do
  if click_num[] = 0
    set_ul_corner[pos]
  elseif click_num[] = 1
    set_br_corner[pos]
  end
end
on click_num do
  click_field.value
end
on set_ul_corner pos do
  offset_pos:me.pos+pos
  hitbox.pos:offset_pos
  click_field.value:1
end
on set_br_corner pos do
  offset_pos:me.pos+pos
  hb_pos:hitbox.pos
  size:offset_pos-hb_pos
  if size[0] < 0
    hb_pos[0]:hb_pos[0]+size[0]
    size[0]:-size[0]
  end
  if size[1] < 0
    hb_pos[1]:hb_pos[1]+size[1]
    size[1]:-size[1]
  end
  hitbox.pos:hb_pos
  hitbox.size:size
  click_field.value:0
end
on drag pos do 
end
on release pos do
end
{end}
{script:sprite_maker.2}
on click pos do
end 
on drag pos do
end
on release pos do
end
{end}
{script:sprite_maker.3}
current_card:deck.card
on click do
  pos:sprite.offset
  size:sprite.size
  card_background:current_card.image
  cropped:card_background.copy[pos size]
  sprite.paste[cropped (0, 0)]
end
{end}
Developer (1 edit) (+1)

From the documentation of the Prototype Interface,

Modifying the attributes of a Prototype will automatically update Contraption instances in the current deck. [...]when a definition is updated, the nameposshowlockedanimatedfont, and script attributes of Contraptions will be preserved, as well the valuescrollrow and image attributes of the widgets they contain (as applicable) if they have been modified from their original values in the prototype, but everything else will be regenerated from the definition. The state of contraptions is kept, and the behavior and appearance is changed.

While it is not directly stated (and perhaps bears clarifying), the same rules apply when a deck is saved and restored; the "size" and "pos" attributes of internal widgets  are discarded and regenerated based on the layout defined in the Prototype. If these properties were persistent for contraption instances it would pose problems for altering the layout of the Prototype. These constraints can be a bit annoying to work around, but the design of Decker's Contraption mechanism has to square the circle between persisting essential state and inheriting changes from a Prototype.

You could solve this problem by keeping the source of truth for the dimensions of the bounding box in another hidden auxiliary widget; perhaps a field. When a contraption is resizable you also need to be rather careful about the image content of canvases, since layout could squish the canvas and crop its content smaller. For this reason, many of the contraptions in the Bazaar use rich text fields to stash one or more images persistently.

(+1)

aha I see, thanks!

So now I have added a widget to store the canvas position and size. I am running into another problem though, by default the placement and size of that canvas will be the one defined in the prototype so I have put some logic in the prototype's view function to apply the stored data persistence to the contraption. That seems to be working correctly as: when I navigate to the card containing my contraptions and enter "Interact" mode, I see the canvas getting in place properly. However I would like this to happen without having to navigate to that card at all (it's not a card meant to be seen by the user), so I'm trying to send a "view" event to the widgets from the deck's script

me.cards.sprites.widgets.sprite1.event["view"]

or even from the listener, while sitting on any card

deck.cards.sprites.widgets.sprite1.event["view"]

 but this doesn't result in anything. It seems like the event never reaches the widget.

Here's the newer version of the prototype, for reference:

{contraption:sprite_maker}
size:[140,140]
resizable:1
margin:[0,0,0,27]
description:"make a sprite"
script:"sprite_maker.0"
attributes:{"name":[],"label":[],"type":[]}
{widgets}
sprite:{"type":"canvas","size":[134,107],"pos":[4,4],"locked":1,"script":"sprite_maker.1","show":"transparent","image":"%%IMG2AIYAawD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8AOg==","scale":1}
hitbox:{"type":"canvas","size":[35,73],"pos":[47,16],"locked":1,"script":"sprite_maker.2","show":"transparent","draggable":1,"scale":1}
copy:{"type":"button","size":[109,20],"pos":[10,116],"script":"sprite_maker.3","text":"copy background"}
click_field:{"type":"slider","size":[100,25],"pos":[147,4],"show":"none","interval":[0,1]}
hb_persistence:{"type":"grid","size":[100,50],"pos":[147,35],"show":"none","value":{"x":[],"y":[],"width":[],"height":[]}}
{script:sprite_maker.0}
on init do
  me.show:"transparent"
  # if some hitbox is save -> load, otherwise -> leave as default
  if count (rows hb_persistence.value) > 0
    init_hb_pos[]
  end
end
on init_hb_pos do
  persistent_hb_infos:first (rows hb_persistence.value)
  hitbox.pos:(persistent_hb_infos.x, persistent_hb_infos.y)
  hitbox.size:(persistent_hb_infos.width, persistent_hb_infos.height)  
end
on view do
  init[]
end
on get_sprite do
  sprite.copy[(0, 0) sprite.size]
end
on get_hitbox do
  offset:hitbox.pos
  size:hitbox.size
  ("offset", "size") dict (list offset), (list size)
end
on get_hb_persistence do
  first (rows hb_persistence.value)
end
{end}
{script:sprite_maker.1}
on click pos do
  if click_num[] = 0
    set_ul_corner[pos]
  elseif click_num[] = 1
    set_br_corner[pos]
  end
end
on click_num do
  click_field.value
end
on set_hb_pos pos do
  hitbox.pos:pos
  hb_persistence.value:
    insert 
      x:pos[0] y:pos[1]
      width:hitbox.size[0] height:hitbox.size[1]     into 0 end on set_hb_size size do   hitbox.size:size   hb_persistence.value:     insert
      x:hitbox.pos[0] y:hitbox.pos[1]
      width:size[0] height:size[1]     into 0 end on set_ul_corner pos do   offset_pos:me.pos+pos   set_hb_pos[offset_pos]   click_field.value:1 end on set_br_corner pos do   offset_pos:me.pos+pos   hb_pos:hitbox.pos   size:offset_pos-hb_pos   if size[0] < 0     hb_pos[0]:hb_pos[0]+size[0]     size[0]:-size[0]   end   if size[1] < 0     hb_pos[1]:hb_pos[1]+size[1]     size[1]:-size[1]   end   set_hb_pos[hb_pos]   set_hb_size[size]   click_field.value:0 end on drag pos do end on release pos do end {end} {script:sprite_maker.2} on click pos do end on drag pos do end on release _ do   pos:hitbox.pos   hb_persistence.value:     insert       x:pos[0] y:pos[1]
      width:hitbox.size[0] height:hitbox.size[1]     into 0 end {end} {script:sprite_maker.3} current_card:deck.card on click do   pos:sprite.offset   size:sprite.size   card_background:current_card.image   cropped:card_background.copy[pos size]   sprite.paste[cropped (0, 0)] end {end}
Developer

As described in the v1.36 release notes, sending a "synthetic" event to a contraption instance would cause it to be handled by the contraption's "external" (per-instance) script.

The deck-level script, like most scripts, is not executed while Decker is not in Interact mode. The only opportunity for a Contraption's scripts to execute during editing is the code inside custom attribute handlers, which is capped within a very brief quota. In both cases, these constraints exist to prevent incomplete or malformed scripts from wedging Decker and rendering it unusable.

(1 edit)

got it! Thanks for replying my flurry of questions and giving me leads to overcome my issues :)

I'm documenting my fix here, in case someone has similar needs.

1. Adding a template script.

I have created a view function in the prototype's template script, calling me.init[], the function that uses internal widgets to restore persistent data

on view do
  me.init[]
end

2. Exposing the inner init function.

The init function is located in the prototype's script, so it cannot be reached from a contraption's script. It has to be exposed by creating a get_x function in that same script

on init do
  # here should be some logic to restore persistent data
end
on get_init do
  # expose init function, so that it can be called from outside the prototype's script
  init
end

With these two changes: every contraption made from that prototype is created with that view function, which lets me send "synthetic" view events & the way these events are processed is still relying on the prototype's functions.