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 - Crimson Wizard

#1061
About using GUIDs in saves, in general. I had a conversation about migrating save data with somebody several years ago, and they also proposed to use GUIDs for all things instead of relying on ScriptNames.

I was thinking about it, and came to a thought that using guids has one non-obvious downside.
When you have a ScriptName, it's easy to understand and edit by a human. GUIDs are not, they are suited for a one-time automatic assignment.

I imagined a situation, where user made a mistake and deleted a game object. Realizing this mistake, a user then recreated that object with the same script name. But if GUIDs are generated, the recreated object will have a different GUID. If guids are used to match data from restored saves, that action will unexpectedly break all older saves. In order to fix this, user will have to find out the GUID used before, and manually insert it into the data.

There may be a opposite situation: a user decided to use a game object for another role, and changed its script name, looks, and so on. But if saves rely on guids, then old data will "unexpectedly" load into the same object again.

Relying on ScriptNames seems to be simpler, because you may have to do manual instruction like "read data with name X into object with name Y" in case of a complex data migration. Of course you can do same with GUID, both are strings, but ScriptNames are human-readable, while GUIDs are not.

In a broader sense this is a topic of "how to tell what loads into what", and how to make this convenient for both engine and users.
#1062
Quote from: Snarky on Fri 23/08/2024 11:32:43One thing not quite clear from the description: Does this mean that any function that includes a waitfor statement must have WaitObject as its return type?

Apparently so, because this "waitfor" has to pause the current function and get out of it, returning a wait object, that would let resume same function.
But what else I forgot is that there has to be 2 types of "waits":
- one that pauses this function and returns a WaitObject back to external caller (that's what I've been talking about earlier);
- one that actually waits in place until an action is completed (aka "synchronous wait").

The latter is a advanced equivalent of this classic snippet which we use to block a function while using non-blocking commands:
Code: ags
while (player.Walking) {
   Wait(1);
}

So, perhaps "waitfor" keyword that I proposed earlier is not optimal, or perhaps it should mean the second kind of command instead, and the first (pause and exit coroutine) will need a better name.
#1063
Quote from: eri0o on Sat 07/09/2024 14:33:30I think only start and the update and finish can be the same as if it was called directly unless there's a situation I am not seeing.

This is a good example of why it is sometimes difficult to understand what you are saying.
Is it "start and the update and finish" - "can be the same as...", or
"only start and the update" (something about them?), and then "and finish can be the same as..."?
Punctuation, or splitting into two separate sentences could've helped. Here I had to spend time figuring this out, trying to fit in the context of the previous posts.

The rest also rather cryptic. I may make guesses, because I understand the concepts of asynchronous programming, and even then there's no guarantee that I understand precisely. But what about other people? This is "Beginners Technical Questions" forum.
#1064
Quote from: Snarky on Sat 07/09/2024 13:27:27What 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.

I suppose for getting most customization options there has to be 3 replaceable functions, which start, update and finish/cleanup background speech. Even if some of the default ones don't do anything.
#1065
If we speak about a generic scheduling module, I once worked on a project that had a "Sequencer" system for running background actions, and I did its restructuring into 2 or more components, where the base component only ran action state logic, updating in rep exec, and other components read the state of their respective groups of actions and performed necessary work.
The trick here was that each component had its own array of action parameters and states. This let to define each custom action's data in a clear way instead of breaking your head about how to pack it in a generic "actiondata" variable(s).

It was a part of someone else game, so I cannot give the exact code out (otoh... I am not sure if I still have it).

But I used similar idea with modules like TypedText, or DragDrop, where a base component does not know what is going on exactly and only sets "state" flags, and then other components do something based on that.

In simple terms, you have a Sequencer which stores an array of Commands for instance. These describe:
- string Command's group name (or component name)
- string Command's name
- Command's state (init, running, suspended, completed...)

