Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] Use maps instead of strings for messages #7971

Draft
wants to merge 18 commits into
base: master
Choose a base branch
from

Conversation

csbisa
Copy link
Contributor

@csbisa csbisa commented Feb 22, 2025

This is a draft change for moving the message system to use maps instead of strings. The main intent is to enable localization of messages in the front-end by transforming those maps into strings, with some other benefits such as consistent templating for all messages. This is not a complete change -- I'm posting this to get some feedback on the approach and figure out what adaptions are needed in order to move forward with this change. (Please note the changes
are on a ~6 month old branch and will be rebased later.)

Today, the message that gets passed to the client is:
{:username <username> :text <string>}
which has been changed to:
{:username <username> :side <:corp/:runner> :type <TYPE> :cost {COST ...} :effect {EFFECT ...} <other-args ...> (:raw-text <string>)}
This is turned into a string in the user's language via render-map.

Type is more or less the main verb for the sentence ("use", "install", "rez", "steal", etc.), cost is self-explanatory, and effect is the result (i.e. anything that's not a cost). There's plenty of examples in test/cljc/i18n/en_test.clj (en: Add test for rendering).

The changes are:

  • Update the cljs side to accept maps and call out to render-map based on the language to get the string say: Return text as a map
  • A bunch of commits updating core functionality (basic action card, costs, etc) to use maps
  • Implement render-map for English en: render-map support
  • Update most SG card abilities to pass maps instead of strings [WIP] SG cards

I have a couple other changes (render-map for Japanese, more updates for NSG cards, etc.) but I'm excluding these for now to focus on the core changes.

As part of working through effects, I was poking around to see if it was possible to have effects generate messages instead of having to specify a :msg for every effect. More or less every effect should have a message (and we could just mark the ones that shouldn't as silent), so in theory it should be possible to coalesce and pass down effects -- instead of what you see here, which is more or less updating every single :msg in cards to follow an effect format. I haven't been able to figure out if it's actually practical to do this, so I've just been updating cards with the new format for messages.

This change is also backwards compatible. With the current approach, generated messages are still the same even if :msg hasn't been updated to the new format. If a string in {:msg ability} ends up at print-msg, it just stuffs that into :raw-text which is later appended to the transformed string with the type and cost. The only exception I've found so far is that some messages incorporate the cost (which is now a map) into :msg directly, which requires wrapping all such usage to transform the cost map back into a string (look at converting costs to strings for unconverted routines for an example). Other than a few edge cases that I haven't gotten around to fixing, the tests are passing.

And now onto the downsides of the current implementation:

  • People writing cards are going to have to figure out and remember how to write the messages. This is a lot more effort compared to just writing English. There'll need to some kind of tooling and/or documentation -- if anyone has ideas on a good approach for this I'm all ears.
  • Lack of compile-time checks, especially in effects (since it's handcrafted for every card and not generated), means it's very easy to make errors that are only caught at runtime. Tests have caught some of these mistakes but the test suite isn't exhaustive, so I'd like to have something in place to make this less error-prone.

This does not cover:

  • The other two major source of strings -- prompts and labels. These are separate undertakings.
  • Card titles, which are still passed down as strings. While I'd like to make this a bit better (i.e. pass down card IDs), the current front-end i18n logic does a fairly good job of translating those strings so I haven't bothered with this.

Other questionable things you might see when you're looking over the changes -- I'm aware of the following (and open to suggestions):

  • Using maps for costs/effects. While this is nice in terms of always enforcing a value is provided, it has a few flaws, such as not being able to order effects (so the print could come out with random ordering, i.e. 'end the run' not being the last effect) and overwriting values during merge for some cases (two separate effects with the same type should be summed, not overwritten). Using vectors might be better overall.
  • Really messy keyword handling as seen in src/cljc/i18n/en.cljc. I'm not an expert but I'm assuming messages are converted to another format (JSON?) when going to the client and it ends up turning a some (but not all) keywords into strings.
  • Having both map-msg and map-msg-apply is silly, I just need to bulk update everything to always provide a hashmap.

@NoahTheDuke
Copy link
Collaborator

This is an insane amount of work. I'm quite busy this weekend but will review on Monday!

Copy link
Collaborator

@NoahTheDuke NoahTheDuke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small things to think about

@@ -1225,7 +1224,7 @@
state side eid
{:msg (->> payment-result
(keep :paid/msg)
(enumerate-str))
(apply merge {}))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will overwrite, which you don't want.

(assert (= catch 'catch))
(assert (symbol? sym))
(if (cljs-env? &env)
`(try ~@try-body (~'catch js/Object ~sym ~@catch-body))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`(try ~@try-body (~'catch js/Object ~sym ~@catch-body))
`(try ~@try-body (~'catch :default ~sym ~@catch-body))

;; This is inverted -- corp forcing effect means it's forcing runner to take the effect.
(if (= (keyword side) :corp) "Runner" "Corp")
" to "
;; oh god
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lmao

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just as another note, in the current branch I've added an optional :display-side key that can be used to set the msg source without changing the side of the ability itself, and you can also just do :msg :cost to get something like player pays [cost] to satisfy [card].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that's a good call-out. It might be possible to pass down both the side and display-side and then use that to figure out if "force" wording should be applied. It looks like only a few cards use this right now (Wildcat Strike, Public Trail), from which it seems something like (when (and (= display-side :runner) (= side :corp)) <add force>) would work. I'll check some more cards to see if that logic holds up.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's the current usage because it's something I've ported over from playtest, but it will be more relevant for future cards (ie dawn/elevation, and the set coming after it). Anything which uses cost-option as part of the choose-one-helper will use this functionality.

For reference, wildcat strike would be force the runner to x, and public trail would be pays x to satisfy y

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public Trail (and other cards that would use "satisfy" wording) are easy enough to deal with. For Wildcat Strike, I came up with some code to be able to handle it, but looking into Funhouse, it doesn't seem like the logic translates well.

Funhouse (corp card, corp display side, runner selection) encounter effect:

  • force the runner to take 1 tag (Runner effect) on encountering it
  • end the run (Corp effect)

and its subroutine (corp card, corp display side, runner selection):

  • force the runner to pay 4c (Runner effect)
  • give the runner 1 tag (Corp effect)

I can't figure out a way to make display-side work with this nicely, since in theory the display-side would need to be for each individual effect, not the overall ability. I'd like to know your thoughts on how this should (or should not) extend to ice abilities. In essence, I need a way to indicate that it's the other side paying for it, which is why I currently have this <effect>-force keyword logic (which I don't like, for the record).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm - so if the ability is formatted specifically as {:cost ... :msg :cost}, then regardless of side, it's (active player) pays (cost) to satisfy (card). Is that helpful?

In any case, funhouse is done completely manually, and is very verbose, so that could simply be rewritten. I'll look at rewriting it a little later if you like.

This is the first phase of moving text rendering for log messages to
the client. The :text field now returns a map, which contains
the username (depending on the action) and raw-text, which contains
the text without the default "<username> <text>." rendering.

The client side in this change now takes care of the rendering of the
username (if present) and raw text.

Text generation will incrementally move into additional fields in this
map, leading to an incremental process where more and more logic can
be moved to the client.
The tests rely on the raw log state, which is now maps instead of
strings. To maintain compatability, all such functions now call
render-map to get the string that would otherwise be displayed.
currently dropping the sequential nature
furthermore, this uses merge, so it won't add credits and other stuff

tset pays 2 from Overclock and trashes Self-modifying Code to use Self-modifying Code to install a program from the stack.
tset pays 3 from Overclock and trashes Self-modifying Code to use Self-modifying Code to install Unity from the Stack.

this is example of redundancy that could be used to justify the split cost thing

more example output after the change:
tset trashes Simulchip, trashes 1 installed program (Gauss), and pays 0 to use Simulchip to install Gauss from the Heap (paying 3 less).
current output:
tset trashes Simulchip and trashes 1 installed program (Self-modifying Code), and then pays 0 , to use Simulchip to install Fermenter from the Heap (paying 3  less).

need to fix this properly
This is the equivalent of msg, except the output is a map instead of a
string.
pronoun subsitution isn't working quite right in test mode, still need
to figure that out
i think it's some missing local config since the test is technically
passing
but the lack of 'is' is making is ad
@csbisa
Copy link
Contributor Author

csbisa commented Feb 23, 2025

Just getting the rebase out of the way. No significant changes yet.

@@ -435,15 +435,10 @@
:else
(name-zone :runner (:previous-zone card))))
"")
pre-lhs (when (every? (complement empty?) [cost-str prepend-cost-str])
(str prepend-cost-str ", and then "))
;; currently loses ", and then" -- all costs are squashed into one
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fixable, but I want to check if people actually like the way this is. I'm sure there's a good reason for it, but it seems a bit contrived to do it this way instead of having two separate messages.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you give an example? what's wrong with the current message?

Copy link
Collaborator

@NBKelly NBKelly Feb 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there are two reasons for this:

  • I wanted to reduce clutter in the log. This is relevant for quite a few cards (ie SMC, tucana) - these used to be like 5 lines for one effect, now they're one concise and readable line (they're still technically two checkpoints, but there's maybe one card in the game that cares about that)
  • I wanted to make it so we aren't rewriting the same display code with every install. So you simply do (install ... {:msg-keys {...}}) and let that handle the formatting. Sooner or later I'm going to add this system to rezzing, and a few other areas too. (currently, if an effect rezzes a card, you will get several lines, one for the cost, one for the discount, one for the card being rezzed, and one for whatever the ability map does - when there should just be one)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This felt a bit complicated from the code POV, but if the reason is to reduce the number of logs, then that makes perfect sense. I'll fix this up so costs can be sequenced -- something like cost being a vector of map/vectors instead of a single map.

@NBKelly NBKelly marked this pull request as draft February 25, 2025 03:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants