Saturday 24 December 2022

A major milestone - on Xmas Eve no less!

I have finally reached a major milestone in the project; with the debugging of the high score entry screen, the transcoding from Z80 to 68K is essentially complete! 😀

High Score entry screen complete to round out the transcode!

I do know of a couple of minor bugs in the transcode, and lack of proper support for flipping of 2x2-tile sprites, but I've been focusing on getting the remainder of the code RE'd and converted to 68K.

Next task is to go back through the listing for both side-by-side, add or clarify any missing annotations, fix typos in labels etc, and identify and document any unused code. I also need to tidy up the memory variable area(s) and ensure they're initialised (to zero) in the right places. It's a bit of a dog's breakfast in the original code as there are so many different areas of shared RAM.

Then it'll be onto Neo Geo optimisation and getting it running at 100% on real hardware, with little or no disappearing (Neo Geo) sprites (due to scanline limit). Somewhat fortuitously, visible display masks and the foreground tile layer are the first to disappear, so at least the 'sprites' won't be affected. But of course ideally there will be no disappearing (Neo Geo) sprites at all!

I'm also working with another developer to get this running on the Amiga! Once the ball starts rolling I'm hoping there won't be a huge amount of effort required; it's basically 'just' replacing the Neo Geo-specific code with Amiga-specific code! Having said that, there's currently about 1,000 lines of Neo Geo-specific code... though in contrast about 7,000 lines of Xevious core code.

Due to circumstances beyond my control, I've had a little more time on holidays to work on Xevious, though - again - that may not be the case (hopefully) for the remainder of the holidays. Either way, I might take a bit of a break after finishing the transcode before I dive back into it.

Wednesday 21 December 2022

Light at the end of the tunnel - and it's bright!

I've found some time - somewhat unexpectedly - to work on Xevious on holidays...

I finally finished the flying enemies getting destroyed, with a doozy of a bug that again, I had to sleep on. Turned out to be an inadvertant re-use of d7 in an inner loop subroutine call.

And that was the last piece of actual gameplay code that remained to be transcoded. Or at least so I thought... turns out during subsequent play testing that the Bacura don't destroy the Solvalou yet! Oops, I'll have to find that!

I've also started on what should be the last remaining piece of 'housekeeping' code - the high score entry screen. Somehow I've managed to introduce a glitch in the in-game high score update which I've never seen before?!? The high score entry isn't complete, and it's not quite right, but probably only a half-hour effort remains.

EDIT: Wait, I haven't explicitly coded or tested a 2P game yet...

Despite all of the above, the transcode is very, very close to being 'finished'. And by that I mean a first-pass, unoptimised transcode. The MAIN and SUB ROM code shouldn't require much in the way of optimisation; it's mainly the Neo Geo code. Other than that, it's a case of going back through the Z80 listing, reviewing the 68K code, and of course a lot of play-testing. There are some bugs, but it's basically completely playable as-is.

No eye-candy today. Aside from high score entry there's not really much new to show anyway. In my next post I should be announcing a major milestone as described above!

UPDATE: Transcode is complete! Buggy, but complete.

UPDATE #2: More work on the high score entry screen. Almost working...

Thursday 15 December 2022

GAME OVER - for 2022 at least!

In the process of transcoding all the enemy AI logic, I ended up finishing off a few other housekeeping routines as the logic was embedded in them. Those routines comprised transition of one Solvalou to the next, and ending the game. Not quite 100% there as there's still no high score entry, but close now.

Game Over!

I also implemented the Solvalou being hit, exploding and ending the turn. A few bugs in the transcode made harder work of it than I'd hoped - including one I had to sleep on - but done and dusted now.

I also discovered that I haven't implemented the tile flip for 2x2 tile sprites completely. As the Solvalou explodes, the same sprites are displayed for a handful of frames and they are periodically flipped based on an asynchronous timer. I haven't tackled this just yet; I'll have to consider the best approach. I may even leave it until I work on optimising the Neo Geo code, as anything further I do now may end up getting re-written anyway.

I had actually looked at the destruction of the flying enemies first, but there was a snag I hadn't expected. It's just a technical detail concerning the difference in implementation between the Z80 and 68K. I just have to work out the optimal solution. Not a big deal, but finishing off the Solvalou seemed more straightforward. A moot point now as I have little option but to tackle it next.