Then you make more structs, like CharacterSequencer, which stores array of CharacterCommands.
The CharacterSequencer lets you to sequence character actions, it adds its CharacterCommand object into its array, and then adds a command in base Sequencer, linking CharacterCommand with base Command with an index.
For a dirty example:
Code: ags
int CharacterSequencer::AddCharCommand( Character *c, .... )
{
    int this_index = _nextFreeIndex;
    int base_cmd_index = Sequencer.AddCommand("charactergroup", cmd_name);
    CharCommands[this_index]._CmdIndex = use_cmd_index; // <--- bind charactercommand to a basic command
    CharCommands[this_index]._Char = c;
    CharCommands[this_index]._MoreParams = moreparams;
    return this_index;
}

And then someone may add more "sub-sequencer" components like that.


EDIT:
But frankly, no matter how you look at this, this is ugly, and only is because AGS script does not support proper inheritance and virtual methods (or function pointers).
#1066
Quote from: DiegoHolt on Sat 07/09/2024 04:00:26That's my attempt to add multiple and random answers at an interaction, but the thing is that sometimes none of the dialogs is displayed and it looks to the player like no action has ocurred. What am I missing here?

Random(N) function returns any number from 0 to N inclusive. You call Random(3), it may return 0,1,2,3.
https://adventuregamestudio.github.io/ags-manual/Globalfunctions_General.html#random
#1067
Could you open a video in VLC and look for "Tools -> Codec information", what does that page sais?

Which version of Krita do you use? I never used it myself, but I found documentation here:
https://docs.krita.org/en/reference_manual/render_animation.html
and it mentions that it may have extra options for each render type opened by [...] button.
#1068
I'd like to clarify: you are running the Animation1.ogv file in a video player and it displays correct colors?
Which export settings (encoding etc) do you use to convert this video to OGV?
#1069
I once wrote a AGS plugin that recorded input from a mic into the file (using SDL2), that in turn could be loaded an played in the game; so that's definitely doable.
#1070
Well, at this point this becomes a generic coding problem. You need to run two commands, but not immediately, because the second will overwrite the first one. Instead you need to run the second command after you learn that the first one completes.

Code: ags
  if (ttdraw.IsActive && ttdraw.IsIdle)
  {
    ttdraw.Clear();
    DrawingSurface *ds = papersprite.GetDrawingSurface();
    ds.Clear(65535);
    ds.Release();
    oPaper.Graphic = papersprite.Graphic;

    <------------ HERE
  }

How to do that?
Make an array of strings of certain limit.
Make a function that schedules next string.
Keep record of current string.


Following is the most basic example that I may come with. You may use this directly, or as a reference for your own code, and expand from there:
Code: ags
#define MAX_PAGES 100
String PageStrings[MAX_PAGES];
int TotalPages = 0; // remember how many pages scheduled
int CurrentPage = -1; // remember which is the current page

function ClearPages()
{
    TotalPages = 0;
    CurrentPage = -1;
}

function AddPage(String text)
{
    if (TotalPages == MAX_PAGES)
        return; // no more place to store pages
    PageStrings[TotalPages] = text;
    TotalPages++;
}

function StartTypeNextPage()
{
    if (CurrentPage < TotalPages - 1) // has more pages to display?
    {
        CurrentPage++;
        ttdraw.Start(PageStrings[CurrentPage]);
    }
}

And then in UpdateTypewriter():

Code: ags
  if (ttdraw.IsActive && ttdraw.IsIdle)
  {
    ttdraw.Clear();
    DrawingSurface *ds = papersprite.GetDrawingSurface();
    ds.Clear(65535);
    ds.Release();
    oPaper.Graphic = papersprite.Graphic;

    StartTypeNextPage();
  }


Use AddPage(text) to schedule a number of pages, then call StartTypeNextPage() to start with the first page whenever you need to.
#1071
Quote from: TypewriterTextLover on Thu 05/09/2024 18:58:23It also never enters a "wait for reader" status, although I'm not sure if that is an issue or not.

This "wait for reader" state is entered if the text ended typing, but the typewriter "thinks" that player did not finish reading yet. This is achieved by roughly calculating amount of time necessary to read whole text, multiplying text's length by TextReadTime parameter. If text is typed slower than the resulting reading time, then "waiting" state will never be entered.


