Menu

Show posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Show posts Menu

Messages - Snarky

#221
Quote from: eri0o on Mon 09/09/2024 15:25:59Anyway, this may take a little bit I will get there, sorry for the wait.

No worries! I've made the fixes for the issues I encountered in my local copy, so they're not showstoppers. (I thought I'd even experiment with forking the repo and trying to submit my edits as pull requests.)

Ultimately I'm hoping to be able to do something like:

Code: ags
// In Player.asc, for a custom "Player" struct 
void LoadFromJson(this Player*, JsonParser* json, String path)
{
  if(json.KeyExists(path, this.Id))
  {
    JToken* t = json.Select(path, this.Id);
    this.name = t.StringValue("name");
    this.age = t.IntValue("age");
    this.leftHanded = t.BoolValue("left-handed");
    this.hairColor = HexToAgsColor(t.StringValue("hair-color"));
  }
}

// In GlobalScript.asc, on startup
bool LoadJsonConfig(String jsonString)
{
  JsonParser* json;
  // Omitted: parse the string

  // Load player config
  String path = "players";
  for(int i=0; i<playerCount; i++)
    player[i].LoadFromJson(json, json.SelectArrayIndex(path,i).path);

  // TODO: Load other stuff
  // ...
}

(I haven't thought through the precise API, but essentially I want to be able to freely select/search for particular keys within a particular token and its children and read the corresponding values.)
#222
OK, I spent some time delving into the code, and I found the source of the second bug. In MiniJsonParser::Init() we need to add:

Code: ags
  this._stk_keyidx_idx = 0;

I also think I probably should build the features I have in mind directly on top of JsonParser instead of MiniJsonParser, since JsonParser provides direct access to the parse tree. (Though it would be easy for MinJsonParser to do the same, simply by having MiniJsonParser.Init() return the _Tokens[] array.) My current thinking is that most of the functionality I'm looking for can be implemented as JsonToken extension methods.
#223
Quote from: DiegoHolt on Mon 09/09/2024 01:21:02Hi, Khris! I'm sorry, what I meant to say was that I was thinking that in the total numbers of the random integer (for example, 4) it only counted a total of FOUR, including the 0. (0,1,2,3 and that was my mistake)

That is a very reasonable assumption, and as I have argued previously, I feel that the fact that it doesn't work like that is a mistake in the AGS API.
#224
Hi @eri0o, this is good stuff!

I'm trying to use this parser, and I think I've come across a couple of bugs.

The first is that the parser chokes on horizontal tab (ASCII value 9). You have a case that supposedly treats "\t" as whitespace (two places in the code, lines 60 and 266), but it uses ASCII value 20 instead, which in the ASCII tables I see is "Device Control 4" (whatever that might be). (Edit: The codes for "\r" and "\n" also appear to be wrong, given as 18 and 14 instead of 13 and 10.)

The other is in MiniJsonParser. Basically, it seems that calling MiniJsonParser.Init() repeatedly on the same instance will not fully reset the parser.

If I call Init() once, do NextToken() until I hit a particular token, and then stop, if I then call Init() again, the parser now prepends the CurrentFullKey from the token I stopped at to the CurrentFullKey of every token on this parse.

If I on the first parse make sure to call NextToken() until I reach the end before calling Init() to do another parse, it crashes at some point during the second parse with an "Array index out of bounds" error on jsparser.asc:421 (from :492 in NextToken()).

I can't figure out exactly what the problem is, but clearly some state from the first run is preserved even after calling Init() again.

Actually, would it be possible to add an API call to reset the parser to start at the first token again without re-parsing the JSON string?

The background for my question is that I'm trying to build a more convenient layer on top of the parser, something like jq, where you can have random-access lookup of any key path, and not depend on a certain order in the JSON file. The simplest (though inefficient) way seemed to be to simply do a linear search for each key you try to look up, but that means resetting the parser each time. Hence my issue.

(Edit: I forgot I added some lines to the module script in order to get more helpful error messages. Corrected the line number references to match the official version.)
#225
The Rumpus Room / Re: What grinds my gears!
Sun 08/09/2024 15:27:33
I think it's because music taste tends to be popularly associated with tribal identity/subcultures to a greater extent than favorite books, movies or games.

If someone is a punk, a juggalo, a country music fan, an EDM fan, a rap fan, a Swiftie or a metalhead, others feel like that says something about who they are.

(I'll also argue that while many people listen to many different types of music, it's common for there to be some overall trends or tendencies that can be expressed in a few words. I'm sure if you've been following this thread, you have a pretty good idea of the tastes of the regular contributors. In my case, while the tracks I've posted range from Vivaldi to Ethiopian funk to Tom Lehrer, I think it's pretty clear that I'm mainly an indie/alternative rock/pop guy.)

I don't disagree that it's not much of a conversation starter, though. To get something out of it as a conversational gambit you probably have to do a two-way version of the old Emo Philips joke:


In other words, start broad and see how much common ground you share before your tastes diverge.
#226
Quote from: Crimson Wizard on Sat 07/09/2024 17:14:38No, it won't, as callback is a separate function with a separate local stack.

I realize that it won't unless AGS makes a special effort to make it happen, but if it was decided that this would be essential to make the feature usable, it would be possible to always make a copy of the local stack and attach it to the other function. But as I hinted, I don't think that's necessarily a good idea. (For one thing, there could be a lot of data in the local stack, for example arrays; and for another, if we think about generalizing this functionality there might be cases where the event never happens, so that the stack copy is never unwound.)

A way to explicitly copy data into the event handler, like the "capture" mechanism you mention, would probably be better. It might also be useful to be able to get a return value from the non-blocking function. The syntax might for example look like:

Code: ags
void TakeThingy(Object* thingy)
{
  oncomplete(bool arrived = cEgo.Walk(thingy.x, thingy.y, eNoBlock), Object* thingy = thingy)
  {
    if(arrived)
    {
      cEgo.Say("That looks useful!");
      cEgo.PickUp(thingy);
    }
  }
  Display("This will display before cEgo starts walking");
}

This assumes that Walk() now returns a bool for whether it reached its destination (i.e. wasn't either interrupted or met an obstacle). Alternatively, functions with a non-blocking option could all return some custom type; probably either an enum or a struct.

(And yes, I realize that the syntax here is a bit "dirty"/non-standard, and that there could be difficulties with the parsing since each "argument" is actually a variable assignment. Suggestions welcome.)
#227
I think I was the one who suggested waitfor, and I agree that it's probably not the best name.

I'm still not quite ready to abandon the idea of doing this in terms of events and anonymous event handlers. Here's a revised proposal for syntax I think wouldn't be too difficult for users to grasp:

Code: ags
void TakeThingy()
{
  oncomplete(cEgo.Walk(oThingy.x, oThingy.y, eNoBlock))
  {
    if(cEgo.x == oThingy.x && cEgo.y == oThingy.y)
    {
      cEgo.Say("That looks useful!");
      cEgo.PickUp(oThingy);
    }
  }
  Display("This will display before cEgo starts walking");
}

Here, the oncomplete keyword (replacing "waitfor") marks the following block as an event handler for the event that is raised once the non-blocking function completes. So that whole block is deferred, and the script continues to the next line below (here Display).

(There is a question of whether the event handler will be able to access local variables from the function in which it was declared. If so, I guess they have to be copied and stashed somewhere. It's probably easier to say no.)
#228
I feel like that goes into overengineering territory. What I have done in some modules is to factor out the calls to engine functions that users may want to replace into its own little function. So in this case, every call to SayBackground could be turned into a call to SayBackgroundBubble (or whatever) by changing one line.

(Ideally you would do it as a macro, but the AGS macro language isn't sufficiently powerful.)
#229
It seems to me that if there were an animated + optionally queued + optionally voiced background speech function, along with an API to interrupt a particular character's background speech or all ongoing background speeches, that's 99% of the problem solved. (The remaining one percent is to integrate it with other custom speech functions. Oh, and that you have to set the game.bgspeech_stay_on_display flag.)

I don't remember exactly what the different modules do, but I seem to recall there's one that does most of this (probably not voice, since there didn't use to be a way to play voice clips as audio clips).

Not sure I understand your point about repeatedly_execute, @eri0o. Just put a repexec in the module, with other APIs that control how it runs via flags, surely?
#230
The Rumpus Room / Re: What grinds my gears!
Sat 07/09/2024 07:25:01
While I'm on a roll about Very Important Issues, it grinds my gears when people nitpick or tediously explain that vampires would have a reflection, actually, because the reason they don't appear in mirrors is that they don't reflect in silver (being a symbol of purity and holiness), but modern mirrors no longer use silver for their reflective backing. As if that's some sort of law of physics.

Why don't vampires have a reflection? Because Bram Stoker made it up in Dracula and then others copied him! As far as anyone has been able to trace, it doesn't come from folklore, it's a modern pop culture trope. (Though it's a variation on the folkloric trope of supernatural beings that appear human but can be spotted by certain clues, and is probably inspired in part by other superstitions about mirrors—irrespective of material.)

Stoker never gives any kind of rationale or explanation for it in the book, but does mention that the Count and his brides cast no shadow either (a detail very much ignored by many movie adaptations), and his notes also mention that "painters cannot paint him—their likenesses always like some one else" and "Could not codak him—come out black or like skeleton corpse." So it's not something specific to mirrors (much less a particular type of mirror), but a general inability to capture an image of the monster. Also, it's worth mentioning that Stoker's Dracula is fine with silver: the first time Harker sees him in his own guise, he's carrying a silver lamp, and he also arranges silver cutlery for his guest.

The idea that it has anything to do with mirrors being silver-backed is merely imaginative speculation that has been incorporated as an explanation in some vampire stories; it's not a genuine fact or the real truth of the "phenomenon," and different stories play by different rules and use different explanations (or no explanation).
#231
AGS doesn't natively have any way to detect microphone input. You would need a plugin.
#232
The Rumpus Room / Re: What grinds my gears!
Thu 05/09/2024 23:45:02
Quote from: heltenjon on Thu 05/09/2024 20:26:05And perhaps in fiction it is, while in history most such ages are processes without clear beginnings and endings.

Well yeah, I'm talking about Tolkien here. And his ages are definitely bounded by specific, momentous events (or in one case, one of two events). These ages form the basis for the major calendars in his world, so they are pinpointed with some accuracy.
#233
The Rumpus Room / Re: What grinds my gears!
Thu 05/09/2024 17:49:33
What grinds my gears is that Tolkien's ages of the world make no sense.

First of all, there's a bunch of history before the first age. There's a lengthy prehistory during the creation of the world, but eventually, after one of the wars with Morgoth, Middle-Earth was created along with the two trees. But then there's about a thousand years before the first age starts!

The starting point is set to be the awakening of the elves (problematic since it isn't an exact known date, but whatevs). At least it makes some amount of sense since it's the beginning for the non-divine races.

But then there are two different opinions on what marks the end of the first age. In one view it is the destruction of the trees and the creation of the sun and moon, in another it's the final banishment of Morgoth after the war of the silmarils. That's almost six hundred years difference! The bulk of the Silmarillion happens in that gap!

And arguably they should both count as marking a new age, since they're both of cosmic significance. I mean: the creation of the sun?! Satan being physically expelled from the universe?! Big deals!

But the thing that really chafes me is the boundary between the second and third age. In the second age, Sauron tricks the Numenoreans into declaring war on Valinor, and the Valar destroy Numenor (apart from a few refugees) and change the flat disc-world into a round planet! (The last time in history they directly intervene on Middle-Earth.) Sauron's body is also destroyed, so that he's forever unable to take on a pleasing shape. But somehow that doesn't rate as a big enough event to mark a new age. Instead, we wait a few hundred years for the refugees to rebuild in Gondor and have yet another war in which they defeat Sauron (for the third time, I think, off the top of my head), though of course it's only a matter of time before he comes back.

How the hell is that a bigger deal than turning the damn world into a ball and airlifting heaven off the planet? (And also destroying Sauron?) Make it make sense, Johnny-boy!
#234
It's in TwoClickHandler:

Code: ags
bool check_show_distance(int y)
{
  if (y < popup_distance)
  {
    return true;
  }
  
  if (interface_inv != null &&
    y < FloatToInt(IntToFloat(interface_inv.Height) * popup_proportional, eRoundNearest))
  {
      return true;
  }

  return false;
}

bool check_hide_distance(int y)
{
  if (interface_inv == null)
  {
    return y > popup_distance;
  }
  
  return y > popup_distance &&
    y > interface_inv.Height &&
    y > FloatToInt(IntToFloat(interface_inv.Height) * popup_proportional, eRoundNearest);
}

Notice how check_show_distance uses "y < " tests while check_hide_distance uses "y > " tests. That means that the GUI will show itself if the cursor is above some threshold on the screen, and hide itself if it is below some other threshold. By changing this logic and calculations you can make it appear when the mouse is at the bottom of the screen instead.
#235
Pick one of them as the "primary" speaker. For the others, use SayBackground and animate them manually. Then remove the background speech and end the animation when the primary speech ends:

(Rough sketch without all the function arguments, for three characters)

Code: ags
o1 = c1.SayBackground();
c1.LockView();
c1.Animate();
o2 = c2.SayBackground();
c2.LockView();
c2.Animate();
game.bgspeech_stay_on_display = 1;

c3.Say();

o1.Remove();
o2.Remove();
c1.UnlockView();
c2.UnlockView();
game.bgspeech_stay_on_display = 0;

You can wrap up all of this into a convenient function if you're going to do it multiple times.
#236
Khris responded first, but it might still be useful to explain indentation. (That means when lines don't start out at the left edge, but are moved in by some number of spaces.)

Indentation is used in programming to help show the structure of the program. Basically, the rule is that whenever you have a bunch of lines (often called a "block") that are grouped together (or even if it's just a single line) under some kind of "header," you move them one tab to the right, and when the group of lines is over (when you get to the next "}"), you move back left. A "header" is anything that is followed by {} brackets, like a function header, an if condition, a for loop, etc. So by indenting the lines that belong under that header, it shows that those are "inside" the header (they're the "body" under the header). It's a lot like when you make bulleted lists, and have some bullets indented by one level: it means that those bullets belong together under the bullet above.

So, your code should be formatted like this:

Code: ags
function on_mouse_click(MouseButton button) {
  // called when a mouse button is clicked. button is either LEFT or RIGHT
  if (IsGamePaused() == 1) {
    // Game is paused, so do nothing (ie. don't allow mouse click)
  }
  else if (button == eMouseLeft) {
    Room.ProcessClick(mouse.x, mouse.y, mouse.Mode);
    if (button==eMouseLeftInv) {
       // inventory[game.inv_activated].RunInteraction(mouse.Mode);
      cXeno.Say("qualcosa triggera");
    }
  }
}

The first line starts out at the left edge because it isn't "under" or "inside" anything else. Then everything that is inside that function (the function body), between the {} brackets, is indented one tab (which is set to two spaces by default in AGS). Line 4 is inside an if-statement, so it's indented another level, but as soon as we leave that block, we go back to the normal indentation level for the function. Next, we have an else if-statement on line 6, which acts as a header for the block that follows (lines 7-11), which are therefore indented another level again. Inside of this block there's another if-statement with its own body, which is indented yet another level.

As Khris points out, what becomes clear when the code is formatted like this is that line 8,  "if (button==eMouseLeftInv)", is inside the condition (part of the body under the condition) on line 6, "else if (button == eMouseLeft)". So it will only reach this line if button == eMouseLeft, and then you're checking if button is a different value, eMouseLeftInv, which will necessarily be false.

The other part is that you need a line inside the "if (button==eMouseLeftInv)" condition to actually do what you want, namely to set the selected inventory item to be the active item:

Code: ags
    player.ActiveInventory = inventory[game.inv_activated];
#237
First of all, there is no code in your sample to handle clicking in the inventory GUI. Do you have "Override built-in inventory window click handling" set in the project settings?

If you do need to add code to make things happen when you click, there is another point: For historical reasons, on_mouse_click reports different values when it is clicked over inventory items, so instead of checking if (button == eMouseLeft) you have you use if (button == eMouseLeftInv).

Edit: Basically, you haven't done what Khris suggested earlier in the thread:

Quote from: Khris on Tue 03/09/2024 23:15:552) add your own eMouseLeftInv and eMouseRightInv blocks to the global on_mouse_click function
#238
Quote from: Crimson Wizard on Mon 02/09/2024 16:48:05
Quote from: Snarky on Mon 02/09/2024 16:37:31In LucasArts speech mode it is also possible to override the speech animation by setting each frame manually.

Does not it let to do this in any speech style?
Oh right, in Sierra style you have to change not Character.Frame, but Speech.PortraitOverlay.Graphic.

But yes, there are two alternatives in general:
- Use Animate command to run a loop, or chain loop after loop in a sequence;
- Change current view frame by frame. That is more low-level but has the most precise control.

Argh, I didn't know about Speech.PortraitOverlay.Graphic. Will have to fix TLS to use that. (Though I'm lowkey proud of the hack it uses currently: setting the speech view to a single-frame animation and changing the sprite in that view.)

Will Animate commands (on the speaking character) run during blocking speech?
#239
Quote from: Crimson Wizard on Mon 02/09/2024 15:58:21Default speech animation does not let specify exact frames (unless that's a lipsync, but that is a completely separate story).

In LucasArts speech mode it is also possible to override the speech animation by setting each frame manually. (That's what the TotalLipSync module does.) If you need advanced/flexible control over the animation, that would be a way to do it (and is probably easier than reimplementing speech). As CW says, the challenge is to define exactly what you need, plan and implement it.
#240
Write a helper method (or multiple methods) to do what you want.
SMF spam blocked by CleanTalk