EDIT: I forgot the Bacura! 😳

At a bit over 5,500 lines in the MAIN program, the 2:1 ratio of Z80 to 68K instructions is almost exactly right; I'm really getting down to the last few functions to be transcoded now. Annoying that I've pretty much run out of time to work on it this year, but sometimes you just have to take holidays. 😜

UPDATE: I managed to squeeze a bit more time on the transcode; some more housekeeping so that the high score is updated on-the-fly, and bonus Solvalou awarded according to dipswitch settings.

Bonus Solvalou at 20K and updated High Score

In the process I decided to byte-reverse the storage of all the scores in the game. It made the bonus calculations easier, without affecting the efficiency of any of the other routines.

Finally, I fixed a bug that has been in the transcode and eluding me for weeks now if not longer. It involves the score occasionally appearing as a solid black line, and after implementing high score update, it started occurring at the same time for the high score as well - so doubly annoying! A theory came to me out of the blue tonight - it turned out to be completely wrong, but by then I was determined to find the issue.

It was between the core and the Neo Geo layers; in the core's score display code (a BCD print routine) I was sending the tile code as a byte but the Neo Geo code was assuming the upper byte of the word was zero... which it was... most of the time... until it wasn't. So nice to have finally fixed it!

UPDATE: found a few free minutes to do the Bacura. I had to add support for double-height (only) sprites.

Bacura - the forgotten foe!

Wednesday 14 December 2022

Xevious reveals all its secrets.

I've spent the last few sessions trying to finish off the RE and the missing pieces of the AI puzzle.

As a result, the SUB CPU ROM is now 100% complete. All the variables related to the game logic have been understood, named and the routines documented. I could probably spend an hour or so just tidying things up, adding a bit more annotation, but it's essentially done.

Correspondingly, the MAIN CPU ROM is all-but-complete. I haven't actually gone through the disassembly with a fine-tooth comb just yet, but the only missing annotations should be the miscellaneous housekeeping such as score and bonus updates. If I had to guess, I'd say it's 95% complete now.

As for what I've loosely referred to as 'AI' - for want of a better (more concise) term - I believe I've now RE'd all the logic that controls the difficulty of the game. It's not quite as complex as the advertisements suggested back in the day, and I wonder if plenty of other games don't have similar logic built in?!?

At the core of the logic is a couple of tables. First is a table that contains pairs denoting the number of flying enemies and an offset into a 2nd table. That second table is a list of enemy types in (roughly) increasing order of difficulty. So Toroids tend to appear first, with Zoshis and Brag Zakatos appearing later.

The map data contains a 'place holder' object that indexes into the 1st table; for example it may specify 3 Toroids. However there is another variable that is added to the index read from the 1st table, and used for the 2nd table - that variable is what I've named enemy_AI_level. This variable starts out at 0, and is incremented throughout the game - the effect being that the latter index gets larger and larger, causing more potent enemies to appear.

The enemy_AI_level is increased using a few different mechanisms:

  • decremented at the start of each new Solvalou according to the STARTING LIVES dipswitch setting
  • incremented explicitly in the map data accoring to the DIFFICULTY dipswitch setting
  • incremented explicitly in the map data depending on the current score
Incidentally, the enemy_AI_level is decreased by 2 each time a Zolbak is destroyed.

The adjustment depending on the current score is interesting; the amount of adjustment is calculated by dividing the current score (in thousands) by the number of Solvalou that have been used thus far. IOW the average score per Solvalou for the current game. The adjustment factor is (seemingly) equal to the thousands of points scored per Solvalou, with an upper limit of 16, so if you average more than 10,000* points per Solvalou - which is pretty easy for a seasoned player - then the game will play at effective maximum difficulty for that dipswitch setting.

