Skip to navigation

The Sentinel J source

Name: GetObjectAngles [Show more] Type: Subroutine Category: 3D objects Summary: Calculate the angles and distances of the vector from the viewer to a specific object
Context: See this subroutine on its own page References: This subroutine is called as follows: * CheckEnemyGaze (Part 1 of 2) calls GetObjectAngles * DrawObject calls GetObjectAngles * GetObjVisibility calls GetObjectAngles

This routine calculates the following for an object: 1. objTypeToAnalyse: Set to the type of the object we are analysing. 2. xDelta(Hi Lo), yDelta(Hi Lo), zDelta(Hi Lo): Calculate the difference (the delta) in all three axes between the viewer and the object we are analysing, to give us the 3D vector from the viewer to the object. 3. angle(Hi Lo): Calculate the angle of the hypotenuse of the triangle formed by the x-axis and z-axis, which is the projection of the 3D vector from the viewer to the object down onto the ground plane (so imagine a light shining down from above, casting the vector's shadow onto the y = 0 plane, and that's the hypotenuse). 4. hypotenuse(Hi Lo): Set to the length of the hypotenuse in the above triangle, so that's the length of the 3D vector from the viewer to the object when projected down onto the ground plane. 5. objectViewYaw(Hi Lo): The angle of the hypotenuse is the yaw angle of the 3D vector from the viewer to the object we are analysing. We subtract the viewer's yaw angle and the yaw adjustment, and add half a screen width to get the yaw angle delta from the viewer's gaze to the object, relative to the viewer's gaze, i.e. the screen). This gives us the yaw angle relative to the view. You can think of this as the screen x-coordinate of the object, or how far the object appears from the left edge of the screen. 6. objectGazeYaw(Hi Lo): Set to the object's gaze relative to the viewer's gaze. 7. If this is the landscape preview, rotate the object to face forwards and scale it so it looks good. 8. Set objectAdjacent(Hi Lo) to hypotenuse(Hi Lo) so it can be used as the length of the adjacent side in the vertical right-angled triangle with the projected vector along the bottom and the vector from the viewer to the object as the hypotenuse. 9. Set objectOpposite(Hi Lo) to the length of the opposite side in the vertical right-angled triangle with the projected vector along the bottom and the vector from the viewer to the object as the hypotenuse.
Arguments: Y The number of the object to be analysed viewType Landscape preview flag: * Zero = this is the landscape preview, so rotate all the objects in the preview to face the viewer * Non-zero = this is not the landscape preview, so leave the objects alone viewingObject The viewing object
RTS \ This instruction appears to be unused .GetObjectAngles STY objectToAnalyse \ Set objectToAnalyse to the number of the object being \ analysed LDA objectTypes,Y \ Set objTypeToAnalyse to the type of the object being STA objTypeToAnalyse \ analysed \ We start by calculating the difference (the delta) in \ all three axes between the viewer and the object we \ are analysing, to give us the 3D vector from the \ viewer to the object LDX viewingObject \ Set X to the object number of the viewer JSR GetHorizontalDelta \ Calculate the following: \ \ xDelta(Hi Lo) = x-coordinate of object #Y \ - x-coordinate of object #X \ - xTitleOffset \ \ xDeltaAbsoluteHi = |xDeltaAbsoluteHi| \ \ zDelta(Hi Lo) = z-coordinate of object #Y \ - z-coordinate of object #X \ \ zDeltaAbsoluteHi = |zDeltaHi| \ \ So this calculates the difference in both horizontal \ axes between object #X (the viewer) and object #Y (the \ object being analysed) \ \ Note that xTitleOffset is zero during gameplay, and is \ only non-zero when we are drawing large 3D text on the \ title screen JSR GetVerticalDelta \ Calculate the following: \ \ yDelta(Hi Lo) = y-coordinate of object #Y \ - y-coordinate of object #X \ \ So this calculates the difference in altitude between \ object #X (the viewer) and object #Y (the object being \ analysed) \ We now have deltas for all three axes, so we now can \ calculate the angle of the hypotenuse of the triangle \ formed by the x-axis and z-axis, which is the \ projection of the 3D vector from the viewer to the \ object down onto the ground plane (so imagine a light \ shining down from above, casting the vector's shadow \ onto the y = 0 plane - that's the hypotenuse) JSR GetHypotenuseAngle \ Calculate the angle of the hypotenuse in the triangle \ with the following non-hypotenuse sides: \ \ * xDelta(Hi Lo) \ \ * zDelta(Hi Lo) \ \ and return the angle in angle(Hi Lo), the tangent in \ angleTangent, the length of the longer side in \ a(Hi Lo) and the length of the shorter side in \ b(Hi Lo) \ The angle of the hypotenuse is the yaw angle of the \ 3D vector from the viewer to the object we are \ analysing \ \ We also subtract the following: \ \ * objectYawAngle,X from the high byte so the result \ is the relative yaw angle from object #X (the \ viewer's gaze) to the object \ \ * yawAdjustmentLo from the low byte so if we are \ drawing an object for updating, we include the \ correct yaw offset to enable the object to be \ drawn flush with the left edge of the screen \ buffer (see the DrawUpdatedObject routine) \ \ and add the following: \ \ * (10 0) to add half a screen width (as the screen \ is 20 yaw angles wide) \ \ So the result in objectViewYaw(Hi Lo) is the yaw angle \ delta from the viewer's gaze to the object, plus \ adjustments and half a screen, to give us the yaw \ angle relative to the view \ \ You can think of this as the screen x-coordinate of \ the object, or how far the object appears from the \ left edge of the screen LDX viewingObject \ Set X to the object number of the viewer LDA angleLo \ Set objectViewYaw(Hi Lo) = SEC \ angle(Hi Lo) - (0 yawAdjustmentLo) SBC yawAdjustmentLo \ - (objectYawAngle,X 0) STA objectViewYawLo \ + (10 0) LDA angleHi SBC objectYawAngle,X CLC ADC #10 STA objectViewYawHi LDY objectToAnalyse \ Set Y to the number of the object being analysed LDA #0 \ Set objectGazeYaw(Hi Lo) = SEC \ (objectYawAngle,Y 0) - angle(Hi Lo) SBC angleLo \ STA objectGazeYawLo \ This is the difference in yaw angles between the LDA objectYawAngle,Y \ hypotenuse (i.e. the viewer's gaze) and the direction SBC angleHi \ in which the object is facing (i.e. the object's gaze STA objectGazeYawHi \ to wherever it is looking), so that's the object's \ gaze relative to the viewer's gaze JSR GetHypotenuse \ Calculate the length of the hypotenuse in the triangle \ with side lengths of a(Hi Lo) and b(Hi Lo) and angle \ angleTangent, which the call to GetHypotenuseAngle set \ to the values for the projection of the 3D vector from \ the viewer to the object down onto the ground plane \ \ The hypotenuse length is returned in hypotenuse(Hi Lo) LDA viewType \ If viewType is non-zero then this is not the landscape BNE oang1 \ preview, so jump to oang1 to skip the following \ This is the landscape preview, so we rotate the object \ to face towards the back of the landscape, so the \ Sentinel and sentries all neatly look out of the \ screen in the preview LDA #128 \ Set objectGazeYaw(Hi Lo) = (128 0) STA objectGazeYawHi \ LDA #0 \ The degree system in the Sentinel looks like this: STA objectGazeYawLo \ \ 0 \ -32 | +32 Overhead view of object \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ So this makes the object face directly out of the \ screen CPY #63 \ If we are analysing object #63, then this is either BEQ oang1 \ a block of 3D text or the Sentinel's tower, and the \ landscape preview doesn't use 3D text so it must be \ the Sentinel's tower, so jump to oang1 to skip the \ following \ \ This ensures that all objects apart from the tower \ are drawn at double size, so they stand out more \ clearly in the landscape preview (the tower is the \ biggest object by far, so doubling its size would \ make it too large) LSR hypotenuseHi \ Halve the hypotenuse length to make the object appear ROR hypotenuseLo \ to be half the distance away, so it is drawn at twice \ the size SEC \ Halve the object's altitude in yDelta(Hi Lo) for the ROR yDeltaHi \ same reason ROR yDeltaLo \ \ Because this is the landscape view, the viewer is very \ high above the landscape (as well as a long way in \ front of the front edge), so we know that \ yDelta(Hi Lo) will be negative, as it's calculated as \ the altitude of the object being analysed minus the \ altitude of the viewer, and the latter is higher than \ any object on the landscape \ \ So we can halve the value of yDelta(Hi Lo) by shifting \ a set bit into the top end of yDeltaHi, so we keep the \ sign correct LDA yDeltaLo \ Set yDelta(Hi Lo) = yDelta(Hi Lo) + 112 CLC \ ADC #112 \ Starting with the low bytes STA yDeltaLo BCC oang1 \ And then the high byte INC yDeltaHi \ \ This moves the Sentinel and sentries up so they stand \ out more clearly in the landscape preview, as \ otherwise they would appear to be sinking into the \ landscape rather than perching on top of it .oang1 LDA hypotenuseLo \ Set objectAdjacent(Hi Lo) to the length of the STA objectAdjacentLo \ hypotenuse, which is the length of the adjacent side LDA hypotenuseHi \ in the right-angled triangle with the vector from the STA objectAdjacentHi \ viewer to the object as the hypotenuse LDA yDeltaLo \ Set objectOpposite(Hi Lo) to the height of the object STA objectOppositeLo \ relative to the viewer, which is the length of the LDA yDeltaHi \ opposite side in the right-angled triangle with the STA objectOppositeHi \ vector from the viewer to the object as the hypotenuse RTS \ Return from the subroutine
Name: GetObjPointAngles [Show more] Type: Subroutine Category: Drawing objects Summary: Calculate the view-relative pitch and yaw angles of all the points in an object
Context: See this subroutine on its own page References: This subroutine is called as follows: * DrawObject calls GetObjPointAngles

