Understanding the GameEngine
How To Build a Game
Text and Fonts
The Itty Bitty GameEngine -- Overview (You are here)
Your Own Java Game -- Step by step tutorial to build a simple "Pong" game
Class GameWgt -- The visual components of a GameEngine game
Overriding GameEvent -- The programmatic components of a GameEngine game
Widget Specification Text -- File format for GameMaker.txt and how to make your own
Useful Tech Issues -- Technology not specific to Game Engine, but it will help you to understand it
The same GameEngine is used for the user interface of a "GameMaker" tool for constructing the game widget specification of a user-defined video game. The student programmer can lay out a game using GameMaker, then add game-specific Java code to a subclass of the GameEvent class partially generated by GameMaker, then compile it with the GameEngine to be the game so designed.
All the GameEngine code is open-source Java for maximum benefit to the learning experience of beginning programmers. It is expected that this tool will define the third and final segment of an online "Learn Programming in Java" tutorial. The software is still under development. See the "Features" section for a (probably incomplete) list of components or functionality to be added or not yet fully operational.
GameEngine creates a Java "JFrame" window and manages the user interface for timing and keyboard/mouse interaction. The frame rate is set to a leisurely 10fps so that inefficient user code is less likely to cause problems, but this can be adjusted up or down as desired, because the source code is fully available for inspection and modification.
A GameEvent class is defined for
the entire student API, so that all they need to do
is subclass GameEvent, then override
the methods that specify what the game should do in the case of various
events. These are described in the "Overriding
web page. Included in GameEvent are
some methods for initializing the game structure. Some of this information
is filled in by the GameMaker, so the you can focus your attention on the
essence of your game without worrying about low-level graphical and sequential
issues.. The API also defines numerous utility functions
for accessing various parts of the game engine to control its behavior.
Here are the error alerts GameEngine can give you:
You tried to give a name to your game that contains other than letters and digits.
You can use a widget name for only one widget. Try adding a number to the end.
When you choose a name for yout game and click the Build button, GameEngine creates a new text (Java) file with the generated code and stubs of the overridable methods for you to modify. Each time you click the Build button, GameEngine reads this file back in and tries to preserve your added code. However, if you modify the lines that are marked "Do not modify" then GameEngine will not be able to update the file while preserving your code. You can just delete the file (in the system File Explorer or OSX Finder) and then GameEngine will create a new one, or else you can move it out of the BlueJ project folder so that you can read it and copy your added code to the new created file).
This can only happen if something went badly worng. You might be able to save your working data ("GameMaker.txt" and whatever Java file you are currently working on -- you have backups, right? "Save early, save often" because Bad Things Happen) and re-install GameEngine from the most recent download.
0. Plan what your game should look like and how it should play. Sketch out your layout on paper if necessary, and write up a play book of how a typical game will proceed.
1. Open up GameMaker with a clean slate. If you have already done some work in it, there may be leftovers to be disposed. The simple way is to use your computer's file manager (Windows Explorer, or OSX Finder) to move the "GameMaker.txt" file to some other folder, or delete it, before you start GameMaker. See the detailed instructions in the "Getting Started with the Java GameEngine" section of the "Your Own Java Game" tutorial to start up GameMaker the first time.
2. Using the widget tool palette, drag widgets onto the game board. Select each one in turn and type in the configuration parameters for that kind of widget. The name you give to the game board widget will be the name of your subclass, for example (in the "Your Own Java Game" tutorial) I named the example "Pong" so GameMaker creates a "Pong.java" file for you to edit with your game semantics code.
3. Open your generated source file (for example, "Pong.java") and add the required Java code for your game semantics and behavior.
4. Compile your source file and run its main() program to play the game. You may need to iterate steps 2 & 3 several times to work out any kinks in your implementation of the initial design. Sometimes -- perhaps often -- it may be necessary to repair your design and start over from step 0 above.
When you click on one of the three tools, the default information for that tool is displayed above. The color swatch changes to match that tool's major color (black text, red ball, etc.) and it puts up the tool name. The size and location is pretty much stuck in its slot in the tool panel, but you can change the color and add flags -- and if it's text, you can also change the font and the actual text -- and when you click OK, those changes become permanent, so that they are replicated onto the game board every time you drag the tool over. Until you change them again. I think they also revert to the original settings next time you start the program.
Even if your widget is not visible on the game board -- perhaps behind some other widget -- you can select it and edit its properties by clicking on its name in the widget list on the right. When the game (the top widget in the list) itself is selected, there is another checkbox "Alert+InputDlog" which enables your game to have access to the built-in alert and user input dialog boxes.
To change the color of a widget, double-click the color swatch square and it will open an input dialog offering the current color value. Back up over what's there [Sorry, the text entry fields do not let you select the whole text for retyping, you need to use the backspace key; Real Soon Now] to type in your replacement color number. See "Useful Tech Issues: Color by the Numbers".
When the Text tool (or a widget it created) is selected, you can 2-click the text to put up an input dialog for you to enter something else as the text.
Once a widget tool has been dragged onto the game board, changing the position numbers will change the position of the widget, and changing the size numbers will change the size -- typically of the containing rectangle, but it does change the ball diameter to whatever known circle size is closest to what you asked for.
After you have instantiated a tool by dragging it onto the game board,
you should change the Name to something without a dot (if you want
your code to have access to it when the game is running) so it's a legal
Java variable name.
Bump (+256)With this flag enabled, you can get collision events when this widget overlaps another similarly Bump-able widget. See the Collided method for details. When the game (the top widget in the list) itself is selected, its Bump checkbox enables the Collided method to receive event notifications when a sprite strays off the edge of the game board.
Key (+512)With this flag enabled, you can get keyboard events when this widget has focus and the user types something at the keyboard. See the Keyboardable methods.
Click (+1024)With this flag enabled, you can get events when the user clicks this widget (mouse down, then up on the same widget). See the ClickEvt method for details.
Drag (+2048)With this flag enabled, you can get drag events, both when the user presses the mouse on this widget and then releases it, plus all the events related to dragging this widget to some other place. See the Dragable methods.
Rollover (+4096)With this flag enabled, you can get rollover events, starting when the mouse first enters this widget and then repeatedly until finally the mouse leaves it. See the Roll-able methods. [This has not been tested]
A GameSprit widget is essentially
the same as a group widget, except for the added velocity. Since all child
widgets are positioned relative to their parent, when the sprite moves,
all its child widgets move with it.
TimesThe default, it approximates the newspaper font by the same name, and is used in the list of widgets.
BoldThis is a thicker sanserif font used in the GameEngine button labels.
LargeThis is the same typeface as Bold, but larger.
MonoSpA monospaced font like the old typewriters, it has been crafted so that every character is at least slightly different from every other, and where it matters (like within hexadecimal numbers) significantly. It is the default used in GameEngine text input panels.
ItalThis resembles the italic face usually associated with Times Roman.
tinYThis is a smaller font, probably hard to read on the modern high-res screens. Note that the one-letter designator is not the first letter of its name.
NanoI think this is as small as you can make a readable font. I threw it in for fun, in case you want to put a "Santa Clause" (fine print, like legal documents nobody is expected to read, but they gotcha if you don't) around the edge of your game board.
ScoreThis font doesn't have the whole alphabet, but it's pretty large, so you can use it for game scores and stuff like that. It only has these characters (anything else will not be shown)! $ % ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 < = > ? A B C D E F O T X x
The "GameMaker.txt" file is reloaded the next time GameMaker starts up, so you can resume where you left off. If you copy the file off at various points during your game development, you can restore it from one of those copies, and thus revert to a previous checkpoint. It is a good idea to save what you are doing every hour or so (change the file name to reflect the time and/or what you were doing at that time), so that if Something Bad Happens (it will, count on it), you have not lost everything. I often say, "Save early, save often."
Once OK is dismissed, it is replaced with the Build button,
which saves the current game, both as a reloadable "GameMaker.txt"
file and also as a Java file with whatever name you gave your game, which
you can open in BlueJ and compile into GameEngine as your game.
On successful completion output of the Java file, you are offered a third
button "->JS" which (Real Soon Now, not the
Right now it doesn't do anything.
The default circles have diameters from 1 to 32 pixels, all filled.
If you want sa hollow circle you can overlay two circles of different sizes
and a common center, the smaller on top with the same color as the background,
or if you really need it to be transparent, you can set your own pixel
This same tool can fill the half-rectangle Over or uNder
the diagonal line by using the respective letters "o" or "n"
instead of thickness. You can butt triangles together to get arbitrary
polygonal shapes. For example, four half-square rectangles with their right
angles all adjacent will make a rotated square (diamond).
Wgt_Macros is the default macro definitions. For more information on the format of this array, see the "Widget Specification Text" documentation.
Following that are the (final=constant) arrays specifying various icon types mostly used in GameMaker, including five hard-coded circle diameters: 4,5,12,13, and 16. These should go away after I write algorithmic code to do circles.
The Antz array has 32x64 full-field diagonal bars, then another 192 1-bit ants, good for horizontal ants out to 256 (see Antsify). Mostly only the leftmost 8 pixels and the top-row (least significant bit) out to 263 are used.
Following the ants are seven pixel tables for the seven built-in fonts. For details on the font table format, see the "GameFont" widget type.
After a few more numbers mostly used for debugging, we have two large arrays, first thePixels defined to be as big as your game board (512x640 in Maker mode), then AllWgts, with space allocated for a thousand widget pointers. Any more than that and the mechanism will be too slow to be playable as a game.
I always try to declare all my data before all the code, but it doesn't work out that way very well in OOPS languages, where class definitions (containing method code) become data types for defining more data structures. I have some library utility functions I use all over the place (see the Method Summary for Useful Tools), and they get used in the embedded classes, so they come first. Then the widget subclasses, then the game code, which you can review in the documentation that follows next.
The only other structure of concern is the hierarchy of widgets. Games
are pretty flat for now. GameMaker's screen is divided into the game board
space on the left, and a tool panel on the right, pretty much WYSIWYG
(What You See Is What You Get), some entry fields for specifying the widget
particulars (some of them optional), and below that, a list of user widgets
so far on the right, and a column of tools on the left. See the "Widget
Specification Text" documentation for more discussion on the GameMaker
AddPair IffyStr RestOf StrLength CharAt NthItemOf SafeAryElt Substring Countem NthOffset SafeArySz TopBit CvInt2Str PairNeg SafeParseInt WriteWholeTextFile FormFixt ReadWholeTextFile SeeHex HexIfMore ReplacAll SignExtend
These next ("Game Operations") methods are useful within the context of a GameEngine game, so you usually need a reference to an object of the JavaGame class to use them, which is captured for you as instance variable "myGame" in overridden method GameList in the generated subclass of GameEvent. Except for the static methods (see above), to use these methods, just prefix the variable name "myGame." (including the period).
AntsIcon FindSubWgt KnownCircleSz SetInputText Antsify FloatDlogs LogAlWgts SetSwatchColor ChoseFont GameAlert LogWgtTree SpritePause ChoseRPS GameWinSize MkSpriteAnts StartGame CircleSize GetGameValue OpenInputDlog SwatchGetColor DlgEvData GetInputTxt ParseInFld GameWinTall FormFixt GetIxWgt ParsFixPt GameWinWide FindListWgt IsDlgEvent SafeWgtNum
The remaining ("GameMaker Methods") methods in this section are internal to the GameMaker subclass of GameEvent (which itself is internal to the JavaGame class), or else ("Others") internal to the operation of the GameEngine, and are included here for completeness. You normally do not need to use them in your own code. These ("GameMaker Methods") are in class GameMakerEv:
CapturCoords GotEscKey MakeUserJS NewListName CapturInfo2Wgt InvalidJava MakeWgtLst Prescan ChooseBtn IzTool MoveWgtUpDn SelectMe CliKnownStuff IzUsrWgt NameIsTaken ShoCoords DoMyButn IzzitMeta NeedsVelo ShoInfoFrom Fit3 LoadUserWgts NewData GotEnterKey MakeUserJava NewDragWgt
These ("Others") methods are internal to the JavaGame class:
AdjuDim Every100ms LocalizePos paint Ary2Str ImportGamList main ServerUpload BestCircData InstallMyEvts MakeJSwgts Str2intAry BuildThisMacro Int2BufImg MouseDispatch toJS CloneWgt IzLetter NetEncode TimerStart Collider JavaMouse NewArray Widen1stWgt Decapit JstartTimer NewGameWgt EncodeID KeyDispatch NewMaker
static int AddPair(int here, int thar)Adds two pairs of half-integers in a single integer, see also PairNeg.
Parameters here The first pair thar The second pair Returns The pair of sums
When you write programs that have a lot of data with medium-sized numbers (nothing bigger than a few thousand), and many of those numbers come in pairs, like vertical and horizontal coordinates, then it makes sense to pack each pair into a single integer. Besides, you only get one result value from a function call, and the function call overhead is much more expensive than packing and unpacking a pair of numbers. Unless it's really trivial like AddPair, which some compilers will back-substitute and eliminate all the overhead of the method call.
So the GameEngine has numerous widget methods that return two coordinates in a single integer. If you just want to do some adding or comparing (subtracting), it's often easier to do both numbers as a single integer. If there are no negatives involved, just to the math on the whole 32-bit integer. Otherwise, you can use AddPair and it gets the carry out of the low half right. To subtract, use PairNeg on the number pair you want to subtract, then AddPair. Or you can just tear the numbers apart, do the math, then put them back together again. See "Packed Numbers" in the Useful Tech page.
static char CharAt(int here, String aStr)A safe (no exceptions) non-OOPS way to extract a character from a string.
Parameters here The (0-base) character position aStr The string from which to extract that character Returns The character extracted, or '\0' if out of bounds
Java has an OOPS-based CharAt method, but it requires a valid object pointer to work correctly; otherwise it just crashes (throws an exception). A well-written program is not going to encounter this problem, so one hopes not to deal with it. But accidents happen. This function returns an easily tested default value when the parameters are out of bounds.
static int Countem(String aWord, String aStr)A safe (no exceptions) way to count the number of (non-overlapping) occurrences of a particular string within a larger string. An empty string cannot be counted.
Parameters aWord The string to look for aStr The string in which to find and count it Returns The number of occurrences found, or 0 if none
Mostly I use this for counting lines or commas, but sometimes a little cleverness will see other uses for it.
Parameters whom The integer Returns The same number as String
GameWgt FindListWgt(String msg)This is search tool to find uniquely identified widgets built from the list returned by the client GameList, so to assign references to them in variables usable in the game. Mostly it is called from pre-built Startup code.
Parameters msg the unique string to search for Returns the first widget specified in theImpoList with that string
static String FormFixt(String before, int whom)Formats a 16-bit integer as 8.8 fixed-point and returns it with a prefix.
Parameters before The prefix whom The number to be formatted Returns The result string
The GameSprit velocity vector is two 16-bit fixed-point velocity values packed into a single 32-bit integer. This method formats them as two numbers with decimal points, for use in console logs and other places where it could display in human-readable form.
static GameWgt GetIxWgt(int ix)GetIxWgt converts a widget reference number (RefNum) into a reference (pointer) to that widget. Every widget, upon creation, is added to a global array of widgets, AllWgts, indexed by the widget's RefNum. GetIxWgt fetches that reference.
Parameters ix an index into the AllWgts array = RefNum Returns the widget with that RefNum
static int GetMilliSecs()A language-neutral way to get a (32-bit) time in milliseconds. The first time this is called the caller sets TimeBase (initially =0), so that all subsequent calls are based off that start value, despite being less precision than normal Java.
Returns The number of milliseconds since game start
static String HexIfMore(String before, int whom, String after)Formats an integer as hexadecimal if larger than 32K and returns it with a prefix and suffix, for easier reading.
Parameters before The prefix whom The number to be formatted after The suffix Returns The result string
static String IffyStr(boolean whom, String tru, String fls)Strongly-typed (not overloaded) string selector. This is part of a collection of tools to build diagnostic print lines.
Parameters whom The selector tru Returned if whom=true fls Returned if whom=false Returns The selected string, tru if whom=true, otherwise fls
static String LogVH(String before, int whom, String after)Formats a 32-bit integer as two 16-bit values and returns it with a prefix and suffix. This is useful when printing numbers that pack two smaller integers (like widget coordinates) into a single number.
Parameters before The prefix whom The number to be formatted after The suffix Returns The combined string
static String NthItemOf(char delim, int whom, String aStr)A safe (no exceptions) way to extract an indexed item (or word or line) from a string. When getting words, excess white space is ignored, so the returned string is empty only when requesting a word beyond the last. Otherwise consecutive delimiters result in empty items being returned, so that you always get the nth item of a multi-item string. Items past the end or before the front (0 or less) are empty.
Parameters delim Use '\n' to get a whole line, ' ' to get a word whom The (1-base) item number to get aStr The string from which to extract that item Returns The extracted string
static int NthOffset(int whom, String aWord, String aStr)A safe (no exceptions) way to find the (0-based) offset of a particular string within a larger string, while ignoring zero or more initial (non-overlapping) occurrences. An empty string cannot be found.
Parameters whom The number of occurrences to skip over aWord The string to look for aStr The string in which to find it Returns The (indexOf) offset, or -1 if not found
static int PairNeg(int here)Returns in a single 32-bit integer the negatives of a pair of half-integers, see also AddPair.
Parameters here The pair Returns Its negative
GameEngine has numerous widget methods that return two coordinates in a single integer. If you just want to do some adding or comparing (subtracting), it's often easier to do both numbers as a single integer. When there are no negatives involved, just to the math on the whole 32-bit integer. Otherwise, you can use AddPair and it gets the carry out of the low half right. To subtract, use PairNeg on the number pair you want to subtract, then AddPair. Or you can just tear the numbers apart, do the math, then put them back together again. See "Packed Numbers" in the Useful Tech page.
static String ReadWholeTextFile(String filename)A safe way to read a whole text file. The line-ends in the input file are converted to '\n' regardless of how they are on the file.
Parameters filename The name of the text file Returns The text as read
static String ReplacAll(String nuly, String prio, String theText)A safe (non-OOPS, no exceptions) way to replace every instance of a particular string within a larger string. An empty string cannot be replaced.
Parameters nuly The replacement string prio The string to replace theText The string in which to make the substitutions Returns theText as changed
static String RestOf(int here, String aStr)A safe (no exceptions) way to get the rest of a string. Text past the end is considered empty.
Parameters here The (0-base) position to start aStr The string from which to extract that substring Returns The rest of the string, after here
static int SafeAryElt(int ix, int data, int defalt)A safe (no exceptions) way to get an element from a (possibly null) array. Returns a specified default value if the array is null or the given index is out of bounds. The magical index -1 with a default value also -1 returns the array size (if not null).
Parameters ix The index into the array data The array defalt A default value to return if can't access Returns Its length, or =0 if null
static int SafeArySz(int data)A safe (no exceptions) way to get the size of a (possibly null) array.
Parameters data The array Returns Its length, or =0 if null
static int SafeParseInt(String aStr)A safe (no exceptions) way to extract an integer number from a string. If the number begins with "0x" then it is assumed to be hexadecimal. Leading white space is ignored, and the first character that is not part of the number ends the scan.
Parameters aStr The string from which to extract that character Returns The number parsed, or 0 if none
static String SeeHex(String before, int whom, String after)Formats an integer as hexadecimal and returns it with a prefix and suffix. This is useful when printing numbers that you want to see the bits of.
Parameters before The prefix whom The number to be formatted after The suffix Returns The combined string
static int SignExtend(int here)Extracts and sign-extends the second (low end) of a pair of half-integers.
Parameters here The pair Returns The low half, sign-extended to a full integer
static int StrLength(String aStr)A safe (no exceptions) non-OOPS way to get the length of a string. Part of the reason for this is that GameEngine is multi-targeted, and this provides a consistent API for whichever language is being used.
Parameters aStr The string to get the length of Returns Its length, or 0 if null
static String Substring(int here, int lxx, String aStr)A safe (no exceptions) way to extract a substring. Text past the end is empty.
Parameters here The (0-base) position to start lxx The desired length (the result could be less) aStr The string from which to extract that Substring Returns The Substring
static int TopBit(int whom)Find the most significant non-zero bit in a 32-bit integer. The least significant bit is easily extracted by (whom&-whom) but there is no similarly trivial way to get the other end.
Parameters whom an integer Returns the most significant bit of that number, or =0 if none
static void WriteWholeTextFile(String filename, String data)A safe way to write a whole text file.
Parameters filename The name of the text file data The text to be written
boolean AntsIcon(boolean sho, GameWgt whom)There are two ways to display marching ants around the edge of a widget. AntsIcon is used for sprites with specially created animated icons (see MkSpriteAnts) that trace around the odd shape of a sprite.
Parameters sho true to display the ants, false to hide them whom the group or sprite widget to turn the ants on or off for Returns true if successful
If the group contains a group with ID 'AntI' it is assumed that AntsIcon can do it. Otherwise Antsify traces its boundary rectangle if it is a widget group head.
void Antsify(int why, String aLine)There are two ways to display marching ants around the edge of a widget. Antsify is used for group rectangles which can contain the predefined mAntsWgt group as a sub-widget. Each icon in the ants group is good for one vertical or horizontal line of ants, up to 32 pixels tall or up to 256 pixels wide, and they are mostly pre-positioned for all edges except the bottom and right. Antsify adjusts the positions of those two edges and hides (or shows) the rest as needed for the rectangle size.
Parameters sho true to display the ants, false to hide them whom the group or sprite widget to turn the ants on or off for
If the group contains a group with ID 'AntI' it is assumed that AntsIcon can do it.
int ChoseFont(GameWgt whom, int name)ChoseFont is a multi-purpose font selector.
Parameters whom the widget clicked on name either a font name (number) or (negative) RefNum Returns the chosen/converted font name/RefNum
If you pass it null for a widget, it converts one form of font selection -- such as the negative of an existing font RefNum, or the numeric (ASCII) value of the 1-letter font name -- into the other. The letter names are:Bold=66, Head=72, Italic=73, Monosp=77, Nano=78, Text=84, tinY=89If you give it a GameTxLn widget or a radio button group or the text tool or a user text widget on the game board and a negative number, it will return the numeric font name associated with that widget; if you give it the 1-letter name of a font, it will set that radio button or widget to that font.
int ChoseRPS(GameWgt whom, int doit)ChoseRPS is a RPS icon selector that works more or less like ChoseFont.
Parameters whom the RPS sprite widget doit either a setting number or (negative) to ask Returns the current selection number
These values: 0=none, 2=rock, 1=paper, 3=sci, 4=cloud, 7=all (6 shows cloud) in the 2nd parameter doit select that icon to display.
If you give it a RPS sprite widget or the RPS radio button group and a negative number, it will return the number associated with that widget; if you give it a positive number from the above list, it will set that radio button or widget.
If you give it a RPS sprite widget (tool or deployed) and a number greater than 8, the low 3 bits (3 or 4) selects that one layer to set this animation for, and the next bit (+8) makes it one-shot, and the next four bits is the actual number of frames (scissors=2, cloud=6) and the next six bits is the fractional frame rate, frames*16/100ms, then this call starts the animation on that layer (step size is assumed =32). The two animations of interest at this time are:
i = ChoseRPS(wgt,0x323); // scissors: snip, snip, ~1 sec cycle
i = ChoseRPS(wgt,0x86C); // exploding wgt cloud, ~1.2 sec total
i = ChoseRPS(wgt,0x034); // show small cloud frame
i = ChoseRPS(wgt,0x081); // hide paper, no effect on others..
// .. 0x82: rock, 0x83: scissors
i = ChoseRPS(wgt,0xCC9933); // change cloud color to gray-red (frex)
// rec'd animation color sequence: FF0, C93, A75, CA8, DDD
If you give it a RPS sprite widget (tool or deployed) and doit=-32 it will return the animation frame numbers of each of the four icons in the four bytes of the return value, low byte is the cloud, high byte is paper (should always be 0). If doit=-31 it returns the cloud color.
void CircleSize(GameWgt whom, int rx)Given a widget that is either a circle icon or a group over a circle icon, and a preferred diameter, choose the best circle size and update the widget (and its sub-widget, if so) to be that size, with a data array to match the selected size.
Parameters whom the widget to update rx the desired diameter
CircleSize is the public access to BestCircData, which does the work.
boolean MkSpriteAnts(GameWgt whom, boolean doit)There are two ways to display marching ants around the edge of a widget. AntsIcon is used for sprites with specially created animated icons that trace around the odd shape of the sprite. MkSpriteAnts creates those icons.
Parameters whom the sprite widget to create the ants for doit true to display the created ants, false to hide them Returns true if successful
The first step is to work through all the visible sub-widgets and accumulate their opaque pixels into an array of pixels.
Then the resulting blob of opaque pixels is examined from all sides to find the edges, in a second icon-ready array. This will be the icon data for the white outline [and for now, also the black outline that blinks on and off in lieu of ants].
Then [TBD RSN] the outline is traced and the number of pixels counted, so to divide them into an integral number of 6- to 10-pixel white/black ants (nominally four on then four off).
The outline is then traced again (nominally eight times) to turn off the white segments (the black is overlaid on the solid white outline, so the transparent pixels show white), but advanced one more step each iteration, so when the sequence is animated, the ants appear to march around the composite sprite body. [TBD RSN]
boolean SetInputText(GameWgt whom, String prompt, String data, boolean doit)SetInputText is a utility to set up an input macro panel with its prompt and predefined input text. Use GetInputTxt to capture the input text after the user has clicked "OK" or otherwise accepted it.
Parameters whom the group widget over a pair of GameTxLn widgets prompt the prompt text, or "" to leave it unchanged data the predefined input text doit true to set the predefined input Returns true if successful
String GetInputTxt(GameWgt whom)GetInputTxt is a utility to capture the input text after the user has clicked "OK" or partial input any time before then. Use SetInputText to set up the input panel with its prompt and predefined input text.
Parameters whom the group widget over at least a GameTxLn input widget Returns the text from that widget
int KnownCircleSz(int whom)This returns the nearest circle diameter, 0<whom<33.
Parameters whom the requested circle diameter, in pixels Returns the implemented diameter closest to whom
void LogAlWgts(String why)All known widgets are logged to the system console. Short prefixes are repeated every line, longer prefixes are replaced with ".. " after the first line.
Parameters why a prefix string inserted in front of first/each log line
void LogWgtTree(GameWgt whom, int deep)This prints out to the console log an (indented) tree representation of a specified widget with all its sub-widgets. It calls itself recursively with deeper indentations for the sub-widgets.
Parameters whom The pixel array deep Its height
int ParseInFld(GameWgt whom)Calls GetInputTxt to capture the input text, then converts it to a number using SafeParseInt.
Parameters whom the group widget over at least a GameTxLn input widget Returns the numeric (integer) value from that widget's text
int ParsFixPt(String whom)A safe (no exceptions) non-OOPS way to convert a text number into a fixed-point value with eights bits to the right of the binary point.
Parameters whom a string containing a number Returns the same number as a fixed-point integer, or =0 if fails
int SafeWgtNum(GameWgt whom)A safe (no exceptions) way get the reference number from a (possibly null) widget. If it is null, SafeWgtNum returns zero.
Parameters whom the widget, or null if none Returns that widget's RefNum, or =0 if none
boolean OpenInputDlog(String prompt, String data)Open the Input Dialog (macro) widget with a specified prompt and predefined input text.
Parameters prompt the prompt text data the predefined input text Returns true if successful, false if it failed
Floats alert & input dialogs to top of sprite list. Called once, either the first time one of these dialogs is opened, or else from your StartUp.
void GameAlert(String msg)Open the Alert (macro) widget with a specified message.
Parameters msg the message text
int IsDlgEvent(GameWgt whom, int vert, int horz)Determines if this ClickEvt was directed to an open Alert or Input Dialog, and returns 1 if the OK button was clicked, 2 if the Cancel button was clicked, otherwise 0 if the dialog is still open and used this event, or -1 if it's not an Alert or Input Dialog.
Parameters whom The widget that was clicked vert The vertical offset into this widget where it was clicked horz The horizontal offset into this widget Returns 1 if OK, 2 if Cancel, -1 if not Alert or Input, otherwise 0
void SetSwatchColor(GameWgt whom, int info)One of the features of the GameMaker tool palette is a color swatch representing the designated color of whatever tool or user game widget is currently selected, or else a checkerboard gray if no color is set. SetSwatchColor sets the swatch color to a numeric value, 0x00RRGGBB, or else -1 if none.
Parameters whom the group widget over a color swatch widget group resembling the one created by that macro info the color to set it to, 0x00RRGGBB or -1 if none
int SwatchGetColor(GameWgt whom)One of the features of the GameMaker tool palette is a color swatch representing the designated color of whatever tool or user game widget is currently selected, or else a checkerboard gray if no color is set. SwatchGetColor returns the currently visible swatch color as a number, 0x00RRGGBB, or else -1 if none.
Parameters whom the group widget over a color swatch widget group resembling the one created by that macro Returns the current swatch color, 0x00RRGGBB or -1 if none
String DlgEvData()Gets the current or most recent input text (see OpenInputDlog).
Returns the most recent text value from the Input Dialog
GameWgt FindSubWgt(GameWgt whom, int data, int deep)A search tool to find uniquely identified widgets based on their Ginfo values or a combination of type and info. The whom parameter is the widget to start this search, and is returned if it matches the criteria. Otherwise FindSubWgt searches its sub-widgets (if any) recursively.
Parameters whom the unique string to search for data the unique value to search for deep the current search depth, and if greater than 999, a unique logging identifier Returns the widget found that matches data
If the search number is larger than 16 bits, and it is equal to Ginfo of the widget, then that widget is returned. Otherwise the upper half of the search number is compared to the low 14 bits of Ginfo, and the lower half of the search number is compared to the type+plus flags for each respective widget. The usual search values are either a 4-char ID value, or else a GameTxLn with specified flags and a particular font #, so to pick out the parameters of a draggable widget or tool.
Null is returned if the specified widget is not found.
The code is not very robust, so sometimes it fails when the sought widget is actually there. It probably needs to be rewritten, but it seems to be good enough for now.
int GameWinSize()A safe (no exceptions) way to get the size of the game window.
Returns The (packed) window size = (height<<16)+width
int GameWinTall()A safe (no exceptions) way to get the height of the game window.
Returns The window height
int GameWinWide()A safe (no exceptions) way to get the width of the game window.
Returns The window width
void CapturCoords(GameWgt whom)Whenever you click on the OK button, CapturCoords is called to parse and copy the position and size values to the selected widget group, hopefully not a tool (but the caller should check) because the position of the tool group widget is fixed in its frame.
Parameters whom the selected widget
void CapturInfo2Wgt(GameWgt whom, boolean doit, boolean dont)Whenever you click on the OK button, CapturInfo2Wgt is called to copy the displayed attributes to the selected widget group.
Parameters whom the widget clicked on doit true to update position and size (not a tool) dont true to stop after updating the font
void ChooseBtn(int whom)There are four global response buttons in GameMaker: 1=OK, 2=Undo, 3=Build, and 4="->JS". ChooseBtn exposes one of the four response buttons and hides the others.
Parameters whom the chosen button 1..4
boolean CliKnownStuff(GameWgt whom, int vert, int horz)CliKnownStuff tries to find what widget got clicked among the known widgets it manages, such as a tool or a game widget or a radio button (checkboxes and buttons are handled elsewhere), and does it if so, otherwise returns false.
Parameters whom the widget clicked on Returns true if it succeeded
boolean DoMyButn(GameWgt whom)Where ChooseBtn selects one (or none) of four buttons for the user to click on, DoMyButn does whatever that button is specified to do when the user clicks on it, and returns true if it did it.
Parameters whom the widget clicked on Returns true if it did it, false if not one of these 4 buttons
String Fit3(int whom)GameMaker sizes and positions are limited to three decimal digits. Fit3 tries to format whatever number you give it into three characters.
Parameters whom the number to be formatted Returns the 3-digit text as formatted
void GotEnterKey(GameWgt whom)GotEnterKey is called when the user types the EnterKey, if FocusWgt is not null, or else if DefaultFocus is not null, and that widget accepts keystrokes. This override lets super handle an open alert or input dialog, and if true it acts accordingly. Otherwise it directs the action to whatever OK button is showing, or if none, then if the color swatch is selected, it opens the input dialog to accept a new color, or if a text tool or game widget is selected, it opens the input dialog to accept new text.
Parameters whom The widget that has focus Returns true if taken (by alert or input dlog)
void GotEscKey(GameWgt whom)GotEscKey is called when the user types the EscapeKey, if FocusWgt is not null, or else if DefaultFocus is not null, and that widget accepts keystrokes. This override also looks to see if a dialog is open, and closes it.
Parameters whom The widget that has focus
boolean InvalidJava(String aWord)InvalidJava tests if a name is valid Java for a variable name and returns true if not. Reserved words are thus disallowed for widget names in your game code.
Parameters aWord the proposed variable name Returns true if it's not valid
boolean IzTool(GameWgt whom)IzTool distinguishes between tools in the tool palette and other things the user might click on in a GameMaker window.
Parameters whom the widget clicked on Returns true if it's a tool
boolean IzUsrWgt(GameWgt whom)IzUsrWgt examines a group widget clicked on, and returns true if it's a widget already on the game board.
Parameters whom the widget clicked on Returns true if it's on the game board
boolean IzzitMeta(GameWgt whom, int prnt)When creating an exported widget list, it is important to ignore the "meta-widgets" which carry extra information needed for selection and dragging, but are not part of the user data. IzzitMeta returns true for those widgets that are not part of the user's game.
Parameters whom the widget being considered for export prnt the widget's presumed parent Returns true if it's a meta-widget
LoadUserWgts takes the import file "GameMaker.txt" (currently in instance variable AddUserWgts) and feeds it one line at a time to NewDragWgt to populate the game board as it was when the file was saved.
Parameters whom the user name, or none to ask for it
boolean MakeWgtLst()MakeWgtLst builds an exportable widget list in instance variable WgtList and writes it to file "GameMaker.txt", or else returns true to signal failure.
Returns true if it failed
void MoveWgtUpDn(GameWgt whom, boolean uppy)If this widget is a group child, move it up or down in its group's data.
Parameters whom The widget to be moved uppy true to move it up in the list
boolean NameIsTaken(String aWord)NameIsTaken compares a proposed variable name against those already used and returns true if it is taken. It is used with InvalidJava so that widget names in your game code will be consistent and valid Java.
Parameters aWord the proposed variable name Returns true if it's taken
boolean NeedsVelo(GameWgt whom)NeedsVelo returns true if the tool or game board widget is a sprite (and therefore needs to have the velocity entry fields shown).
Parameters whom the widget clicked on Returns true if it's a sprite
void NewData(boolean doit)NewData is informed when the user has made some change to the game which should be saved, and displays the OK button to do it.
Parameters doit true to force the condition
void NewDragWgt(String aLine, int lino)NewDragWgt creates a draggable game widget corresponding to a line from the saved file "GameMaker.txt" and updates its parameters from the attributes on the input line. This is more complicated than just creating a widget from the input line as normally happens at startup, because the items on the GameMaker game board have all the attributes of a tool, so that their meta-info can be displayed when selected.
Parameters aLine one line of import data to convert to game widget lino its line number
boolean NewListName(GameWgt whom)When a tool is dragged to the game board, NewListName is called to add its name to the list of user game widgets and then to select it.
Parameters whom the widget to be added to the list Returns true if it succeeded
int Prescan(GameWgt whom, int bitz, int omit)When preparing to export a widget list, it is necessary to pre-scan all candidates to find all fonts that might be in use, and to build a list of exportable widgets so they can be renumbered in the export file. Prescan calls itself recursively from the game list root. The listing is made in instance variable WgtMap, which is used by the caller to build an export file.
Parameters whom the root group widget to export bitz one bit for each known used font omit <0 (draggable group head) to search this level but not list Returns updated bitz
boolean SelectMe(int user, GameWgt whom)When the user clicks on a tool or game widget, SelectMe is called to put that widget's information up on the tool panel. It returns false if it is not a tool or game widget.
Parameters user >0 if it's a user game widget, =0 if a tool whom the widget clicked on Returns true if it's now selected, false if failed
void ShoCoords(GameWgt whom, boolean all)Whenever a tool or user widget is displayed or dragged, ShoCoords is called to put its position and size up on the tool pane.
Parameters whom the widget being shown all true forces display even if no obvious change
void ShoInfoFrom(GameWgt whom)Whenever a tool or user widget is selected, ShoInfoFrom is called to display it attributes.
Parameters whom the widget clicked on
int AdjuDim(int why, int thar, int norm, int act)An internal way to adjust one dimension of a newly created widget from its default (created) size to conform to the user-specified size of its containing widget.
Parameters why a reference number, for diagnostic log thar the default size in the macro norm the nominal size, against which to measure the default act the actual size supplied by the container Returns the calculated adjusted size = act-(norm-thar)
For a nominal size of (frex) 64, if the macro specifies 62, then the returned value is the actual container size reduced by -2 (the difference) unless the macro default is not close to the nominal (like being at the other end of the created widget).
String Ary2Str(int whom, int fold, char sep, String before, String after)A type-safe method to build a string representation of an array of integers. The string is folded into lines after a specified number of items, or else <80 unless sep is less than a space, in which case it is not folded.
Parameters whom The array to turn into a string fold The number of items per line, negative if the result should be quoted, or if unfolded then negative is the max length sep The character to separate items, (=space if unfolded) before The string to put at the front after The string to to put at the end Returns The resulting string
int BestCircData(GameWgt whom, int tall, int ins)Given a widget that is either a circle icon or a group over a circle icon, and a preferred diameter, choose the best circle size and update the widget (and its sub-widget, if so) to be that size, with a Gdata array chosen to match the selected size.
Parameters whom the widget to update tall the desired diameter ins if >0, the size of a space to center the widget in Returns the chosen size
BestCircData is recursive and private, it is expected that clients will call CircleSize and not BestCircData directly.
int BuildThisMacro(GameWgt whom, String aLine, int why)Widgets are specified by text returned by the user's override of the GameList method, each widget defined as ten or more integer numbers on a single line, the second number being a type number (plus flags). Type numbers >224 are deemed to be macros, where multiple widgets are created under a single group. BuildThisMacro adds them as sub-widgets to an existing group, whatever the specified macro calls for. For more information see the "Widget Specification Text" documentation.
Parameters whom the parent group widget to build this macro under aLine the text line containing parameters this macro can use why an error code base (the highest used in the caller) so that returned error codes (if any) will not collide Returns an error code > why, or else =0 if no error
GameWgt CloneWgt(GameWgt whom, int prnt)Recursively copy a widget with all its sub-widgets. This recognizes tool widgets as a special case, so that these differences happen:
Parameters whom a widget to copy prnt a parent reference number, or 0 to copy it from whom Returns the copied widget group, or null if it failed
(a) the parent is changed to be the Sprite list,
(b) the top coordinates are globalized,
(c) an unparented text (tool name) is preserved, and
(d) the ID info is changed to `**DW`
void Collider(GameWgt whom)Test for GameWgt collision. Only sprites can move on their own, so they only can collide, but only if collidable ("Bump") and only with objects marked collidable (+256).
Parameters whom A sprite or list that wants collision testing
String Decapit(String aLine)Safe (no exceptions) decapitalizer.
Parameters aLine The text to decapitalize Returns The decapitalized text
int EncodeID(String aLine)An internal way to construct a 32-bit integer representation of a 4-char string ID code. The code is enclosed in reverse-single-quotes `xxxx` in the input line, and if not there or invalid, 0 is returned.
Parameters aLine the string to find the code in Returns the calculated (integer) ID code
This is the timer-driven animation engine. The timer is started by TimerStart and fires ten times per second. This handler is responsible for redrawing the game board (see DrawMe), which also drives any animation.
void ImportGamList(int why, String aLine)
A text list of widgets is retrieved from the user's GameList method, and a complete set of game widgets is constructed from it. For more information on the format of this text list, see the "Widget Specification Text" documentation.
static int GetGameValue(int whom)GetGameValue is an access method to get one of the private data items from outside the JavaGame game instance. These are the defined access codes:
Parameters whom A selector, 0..9 Returns the selected value0..6: The current widget RefNum for one of the defined fonts (if installed) or else 0.The mouse position comes from the Java run-time in screen coordinates, which is quickly converted to window-relative before looking to see which widget it hits on. Because GameEngine defines the mouse coordinates as widget-relative, LocalizePos is called to do that conversion. If you need the mouse position relative to the window, you can call AddPrntsPos to convert it back, or you can reach down into the GameEngine to get MouseVert and MouseHorz (but they might have changed).0: MonoFontNo7: LocalVert, the other half of the result after calling LocalizePos
2: TinyFontNo ('Y')
8: MouseVert, the vertical component of the window-relative current mouse position
9: MouseHorz, the horizontal component of the window-relative current mouse position
10: DlogIsOpen, non-zero if the alert or input dialog box is currently open, zero if neither
The font widget numbers are necessary to display a GameTxLn in that font. Usually the font is set up in GameMaker when you choose it from the radio button, but if you want to change the font on the fly, you would need the widget number. Be sure that any font you want is used in your game somewhere when you Build it, or GameMaker will not include it in the widget load.
void InstallMyEvts(GameEvent whom)Install user event handler. StartGame, which is called by your main(), uses this to install your event handler. You should not try to change it.
Parameters whom A subclass of GameEvent that handles the desired events
BufferedImage Int2BufImg(int pixels, int width, int height)Converts a RGB pixel array to BufferedImage used in painting. This is Java magic adapted from example code found on StackExchange.
Parameters pixels The pixel array width Its width height Its height Returns The BufferedImage result
boolean IzLetter(int whom)Determines if the ASCII code of a character is a valid letter or digit. Only the original ASCII character set is supported. '_' is a letter.
Parameters whom the ASCII code of a character Returns true if it's a letter or digit
void JavaMouse(int whom, MouseEvent evt)JavaMouse accepts mouse activity from whatever source, translates the coordinates to window-content-relative, then directs it to MouseDispatch, which then calls your event handlers.
Parameters whom activity kind: 0=enter/roll, 1=press/click, 3=up, -1=exit evt some kind of Java magic containing the click location
void JstartTimer(int ms)Starts the game timer. See Every100ms.
Parameters why =100, nominally the number of milliseconds
void KeyDispatch(char xCh, int mods)KeyDispatch accepts keystroke activity from whatever source, then directs it to the KeyData event handler, which then calls your event handlers.
Parameters xCh the key code mods modifiers (unused, always =0)
@Override public void keyPressed(KeyEvent evt)
Accepts keystrokes to control action, because Java does not do these as keyTyped.
@Override public void keyTyped(KeyEvent evt)
Accepts keystrokes to input text.
@Override public void mouseClicked(MouseEvent evt)
Accepts clicks on the screen image to control operation.
int LocalizePos(GameWgt whom, int vert, int horz)Converts a mouse click position from window-relative coordinates to be relative to the widget clicked in. The calculated horizontal offset is returned directly, the vertical offset is cached in LocalVert for later recovery (see GetGameValue).
Parameters whom The widget that was clicked vert The vertical offset relative to the window horz The horizontal offset relative to the window Returns The horizontal offset into this widget
static void main()
This main should be called to run JavaGame in GameMaker mode. To run in user game mode, use the main() code in the generated Java file.
Parameters theText the widget list as returned by GameList Returns the text of an array of numbers for the JS GameEngine
void MouseDispatch(int msg, int vert, int horz)MouseDispatch accepts mouse activity from whatever source, then chooses one or more event handlers to direct it to.
Parameters msg a number representing what happened to the mouse: =0: rollover/idle, 1: mouse down, 2: drag, 3: mouse up, +4: right-click, +8* gives reason/source for log
@Override public void mouseEntered(MouseEvent evt)
Accepts clicks on screen image to control operation.
@Override public void mouseExited(MouseEvent evt)
Accepts clicks on screen image to control operation.
@Override public void mousePressed(MouseEvent evt)
Accepts clicks on screen image to control operation.
@Override public void mouseReleased(MouseEvent evt)
Accepts clicks on screen image to control operation.
String NetEncode(String theText)Converts the plain-text user name or JS code into a form suitable for upload over the internet. All non-letters are converted to the form %xx where xx is the hexadecimal code for that character.
Parameters theText the raw text Returns the same text as encoded
Parameters lxx The length desired Returns The array new int[lxx]
GameWgt NewGameWgt(int tye, int top, int left, int tall, int wide, int prnt, int color, int info, int data, GameWgt whom)Create a new widget with specified properties, or copy an existing widget, or apply the properties to an existing widget.
Parameters tye the type of the new widget, or -1 to clone whom top the vertical component of the top-left corner left the horizontal component of the top-left corner tall the vertical height wide the horizontal width prnt the parent reference number, or 0 if none color the widget color, or -1 if none info the information value data an array to use for its Gdata, or null if none whom a widget to copy or update, or null if none Returns the created widget, or null if it failed
The recommended way to make new widgets in a running game is to call NewGameWgt to clone an existing game widget (originally created in GameMaker), then to adjust the properties as needed. Don't forget to add your new widget to its parent's child list, or it will not display [this will become automatic in a future revision].
If the specified type is negative and the given widget is not null, then that widget is mined for the type and all the parameter values given as 0. In particular, a reference to the same data array is used in the copy, unless a non-null data is supplied. So if you want a nullGdata to replace an existing array in whom, or an actual value 0 for Gcolor or Parent when the original is non-zero, you need to fix it after the new widget is returned.
If the type (without flags) is greater than 7, then the only way to create a widget of that type is to create it yourself, then (optionally) call NewGameWgt to populate the base class instance variables in it.
GameMakerEv NewMaker()NewMaker is an access function to create a new GameMakerEv instance when JavaGame is started from its own main().
Returns the new GameMakerEv instance
@Override public void paint(Graphics graf)
All the heavy lifting happens here. Called on timer activation.
boolean SpritePause()SpritePause is called by sprite widgets to see if they should pause their motion (like when an alert or input dialog is open).
Returns true to pause
void StartGame(int tall, int wide, int color, String title, GameEvent evh)StartGame is called from either the user game's main() or else from JavaGame's main(), in either case with the desired window title, size, and base color, plus an instance of the GameEvent subclass that will be implementing the game (or GameMaker) logic. StartGame creates the specified window, builds all the widgets, calls the user StartUp method, then begins processing timer (and mouse and keyboard) events.
Parameters tall the window height wide the window width color the window background color title the window title evh the GameEvent subclass instance
int ServerUpload(String whom, String data)Uploads a particular user's (JS) game code to a game server.
Parameters whom the user name and password as name_password data the JS program as translated by toJS() Returns an error code, or =0 if none
int Str2intAry(int why, String aLine)A type-safe method to extract an array of integers from a string source text. Java Script is untyped, but this only makes int arrays.
Parameters why A reference number (for the log, if needed), who called it aLine The string to parse Returns The resulting array
Parameters theText the Java code fiName the game name, should = the class name Returns true if successful
void TimerStart(int why)Start the animation timer. See Every100ms.
Parameters why The initial timer count, >0 when animating
void Widen1stWgt(GameWgt whom, int plus)Widgets specified by macro do not always get their dimensions adjusted properly by AdjuDim. This is one of several ad-hoc fixups.
Parameters whom the parent group widget to look under plus how to adjust its size
The caller gives a negative number to adjust the child widget width down from its parent, then this is added to the group widget size to give a smaller (positive) number to set the size of the child widget.
String toString()Return the class name and RefNum for this widget, plus its ID code if any.
Returns The reference String for this widget
@Override public void windowClosing(WindowEvent evt)
Accepts window-about-to-close notification. (Not known to work)
GameEngine does not support the full range of UniCode characters, only the ASCII subset (' '..'~').
In this alpha release there are seven widget tools in the tool palette. I expect to add a scrollbar widget for building objects larger than the available window space; a full-featured sprite widget, complete with a pixel editor probably won't happen this year.
The only way to delete an unwanted widget is to drag it off the top or bottom of the game board (onto the gray background, where it will disappear) and then it will not show up in the Build, so when you reload (next time you start GameEngine) it will be gone. If you change your mind (before you quit), you can select the widget name in the list, then type in coordinates that are on the game board and click OK, and it will come back.
When typing stuff into a text field, there is no way to select a range to delete them, the work-around is to click to the right of what you want deleted, then back up over it, one character at a time. If you click or back up to the left edge the cursor may disappear; it's still there, but not drawn.
The tab key should advance to next entry field, but it doesn't yet. It doesn't know about the "forward delete" key, so you may get strange results. Too many things to fix in not enough time.
When you are modifying the properties of a widget, you can usually go from item to item in the entry panel entering new values, then click the OK button once to accept all those changes. However if you do anything that opens a dialog box, your changes may be lost. I recommend you "Save early, save often" to prevent losing your work.
Double-clicking a widget on the game board (or the board itself) jumps that widget so the top-left corner is where you clicked. This is a bug, to be fixed as soon as possible. Work-around: you get to reset the coordinates of your text widgets after changing the text. Don't 2-click the other widgets.
If you drag a widget from the tool panel, but off the game board, the OK button may stop appearing. Work-around: quit and restart GameMaker. It will revert to the last time you clicked OK.
If you want to discard your current game and you click on the Start Over button (and confirm it), it seems to get into an unstable state until you also click the OK button (and maybe also Quit and restart the GameEngine). Dunno yet what the problem is.
When you have more than 15 or 16 widgets, a scrollbar appears in the widget list. Mostly it works OK, but I have not yet connected the mouse scrollwheel to it. Also, it overlaps the widget list slightly, so if you click too near the left edge , GameMaker might think you meant to click the widget there.
So many bugs, so little time.
Rev. 2021 July 14