Quote from: TypewriterTextLover on Thu 05/09/2024 18:58:23Here is what I changed it to, although it doesn't work:

Doesn't seem like ttdraw.Clear isn't doing what it needs to do here?

What does it not doing? what is the behavior you are trying to achieve with this code?

Note that Clear() does not clear the sprite, it resets the typewriter state. If you like sprite to be cleared, that would be:
Code: ags
    DrawingSurface *ds = papersprite.GetDrawingSurface();
    ds.Clear( some color );
    ds.Release();


EDIT: Also, you have this line with label text under "if (!idle)" condition, so it will never display idle off (if that matters).
#1072
Quote from: TypewriterTextLover on Thu 05/09/2024 16:17:23Thanks Crimson Wizard for the quick response and thank you for creating such a great module! This module has over 700,000 views which shows you how desired this feature is to people! The # 1 most viewed module!

Well, thank you, although even though there are many views, I never had statistics about actual number of uses.


Quote from: TypewriterTextLover on Thu 05/09/2024 16:17:23Is there any way to get the typewriter to be in a "Waiting for user" or "IsIdle" state? I have used Display commands to see if it ever goes in a Idle status and it does not. If the typewriter would idle, i could simply say if typewriter idle, then clear, and start typing new string.

Indeed, that's how it is supposed to work.
If you open the Demo Game that is distributed along with the module, you may add a small test for the big typewriter machine:
- Add some kind of a "status bar" gui with a label, for example on top of the screen.
- Put this inside "void UpdateTypewriter()" function in a room script:
Code: ags
Label1.Text = String.Format("ttdraw active: %d, idle %d, wait %d", ttdraw.IsActive, ttdraw.IsIdle, ttdraw.IsWaitingForReader);
- Then run the game and click on a typewriter's keyboard.
You will notice that:
- At game start "active" is false, and "idle" is true.
- After you start it, "active" becomes true and "idle false.
- As soon as it is done typing, "idle" becomes true.

It does work in the demo game. The question then is how to use this properly in your case.