[* Technically the score is represented in BCD, whilst the division operates as if it were in binary. 9,000 points on 1 Solvalou ($09/1) results in an increment of 9, whilst 10,000 points ($10/1) results in an increment of 16. Confusing, but there's still a monotonic increase.]

The other piece of RE I finished today was the Bacura, which operate with their own set of variables, not affected by the enemy_AI_level but rather explicit values in the map data. I'm yet to implement any of the Bacura logic in the transcode; that's next!

Once I finish the Bacura code and debug a few minor issues in the flying enemies, it'll be a case of implementing the destruction of both flying enemies and the Solvalou, and then tidying up all the remaining housekeeping functions. Probably a week or so there, but I'm fast running out of time for the year.

The final stage (before sound) is then optimising the Neo Geo code, and getting it running at 100% on my real AES hardware!

UPDATE: enemy_AI_level is reduced at the start of each Solvalou, not adjusted at the start of each area.

Monday 12 December 2022

Come fly with me...

I've started on the flying enemies. As a direct result, the SUB CPU transcode is 99.99% complete. There's one small AI-related routine that I'll leave until I've RE'd the logic. Otherwise, the Toroid now appears!

The first AI-driven flying objects - Toroids

As I've mentioned before, the ground-based enemies are not affected by the adaptive AI; every enemy and even their firing frequency is fixed in the game, regardless of the DIFFICULTY dipswitch setting or how well the player is playing. It's the flying enemies that respond to these parameters.

I haven't RE'd all the AI logic just yet, but I do know roughly how the flying enemies are selected.

In addition to all the ground-based objects, the map data contains 'placeholder' objects which invoke different functions for the flying enemies. These include:

  • set the number and type of flying enemies explicitly
  • increment the AI level (using the DIFFICULTY dipswitch setting) and re-calculate the number and type of flying enemies
  • reset the number and type of flying enemies (stops new ones appearing)
The MAIN CPU ROM contains a table of flying enemy numbers and types in order of increasing difficulty; the 2nd function above uses the AI level to index into this table.

Once those variables have been set in the SUB CPU program as the map is being generated, the MAIN CPU program loops over a function that spawns flying enemies according to the current number and enemies type setting.

Obviously I had to implement these functions in order to have the Toroids appear, but I will note that I wasn't sure how/when those variables were being cleared. And when I was debugging the spawning, I actually couldn't find where they were being cleared - this meant that the routine was spawning flying enemies every single invocation - once every VBLANK!?!

So I went back to the arcade code and found exactly the same behaviour! And then it finally dawned on me... the number of flying enemies represents the number AT ANY ONE TIME, not the total number! So for example, 3 Toroids actually means 3 Toroids on-screen, and as soon as one is destroyed or leaves the screen, another one is spawned! FTR spawning an object that already exists has no effect.

I should have realised that a lot sooner, as there are a lot more Toroids than the handful that appear in the map data at the start of Area 1, for example. But I haven't been focusing on flying enemies for quite a number of weeks now.

Now that I understand this, I can proceed with transcoding more flying enemies.

FUN FACT: On the EASY dipswitch setting, the AI level increment used in the above function is set to 0. However it is actually incremented elsewhere, so it does get a little harder.

UPDATE: Toroid, Jara and (1 type of) Zoshi now complete. After some debugging, I'm confident now that the AI is levelling up correctly, as I see the same sequence and number of flying enemies as on the original. I'll have to ramp up the difficulty to HARDEST now to get all the enemies to appear sooner.

In the process I also did some miscellaneous code tidy-up - including the elimination of some duplicated inline code - and added the missing messages at the start of each Solvalou!

Didn't realise these messages were missing!

The MAIN program is around 5,000 lines now. If I'm using the metric that the 68K code is round 1/2 the number of lines of the Z80 code (~11,000 lines) it means I'm getting close to the end now. Aside from finishing off a handful of flying enemy object handlers, there's the destruction of said enemies (common routine), destruction of the Solvalou, and some miscellaneous housekeeping like zeroing variables, updating high scores, awarding bonuses based on score etc. And that's about it, except for sound!

UPDATE #2: I've been flying through the flying enemies and they're all transcoded now except for the Bacura (spinning mirrors) as they are handled in their own routine. There are a few minor bugs but they shouldn't be too difficult to fix. I will need a way of injecting specific enemies into the game at a certain point; that's what build options are for! I also need to document the behaviour of the different variants of certain enemies (Zoshi, Zakato, etc).

I've also done a little bit more of the AI RE in the process; I now know it keeps track of the number of Solvalou you have used in the game, and does some calculation with your score; I'm betting it's working out your average points per Solvalou - but yet to RE any further.

Saturday 10 December 2022

The hidden beauty of the Brag Spario and another bug!

Finally finished the transcode of the Andor Genesis (mothership) and its preceding entourage; the Sheonites, the Garu Zakato (big exploding bomb) and Brag Spario (chaser bullets). Oh and the escaping Bragza!

The Andor Genesis descends,
and the sprite-per-scanline limit is just being exceeded

Interesting things happen when you're invincible. Whenever I've played the game, when awaiting the Garu Zakato I've always lurked at the bottom of the screen in order to more easily dodge the bullets. The Brag Spario approach the Solvalou but disappear off the bottom of the screen when you dodge them.

Whilst tesing the transcode, I ventured right up to the top of the screen before the Garu Zakato exploded! Ordinarily, you'd be destroyed by the radiating bullets almost immediately. But being invincible, the Brag Spario started orbiting the Solvalou like a display of orbital mechanics! It looked pretty cool, but I suspected a bug in the transcode. However when I fired up the arcade original and tried the same with invincibility - the same thing happened!

The Brag Spario should have featured more prominently in the game, to show off their fancy flying!

I also found a bug in the game, and again, I verified it on the arcade original! When destroying the Andor Genesis core, you score more than the allotted 4,000 points! See below, where I scored 4,010.

Can't really show the Brag Spario in a screen shot,
so instead a blew up the Andor Genesis core

It all comes down to how the Andor Genesis is represented in the object table. Last post I mentioned it comprises 9 'body' objects, 5 objects for the 4 gun ports and central core, and a 15th 'master' object which initialises the other objects and is used as the reference for the sprite coordinates etc.

Well, the way the bombing code works is that whenever your bomb explodes, it loops through the first 16 objects (ground-based objects) in the object table, destroying any object within range of the explosion. This is how you can destroy multiple objects, such as adjacent Lograms and Zolbaks, on the ground with single bomb.

So when you bomb the core, it first matches the core object (object #14) and you score the allotted 4,000 points. But then it continues and also matches the master object (object #15). However the master object never initialises the POINTS value (incidentally, there is no value that represents 0 points). So you are awarded the point value for the last object to occupy that slot.

For the Andor Genesis in Area 9, the last object was a Zolbak in Area 8, and so you are awarded an additional 200 points. When I was testing in the above screenshot, I was starting in Area 9; no object had been previously assigned to that slot, so POINTS was 0, which represents 10 points. You may recall, this is the same reason the Easter Egg at the start of the game is also worth 10 points.

I guess this means I'm finally up to the flying objects. The handlers for these tend to be a little more complex due to the flight patterns and the AI that affects their behaviour. Should be fun... but I might see if I can finish off the SUB CPU completely first...

Friday 9 December 2022

The Mother of all Objects!

Finally debugged the Sheonites. Aside from more than the usual number of bugs in the transcode, I also updated the RE with more accurate subroutine descriptions as a result of my debugging. As it turns out, the left and right Sheonites are almost identical, except for the fact that when they 'combine' just before leaving, the left-hand Sheonite object is destroyed, and the right-hand Sheonite represents both as it shoots up the screen.

Sheonites done and debugged - the game
would never have been the same without them!

Having completed the Sheonites I moved on to the Andor Genesis (mothership). There was a bit of code in the SUB program that I had to transcode, and then work out which part(s) of the MAIN program handlers I had to implement first - the Andor Genesis comprises no less than 15 distinct objects in the object table.

In my RE up until now, I sped through the Andor Genesis handlers using 'place holder' names for all the parts. Now that I am implementing them one-by-one, I can actually back-annotate which part of the ship is handled by each object.

The ship comprises nine (9) 2x2-tile sprite 'body' objects (3x3), overlaid with 5 objects (sprites) for the turrets/reactors, and one 'master' object for the entity as a whole. I had to implement the latter object first, as all other objects reference that for state, position, etc. Thus far I have implemented the master (in part at least), the 9 'body' objects and one turret/reactor (bottom right).

Andor Genesis - partly complete

Xevious maintains a table of 64 objects - or rather a set of tables - that includes a table with the function handler address. As you'd expect, handlers (ordinarily) operate on the object with the same index as the handler is currently executing from. eg. A handler in slot #4 is operating on object #4.

The Andor Genesis, however, is unique in that a whole bunch of handlers - the 'body' handlers for example - don't reference the current object at all, but rather comprise hard-coded references to another object in the table. eg. The handler for object #1 (explicitly) modifies object #9 instead.

I can't say for certain why this has been coded this way, but it looks like they've had to reverse the order of objects in a hurry, and for some reason, they've chosen to do it this way. I should note that the lookup table for objects that comprise the Andor Genesis is actually in the SUB CPU ROM, so perhaps that has something to do with it?

Anyway, I've tried to remain true to the original code, and have preserved this aspect in the transcode. It did have the potential to mess things up a bit if the offending objects weren't all benign, but fortunately they don't call any common routines that expect the object pointer in A5 to be consistent with the index of the handler. Conversely, the not-so-benign turrets/reactors are in the right 'slot', so routines like firing at the Solvalou work properly without juggling registers.

I have seen the 96 sprites-per-line limit again with the Andor Genesis. In line with each turret there are actually 8 Neo Geo sprites, and that's without bullets. Line up the Solvalou to bomb a turret and that's another 4. Add a few bullets... that 20-sprite overhead is getting eaten up quickly.

I had the idea of turning off the middle of the tilemap layer during gameplay, as only the top 2 lines and bottom line or so is ever used during a game. A few more lines are used during attract mode, but it would at least give me another 12-30 sprites head-room...

Thursday 8 December 2022

Much ado about nothing!?!

Even more RE as I wade through the code for the Garu Zakato (big exploding bomb) and the Sheonites (spinning pyramids) that precede it.

The Garu Zakato explodes with 16 radial bullets
and 4 Brag Spario that target the Solvalou

I can't quite believe the amount of code - some 409 lines assembling to 610 bytes - devoted to two objects that have absolutely no bearing on the game whatsoever! Incidentally, the 68K code is almost exactly half that, some 200 lines.

For those not intimately familiar with Xevious, there are two Sheonites that appear to mirror each other perfectly. They decend from the top of the screen, 'dock' with the Solvalou for a handful of seconds, and the join together before shooting off the top of the screen again. They are indestructible, and serve no purpose whatsoever other than be something to look at.

One of the Sheonites appears to be implemented correctly,
the other, not so much...

It would be perfectly reasonable to assume that they are both handled by the one routine. But it's a strange mix; two independent routines that share a lot of (duplicated) code, but are oddly asymmetrical. And one of them is explicitly destroyed; the other left to be cleaned up after leaving the screen. I've yet to debug the transcoded routines completely, but it has left me scratching my head.

When I first implemented one of the Sheonites, it stubbornly refused to appear at all. Eventually I discovered that the object handler wasn't being called, and that was because a few objects were inadvertently skipped when the handlers were scheduled. In the process of looking for the bug, I decided I didn't quite like the way I'd written the round-robin scheduler, and in the process of cleaning it up and optimising it, I found the bug. Two birds with one stone...

I think I will tackle the Andor Genesis (mothership) next after debugging the other Sheonite. Part of that code comprises the last remaining section of the SUB CPU program to transcode. Nothing tricky at all, just lots of hard-coded references to object attributes.

Sunday 4 December 2022

More RE and more transcoding!

Some progress in small chunks over the weekend. I've now finished all the (23) ground-based objects, and moving on to the flying objects that are explicitly defined in the map data. These objects don't vary with the difficulty/AI level. So far I have done a lone Kapi, Terrazi, Torkan and currently working on the Garu Zakato (big exploding bomb in Area 9).

Garu Zakato WIP

I've been forced to do more of the RE in order to complete the implementation of the last few objects. So much of the code has been understood now, the last few routines are falling into place fairly easily. For instance the Garu Zakato loops through the bullet objects looking for 16 idle bullets, and initialises them in turn with every 2nd angle in the firing angle table; something I had no idea about until just a few days ago. It also creates a further 4 (flying) objects in the table - objects I had been unable to identify until now.

During my testing I've finally seen evidence of the Neo Geo's 96 sprites-per-scanline limit being exceeded. It's a bit of a false alarm atm, because I'm not optimising sprite usage, and the skipping of areas that produces the "bullet-hell" effect isn't realistic. The good news is that it will be very easy to identify if/when it happens, as the last sprites in the priority - the first ones that will not be rendered - are what I call the "visible display masking sprites" at top and bottom of screen, and mask the 'bouncing' effect of the scrolling background layer.

Saturday 3 December 2022

Transcoding is flying along!

Progress has been relatively quick through the ground-based objects; it has mostly been a case of filling out a few more simple instruction sequences for handlers that were half-finished. Most of the lower-level routines used in the handlers have already been transcoded and tested.

The first flying enemy has appeared; the lone Kapi in Area 7. Unlike almost every other flying enemy in the game, its appearance is not contingent on the difficulty level nor the AI and as such, it appears explicitly in the map data. Although not technically a ground-based object, it is handled in the same manner.

A lone Kapi appears in Area 7

A total of 21 objects now fully transcoded and tested. That includes no less than 9 variants of Grobda (tanks) exhibiting 7 distinct behaviours; I think there's just another 2 variants to code. And just the one (flying) object left until Area 7 (ground-based or more correctly explicitly from map data) is complete.


Friday 2 December 2022

Progress is on target!

I've fixed the shooting routines; now there's no doubt they're all targeting the Solvalou!

Those Derotas are on target!

The "bullet-hell" I was seeing had me concerned; it's not as if the firing frequency logic was all in one routine - every object has its own logic, even if it is essentially the same sequence of instructions. But how can that be wrong for every object?

Fortunately it wasn't a mystery for very long at all. The firing frequency for each of the ground-based objects is encoded within the map data, so that as you progress through the map, certain objects fire more frequently to make it harder.

Now I have a build option that lets me specify the starting level, so I can quickly test specific areas of the game. But when I start at a later level, the firing frequency parameters of some/all of the objects haven't been set (as those areas of the map have been skipped) and all hell breaks loose! If I start at level 1, the problem disappears.

I'll now continue with the next ground-based object in Area 5.

Fun fact: encoded within the map data is a parameter that determines at which row on the screen the ground-based objects will stop shooting. This is obviously another variable to temper the difficulty of the game as you progress through the areas.

The Garu Derota (large octogon) is the only grond-based object I've completed thus far that ignores this variable and does NOT stop shooting at the Solvalou until it scrolls off the screen!

A shot in the dark at how this all works!

Today I decided to implement bullets which meant going back to RE some of the code that calculates the bullet trajectory. TBH it did (and still does) my head in a bit; I understand what it's trying to do and roughly how it's doing it, but some of the finer details still escape me. That's not absolutely critical as long as I can replicate the effect of each instruction (or routine). I freely admit that I had to resort to this approach in a couple of low-level routines in Knight Lore, for example.

The lowest level routine takes the absolute values of a (dX,dY) pair between source and target and calculates the ratio of the smaller to the larger; IOW the tan of the angle in the 1st octant. That gets scaled from 0.0-1.0 (0-256) down to 0-32 which then indexes into a table that in turn returns another value (0-32) from a non-linear but monotonically non-decreasing distribution. Looking at a graph of this distribution (below) it doesn't make a lot of sense to me atm*. Then it adjusts this value for 2nd octant if applicable by examining whether dX or dY was larger, and finally adjusts for the correct quadrant by examing the signs of dX & dY in turn.

The function that is applied to the tan of an angle in an octant

* Does this function correct for pixel aspect ratio, so that bullets splay out at regular angles on the screen (and/or at the (purposeful) expense of accuracy)?

That final value is scaled (again) from 0-31, and used as an index into a table of (dX,dY) pairs for bullet trajectories. IOW, objects can fire at 32 different angles.

It has all been transcoded but not debugged; the trajectories are definitely not correct and the ground-based objects seem to almost always shoot in a SW direction regardless of where the Solvalou is located on the screen.

That Garu Derota is firing way too quickly -
Xevious is not supposed to be a bullet-hell!!!

Some objects also seem to be firing a lot more frequently than they should, but that's another issue.

On the note of firing frequency and AI: none of the gound-based objects' behaviour (for the objects I've completed at least) is contingent on the difficulty level set by the dipswitches, nor the adaptive AI logic that depends on the player's skill. The dipswitches and AI level only seem to affect the flying enemies.

When the RE is complete, I'll do a comprehensive analysis of the difficulty settings and the AI logic.

Until then, I need to debug the bullets and the firing logic of a few objects it seems...