This routine loops through each point in an object definition and calculates the following: 1. A: Calculate the yaw angle of the point, rotated by the rotation of the object itself (i.e. its gaze), so this is the yaw angle of the object point within the object, but with the correct rotation for the direction the object is facing. 2. xDelta(Hi Lo), zDelta(Hi Lo): Convert this yaw angle and the polar distance into x- and z-coordinates for the point in the y = 0 plane (i.e. on the ground) using cos(A) and sin(A) on a triangle with the line along the polar distance to the point as the hypotenuse. So these are the x- and z-coordinates of the object point within the object itself (i.e. relative to the object's origin and rotated to the correct gaze). 3. angle(Hi Lo): Calculate the angle of the right-angled triangle made up of the object point's object-relative x- and z-coordinates, to give us the angle of the vector from the object's origin to the to the point within the object. 4. drawViewYaw(Hi Lo): Rotate the yaw angle of the point within the object definition by the object's view-relative yaw angle to get the view-relative yaw angle of the point. This is the yaw angle of the vector from the viewer to the object point. 5. drawViewPitch(Hi Lo): Construct the vertical triangle that has the vector from the viewer to the point as the hypotenuse, the point altitude as the opposite side and the projection onto the ground of the vector as the adjacent side, and use this to calculate the pitch angle of the vector from the viewer to the object point.
.GetObjPointAngles LDX objTypeToAnalyse \ Set X to the number of the object we are drawing LDA #64 \ Set drawingTableIndex to 64 to use as the index into STA drawingTableIndex \ the drawing tables for the object we are drawing, so \ we store the results in the following drawing tables: \ \ drawViewYaw(Hi Lo) \ \ drawViewPitch(Hi Lo) \ \ Note that these drawing tables live just after the \ tile drawing tables in memory, and the index is set \ accordingly \ \ Specifically, the tile view angles is stored with a \ drawing index of 0 or 32, thus taking up the first 64 \ bytes of each table, while the object point angles \ are stored from drawing index 64 \ \ Each drawing table is 96 bytes in total, so they fit \ together nicely LDA objPointRange+1,X \ Set objectLastPoint to entry X + 1 from the table at STA objectLastPoint \ objPointRange, so this now contains the number of the \ first point for the next object, object #X + 1, which \ is one greater than the last point number for object \ #X, as the lists of object points are sequential (so \ object #0's points are first in the object data \ tables, then object #1's points, and so on) LDY objPointRange,X \ Set Y and pointNumber to entry X from the table at STY pointNumber \ objPointRange, so this now contains the number of the \ first point for object #X \ We now work through the points in the object, using Y \ as the point number and working through the points \ from first to last \ \ Object points are stored as polar coordinates plus a \ height, to define where they appear within the object \ \ The polar coordinate consists of a polar yaw angle and \ a polar distance (where these are relative to the gaze \ and origin of the object in the object's definition), \ which together define the position of the point on the \ ground plane (y = 0), as if we were looking at the \ object from above \ \ The height defines the y-coordinate of the point \ relative to the object's origin, so that's the height \ of the point within the object \ \ You can think of the polar coordinate as defining the \ positions of the points on a piece of paper, and the \ height then extruding those points up and down, away \ from the paper, to create a 3D model .obpt1 LDA objectGazeYawLo \ Set the following STA T \ LDA objectGazeYawHi \ (A T) = objectGazeYaw(Hi Lo) + yaw angle for point Y CLC \ ADC objPointYaw,Y \ So this is the yaw angle of point Y in the object, \ rotated by the rotation of the object itself (i.e. its \ gaze), so this is the yaw angle of the object point \ within the object, but with the correct rotation for \ the direction the object is facing JSR GetSineAndCosine \ Calculate the following: \ \ sinA = |sin(A T)| \ \ cosA = |cos(A T)| \ \ where (A T) is the yaw angle of the object point \ \ This also returns the sign of the result in H, as \ follows: \ \ * Bit 7 of H is the sign of sin(A) \ \ * Bit 6 of H is the sign of cos(A) \ \ where 0 = positive and 1 = negative LDY pointNumber \ Set U to the polar distance of the object point LDA objPointDistance,Y STA U LDA cosA \ Set (A T) = A * U JSR Multiply8x8 \ = distance * cos(A T) STA T \ Set (A T) = distance * cos(A T) / 256 LDA #0 \ \ This discards the fractional part of the result BIT H \ If bit 6 of H is clear then cos(A T) is positive and BVC obpt2 \ the result already has the correct sign, so jump to \ obpt2 to skip the following JSR Negate16Bit \ Otherwise cos(A T) needs to be negative, so set: \ \ (A T) = -(A T) \ \ So the result now has the correct sign .obpt2 STA U \ Set (U T) = (A T) \ = distance * cos(A T) \ \ So this is the length of the adjacent side in the \ triangle with the polar distance as the hypotenuse LDA objectAdjacentLo \ Set zDelta(Hi Lo) = objectAdjacent(Hi Lo) + (U T) CLC \ ADC T \ So this adds the adjacent side of the vector from the STA zDeltaLo \ viewer to the object, to the adjacent side of the LDA objectAdjacentHi \ polar coordinate of the object point ADC U \ STA zDeltaHi \ This gives the distance along the axis of the object \ point, i.e. the point's coordinate BPL obpt3 \ If the high byte of the result is positive, jump to \ obpt3 as the result in A is already correct for the \ absolute value (i.e. A = zDeltaHi = |zDeltaHi|) LDA #0 \ Negate zDelta(Hi Lo) so it contains the absolute value SEC \ of the coordinate SBC zDeltaLo STA zDeltaLo LDA #0 SBC zDeltaHi .obpt3 \ As the angle of the triangle is at the viewer end of \ the hypotenuse, the length of the adjacent side is \ effectively the length of the side along the z-axis, \ so we can use the distance as a z-coordinate STA zDeltaAbsoluteHi \ Set zDeltaAbsoluteHi = |zDeltaHi| \ \ So we now have the absolute z-axis length in: \ \ (zDeltaAbsoluteHi zDeltaLo) \ \ and the original high byte of the signed z-axis length \ is still in zDeltaHi \ We now do a similar calculation for the x-axis, but \ using sin(A) instead of cos(A) to get the length of \ the opposite side of the triangle instead of the \ adjacent LDA objPointDistance,Y \ Set U to the polar distance of the object point STA U LDA sinA \ Set (A T) = A * U JSR Multiply8x8 \ = distance * sin(A) STA xDeltaLo \ Set (A xDeltaLo) = distance * sin(A) / 256 LDA #0 STA xDeltaAbsoluteHi \ Set xDeltaAbsoluteHi = 0 so it is correctly set to the \ high byte of (A xDeltaLo), as A is 0 LDA H \ Set xDeltaHi to the sign of sin(A T) from above, so STA xDeltaHi \ the calculation in GetHypotenuseAngle uses the correct \ sign for opposite side (only the sign bit from \ xDeltaHi is used in GetHypotenuseAngle, so this will \ work as bits 0 to 6 of xDeltaHi will be ignored) \ So we now have the x- and z-coordinates of the object \ point within the object itself (i.e. relative to the \ object's origin and rotated to the correct gaze), so \ we can calculate the angle in that triangle, to give \ us the yaw angle of the point relative to the object's \ origin JSR GetHypotenuseAngle \ Calculate the angle of the hypotenuse in the triangle \ with the following non-hypotenuse sides: \ \ * xDelta(Hi Lo) \ \ * zDelta(Hi Lo) \ \ and return the angle in angle(Hi Lo), the tangent in \ angleTangent, the length of the longer side in \ a(Hi Lo) and the length of the shorter side in \ b(Hi Lo) \ \ So angle(Hi Lo) is the angle of the right-angled \ triangle made up of the object point's object-relative \ x- and z-coordinates, i.e. the yaw angle of the point \ within the object, or the angle of the vector from the \ object's origin to the to the point within the object \ We now calculate the yaw and pitch angles of the point \ in terms of the 3D world, storing the results in the \ following tables: \ \ drawViewYaw(Hi Lo) \ \ drawViewPitch(Hi Lo) \ \ We do this by constructing two right-angled triangles, \ one flat on the ground (to calculate the yaw angle) \ and the other standing vertically (to calculate the \ pitch angle) \ \ Let's start with the triangle on the ground \ \ The object has a view-relative yaw angle of: \ \ objectViewYaw(Hi Lo) \ \ which was set up by the call to the GetObjectAngles \ routine before we called this routine \ \ This contains the yaw angle of the object relative to \ the view \ \ We just calculated the object-relative yaw angle of \ the object point in: \ \ angle(Hi Lo) \ \ If we add these two yaw angles together to get: \ \ angle(Hi Lo) + objectViewYaw(Hi Lo) \ \ then this gives us the view-relative yaw angle of the \ object point, which is what we are trying to calculate \ in this routine LDY drawingTableIndex \ Set Y to the drawing table index for this object point \ so we store the results in the last 32 bytes of the \ tile view drawing tables (i.e. in the object point \ drawing tables) LDA angleLo \ Set drawViewYaw(Hi Lo) for this point to: CLC \ ADC objectViewYawLo \ angle(Hi Lo) + objectViewYaw(Hi Lo) STA drawViewYawLo,Y \ LDA angleHi \ So this is the angle in our first triangle, which is ADC objectViewYawHi \ the view-relative yaw angle of the object point, so STA drawViewYawHi,Y \ store this in the drawing table at drawViewYaw(Hi Lo) \ We now move on to calculating the view-relative pitch \ angle of the object point by looking at the second \ triangle \ \ In the first triangle, we calculated the length and \ angle of the hypotenuse along the ground, which \ represents the projection from above of the vector \ from the viewer to the object point \ \ We're now interested in the y-axis element, so let's \ construct a triangle that stands vertically, and whose \ hypotenuse is the 3D vector from the viewer to the \ object point \ \ The hypotenuse that we used to calculate the yaw angle \ is now along the ground, acting as the bottom side \ of the new triangle, i.e. the adjacent side where the \ triangle's angle is the pitch angle \ \ The opposite side of the triangle is the height of the \ point above the ground (if we're looking up) or below \ ground (if we're looking down), which we can calculate \ using the objPointHeight table, which contains the \ height of the point within the object, and adding the \ view-relative y-coordinate of the object \ \ Given these two sides, we can then calculate the pitch \ angle of our new triangle to give the view-relative \ pitch angle of the object point, which is what we are \ trying to calculate in this routine JSR GetHypotenuse \ Calculate the length of the hypotenuse in the triangle \ with side lengths of a(Hi Lo) and b(Hi Lo) and angle \ angleTangent, which are still set from the call to \ GetHypotenuseAngle above to the values for the vector \ from the object's origin to the to the point within \ the object \ \ So this is the adjacent side of our new triangle, \ along the ground \ \ The hypotenuse length is returned in hypotenuse(Hi Lo) LDY pointNumber \ Set A to the objPointHeight entry for the object point LDA objPointHeight,Y \ \ The height is stored in objPointHeight as follows: \ \ * Bit 7 contains the sign bit \ \ * Bits 0 to 6 contain half the magnitude of the \ height \ \ So the height stored as a scaled, sign-magnitude value \ that we now need to extract into a signed number ASL A \ Shift the sign bit into the C flag and double the \ value in bits 0 to 6 to get the magnitude of the \ height in A, so: \ \ A = |height| STA T \ Set (A T) = |height| LDA #0 BCC obpt4 \ If the sign bit is clear then the height is positive \ and (A T) already has the correct sign, so jump to \ obpt4 to skip the following \ Otherwise the sign bit is set and the height is \ negative, so we need to negate the absolute value to \ get the signed height JSR Negate16Bit \ Set (A T) = -(A T) \ \ So the result now has the correct sign .obpt4 STA U \ Set (U T) = (A T) \ \ So (U T) contains the height of the object point \ relative to the object's origin, i.e. within the \ object \ We already calculated the object's view-relative \ y-coordinate in objectOpposite(Hi Lo), so now we add \ the two together to get the height of the object \ point, relative to the view LDA T \ Set (A xDeltaLo) = (U T) + objectOpposite(Hi Lo) CLC \ ADC objectOppositeLo \ So (A xDeltaLo) now contains the vertical delta, ready STA xDeltaLo \ to pass to GetPitchAngleDelta LDA U ADC objectOppositeHi LDX viewingObject \ Set X to the object number of the viewer so the call \ to GetPitchAngleDelta calculates the pitch angle \ relative to the viewing object's gaze JSR GetPitchAngleDelta \ Set pitchDelta(Hi Lo) to the pitch angle of the vector \ along the hypotenuse of our new triangle, given the \ length of the vertical side in (A xDeltaLo) and the \ length of the adjacent side in hypotenuse(Hi Lo), \ relative to the viewing object's gaze \ \ So we now have the view-relative pitch angle of the \ object point, which is what we are trying to calculate \ in this routine LDY drawingTableIndex \ Set Y to the drawing table index for this object point \ so we store the results in the last 32 bytes of the \ tile view drawing tables (i.e. in the object point \ drawing tables) LDA pitchDeltaHi \ Store pitchDelta(Hi Lo) in the drawing table as STA drawViewPitchHi,Y \ drawViewPitch(Hi Lo) LDA pitchDeltaLo STA drawViewPitchLo,Y INC pointNumber \ Increment the point number to move on to the next \ point in the object INC drawingTableIndex \ Increment the index into the drawing table so we store \ the results for the next point in the next byte in the \ drawing tables LDY pointNumber \ If we have reached the first point of the next object CPY objectLastPoint \ then we have processed all the points in this object, BEQ obpt5 \ so jump to obpt5 to return from the subroutine JMP obpt1 \ Loop back to process the next point in the object .obpt5 RTS \ Return from the subroutine
Name: DrawObject [Show more] Type: Subroutine Category: Drawing objects Summary: Draw a 3D object
Context: See this subroutine on its own page References: This subroutine is called as follows: * DrawObjectStack calls DrawObject * DrawTileAndObjects calls DrawObject * DrawTitleObject calls DrawObject * DrawUpdatedObject calls DrawObject

Arguments: Y The number of the object to be drawn
Returns: Y Y is preserved
.DrawObject JSR GetObjectAngles \ Calculate the angles and distances of the vector from \ the viewer to object #Y and put them into the \ following variables: \ \ * Set objectViewYaw(Hi Lo) to the yaw angle of the \ viewer's gaze towards the object, relative to the \ screen \ \ * Set objectGazeYaw(Hi Lo) to the difference in yaw \ angles between the viewer's gaze towards the \ object and the object's gaze to wherever it is \ looking (so that's the object's gaze relative to \ the viewer's gaze) \ \ * Set hypotenuse(Hi Lo) to the length of the 3D \ vector from the viewer to the object when \ projected down onto the ground plane \ \ * Set objectAdjacent(Hi Lo) to the length of the \ adjacent side in the vertical right-angled \ triangle with the projected vector along the \ bottom and the vector from the viewer to the \ object as the hypotenuse \ \ * Set objectOpposite(Hi Lo) to the length of the \ opposite side in the vertical right-angled \ triangle with the projected vector along the \ bottom and the vector from the viewer to the \ object as the hypotenuse \ \ Note that hypotenuse(Hi Lo) and objectAdjacent(Hi Lo) \ are the same value, as the hypotenuse of the ground \ plane triangle is the same as the adjacent side of the \ vertical triangle \ \ This also sets objectToAnalyse to the number of the \ object that we are drawing LDA hypotenuseHi \ If hypotenuseHi (i.e. objectAdjacentHi) is greater or CMP #15 \ equal to 15 then the object is a fair distance away ROR blendPolygonEdges \ from the viewer along the z-axis, so set bit 7 of \ blendPolygonEdges so we draw the polygon edges in the \ same colour as the polygon body (i.e. the fill \ colour), so the edges blend into the body \ \ We do this for distant objects because distinct edge \ colours can make those object look messy JSR GetObjPointAngles \ Calculate the view-relative pitch and yaw angles of \ all the points in the object and put them into the \ drawing tables at: \ \ drawViewYaw(Hi Lo) \ \ drawViewPitch(Hi Lo) \ \ We can use these as screen x- and y-coordinates, using \ the same screen projection that is used in Revs LDA #%01000000 \ Clear bit 7 and set bit 6 of polygonType so the call STA polygonType \ to DrawPolygon below draws the correct type of polygon \ for being part of an object LDA #0 \ Set drawingPhaseCount = 0, so the default is to draw STA drawingPhaseCount \ the object in one phase (we may change this later if \ the object needs to be drawn in two phases) STA drawingPhase \ Clear bit 7 of drawingPhase so for objects that need \ two drawing phases, we start by drawing the first \ phase (as the current phase is determined by bit 7) LDX objTypeToAnalyse \ Set X to the number of the object we are drawing LDA objPolygonPhases,X \ Set A to the phase data for the object we are drawing, \ which helps determine the number of drawing phases BEQ drob4 \ If A = 0 then this object only has one drawing phase, \ so jump to drob4 to move on to drawing the object \ (this applies to the tree, the boulder, the Sentinel's \ tower and the 3D text blocks) \ If we get here then bit 1 of A must be set, as the \ only values used in the objPolygonPhases table are \ %00, %10 and %11 LSR A \ If bit 0 of A is set then this object might need two BCS drob1 \ drawing phases, so jump to drob1 to work out how many \ phases are required by checking the relative altitude \ of the object (this applies to the robot, the sentry \ and the Sentinel) \ If we get here then bit 0 of A is clear and bit 1 is \ set, so we need to work out how many phases are \ required by checking whether or not the object is \ facing the viewer as given in the object's yaw angle \ (this applies to the meanie only) LDA objectGazeYawHi \ Set A to the object's gaze yaw angle + 192, to turn it ADC #192 \ anticlockwise (left) through 90 degrees for the phase \ calculation (the addition works because the C flag is \ clear, as we just passed through a BCS) JMP drob3 \ Jump to drob3 to set drawingPhaseCount to 0 or 2, \ depending on the direction in which the meanie is now \ facing: \ \ * 0 = one phase if the meanie's gaze yaw angle is \ positive after the rotation, which means it must \ be facing towards the viewer (so we leave \ drawingPhaseCount = 0) \ \ * 2 = two phases if the meanie's gaze yaw angle is \ negative after the rotation, which means it must \ be facing away from the viewer (so we set the \ drawingPhaseCount = 2) .drob1 \ If we get here then we are drawing a robot, a sentry \ or the Sentinel, so we need to work out how many \ drawing phases are required LDA objectOppositeHi \ If objectOpposite(Hi Lo) has a non-zero high byte, BNE drob2 \ then jump to drob2 to: \ \ * Draw the object in two phases if objectOppositeHi \ is positive (i.e. the object is reasonably far \ above the viewer) \ \ * Draw the object in one phase if objectOppositeHi \ is negative (i.e. the object is below the viewer) LDA objectOppositeLo \ If objectOpposite(Hi Lo) = 0 then the object is at the BEQ drob4 \ same height as the viewer, so jump to drob4 to draw \ the object in one phase (by leaving drawingPhaseCount \ set to 0) \ If we get here then objectOppositeHi is zero but \ objectOppositeLo is non-zero, so the object is only a \ little bit above the viewer and we draw the object in \ two phases (so now we set drawingPhaseCount = 2) LSR A \ Clear bit 7 of A, so the following EOR instruction \ will set bit 7, so we pass through the BPL instruction \ and set drawingPhaseCount = 2 before falling through \ into drob4 to do the actual drawing .drob2 EOR #%10000000 \ Flip bit 7 of A, which will contain objectOppositeHi \ if we jumped here from the BNE above .drob3 BPL drob4 \ If we jumped here because we are drawing a meanie, \ then this jumps to drob4 if the updated object's gaze \ yaw angle is positive \ \ If we fell through from drob2, then this jumps to \ drob4 if bit 7 of objectOppositeHi is set, i.e. if \ objectOpposite(Hi Lo) is negative \ If we get here then one of these is true: \ \ * We are drawing a meanie and it is facing away from \ the viewer \ \ * We are drawing a robot, a sentry or the Sentinel \ and one of these is true: \ \ * objectOpposite(Hi Lo) is positive and \ objectOppositeHi > 0 (so the object is \ reasonably far above the viewer) \ \ * objectOppositeHi is zero and objectOppositeLo is \ non-zero (so the object is a little bit above \ the viewer) \ \ In other words we are drawing a robot, a sentry or \ the Sentinel and the object is above the viewer LDA #2 \ Set drawingPhaseCount = 2 so we draw the object in two STA drawingPhaseCount \ phases .drob4 LDX objTypeToAnalyse \ Set X to the number of the object we are drawing LDA objPolygonRange+1,X \ Set objectLastPolygon to entry X + 1 from the STA objectLastPolygon \ table at objPolygonRange, so this now contains the \ number of the first polygon for the next object, \ object #X + 1, which is one greater than the last \ polygon number for object #X, as the lists of \ object polygons are sequential (so object #0's \ polygons are first in the object data tables, then \ object #1's polygons, and so on) LDY objPolygonRange,X \ Set Y to entry X from the table at objPolygonRange, so \ so this now contains the number of the first polygon \ for object #X \ \ This means that in the following loop, we start the \ drawing process with the first polygon for object #X .drob5 STY polygonNumber \ Set polygonNumber to Y, so we start from the first \ polygon and work through them in order LDA objPolygonData,Y \ Set A to the polygon data byte for the polygon we are \ drawing LDX drawingPhaseCount \ If drawingPhaseCount = 0 then jump to drob6 to skip BEQ drob6 \ the following, as there is only one drawing phase for \ this object EOR drawingPhase \ If bit 7 of the polygon data byte does not match bit 7 BMI drob7 \ of drawingPhase, then we do not draw this polygon in \ this phase, so jump to drob7 to skip drawing the \ polygon and move on to the next one \ If we get here then bit 7 of the polygon data byte \ matches the phase we are currently drawing, so we draw \ the polygon .drob6 AND #%00111100 \ Extract bits 2 to 5 from the polygon data byte to get STA polygonColours \ the polygon's edge and fill colours, and put them into \ polygonColours LDA objPolygonData,Y \ Set A to the polygon data byte for the polygon we are \ drawing AND #%00000011 \ Extract bits 0 to 1 from the polygon data byte to get CLC \ the number of sides in the polygon (where 0 means ADC #3 \ three sides and 1 means four sides) STA polygonEdgeCount \ \ Then add 3 to get the number of sides in the polygon, \ (three for a triangle or four for a quadrilateral) \ and store the result in polygonEdgeCount LDA objPolygonAddrLo,Y \ Set drawViewAngles(1 0) to the address of the list of STA drawViewAngles \ object-relative point numbers for this polygon so the LDA objPolygonAddrHi,Y \ DrawPolygon routine can use the yaw and pitch angles STA drawViewAngles+1 \ that we calculated for the object points in the \ GetObjPointAngles routine JSR DrawPolygon \ Draw the polygon LDY polygonNumber \ Set Y to the polygon number once again, as the call \ to DrawPolygon will have corrupted it .drob7 INY \ Increment the polygon number in Y CPY objectLastPolygon \ If we have not yet reached the end of the list, loop BCC drob5 \ back to drob5 to draw the next polygon, until we have \ processed all the polygons in the object DEC drawingPhaseCount \ Decrement the drawing phase counter BMI drob8 \ If drawingPhaseCount was zero before being decremented \ then there is only one drawing phase, so jump to drob8 \ to finish up BEQ drob8 \ If drawingPhaseCount was 1 before being decremented \ then there are two drawing phases and we have just \ finished drawing the second phase, so jump to drob8 to \ finish up \ If we get here then drawingPhaseCount was 2 before \ being decremented, so this is a two-phase object and \ we have only drawn the first phase, so now we draw the \ second phase LDA #%10000000 \ Set bit 7 of drawingPhase so we now draw the second STA drawingPhase \ phase BMI drob4 \ Jump back to drob4 to draw the second phase for the \ two-phase object .drob8 LDA #LO(polygonPoint) \ Set drawViewAngles(1 0) = polygonPoint STA drawViewAngles \ LDA #HI(polygonPoint) \ This resets drawViewAngles(1 0) so DrawPolygon will STA drawViewAngles+1 \ return to the default behaviour of drawing using point \ data for tile faces rather than object polygons LSR blendPolygonEdges \ Clear bit 7 of blendPolygonEdges so we return to the \ default setting of allowing drawing polygon edges to \ be a different colour to the polygon fill LDY objectToAnalyse \ Set Y to the number of the object we just drew, so it \ is preserved RTS \ Return from the subroutine
Name: GetHorizontalDelta [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Calculate the difference in the x-axis and z-axis between two objects, as both signed and absolute deltas
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetObjectAngles calls GetHorizontalDelta

This routine calculates the following: xDelta(Hi Lo) = x-coordinate of object #Y - x-coordinate of object #X - xTitleOffset xDeltaAbsoluteHi = |xDeltaHi| zDelta(Hi Lo) = z-coordinate of object #Y - z-coordinate of object #X zDeltaAbsoluteHi = |zDeltaHi| So this calculates the difference (the "delta") in both the x-coordinate and z-coordinate between the two objects, which are the differences along the left-right x-axis and the z-axis the goes into the screen. Note that xTitleOffset is zero during gameplay, and is only non-zero when we are drawing large 3D text on the title screen.
Arguments: X The number of the first object Y The number of the second object
.GetHorizontalDelta \ The x-coordinates for each object are stored as 8-bit \ signed integers, with the x-coordinate for object #X \ in xObject+X and for object #Y in xObject+Y \ \ We want to calculate the deltas in 16-bit accuracy, \ where the low byte is effectively a fractional part, \ so we convert these x-coordinates into 16-bit numbers \ like this: \ \ * x-coordinate for object #X = (xObject+X 0) \ \ * x-coordinate for object #Y = (xObject+Y 0) \ \ and we then do a multi-byte subtraction to get the \ delta, with the title offset included in the \ calculation as (xTitleOffset 0) LDA #0 \ Calculate the following: STA xDeltaLo \ \ xDelta(Hi Lo) = (xObject+Y 0) - (xObject,X 0) \ - (xTitleOffset 0) \ \ starting with the low byte (which we know will be \ zero) SEC \ And then subtracting the high bytes LDA xObject,Y SBC xObject,X SEC SBC xTitleOffset STA xDeltaHi BPL delt1 \ If the high byte of the result is positive, jump to \ delt1 as the result in A is already correct for the \ absolute value (i.e. A = xDeltaHi = |xDeltaHi|) SEC \ The high byte is negative, so set: LDA #0 \ SBC xDeltaHi \ A = 0 - xDeltaHi \ = -xDeltaHi \ = |xDeltaHi| .delt1 STA xDeltaAbsoluteHi \ Set xDeltaAbsoluteHi = |xDeltaHi| \ \ So we now have the absolute x-axis length in: \ \ (xDeltaAbsoluteHi xDeltaLo) \ \ and the original high byte of the signed x-axis length \ is still in xDeltaHi \ We now do the same thing, but with the z-coordinates \ \ There is no title delta to include for the z-axis \ calculation LDA #0 \ Calculate the following: STA zDeltaLo \ \ zDelta(Hi Lo) = (zObject+Y 0) - (zObject,X 0) \ \ starting with the low byte (which we know will be \ zero) SEC \ And then subtracting the high bytes LDA zObject,Y SBC zObject,X STA zDeltaHi BPL delt2 \ If the high byte of the result is positive, jump to \ delt2 as the result in A is already correct for the \ absolute value (i.e. A = zDeltaHi = |zDeltaHi|) SEC \ The high byte is negative, so set: LDA #0 \ SBC zDeltaHi \ A = 0 - zDeltaHi \ = -zDeltaHi \ = |zDeltaHi| .delt2 STA zDeltaAbsoluteHi \ Set zDeltaAbsoluteHi = |zDeltaHi| \ \ So we now have the absolute z-axis length in: \ \ (zDeltaAbsoluteHi zDeltaLo) \ \ and the original high byte of the signed z-axis length \ is still in zDeltaHi RTS \ Return from the subroutine
Name: GetVerticalDelta [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Calculate the difference in the y-axis between two objects as a signed delta
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetObjectAngles calls GetVerticalDelta

This routine calculates the following: yDelta(Hi Lo) = y-coordinate of object #Y - y-coordinate of object #X So yDelta(Hi Lo) contains the difference (the "delta") in the y-coordinate between the two objects, which is the difference in altitude.
Arguments: X The number of the first object Y The number of the second object
.GetVerticalDelta LDA yObjectLo,Y \ Set yDelta(Hi Lo) = yObject(Hi Lo) for object #Y SEC \ - yObject(Hi Lo) for object #X SBC yObjectLo,X \ STA yDeltaLo \ starting with the low bytes LDA yObjectHi,Y \ And then the high bytes SBC yObjectHi,X STA yDeltaHi RTS \ Return from the subroutine
Name: ReadKeyboard [Show more] Type: Subroutine Category: Keyboard Summary: Enable the keyboard and read a character from it
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainGameLoop calls ReadKeyboard * MainTitleLoop calls ReadKeyboard * PlayGame calls ReadKeyboard * SecretCodeError calls ReadKeyboard

Returns: A The character read from the keyboard
.ReadKeyboard JSR EnableKeyboard \ Select the keyboard as the input stream and flush the \ keyboard buffer \ Fall through into ReadCharacter to read a character \ from the keyboard and return it in A
Name: ReadCharacter [Show more] Type: Subroutine Category: Keyboard Summary: Read a character from the currently selected input stream
Context: See this subroutine on its own page References: This subroutine is called as follows: * ReadNumber calls ReadCharacter

Returns: A The character read from the input stream, in ASCII
.ReadCharacter JSR OSRDCH \ Read a character from the currently selected input \ stream into A BCC read1 \ If the C flag is clear then the call to OSRDCH read a \ valid character, so jump to read1 to return from the \ subroutine CMP #27 \ If the character read is not ESCAPE, jump to read1 to BNE read1 \ return from the subroutine \ If we get here then we have an ESCAPE condition, so we \ need to acknowledge it and try again TYA \ Store Y on the stack to we can preserve it through the PHA \ call to OSBYTE LDA #126 \ Call OSBYTE with A = 126 to acknowledge the ESCAPE JSR OSBYTE \ condition PLA \ Retrieve Y from the stack TAY JMP ReadCharacter \ Loop back to read another character .read1 RTS \ Return from the subroutine
Name: EnableKeyboard [Show more] Type: Subroutine Category: Keyboard Summary: Select the keyboard as the input stream and flush the keyboard buffer
Context: See this subroutine on its own page References: This subroutine is called as follows: * ReadKeyboard calls EnableKeyboard * ReadNumber calls EnableKeyboard
.EnableKeyboard LDA #2 \ Call OSBYTE with A = 2 and X = 0 to select the LDX #0 \ keyboard as the input stream and disable the RS423 JSR OSBYTE LDX #0 \ Set X = 0 to denote the keyboard buffer JMP FlushBuffer \ Call FlushBuffer to flush the keyboard buffer and \ return from the subroutine using a tail call
Name: SetColourPalette [Show more] Type: Subroutine Category: Graphics Summary: Set the logical colours for each of the four physical colours in screen mode 5
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainGameLoop calls SetColourPalette * MainTitleLoop calls SetColourPalette * PlayGame calls SetColourPalette * SecretCodeError calls SetColourPalette

Arguments: A Defines how the four physical colours in the mode 5 palette are set: * If bit 7 is clear then bits 0-6 contain the physical colour to set for all four logical colours (so the screen is effectively blanked to this colour) * If bit 7 is set then bits 0-6 contain the offset within the colourPalettes table of the last of the four physical colours to set for logical colours 3, 2, 1 and 0 (so we work backwards through the table from the offset in bits 0-6)
.SetColourPalette STA T \ Store the argument in T AND #%01111111 \ Extract bits 0-6 of the argument into Y TAY LDA #3 \ We now work through the logical colours from 3 down STA U \ to 0, setting the physical colour for each one in \ turn, so set a logical colour counter in U .pall1 LDX T \ Set X to the argument in T BPL pall2 \ If bit 7 of the argument in T is clear, skip the \ following instruction, so X contains the physical \ colour in the routine's argument (i.e. the colour to \ which we set all four logical colours) \ If we get here then bit 7 of the argument in T is set, \ which means bits 0-6 contain the offset within the \ colourPalettes table of the four physical colours in \ the palette \ \ We set Y above to the value of bits 0-6, so we can use \ this as the index into the colourPalettes table (we \ will decrement Y below for each of the four colours, \ so we end up setting all four logical colours to the \ four values in the table) LDX colourPalettes,Y \ Fetch the physical colour from the Y-th entry in \ colourPalettes, which we now want to allocate to \ logical colour U .pall2 LDA #19 \ Start a VDU 19 command, which sets a logical colour to JSR OSWRCH \ a physical colour using the following format: \ \ VDU 19, logical, physical, 0, 0, 0 \ \ which we output as follows: \ \ VDU 19, U, X, 0, 0, 0 LDA U \ Write the value in U, which is the logical colour we JSR OSWRCH \ want to define TXA \ Set A to the value in X LDX #4 \ Set X = 4 to use as a byte counter as we output the \ physical colour and three zeroes .pall3 JSR OSWRCH \ Write the value in A, which is the physical colour we \ want to set (in the first iteration of the loop), or \ one of three trailing zeroes (in later iterations) LDA #0 \ Set A = 0 so the remaining three iterations of the \ loop output the trailing zeroes DEX \ Decrement the byte counter BNE pall3 \ Loop back until we have output the whole VDU command DEY \ Decrement the colourPalettes index in Y DEC U \ Decrement the logical colour counter in U BPL pall1 \ Loop back until we have defined the logical colours \ for all four physical colours RTS \ Return from the subroutine
Name: colourPalettes [Show more] Type: Variable Category: Graphics Summary: The logical colours for two mode 5 palettes
Context: See this variable on its own page References: This variable is used as follows: * SetColourPalette uses colourPalettes * SpawnEnemies uses colourPalettes

This table contains the logical colours that are set in the SetColourPalette routine when it is called with an argument with bit 7 set. This routine is only ever called with an argument of &83 or &87. If the argument to SetColourPalette is &83 then the palette is set to the correct colours for playing the current landscape. The colours are set in the SpawnEnemies routine when the landscape is generated. Colours 0 and 1 are always blue and black, but colours 2 and 3 are set to different physical colours depending on the number of enemies in the landscape. The different palettes are as follows: * Enemy count = 1: blue, black, white, green * Enemy count = 2: blue, black, yellow, red * Enemy count = 3: blue, black, cyan, yellow * Enemy count = 4: blue, black, red, cyan * Enemy count = 5: blue, black, white, red * Enemy count = 6: blue, black, yellow, cyan * Enemy count = 7: blue, black, cyan, red * Enemy count = 8: blue, black, red, yellow Landscape 0000 has one enemy, so the starting landscape is therefore in blue, black, white and green. If the argument to SetColourPalette is &87, the palette is set as follows: * Colour 0 = 4 (blue) * Colour 1 = 0 (black) * Colour 2 = 1 (red) * Colour 3 = 3 (yellow) This palette is fixed and is used for the title screens.
.colourPalettes EQUB 4, 0, 6, 3 \ Palette with SetColourPalette offset 3 EQUB 4, 0, 1, 3 \ Palette with SetColourPalette offset 7
Name: DitherScreenBuffer [Show more] Type: Subroutine Category: Screen buffer Summary: Dither the contents of the screen buffer onto the screen
Context: See this subroutine on its own page References: This subroutine is called as follows: * DrawUpdatedObject calls DitherScreenBuffer

Arguments: ditherOuterLoop The number of outer loops to implement for the dithered effect, where each inner loop dithers 255 pixels to the screen
.DitherScreenBuffer LDA #255 \ Each inner loop of the dithering process updates 255 STA ditherInnerLoop \ random pixels on-screen, so set the inner loop counter \ in ditherInnerLoop accordingly LDA bufferColumns \ Set bufferWidthBytes = bufferColumns * 8 ASL A \ ASL A \ This converts the number of character columns into ASL A \ pixel bytes, as each character block contains eight STA bufferWidthBytes \ bytes SEC \ Set A = bufferWidthBytes - 1 SBC #1 \ We now set bitMask to a bit mask that covers all the \ non-zero bits in the list length in A, so if A is of \ the form %001xxxxx, for example, then bitMask will \ contain %00111111, while A being like %000001xx will \ give a bitMask of %00000111 \ \ To do this we count the number of continuous clear \ bits at the top of A, and then use this as an index \ into the leadingBitMask table \ \ So we count zeroes from bit 7 down until we hit a 1, \ and put the result into Y BEQ dith2 \ If A = 0 then jump to dith2 to skip the following \ process and set bitMaskDither to %00000000, which has \ the same number of leading zeroes as A LDY #&FF \ Set Y = -1 so the following loop counts the number of \ zeroes correctly .dith1 ASL A \ Shift A to the left, moving the top bit into the C \ flag INY \ Increment the zero counter in Y BCC dith1 \ Loop back to keep shifting and counting zeroes until \ we shift a 1 out of bit 7, at which point Y contains \ the length of the run of zeroes in bits 7 to 0 of the \ buffer width in bytes LDA leadingBitMask,Y \ Set A to the Y-th entry from the leadingBitMask table, \ which will give us a bit mask with a matching number \ of leading zeroes as A .dith2 STA bitMaskDither \ Set bitMaskDither to a bit mask with a matching number \ of leading zeroes as the buffer width in bytes .dith3 SEI \ Disable interrupts so we can fetch a random number \ without clashing with the dithering process in the \ handler (which is activated on the game over screen, \ which also uses this routine) JSR GetRandomNumber \ Set A to a random number LDY randomGenerator+1 \ Set ditherRandom to the second byte of the random STY ditherRandom \ number generator, so this is also a random number CLI \ Re-enable interrupts CLC \ Set A = A + pixelByteToDither ADC pixelByteToDither \ \ This sets A to a random value in the range 0 to 255 \ that's unlikely to be the same as pixelByteToDither, \ which is the pixel byte that we dithered last time we \ were here \ \ We don't care that pixelByteToDither isn't initialised \ outside of the reset routine, as we want this to be \ random AND bitMaskDither \ We set bitMaskDither above to a bit mask that has a \ matching number of leading zeroes as the number of \ bytes in the buffer width, so this instruction \ converts A into a number with the same range of \ non-zero bits as bufferWidthBytes CMP bufferWidthBytes \ If A < bufferWidthBytes then jump to dith4 to skip the BCC dith4 \ next instruction SBC bufferWidthBytes \ Subtract bufferWidthBytes from A to bring it into the \ range 0 to bufferWidthBytes (which works because we \ already reduced A to a maximum value of bitMaskDither, \ so A must be less than bufferWidthBytes * 2) \ \ The subtraction works because we just passed through a \ BCC, so we know the C flag is set .dith4 STA pixelByteToDither \ Set pixelByteToDither to the result in A, which is a \ valid pixel byte number within the range of the pixel \ bytes in the screen buffer row \ We now set (ditherStore A) to the address of a random \ row in the screen buffer, as an offset from the first \ screen row buffer at screenBufferRow0, using bits 0 to \ 4 of the random number in ditherRandom LDA ditherRandom \ Set ditherStore to bits 0 to 4 of our random number, AND #%00011111 \ so that's a number in the range 0 to &1F STA ditherStore LSR A \ Set ditherStore = ditherStore / 2 + ditherStore CLC \ ADC ditherStore \ and round the result down to the nearest even number, AND #%11111110 \ so that's an even number in the range 0 to &2E STA ditherStore ASL A \ Set A = ditherStore * 4 ASL A \ \ So that's a multiple of 8 in the range 0 to &B8 ADC ditherStore \ Set ditherStore = ditherStore + A STA ditherStore \ \ So that's an even number in the range 0 to &2E LDA #0 \ Set (ditherStore A) = (ditherStore 0) / 8 LSR ditherStore \ ROR A \ This gives us the address of a randomly picked screen LSR ditherStore \ row in (ditherStore A), as an offset from within ROR A \ screen memory LSR ditherStore ROR A ADC pixelByteToDither \ Set the following: STA fromAddr \ LDA ditherStore \ fromAddr(1 0) = (ditherStore A) + pixelByteToDither ADC #0 \ STA fromAddr+1 \ So fromAddr(1 0) now contains the offset address \ within screen memory of a randomly picked pixel byte \ within the width of the screen buffer \ \ This is calculated by adding the address of a randomly \ picked screen row in (ditherStore A) and a randomly \ picked pixel byte within the width of the screen \ buffer in pixelByteToDither \ The address in fromAddr(1 0) is now the offset within \ screen memory of the random pixel byte that we can use \ for dithering one pixel of the screen buffer to the \ screen (as we will dither just one pixel of this byte \ on each iteration of the inner loop) \ \ The next step is to convert this offset into both a \ screen memory address and the corresponding screen \ buffer address, so we can copy the pixel from the \ screen buffer to the screen \ \ We start with the screen memory address LDA objScreenAddr \ Set (A toAddr) = objScreenAddr(1 0) + fromAddr(1 0) CLC \ ADC fromAddr \ This gives us the address in screen memory of the STA toAddr \ random pixel byte, as objScreenAddr(1 0) contains the LDA objScreenAddr+1 \ screen address of the object ADC fromAddr+1 CMP #&80 \ If the high byte in A >= &80 then the new address is BCC dith5 \ past the end of screen memory, so subtract &20 from SBC #&20 \ the high byte so the address wraps around within the \ range of screen memory between &6000 and &8000 .dith5 STA toAddr+1 \ Store the high byte of the result, so we now have: \ \ toAddr(1 0) = objScreenAddr(1 0) + fromAddr(1 0) \ \ with the address wrapped around as required \ And now we calculate the equivalent address in the \ screen buffer, which is a column buffer, so we start \ by adding the offset in fromAddr(1 0) to the address \ of the first screen buffer row at screenBufferRow0 \ (we can skip adding the low bytes as the low byte of \ screenBufferRow0 is zero) \ \ It's worth noting that the structure of the screen \ buffer is similar to the screen, with each row in \ the screen buffer being spaced out by 320 bytes, just \ as in screen memory, so this means we can just add \ screenBufferRow0 and fromAddr(1 0) to get the address \ \ However, as we use a column buffer to update objects \ on-screen, we need to wrap around after buffer row 15, \ so we also need to check for this LDA fromAddr+1 \ Set fromAddr(1 0) = screenBufferRow0 + fromAddr(1 0) CLC ADC #HI(screenBufferRow0) STA fromAddr+1 CMP #&53 \ If the result of the addition is less than &5300, then BCC dith6 \ we have not reached the end of row 15 in the screen \ buffer, so jump to dith6 to skip the following LDA fromAddr \ Set fromAddr(1 0) = fromAddr(1 0) - &1360 SEC \ SBC #&60 \ If we get here then we have reached character row 16 STA fromAddr \ in the screen buffer, so we need to wrap around LDA fromAddr+1 \ SBC #&13 \ The value of &1360 is calculated as follows STA fromAddr+1 \ \ Character row 15 in the screen buffer is at address \ &51C0, and adding another 320 to that address gives \ us &5300, but this is past the end of the screen \ buffer, so we need to wrap it around \ \ Character row 16 is actually at address &3FA0, so we \ need to subtract &5300 - &3FA0 = &1360 from the \ address to perform the wraparound .dith6 LDA ditherRandom \ Set X to bits 6 and 7 of ditherRandom, shifted into ASL A \ bits 0 and 1 to create a random number in the range ROL A \ 0 to 3 ROL A AND #%00000011 TAX LDY #0 \ Set A to the pixel byte that we want to dither onto LDA (fromAddr),Y \ the screen, taking it from the screen buffer at \ address fromAddr(1 0) AND pixelBitMask,X \ Convert X from a number in the range 0 to 3 into a bit \ mask with that pixel number set (where pixel 0 is on \ the left and pixel 3 is on the right), and clear all \ the bits in A apart from the two bits for that pixel \ \ So if X is 2, for example, the resulting mask will be \ %00100010, in which pixel 2 is set, so only these two \ bits of the pixel byte will be kept STA ditherStore \ Store the pixel that we want to dither in ditherStore LDA (toAddr),Y \ Set A to the current screen contents of the pixel byte \ that we are updating, taking it from the screen at \ address toAddr(1 0) AND clearPixelMask,X \ Clear the pixel number in A by applying a pixel bit \ mask from clearPixelMask where the bits for every \ pixel are set except for pixel X ORA ditherStore \ Set the colour of pixel X to the colour of the pixel \ from the screen buffer by OR'ing in the pixel bits \ that we set above STA (toAddr),Y \ Store the updated pixel byte in screen memory to draw \ the pixel from the screen buffer on-screen BIT doNotDitherObject \ If bit 7 of doNotDitherObject is set then the dithered BMI dith8 \ effect has been disabled (while we focus on processing \ an action key), so jump to dith8 to return from the \ subroutine without doing any more dithering DEC ditherInnerLoop \ Decrement the inner loop counter BNE dith7 \ If we have not yet finished the inner loop, jump to \ dith7 to loop back to dither another pixel onto the \ screen, until we have done all 255 pixels in the \ inner loop JSR ProcessSound \ Process any sounds or music that are being made in the \ background DEC ditherOuterLoop \ Decrement the outer loop counter BEQ dith8 \ If we have finished the outer loop, jump to dith8 to \ return from the subroutine .dith7 JMP dith3 \ Loop back to dither another pixel onto the screen .dith8 RTS \ Return from the subroutine
Name: ShowGameOverScreen [Show more] Type: Subroutine Category: Title screen Summary: Display the game over screen
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainGameLoop calls ShowGameOverScreen * ProcessGameplay calls ShowGameOverScreen

Arguments: A The number of 2400-dot plotting cycles to perform when decaying the screen from the landscape view to the game over screen * 5 = 12,000 dots for when the player runs out of energy when trying to hyperspace * 30 = 72,000 dots for when the player is absorbed by the Sentinel titleObjectToDraw The type of object that caused the player's demise (be it the Sentinel, a sentry, a meanie or the player themselves)
.ShowGameOverScreen PHA \ Store the argument in A on the stack, so we can fetch \ it later JSR FlushSoundBuffers \ Flush all four sound channel buffers JSR ClearIconsScanner \ Clear the energy icon and scanner row at the top of \ the screen LDA #6 \ Set soundEffect = 6 so the sound is processed as the STA soundEffect \ game over sound LDA #250 \ Set gameOverSoundPitch = 250 to start the sound effect STA gameOverSoundPitch \ processing for the game over sound PLA \ Retrieve the argument A from the stack, so it contains \ the number of black dots to draw on the screen JSR DecayScreenToBlack \ Decay the screen to black with a mass of A * 2400 \ randomly placed black dots LDY #0 \ Set sightsByteCount to zero to reset the sights pixel STY sightsByteCount \ byte stash STY sightsAreVisible \ Clear bit 7 of sightsAreVisible to indicate that the \ sights are not visible LDA titleObjectToDraw \ Call SpawnTitleObject to spawn an object of type JSR SpawnTitleObject \ titleObjectToDraw as object #1, using the \ configuration specified in Y (i.e. 0), so the object \ is spawned for use in the game over screen \ \ By the time the game ends, titleObjectToDraw has been \ set to the type of object that caused the player's \ demise (be it the Sentinel, a sentry, a meanie or the \ player themselves), so this call spawns that object \ as object #1 LDA #3 \ Set screenBackground = 3 so the next time the screen STA screenBackground \ is cleared, it shows a black background with stars LDA #1 \ Set currentObject = 1 so the call to DrawUpdatedObject STA currentObject \ draws object #1 LDA #%11000000 \ Set bit 7 of drawLandscape so the interrupt handler STA drawLandscape \ draws random black dots on the screen, so we combine \ dithering of object #1 to the screen while at the same \ time fading the screen to black, to create the \ hypnotic effect of the winning entity fading in and \ out of the screen as the game ends STA ditherObjectSights \ Set bits 6 and 7 of ditherObjectSights so the call to \ DrawUpdatedObject removes the sights from the screen \ and dithers object #1 onto the screen LSR doNotDitherObject \ Clear bit 7 of doNotDitherObject to enable objects to \ be updated on the screen with a dithered effect LDA #50 \ Call the PlayMusic routine with A = 50 to play the JSR PlayMusic \ game over music JSR DrawUpdatedObject \ Draw the current object (or the landscape where the \ object used to be) into the screen buffer and dither \ it onto the screen, pixel by pixel and randomly LDA #30 \ Smother the screen in 30 * 2400 = 72,000 randomly JMP DecayScreenToBlack \ placed black dots to decay the screen to black, \ returning from the subroutine using a tail call
Name: DecayScreenToBlack [Show more] Type: Subroutine Category: Graphics Summary: Smother the screen with randomly placed black dots to decay the screen to black
Context: See this subroutine on its own page References: This subroutine is called as follows: * ShowGameOverScreen calls DecayScreenToBlack

Arguments: A The number of 2400-dot plotting cycles to perform when decaying the screen
.DecayScreenToBlack STA blackDotCounter \ Set blackDotCounter to the number of 2400-dot \ plotting cycles we need to perform .deca1 LDA #30 \ Set loopCounter = 30 for the inner loop, so each STA loopCounter \ iteration of the inner loop draws 30 * 80 = 2400 dots .deca2 JSR DrawBlackDots \ Draw 80 randomly positioned dots on the screen in \ colour 1 (black) to fade the screen to black in a \ slowly decaying manner JSR ProcessSound \ Process any sounds or music that are being made in the \ background DEC loopCounter \ Loop back to plot another 80 dots until we have done BNE deca2 \ this 30 times (to give a total of 2400 dots) DEC blackDotCounter \ Loop back to plot 80 * 30 dots until we have done this BNE deca1 \ blackDotCounter times (to give a total of \ 2400 * blackDotCounter dots) RTS \ Return from the subroutine
Name: JumpToPreview [Show more] Type: Subroutine Category: Cracker protection Summary: An intentionally confusing jump point for controlling the main title loop flow when returning from the GenerateLandscape routine
Context: See this subroutine on its own page References: This subroutine is called as follows: * SmoothTileCorners (Part 2 of 4) calls JumpToPreview
.JumpToPreview EQUB &4C \ This byte is never executed, as the stack modification \ in the SmoothTileData routine sets the return address \ on the stack to JumpToPreview, and the RTS instruction \ will therefore jump to JumpToPreview+1 (as that's how \ the RTS instruction works) \ \ This byte is the opcode for a JMP instruction, so this \ makes it look like there is a JumpToPreview routine \ that contains the following: \ \ &4C &30 &3F JMP &3F30 \ \ as the BMI instruction below assembles into &30 &3F \ \ This would jump to a valid instruction halfway through \ the ConfigureMachine routine, so this byte, although \ unused, is presumably a JMP opcode to confuse any \ crackers who have reached this point in their analysis BMI PreviewLandscape \ We only get here if the stack has been modified by the \ SmoothTileData routine, which makes the RTS at the end \ end of the GenerateLandscape routine jump here \ \ The penultimate instruction in GenerateLandscape is a \ call to the ProcessTileData routine, which happens to \ set the N flag, so when the RTS instruction jumps here \ using the modified return address, this BMI branch is \ taken, so this instruction is effectively a JMP to the \ PreviewLandscape routine
Name: SpawnTitleObject [Show more] Type: Subroutine Category: Title screen Summary: Spawn the title object (robot or the Sentinel) as object #1
Context: See this subroutine on its own page References: This subroutine is called as follows: * DrawTitleObject calls SpawnTitleObject * ShowGameOverScreen calls SpawnTitleObject

Arguments: A The type of object to spawn Y The configuration to use when spawning the object: * 0 = the object on the game over screen (i.e. the enemy that ended the game) * 1 = the Sentinel on the main title screen or the robot on the secret code screen * 2 = the tower on which the Sentinel or robot stands
.SpawnTitleObject \ We set up the object that we want to draw on the title \ screen in object #1, and we set up the viewing object \ for that object in object #2 \ \ The values come from the Y-th entry in each of the \ following configuration tables, so this routine sets \ up the following, depending on the object that we are \ spawning: \ \ * Object #1 (the object to draw) is set to a yaw \ of 128 for the game over screen (so the enemy \ stares directly out of the screen), or -114 for \ the object on the tower (so it looks towards the \ left of the viewer) or -50 for the tower (so it is \ rotated slightly to the left) \ \ * Object #1 (the object to draw) is set to a yaw \ of 128 for the game over screen, so the enemy \ stares directly out of the screen \ \ * Object #1 (the object to draw) is set to a yaw \ of -114 for the object on the tower, so the robot \ or Sentinel looks out of the screen, towards the \ left of the viewer \ \ * Object #1 (the object to draw) is set to a yaw \ of -50 for the tower, so it is rotated slightly to \ the left \ \ * Object #2 (the viewing object) is set to a pitch \ of -12 for the game over screen, so the camera \ points down, thus moving the object down the \ screen \ \ * Object #2 (the viewing object) is set to a pitch \ of -5 and a yaw of -8 for the title screen, so the \ camera points down and left, thus moving the tower \ and object to the right \ \ * Object #1 is always at the same x-coordinate as \ the viewing object \ \ * The tower and game over objects are at the same \ y-coordinate as the viewer (so they at the same \ altitude) \ \ * The object on the tower is higher than the viewer \ by one y-coordinate \ \ * The game over object is at a z-coordinate distance \ of 5 from the viewer, while the object and tower \ are at a distance of 7 STA objectTypes+1 \ Set the object type for object #1 to the argument in A LDA zTitleObject,Y \ Set the z-coordinate for object #1 to the z-coordinate CLC \ of object #2 plus the Y-th entry from the zTitleObject ADC zObject+2 \ table STA zObject+1 LDA yObjectHi+2 \ Set the high byte of the y-coordinate for object #1 to CLC \ the high byte of the y-coordinate of object #2 plus ADC yTitleObject,Y \ the Y-th entry from the yTitleObject table STA yObjectHi+1 LDA xObject+2 \ Set the x-coordinate for object #1 to the x-coordinate STA xObject+1 \ of object #2 LDA titleViewerPitch,Y \ Set the pitch angle of the viewer in object #2 to the STA objectPitchAngle+2 \ Y-th entry from the titleViewerPitch table LDA titleViewerYaw,Y \ Set the yaw angle of the viewer in object #2 to the STA objectYawAngle+2 \ Y-th entry from the titleViewerYaw table LDA #0 \ Zero the low bytes of the y-coordinates for both STA yObjectLo+2 \ objects STA yObjectLo+1 LDA titleObjectYaw,Y \ Set the yaw angle of object #1 to the Y-th entry from STA objectYawAngle+1 \ the titleObjectYaw LDX #2 \ Set the viewing object to object #2 so the screen will STX viewingObject \ be drawn from this perspective RTS \ Return from the subroutine
Name: zTitleObject [Show more] Type: Variable Category: Title screen Summary: The z-coordinate of an object on the title screen, as a delta from the z-coordinate of the viewing object
Context: See this variable on its own page References: This variable is used as follows: * SpawnTitleObject uses zTitleObject
.zTitleObject EQUB 5 \ Object 0 = the game over object EQUB 7 \ Object 1 = the object on the tower EQUB 7 \ Object 2 = the tower
Name: PreviewLandscape [Show more] Type: Subroutine Category: Landscape Summary: Draw an aerial preview of the landscape
Context: See this subroutine on its own page References: This subroutine is called as follows: * JumpToPreview calls PreviewLandscape
.PreviewLandscape JSR SpawnEnemies \ Calculate the number of enemies for this landscape, \ add them to the landscape and set the palette \ accordingly LDX #3 \ Set X = 3 to pass to DrawTitleView so the background \ of the landscape preview is black with stars LDY #0 \ Set Y = 0 to pass to DrawTitleView so it draws the \ screen with the correct perspective for the landscape \ preview LDA #&80 \ Set A = &80 so the call to DrawTitleView draws the \ landscape preview JSR DrawTitleView \ Draw the landscape preview LDX #4 \ Print text token 4: Background colour black, print JSR PrintTextToken \ "PRESS ANY KEY" at (192, 64), print "LANDSCAPE" two \ characters right of (64, 768), move cursor right JSR PrintLandscapeNum \ Print the four-digit landscape number (0000 to 9999) JSR SpawnPlayer \ Add the player and trees to the landscape \ \ If the entered secret entry code in the keyboard input \ buffer does not match the generated secret code for \ this landscape then the call will return here so we \ can display an error \ \ If the codes match then the CheckSecretCode will jump \ to the PlayGame routine instead to play the game JMP SecretCodeError \ The entered secret entry in the keyboard input buffer \ does not match the generated secret code for this \ landscape, so jump to SecretCodeError to display the \ "WRONG SECRET CODE" error, wait for a key press and \ rejoin the main title loop
Name: titleViewerPitch [Show more] Type: Variable Category: Title screen Summary: The pitch angle of the viewer for objects on the title screen
Context: See this variable on its own page References: This variable is used as follows: * SpawnTitleObject uses titleViewerPitch
.titleViewerPitch EQUB -12 \ Object 0 = when viewing the game over object EQUB -5 \ Object 1 = when viewing the object on the tower EQUB -5 \ Object 2 = when viewing the tower
Name: yTitleObject [Show more] Type: Variable Category: Title screen Summary: The y-coordinate of an object on the title screen, as a delta from the y-coordinate of the viewing object
Context: See this variable on its own page References: This variable is used as follows: * SpawnTitleObject uses yTitleObject
.yTitleObject EQUB 0 \ Object 0 = the game over object EQUB 1 \ Object 1 = the object on the tower EQUB 0 \ Object 2 = the tower
Name: titleObjectYaw [Show more] Type: Variable Category: Title screen Summary: The yaw angle of an object on the title screen
Context: See this variable on its own page References: This variable is used as follows: * SpawnTitleObject uses titleObjectYaw
.titleObjectYaw EQUB 128 \ Object 0 = the game over object EQUB -114 \ Object 1 = the object on the tower EQUB -50 \ Object 2 = the tower
Name: titleViewerYaw [Show more] Type: Variable Category: Title screen Summary: The yaw angle of the viewer for objects on the title screen
Context: See this variable on its own page References: This variable is used as follows: * SpawnTitleObject uses titleViewerYaw
.titleViewerYaw EQUB 0 \ Object 0 = when viewing the game over object EQUB -8 \ Object 1 = when viewing the object on the tower EQUB -8 \ Object 2 = when viewing the tower
Name: DrawTitleObjects [Show more] Type: Subroutine Category: Title screen Summary: Draw an object on top of a tower on the title screen
Context: See this subroutine on its own page References: This subroutine is called as follows: * DrawTitleView calls DrawTitleObjects

Arguments: A The type of object to draw on top of a tower on the right side of the screen: * 0 to draw a robot for the secret code screen * 5 to draw the Sentinel for the main title screen
.DrawTitleObjects \ We start by drawing a robot or the Sentinel, according \ to the object type in A LDY #1 \ Set Y = 1 so the call to DrawTitleObject chooses the \ configuration for the object standing on the tower in \ the main title screen or secret code screen JSR DrawTitleObject \ Draw an object of type A, so that's either a robot or \ the Sentinel \ We now draw the tower on which the robot or Sentinel \ standing LDY #2 \ Set Y = 2 so the call to DrawTitleObject chooses the \ configuration for the tower on which the robot or \ Sentinel is standing LDA #6 \ Set A to 6, the object type for the Sentinel's tower, \ so DrawTitleObject draws the tower \ Fall through into DrawTitleObject to draw the tower \ beneath the robot or the Sentinel
Name: DrawTitleObject [Show more] Type: Subroutine Category: Title screen Summary: Draw an object on the title screen
Context: See this subroutine on its own page References: This subroutine is called as follows: * DrawTitleObjects calls DrawTitleObject

Arguments: A The type of object to draw Y The configuration to use when drawing the object: * 1 = the Sentinel on the main title screen or the robot on the secret code screen * 2 = the tower on which the Sentinel or robot stands
.DrawTitleObject JSR SpawnTitleObject \ Call SpawnTitleObject to spawn an object of type A in \ object #1, using the configuration specified in Y LDY #1 \ Call DrawObject with Y = 1 to draw object #1 and JMP DrawObject \ return from the subroutine using a tail call
Name: PlayMusic [Show more] Type: Subroutine Category: Sound Summary: Play a piece of music
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainGameLoop calls PlayMusic * PerformHyperspace calls PlayMusic * ProcessActionKeys (Part 2 of 2) calls PlayMusic * ShowGameOverScreen calls PlayMusic

Arguments: A The number of the music to play (which is expressed as a byte offset into the music data at musicData): * 0 = hyperspace * 25 = player has just transferred onto the Sentinel's tower * 40 = U-turn * 50 = game over * 66 = landscape successfully finished
.PlayMusic STA musicCounter \ Set the musicCounter to the value in A to start the \ music playing from this point LDA #3 \ Set soundEffect = 3 to tell the ProcessSound routine STA soundEffect \ that this is music and should therefore be processed \ using the ProcessMusic routine RTS \ Return from the subroutine EQUB &23 \ This byte appears to be unused
Name: ConfigureMachine [Show more] Type: Subroutine Category: Setup Summary: Configure the custom screen mode, set the break handler to clear memory, move code, reset timers and set the interrupt handler
Context: See this subroutine on its own page References: This subroutine is called as follows: * Entry calls ConfigureMachine

The custom screen mode is based on screen mode 5, but with only 25 character rows rather than 32. This gives the game screen a letterbox appearance. The custom mode has a vertical resolution of 25 * 8 = 200 pixels, compared to the 256 pixels of standard mode 5. The horizontal resolution is the same at 160 pixels. Screen memory for the custom mode runs from &6000 to &7F3F, with four pixels per byte and four colours per pixel.
CLEAR &3F00, &4100 \ Memory from &3F00 to &4100 has three separate uses ORG &3F00 \ \ During startup it is used to store the startup \ routines ConfigureMachine and ClearMemory, which \ aren't needed again once the game has started \ \ While the landscape is being generated it is used to \ store the secret code stash at secretCodeStash \ \ While the game is running it is used to store the \ first few rows of the screen buffer, starting from \ screenBufferRow0 \ \ These lines rewind BeebAsm's assembly back to &3F00 \ and clear the block from that point to the end of the \ startup code at &4100, so we can assemble the startup \ routines \ \ At the end of the ClearMemory routine, the game binary \ actually contains snippets of the original source \ code, left over from the BBC Micro assembly process, \ so we include this workspace noise to ensure that we \ generate an exact match for the game binary SEC \ Set bit 7 of gameInProgress to indicate that a game is ROR gameInProgress \ not currently in progress and that we are in the title \ and preview screens (so the interrupt handler doesn't \ progress the game) \ \ This code is never actually run in this location \ (identical code can be found at the start of the \ ResetVariables routine) .ConfigureMachine LDA #4 \ Call OSBYTE with A = 4, X = 1 and Y = 0 to disable LDY #0 \ cursor editing LDX #1 JSR OSBYTE LDA #144 \ Call OSBYTE with A = 144, X = 0 and Y = 0 to switch LDX #0 \ interlace on LDY #0 JSR OSBYTE LDA #22 \ Switch to screen mode 5 with the following VDU JSR OSWRCH \ command: LDA #5 \ JSR OSWRCH \ VDU 22, 5 SEI \ Disable interrupts so we can update the 6845 registers LDA #6 \ Set 6845 register R6 = 25 STA SHEILA+&00 \ LDA #25 \ This is the "vertical displayed" register, which sets STA SHEILA+&01 \ the number of displayed character rows to 25. For \ comparison, this value is 32 for standard mode 5, but \ we claw back seven rows to create the game's letterbox \ screen mode LDA #7 \ Set 6845 register R7 = 32 STA SHEILA+&00 \ LDA #32 \ This is the "vertical sync position" register, which STA SHEILA+&01 \ determines the vertical sync position with respect to \ the reference, programmed in character row times. For \ comparison this is 34 for mode 5, but it needs to be \ adjusted for our custom screen's vertical sync LDA #10 \ Set 6845 register R10 = %00100000 STA SHEILA+&00 \ LDA #%00100000 \ This is the "cursor start" register, and bits 5 and 6 STA SHEILA+&01 \ define the "cursor display mode", as follows: \ \ * %00 = steady, non-blinking cursor \ \ * %01 = do not display a cursor \ \ * %10 = fast blinking cursor (blink at 1/16 of the \ field rate) \ \ * %11 = slow blinking cursor (blink at 1/32 of the \ field rate) \ \ We can therefore turn off the cursor completely by \ setting cursor display mode %01, with bit 6 of R10 \ clear and bit 5 of R10 set CLI \ Re-enable interrupts LDA #151 \ Call OSBYTE with A = 151, X = &42 and Y = %11111111 to LDX #&42 \ write the value %11111111 to SHEILA+&42 LDY #%11111111 \ JSR OSBYTE \ This sets the direction of all eight ports of the 6522 \ System VIA to output by setting the corresponding bits \ in the Data Direction Register B (SHEILA &42) LDA #151 \ Call OSBYTE with A = 151, X = &40 and Y = %00000101 to LDX #&40 \ write the value %00000101 to SHEILA+&40 LDY #%00000101 \ JSR OSBYTE \ Writing a value of %vaaa to SHEILA+&40 writes to the \ System VIA's addressable latch, setting latch address \ %aaa to value v \ \ This therefore sets address %101 to 1, which is \ address B5 in the System VIA \ \ We now we set B4 as well LDA #151 \ Call OSBYTE with A = 151, X = &40 and Y = %00001100 to LDX #&40 \ write the value %00001100 to SHEILA+&40 LDY #%00001100 \ JSR OSBYTE \ Writing a value of %vaaa to SHEILA+&40 writes to the \ System VIA's addressable latch, setting latch address \ %aaa to value v \ \ This therefore sets address %100 to 0, which is \ address B4 in the System VIA \ \ B4 and B5 in the System VIA control the address of the \ start of screen memory and the screen size, so this \ sets screen memory to &6000 and screen size to 8K (see \ page 429 of the "Advanced User Guide for the BBC \ Micro" by Bray, Dickens and Holmes for details) LDA #0 \ Call OSBYTE with A = 0 and X = 255 to fetch the LDX #255 \ operating system version into X JSR OSBYTE CPX #0 \ If X = 0 then this is either a BBC Micro running an BEQ setp1 \ operating system version of 1.00 or earlier, or it's \ an Electron, so in either case jump to setp1 to skip \ the following \ If we get here then this is not an Electron or an \ early operating system, so it must be a BBC Micro with \ MOS 1.20 or later, or a BBC Master LDA #200 \ Call OSBYTE with A = 200, X = 2 and Y = 0 to set the LDX #2 \ normal action for the ESCAPE key and clear memory if LDY #0 \ the BREAK key is pressed JSR OSBYTE JMP setp3 \ Jump to setp3 to skip the setup for the Electron .setp1 \ If we get here then this is either a BBC Micro running \ MOS 1.00 or earlier, or it's an Electron \ \ In MOS 0.10, OSBYTE 200 does not let you set the BREAK \ key to clear memory, so we need to set this up by hand \ (otherwise crackers could load the game on MOS 0.10 \ and simply press BREAK to access the loaded game code) \ \ The Electron and MOS 1.00 do not need this code, as \ OSBYTE 200 is supported in these versions of the \ operating system, but OSBYTE 0 doesn't distinguish \ between the Electron and MOS 1.00 and earlier (they \ all return X = 0) \ \ There's no harm in manually clearing memory on those \ systems, though, so the following code is run on \ BREAK on MOS 1.00 and earlier and on the Electron \ \ To set this up, we copy the break handler routine from \ ClearMemory to address BRKI (this points to the \ cassette filing system workspace, which is unused \ now that the game is loaded, so it's a suitable \ location for our handler) LDX #&1C \ Set a counter in X for the size of the ClearMemory \ routine .setp2 LDA ClearMemory,X \ Copy the X-th byte of the ClearMemory routine into the STA BRKI,X \ X-th byte of BRKI DEX \ Decrement the byte counter BPL setp2 \ Loop back until we have copied the whole ClearMemory \ routine to BRKI LDA #&4C \ Set the Break Intercept code to the following, so that STA BRKIV \ BRKI gets called when the BREAK key is pressed: LDA #LO(BRKI) \ STA BRKIV+1 \ 4C 80 03 JMP BRKI LDA #HI(BRKI) \ STA BRKIV+2 \ &4C is the opcode for the JMP instruction, and BRKI is \ at address &0380 (part of the cassette filing system \ workspace) .setp3 \ Next we copy a block of game code in memory as \ follows: \ \ * &4100-&49FF is copied to &5800-&60FF \ \ The game binary could easily have been structured to \ avoid this copy, so presumably it's just done to make \ the game code harder to crack LDA #&00 \ Set (Q P) = &4100 STA P \ STA R \ We use this as the source address for the copy LDA #&41 STA Q LDA #&58 \ Set (S R) = &5800 STA S \ \ We use this as the destination address for the copy .setp4 LDY #0 \ Set up a byte counter in Y .setp5 LDA (P),Y \ Copy the Y-th byte of (Q P) to the Y-th byte of (S R) STA (R),Y DEY \ Decrement the byte counter BNE setp5 \ Loop back until we have copied a whole page of bytes INC Q \ Increment the high byte of (Q P) to point to the next \ page in memory INC S \ Increment the high byte of (S R) to point to the next \ page in memory LDA Q \ Loop back until (Q P) reaches &4A00, at which point we CMP #&4A \ have copied the whole block of memory BCC setp4 SEI \ Disable interrupts so we can update the interrupt \ vector and VIA LDA IRQ1V \ Store the current address from the IRQ1V vector in STA irq1Address \ irq1Address, so the IRQ handler can jump to it after LDA IRQ1V+1 \ implementing the custom interrupt handler STA irq1Address+1 \ We now wait for the vertical sync, which we can check \ by reading bit 1 of the 6522 System VIA status byte \ (SHEILA &4D), which is set if vertical sync has \ occurred on the video system LDA #%00000010 \ Set a bit mask in A that we can use to read bit 1 of \ the 6522 System VIA status byte .setp6 BIT SHEILA+&4D \ Loop around until bit 1 of the 6522 System VIA status BEQ setp6 \ byte is set, so we wait until the vertical sync \ We now set timer 1 in the 6522 User VIA to count down \ regularly, triggering the interrupt handler routine at \ IRQHandler every time it counts down \ \ The timer is initially set to count down from 14,592 \ to zero, and then a value of 19,998 is latched into \ the timer so that it triggers the interrupt every time \ it counts down from 20,000 to zero (the latching \ process takes two ticks, which gives us a total count \ of 20,000) \ \ The timer counts down at 1 MHz. or one million times a \ second, so this means the interrupt is triggered every \ 0.02 seconds, or exactly 50 times a second \ \ This regular interrupt is used to progress the game \ counters and manage the screen panning effect (see the \ IRQHandler routine for details) LDA #%01000000 \ Set 6522 User VIA auxiliary control register ACR STA SHEILA+&6B \ (SHEILA &6B) bits 7 and 6 to disable PB7 (which is one \ of the pins on the user port) and set continuous \ interrupts for timer 1 LDA #%11000000 \ Set 6522 User VIA interrupt enable register IER STA SHEILA+&6E \ (SHEILA &4E) bits 6 and 7 (i.e. enable the Timer1 \ interrupt from the User VIA) LDA #&00 \ Set 6522 User VIA T1C-L timer 1 low-order counter STA SHEILA+&64 \ (SHEILA &64) to &00 (so this sets the low-order \ counter but does not start counting until the \ high-order counter is set) LDA #&39 \ Set 6522 User VIA T1C-H timer 1 high-order counter STA SHEILA+&65 \ (SHEILA &45) to &39 to start the T1 counter \ counting down from &3900 (14592) at a rate of 1 MHz LDA #&1E \ Set 6522 User VIA T1L-L timer 1 low-order latches STA SHEILA+&66 \ to &1E (so this sets the low-order counter but does \ not start counting until the high-order counter is \ set) LDA #&4E \ Set 6522 User VIA T1L-H timer 1 high-order latches STA SHEILA+&67 \ to &4E (so this sets the timer to &4E1E (19998) but \ does not start counting until the current timer has \ run down) LDA #HI(IRQHandler) \ Set the IRQ1V vector to IRQHandler, so the IRQHandler STA IRQ1V+1 \ routine is now the interrupt handler LDA #LO(IRQHandler) STA IRQ1V CLI \ Re-enable interrupts JMP MainTitleLoop \ Jump to MainTitleLoop to start the main title loop, \ where we display the title screen, fetch the landscape \ number and code, preview the landscape and then jump \ to the main game loop
Name: ClearMemory [Show more] Type: Subroutine Category: Setup Summary: Clear game memory, so that the BREAK key can remove all trace of the game code in early versions of the operating system
Context: See this subroutine on its own page References: This subroutine is called as follows: * ConfigureMachine calls ClearMemory
.ClearMemory LDA #&04 \ Set (Q P) = &0400 STA Q \ LDA #&00 \ We use this as the start address for clearing memory, STA P \ which is where the game code starts \ \ This also sets A = 0, which we can use to zero memory .cmem1 LDY #&FF \ Set Y = &FF to use as a byte counter .cmem2 STA (P),Y \ Zero the Y-th byte at (Q P) DEY \ Decrement the byte counter BNE cmem2 \ Loop back until we have zeroed a whole page of memory INC Q \ Increment the high byte of (Q P) to point to the next \ page in memory LDX Q \ Loop back until (Q P) reaches &7C00, at which point we CPX #&7C \ have zeroed all the game code (including any code BCC cmem1 \ still left at the higher memory address where it's \ first loaded) LDA #&00 \ Set the Break Intercept code to a BRK instruction to STA BRKIV \ reinstate the default break handler RTS \ Return from the subroutine EQUB &20, &65, &74, &73, &36, &0D, &12, &CA \ These bytes are unused until EQUB &19, &20, &20, &20, &20, &20, &20, &4C \ the game is in progress, at EQUB &44, &58, &23, &36, &3A, &4A, &53, &52 \ which point this whole section EQUB &20, &43, &46, &4C, &53, &48, &0D, &12 \ of memory is reused EQUB &D4, &05, &20, &0D, &12, &DE, &0D, &2E \ EQUB &65, &74, &73, &36, &20, &72, &74, &73 \ These values are workspace EQUB &0D, &12, &E8, &05, &20, &0D, &12, &F2 \ noise and have no meaning EQUB &05, &20, &0D, &12, &FC, &05, &20, &0D \ EQUB &13, &06, &05, &20, &0D, &13, &10, &05 \ They actually contain snippets EQUB &20, &0D, &13, &1A, &05, &20, &0D, &13 \ of the original source code EQUB &24, &05, &20, &0D, &13, &2E, &2A, &2E EQUB &4D, &49, &4E, &49, &20, &4C, &44, &41 EQUB &23, &31, &32, &38, &3A, &53, &54, &41 EQUB &20, &4D, &45, &41, &4E, &59, &2C, &58 EQUB &3A, &53, &54, &41, &20, &4D, &45, &4D EQUB &4F, &52, &59, &2C, &58, &0D, &13, &38 EQUB &1F, &20, &20, &20, &20, &20, &20, &4C EQUB &44, &41, &23, &30, &3A, &53, &54, &41 EQUB &20, &4D, &45, &41, &4E, &59, &53, &43 EQUB &41, &4E, &2C, &58, &0D, &13, &42, &22 EQUB &20, &20, &20, &20, &20, &20, &4C, &44 EQUB &41, &23, &36, &34, &3A, &53, &54, &41 EQUB &20, &4D, &54, &52, &59, &43, &4E, &54 EQUB &2C, &58, &3A, &72, &74, &73, &0D, &13 EQUB &4C, &05, &20, &0D, &13, &56, &1A, &2E EQUB &4D, &45, &41, &4E, &20, &4C, &44, &41 EQUB &23, &34, &30, &3A, &53, &54, &41, &20 EQUB &43, &4F, &56, &45, &52, &0D, &13, &60 EQUB &1B, &20, &20, &20, &20, &20, &20, &4C EQUB &44, &58, &20, &45, &54, &45, &4D, &3A EQUB &53, &54, &58, &20, &58, &54
Name: Entry [Show more] Type: Subroutine Category: Setup Summary: The main entry point for the game
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
ORG &6D00 \ Set the assembly address to &6D00 .Entry \ We start by copying the game code in memory as \ follows: \ \ * &1900-&6CFF is copied to &0400-&57FF \ \ The game binary has a load address of &1900 and an \ execution address of &6D00 (the address of this \ routine) LDA #&00 \ Set (Q P) = &1900 STA P \ STA R \ We use this as the source address for the copy LDA #&19 STA Q LDA #&04 \ Set (S R) = &0400 STA S \ \ We use this as the destination address for the copy .entr1 LDY #0 \ Set up a byte counter in Y .entr2 LDA (P),Y \ Copy the Y-th byte of (Q P) to the Y-th byte of (S R) STA (R),Y INY \ Increment the byte counter BNE entr2 \ Loop back until we have copied a whole page of bytes INC Q \ Increment the high byte of (Q P) to point to the next \ page in memory INC S \ Increment the high byte of (S R) to point to the next \ page in memory LDA Q \ Loop back until (Q P) reaches &6D00, at which point we CMP #&6D \ have copied all the game code BNE entr1 JMP ConfigureMachine \ Jump to ConfigureMachine to configure the computer, \ ready for a new game
Save Sentinel.bin
\ Game addr to file addr COPYBLOCK &4900, &4A00, &6000 \ 4900-49FF to 6000-60FF COPYBLOCK &5800, &6100, &4100 \ 5800-60FF to 4100-49FF COPYBLOCK &0400, &5800, LOAD% \ 0400-57FF to 1900-6CFF SAVE "3-assembled-output/TheSentinel.bin", LOAD%, P%