Quote from: TypewriterTextLover on Thu 05/09/2024 16:17:23Another possible method maybe uses the "[" character. Is there way to say "if typewriter typed "[", then clear typewriter, and type new string? I believe there is a ttdraw.LastChar parameter that maybe useful?

I would not recommend that, the "[" character is used to wrap lines, and may be typed multiple times during the same text.
#1073
Quote from: TypewriterTextLover on Thu 05/09/2024 03:32:53- If the words are too many for the text box: stop, let user click when done reading, user clicks, text clears, and typewriter continues writing from top.
- Click on paper to skip to the next text section.

This module itself does not split text into "sections" or "pages", it has no such functionality. It only splits into lines according to the defined "width" of a text space.
If you like to display a longer text split into pages, where a text space is filled to max, waits, and then starts typing next page, then you should script this yourself on top of the typewriter object. You need some custom system that:
- accepts a input text
- calculates how much text lines can fit into the typewriter's space at once (see functions like GetTextWidth, GetTextHeight, etc)
- splits this input text into substrings, each representing a "page", and stores these in a String array
- remembers which page is currently being printed
- lets to order next page, in which case it stops current typing and starts it again with the next string from array.
- lets to know if this is the last page.


My memories of the demo game are rather vague (it's been several years), but I think I simply hardcoded these "pages" there, they are not calculated by size. So that approach from demo game won't be enough.
#1074
I suppose this would require to script a "system" of background talks. Store pointers to talking characters and their background speech overlay in a struct, and have array of such structs.
Make a function for adding background talks, which creates text, starts animation and stores character and overlay references in a free slot of this array.
Make a function for interrupting all the active background talks, which iterates over array, removes overlays, stops animations, and clears slots.
Finally, make a "Update" kind of function run from repeatedly_execute_always, which iterates this array, and tests if a background speech has timed out automatically, in which case stop animation and clear the particular slot.

I would not be surprised if there's already a script module that does this.


Of course it's a very interesting question why AGS does not provide "background speech with animation". In theory, this must be easily doable within the engine itself. I think one of the reasons to not do this unconditionally is that background speech is meant to be used also while character is walking, in which case you cannot automatically play speech animation. But there could have been a separate function, or an argument, that runs speech animation at background, at least until it is overridden by something else.
#1075
Quote from: DiegoHolt on Wed 04/09/2024 00:59:31Hi, I'm having the same problem here... how did you solve it?  ???

Clearly decide which cursors do you want to use in your game.
Disable all the other cursors, either by disabling "StandardMode" property in the editor, or using Mouse.DisableMode in game_start script function.
https://adventuregamestudio.github.io/ags-manual/Mouse.html#mousedisablemode

This will prevent AGS from switching to unused cursor after active inventory is removed.
#1076
Quote from: tongucci on Tue 03/09/2024 11:39:43Hi, I am a newbie here. Can I make a 1920x1080 or even a 4K res game on AGS? Or will it always be low res pixel art?

This is a technical question, rather than a art question.

But yes you can, and high-res games were or are made, see these for example:
https://www.adventuregamestudio.co.uk/forums/ags-games-in-production/rosewater-a-western-drama-set-in-the-world-of-lamplight-city/
https://www.adventuregamestudio.co.uk/forums/ags-games-in-production/old-skies-time-travel-adventure-by-wadjet-eye!-demo-available!/

But you should keep in mind that 2D pixel art does not downscale well, and so the bigger original resolution is, the more restricted system requirements will be. You can upscale 720p game to any bigger monitor, but you cannot downscale 4K game to a smaller monitor without making it look like a mess.
#1077
Quote from: eri0o on Tue 03/09/2024 18:01:05Regarding script modules, they have a unique ID which both exists in the XML and is written in the SCM file, I don't remember if the editor properly uses it when importing a module.

But it's not saved to a compiled script nor game data though, engine does not know anything about this GUID.
EDIT: I had to double check, and, yes, unfortunately it does not. Could be useful if it did though.
EDIT2: umh, actually, it's not a GUID, it's an integer key, created by a Random function, meaning it's not exactly unique. Frankly, i cannot tell what is its purpose anyway.

Quote from: eri0o on Tue 03/09/2024 18:01:05In theory we could make - at least from now forward - the plugins to also have an unique ID. We just add this and then ask the plugin authors to write a GUID there. It's less clean only because there isn't the automation from the Editor but in theory possible.

That would be great if it worked, but in reality will be prone to dev's negligence.
Just as an illustration to how devs treat this kind of thing:
- When several people made their custom builds of AGS engine, they did not tag it as "custom", even though such field exists in the engine, and left version number unchanged. As a result, with these you cannot tell if the game you're running uses official engine or not.
- When "Clifftop Games" devs made custom variant of SpriteFont plugin, which works differently from original, they did not think to change its name (even like "spritefont2" or something), so now the engine has to guess which plugin to emulate based on each individual game's GUID.



Regarding everything else, I mentioned this in the beginning of this thread, and in PR, that what I currently do is meant as a short term and minimal solution for patching the games.
Not a big game update with new chapters and such, but something that would just let users to release a patch containing few more script variables, new controls on guis, a couple new inventory items to fill game logic's loophole, that sort of thing.
It's also meant as a last resort that a user could use if their game was already released and them forgot to plan save compatibility.

I doubt if that it will be just "easier" to have a custom save utilities, because it's not easy to develop and design such thing, and because it will require users to learn making their custom saves and migration process.
I DO believe that such system is doable, but I don't that it's easy. And that definitely will require more time to develop and flesh out, and document and write tutorials, and so forth.

Separating data between what is "game progress" and what is not will definitely be a key here though, that's absolutely true.
#1078
I posted this in PR recently, but I might repeat here for convenience.

While it's not quite possible to support auto loading old saves when the order of items changed, because unique ids are not enforced in AGS (and some objects dont have any), there are couple of things, for which it would make sense to make an effort and support their reordering. These are:
* script modules
* plugins

Sometimes a dev might want to add a extra feature to the game, and this feature is implemented by a script module or a plugin. Script Modules must be ordered in certain way in game because of their dependencies, and the order of plugins seems undefined.
Also, dev may want to remove or replace a plugin. Unlike other things in game, plugins is not something you like to keep when it's unused, as it's an extra dll coming along with the distributive.

Therefore, it would be nice to be able to make these two things independent of the order in saves.
In order to achieve that we'd need to ensure they are identified by a unique name.

Plugins already are, and their data is actually already written under their respective names in game saves since AGS 3.6.1. So it must be possible to remove plugins without breaking saves.
It's a shame that this is not how it was done from a start.

This leaves script modules.
Script modules also have name. Unfortunately it was never written into game saves. But it's possible to add starting with the next version.

However, everything comes at a price. And so if I do this, the downside will be that if you rename a script module, then it no longer will be restored from a older game save.
Supposedly this should not be a problem when patching the released game, as you don't normally rename something like scripts at that point.
But maybe someone can see potential troubles with that?
#1079
The rule of thumb is: when something depends on a condition, put only that under condition, and leave the rest out.

What does depend on a "page" condition? Only the range of items does. Therefore the only thing that should be done under "if(Tasklist_page == X)" is assigning "first item" and "end item" or "item count" variables.

Code: ags
int start_at;
int end_at;
if(Tasklist_page == 1) {
    start_at = 0;
    end_at = 5;
}
else if(Tasklist_page == 2) {
    start_at = 5;
    end_at = 10;
}
<...>

for (int i = start_at; i < TaskReceivedCount && i < end_at; i++)    
{
    int taskID = TaskReceived[i];
    lstTaskList.AddItem(GameTasks[taskID].Name);
}


But in your case even that is not necessary. As you may see, the start and end indexes have a linear dependency on Tasklist_page value. So the code becomes:

Code: ags
int start_at = Tasklist_page * 5;
int end_at = start_at + 5;

for (int i = start_at; i < TaskReceivedCount && i < end_at; i++)    
{
    int taskID = TaskReceived[i];
    lstTaskList.AddItem(GameTasks[taskID].Name);
}
#1080
I had to make a game and test this script to find mistakes.

The updated script:
Code: ags
function AddTask(int taskIndex, bool addSubtasks)
{
  TaskReceived[TaskReceivedCount] = taskIndex;
  TaskReceivedCount++;
  if (addSubtasks)
  {
    // add subtasks too (they have sequential indexes)
    for (int i = 0; i < GameTasks[taskIndex].NumSubTasks; i++)
    {
      TaskReceived[TaskReceivedCount] = taskIndex + 1 + i;
      TaskReceivedCount++;
    }
  }
}

function AddSubTask(int parentIndex, int subIndex)
{
  int insertAt = -1;
  for (int i = 0; i < TaskReceivedCount; i++)
  {
    if (TaskReceived[i] == parentIndex)
    {
      // Find if other subtasks are already added and skip these
      int maxSubs = GameTasks[parentIndex].NumSubTasks;
      for (insertAt = i + 1;
        (insertAt < TaskReceivedCount) && (TaskReceived[insertAt] > parentIndex && TaskReceived[insertAt] <= (parentIndex + maxSubs));
        insertAt++)
      {
      }
    }
  }

  // Copy everything to the right to free the index, in the reverse order
  for (int i = TaskReceivedCount; i > insertAt; i--)
  {
    TaskReceived[i] = TaskReceived[i - 1];
  }

  // And insert a subtask on its place
  TaskReceived[insertAt] = parentIndex + 1 + subIndex;
  TaskReceivedCount++;
}


Also, this will only work properly if you use AddSubTask when adding sub-tasks, and not AddTask.
Subtask's index is also 0-based:

Code: ags
  AddTask(0, false); // shopping
  AddSubTask(0, 1);  // dress
  AddSubTask(0, 2);  // carpet
  UpdateTasksGUI();
  
  AddSubTask(0, 0);  // food
  UpdateTasksGUI();
SMF spam blocked by CleanTalk