| Name | Author | Game Mode | Rating | |||||
|---|---|---|---|---|---|---|---|---|
| BoxSync | Cranky | Mutator | N/A | |||||
bool DEBUG_ON = false;
bool DEBUG_ALERTS_ON = false;
bool ONLY_COOP_MODES = true; //set to false if you want crates to be solid in all modes (not only SP and COOP)
bool checkGamemode = true;
bool doOnce = false;
bool ok_solid = false;
array<jjOBJ@> solids;
array<int> solidDirection;
array<int> pushLeftPlayers(32, -1);
array<int> pushRightPlayers(32, -1);
array<bool> playerSynced(32, false);
array<int> latchedSolid(32, -1);
array<int> latchedDir(32, 0);
array<float> lastStreamX;
array<float> lastStreamY;
int kk = 0;
const int BOXSYNC_POSITION_SEND_INTERVAL = 1;
const float BOX_PUSH_SPEED = 0.5f;
const int BORDER_THICKNESS_SMALL_OBJECT = 32;
const int BORDER_THICKNESS_BIG_OBJECT = 32;
const int epsilon = 3;
const int BODY_BLOCK_THICKNESS = 8;
const int PUSH_CONTACT_GAP = 2;
const float BOXSYNC_POSITION_EPSILON = 0.05f;
bool isSolidBoxObject(jjOBJ@ obj){
return obj.behavior == BEHAVIOR::AMMO15 || obj.behavior == BEHAVIOR::BIGOBJECT || obj.behavior == BEHAVIOR::CRATE || obj.behavior == BEHAVIOR::MONITOR || obj.eventID == OBJECT::BIGBOX || obj.eventID == OBJECT::BIGROCK || obj.eventID == OBJECT::FIRESHIELD || obj.eventID == OBJECT::WATERSHIELD || obj.eventID == OBJECT::LIGHTNINGSHIELD || obj.eventID == OBJECT::MORPH || obj.eventID == OBJECT::BIRDMORPH || obj.eventID == OBJECT::BLASTERPOWERUP || obj.eventID == OBJECT::BOUNCERPOWERUP || obj.eventID == OBJECT::ICEPOWERUP || obj.eventID == OBJECT::SEEKERPOWERUP || obj.eventID == OBJECT::RFPOWERUP || obj.eventID == OBJECT::TOASTERPOWERUP || obj.eventID == OBJECT::TNTPOWERUP || obj.eventID == OBJECT::GUN8POWERUP || obj.eventID == OBJECT::GUN9POWERUP;
}
bool isBigBoxObject(jjOBJ@ obj){
return obj.behavior == BEHAVIOR::BIGOBJECT || obj.eventID == OBJECT::BIGBOX || obj.eventID == OBJECT::BIGROCK;
}
void normalizeBigBoxObject(jjOBJ@ obj){
if(obj.eventID == OBJECT::BIGBOX || obj.eventID == OBJECT::BIGROCK) obj.behavior = BEHAVIOR::BIGOBJECT;
}
bool hasBoxObjectAtTile(int eventID, int x, int y){
for(int i = 1; i < jjObjectCount; i++){
jjOBJ@ obj = jjObjects[i];
if(!obj.isActive || int(obj.eventID) != eventID) continue;
if(int(obj.xOrg / 32) == x && int(obj.yOrg / 32) == y) return true;
}
return false;
}
void spawnMissingBigBoxObjects(){
for(int y = 0; y < jjLayerHeight[4]; y++){
for(int x = 0; x < jjLayerWidth[4]; x++){
int eventID = jjEventGet(x, y);
if(eventID != OBJECT::BIGBOX && eventID != OBJECT::BIGROCK) continue;
if(hasBoxObjectAtTile(eventID, x, y)) continue;
int objectID = jjAddObject(eventID, x * 32, y * 32, 0, CREATOR::LEVEL, BEHAVIOR::DEFAULT);
if(objectID > 0 && objectID < jjObjectCount) normalizeBigBoxObject(jjObjects[objectID]);
}
}
}
int findSolidIndexByOrigin(int eventID, float xOrg, float yOrg){
for(uint i = 0; i < solids.length; i++){
jjOBJ@ obj = solids[i];
if(obj is null || !obj.isActive) continue;
if(int(obj.eventID) == eventID && abs(obj.xOrg - xOrg) < 8 && abs(obj.yOrg - yOrg) < 8) return int(i);
}
return -1;
}
void ensureSolidSetup(){
if(!doOnce){
spawnMissingBigBoxObjects();
for(int i=0; i<jjObjectCount; i++){
jjOBJ@ obj = jjObjects[i];
normalizeBigBoxObject(obj);
if(isSolidBoxObject(obj)){
obj.clearPlatform();
}
}
doOnce = true;
}
if(!ok_solid){
kk = 0;
solids.resize(0);
for (int i = 1; i < jjObjectCount; i++) {
jjOBJ@ obj = jjObjects[i];
normalizeBigBoxObject(obj);
if((jjGameMode == GAME::SP || jjGameMode == GAME::COOP) && isSolidBoxObject(obj)){
solids.resize(kk+1);
@solids[kk] = obj;
kk++;
}
}
solidDirection.resize(solids.length);
lastStreamX.resize(solids.length);
lastStreamY.resize(solids.length);
for(uint i=0; i<solidDirection.length; i++){
solidDirection[i] = 0; //making sure
if(solids[i] !is null){
lastStreamX[i] = solids[i].xPos;
lastStreamY[i] = solids[i].yPos;
}else{
lastStreamX[i] = 0;
lastStreamY[i] = 0;
}
}
ok_solid = true;
}
}
void BoxSync_OnLevelLoad()
{
checkGamemode = true;
doOnce = false;
ok_solid = false;
solids.resize(0);
solidDirection.resize(0);
lastStreamX.resize(0);
lastStreamY.resize(0);
pushLeftPlayers.resize(32);
pushRightPlayers.resize(32);
for(uint i=0; i<32; i++)
{
pushLeftPlayers[i] = -1;
pushRightPlayers[i] = -1;
playerSynced[i] = false;
latchedSolid[i] = -1;
latchedDir[i] = 0;
}
kk = 0;
}
void BoxSync_OnMain()
{
ensureSolidSetup();
if(jjIsServer){
syncNewClients();
}
syncMovedSolidPositions();
}
void syncNewClients(){
for(uint p = 0; p < 32; p++){
if(jjPlayers[p].isInGame){
if(!playerSynced[p]){
playerSynced[p] = true;
int targetClientID = jjPlayers[p].clientID;
// clientID 0 is the server/local player. Do not use the normal
// broadcast path for join sync, because this is meant only for
// remote clients who just entered.
if(targetClientID > 0){
// Send all current object positions to this specific player.
// IMPORTANT: this must use a positive clientID. The old code used
// send_solidPosPacket(i, targetClientID), but that function sends
// to -clientID, meaning "everyone except this client". That is why
// the joining client never received the sync.
for(uint i = 0; i < solids.length; i++){
send_solidPosPacketToClient(i, targetClientID);
}
// Optional: also send current push state to this one client.
for(uint q = 0; q < 32; q++){
if(pushLeftPlayers[q] != -1) send_solidPacketToClient(pushLeftPlayers[q], q, -1, targetClientID);
if(pushRightPlayers[q] != -1) send_solidPacketToClient(pushRightPlayers[q], q, 1, targetClientID);
}
}
}
}else{
playerSynced[p] = false;
pushLeftPlayers[p] = -1;
pushRightPlayers[p] = -1;
latchedSolid[p] = -1;
latchedDir[p] = 0;
}
}
setSolidControl(-1, 0);
}
void BoxSync_OnDrawLayer4(jjPLAYER@ play, jjCANVAS@ canvas)
{
}
void BoxSync_OnPlayer(jjPLAYER@ play)
{
if(!play.isLocal) return;
solidCode(play);
}
bool BoxSync_OnReceive(jjSTREAM &in packet, int clientID)
{
string text;
packet.pop(text);
if(jjRegexMatch(text, "solidSend", true)){
ensureSolidSetup();
int id, playerID, dir;
packet.pop(id);
packet.pop(playerID);
packet.pop(dir);
if(id < 0 || id >= int(solids.length) || playerID < 0 || playerID >= 32) return true;
int newSolidPlayer = dir == 0 ? -1 : id;
pushLeftPlayers[playerID] = dir == -1 ? newSolidPlayer : -1;
pushRightPlayers[playerID] = dir == 1 ? newSolidPlayer : -1;
setSolidControl(id, dir);
if(jjIsServer){
jjSTREAM solidPacket;
solidPacket.push("solidSend");
solidPacket.push(id);
solidPacket.push(playerID);
solidPacket.push(dir);
jjSendPacket(solidPacket, -clientID);
}
return true;
}
else if(jjRegexMatch(text, "solidPos", true)){
ensureSolidSetup();
int id, eventID;
float xOrg, yOrg, xPos, yPos;
packet.pop(id);
packet.pop(eventID);
packet.pop(xOrg);
packet.pop(yOrg);
packet.pop(xPos);
packet.pop(yPos);
if(id < 0 || id >= int(solids.length)) id = findSolidIndexByOrigin(eventID, xOrg, yOrg);
if(id < 0 || id >= int(solids.length)) return true;
solids[id].xPos = xPos;
solids[id].yPos = yPos;
rememberStreamPosition(id);
if(jjIsServer){
jjSTREAM solidPacket;
solidPacket.push("solidPos");
solidPacket.push(id);
solidPacket.push(eventID);
solidPacket.push(xOrg);
solidPacket.push(yOrg);
solidPacket.push(xPos);
solidPacket.push(yPos);
jjSendPacket(solidPacket, -clientID);
}
return true;
}
return false;
}
void solidCode(jjPLAYER@ play){
ensureSolidSetup();
//crate pushing code
if(jjGameMode == GAME::SP || jjGameMode == GAME::COOP || !ONLY_COOP_MODES){
checkGamemode = true;
for (uint i = 0; i < solids.length; i++) {
jjOBJ@ obj = solids[i];
if(obj is null) continue;
if(!obj.isActive)
{
obj.clearPlatform();
continue;
}
if(obj.isActive && isSolidBoxObject(obj))
{
if(obj.state == STATE::KILL)
{
obj.clearPlatform();
}
else
{
jjANIMFRAME@ frame = @::jjAnimFrames[obj.curFrame];
int width = frame.width;
int height = frame.height;
int hotX = frame.hotSpotX;
int hotY = frame.hotSpotY;
obj.beSolid();
int isSolid = getPushDirection(play, obj, width, hotX, hotY, height);
//removing this IF will cause the players to pass through the object
if(isSolid != 0 && solidDirection[i] != 2)
{
//collision
if(!wouldHitWall(obj, isSolid, width, hotX, hotY, height)){
obj.xPos += isSolid * BOX_PUSH_SPEED;
send_solidPacket(i, play.playerID, isSolid, play.clientID);
}else{
send_solidPacket(i, play.playerID, 0, play.clientID);
}
}
else
{
bool col = false;
bool descend = true;
for (uint j = 0; j < solids.length; j++) {
jjOBJ@ obj2 = solids[j];
if(i != j && obj.objectID != obj2.objectID && obj2.state == STATE::SLEEP){ //state of crate
jjANIMFRAME@ frame2 = @::jjAnimFrames[obj2.curFrame];
int width2 = frame2.width;
int height2 = frame2.height;
int hotX2 = frame2.hotSpotX;
int hotY2 = frame2.hotSpotY;
if(abs(obj.yPos - obj2.yPos) <= 16 && abs(obj.xPos - obj2.xPos) <= 100){
if((abs((obj.xPos + hotX) - (obj2.xPos + hotX2 + width2)) <= 2 && isSolid == -1) || (abs((obj.xPos + hotX + width) - (obj2.xPos + hotX2)) <= 2 && isSolid == 1)){
col = true;
break;
}
}else if(abs(obj.xPos - obj2.xPos) <= 32 && obj2.yPos - obj.yPos >= 0 && obj2.yPos - obj.yPos <= height/2 + height2/2){
descend = false;
}
}
}
bool obj_moved = false;
//can push object
int localDirection = isSolid; //if you use obj.beSolid() multiple times here, then pushing animation won't go into effect
bool keyCondition = (play.keyRight && localDirection == 1) || (play.keyLeft && localDirection == -1);
bool keyCondition2 = (!play.keyRight && localDirection == 1 && pushRightPlayers[play.playerID] == int(i)) || (!play.keyLeft && localDirection == -1 && pushLeftPlayers[play.playerID] == int(i));
float pDis = findNearestPlayerToObject(obj, play.playerID);//abs(jjPlayers[pID].xPos - obj.xPos);
bool pFoundCond = false;
pFoundCond = jjIsServer && pDis < (isBigBoxObject(obj) ? 64 : 32); //= obj.doesCollide(jjPlayers[pID]);
if(col == false && solidDirection[i] != 2 && !pFoundCond && !wouldHitWall(obj, localDirection, width, hotX, hotY, height)){ //&& keyCondition){
obj.xPos += localDirection * BOX_PUSH_SPEED;
obj_moved = localDirection != 0;
}else if(pFoundCond){
localDirection = 0;
obj.xPos += localDirection * 0.5f;
//obj.direction = 0;
//obj.clearPlatform();
//obj_moved = true;
}
//if(i == 0 && jjGameTicks%70 == 0) jjAlert("hmm "+localDirection+" "+obj.state);
if(jjMaskedHLine(int(obj.xPos)+hotX+1, width-1, int(obj.yPos)+hotY+height) == false && descend == true){
obj.yPos += 2;
obj_moved = true;
}
//if this wasn't moved locally
send_solidPacket(i, play.playerID, localDirection, 0, pDis);
if(!pFoundCond){
// Do not predict remote pushes here. The active mover streams xPos/yPos,
// and everyone else only applies solidPos packets. This prevents stale
// non-pusher positions from fighting the pusher.
obj.xPos += 0;
}
}
}
}
}
syncMovedSolidPositions();
}else{
if(checkGamemode){
for(int i=0; i<32; i++){
if(jjPlayers[i].isInGame){
if(jjPlayers[i].platform != 0){
jjOBJ@ obj = jjObjects[jjPlayers[i].platform];
obj.clearPlatform();
normalizeBigBoxObject(obj);
if(obj.isActive && isSolidBoxObject(obj)){
jjPlayers[i].platform = 0;
}
}
}
}
checkGamemode = false;
doOnce = false;
}
}
}
void send_solidPacket(int id, int playerID, int dir, int clientID, float pDis = -1){
int newSolidPlayer = dir == 0 ? -1 : id;
bool enterCondition = ((dir == -1 || dir == 0) && pushLeftPlayers[playerID] != newSolidPlayer) || ((dir == 1 || dir == 0) && pushRightPlayers[playerID] != newSolidPlayer);
if(solidDirection[id] != dir && enterCondition){
if(DEBUG_ALERTS_ON) jjAlert("dir "+solidDirection[id]+" "+dir+" "+pDis);
//solidDirection[id] = dir;
pushLeftPlayers[playerID] = dir == -1 ? newSolidPlayer : -1;
pushRightPlayers[playerID] = dir == 1 ? newSolidPlayer : -1;
setSolidControl(id, dir);
//send control packet only; position is handled by solidPos streaming
jjSTREAM solidPacket;
solidPacket.push("solidSend");
solidPacket.push(id);
solidPacket.push(playerID);
solidPacket.push(dir);
if(clientID == 0) jjSendPacket(solidPacket);
else jjSendPacket(solidPacket, -clientID);
}
}
void send_solidPacketToClient(int id, int playerID, int dir, int targetClientID){
if(id < 0 || id >= int(solids.length)) return;
if(playerID < 0 || playerID >= 32) return;
if(targetClientID <= 0) return;
jjSTREAM solidPacket;
solidPacket.push("solidSend");
solidPacket.push(id);
solidPacket.push(playerID);
solidPacket.push(dir);
jjSendPacket(solidPacket, targetClientID);
}
void send_solidPosPacketToClient(int id, int targetClientID){
if(id < 0 || id >= int(solids.length)) return;
if(targetClientID <= 0) return;
jjOBJ@ obj = solids[id];
if(obj is null || !obj.isActive) return;
jjSTREAM solidPacket;
solidPacket.push("solidPos");
solidPacket.push(id);
solidPacket.push(int(obj.eventID));
solidPacket.push(obj.xOrg);
solidPacket.push(obj.yOrg);
solidPacket.push(obj.xPos);
solidPacket.push(obj.yPos);
jjSendPacket(solidPacket, targetClientID);
rememberStreamPosition(id);
}
void send_solidPosPacket(int id, int clientID){
if(id < 0 || id >= int(solids.length)) return;
jjOBJ@ obj = solids[id];
if(obj is null || !obj.isActive) return;
jjSTREAM solidPacket;
solidPacket.push("solidPos");
solidPacket.push(id);
solidPacket.push(int(obj.eventID));
solidPacket.push(obj.xOrg);
solidPacket.push(obj.yOrg);
solidPacket.push(obj.xPos);
solidPacket.push(obj.yPos);
if(clientID == 0) jjSendPacket(solidPacket);
else jjSendPacket(solidPacket, -clientID);
rememberStreamPosition(id);
}
void rememberStreamPosition(int id){
if(id < 0 || id >= int(solids.length)) return;
if(id >= int(lastStreamX.length)) return;
jjOBJ@ obj = solids[id];
if(obj is null) return;
lastStreamX[id] = obj.xPos;
lastStreamY[id] = obj.yPos;
}
bool solidPositionChangedSinceStream(int id){
if(id < 0 || id >= int(solids.length)) return false;
if(id >= int(lastStreamX.length)) return false;
jjOBJ@ obj = solids[id];
if(obj is null || !obj.isActive) return false;
return abs(obj.xPos - lastStreamX[id]) > BOXSYNC_POSITION_EPSILON || abs(obj.yPos - lastStreamY[id]) > BOXSYNC_POSITION_EPSILON;
}
void syncMovedSolidPositions(){
if(!ok_solid) return;
for(uint i = 0; i < solids.length; i++){
if(solidPositionChangedSinceStream(int(i))){
send_solidPosPacket(int(i), 0);
rememberStreamPosition(int(i));
}
}
}
void setSolidControl(int id, int dir){
for(uint i=0; i<solidDirection.length; i++) solidDirection[i] = 0;
for(uint i=0; i<32; i++){
int localSolidDirection = 0;
pushLeftPlayers[i] = jjPlayers[i].isInGame ? pushLeftPlayers[i] : -1;
pushRightPlayers[i] = jjPlayers[i].isInGame ? pushRightPlayers[i] : -1;
if(pushLeftPlayers[i] != -1 && 0 <= pushLeftPlayers[i] && pushLeftPlayers[i] < int(solidDirection.length)){
if(solidDirection[pushLeftPlayers[i]] == 1){
solidDirection[pushLeftPlayers[i]] = 2; //pushing from both directions
//jjAlert("2to: "+i);
}else solidDirection[pushLeftPlayers[i]] = -1; //for solidDirection[pushLeftPlayers[i]] = -1 or 0
}else if(pushRightPlayers[i] != -1 && 0 <= pushRightPlayers[i] && pushRightPlayers[i] < int(solidDirection.length)){
if(solidDirection[pushRightPlayers[i]] == -1){
solidDirection[pushRightPlayers[i]] = 2; //pushing from both directions
//jjAlert("2to: "+i);
}else solidDirection[pushRightPlayers[i]] = 1; //for solidDirection[pushRightPlayers[i]] = -1 or 0
}
}
if(DEBUG_ALERTS_ON) jjAlert("solid direction set: "+id+" "+solidDirection[id]+", "+
pushLeftPlayers[0]+" "+pushLeftPlayers[1]+" "+pushLeftPlayers[2]+", "+
pushRightPlayers[0]+" "+pushRightPlayers[1]+" "+pushRightPlayers[2]);
}
float findNearestPlayerToObject(jjOBJ@ obj, int skipPlayerID){
float min = 200; int p = -1;
for(uint i=0; i<32; i++){
if (int(i) == skipPlayerID) continue;
jjPLAYER@ play = jjPlayers[i];
//+++if(play.isInGame && !play.isLocal)
if(play.isInGame)
{
if(pushLeftPlayers[i] == -1 && pushRightPlayers[i] == -1 && abs(obj.yPos-play.yPos) < 32){
float value = abs(obj.xPos-play.xPos); //*(obj.xPos-play.xPos) + (obj.yPos-play.yPos)*(obj.yPos-play.yPos);
if(value < min){
min = value;
p = i;
//if(!jjIsServer) jjAlert("min "+p+" "+min);
}
}
}
}
return min;
}
/// <summary>
/// getPushDirection is a replacement for obj.beSolid()
/// as that is unreliable because it can say that the player is pushing for -1/1 frame
/// then for the next frame the player may not be perfectly aligned with the border
/// and the result is "not pushing" 0. So it will be 0,1,0,1...
/// Using these border thickness variables is an attempt to increase the area in which
/// the player is considered to be pushing the object
/// <summary>
int getPushDirection(jjPLAYER@ play, jjOBJ@ obj, int width, int hotX, int hotY, int height) {
int thickness = isBigBoxObject(obj) ? BORDER_THICKNESS_BIG_OBJECT : BORDER_THICKNESS_SMALL_OBJECT;
float leftBorderXMin = obj.xPos + hotX - thickness;
float leftBorderXMax = obj.xPos + hotX + epsilon;
float rightBorderXMin = obj.xPos + hotX + width - epsilon;
float rightBorderXMax = obj.xPos + hotX + width + thickness;
float objTop = obj.yPos + hotY;
float objBottom = obj.yPos + hotY + height;
float playerXCenter = play.xPos;
bool yOK = playerOverlapsY(objTop, objBottom, play);
int result = 0;
// Local player is on left side, pushing object right
if (
playerXCenter >= leftBorderXMin &&
playerXCenter <= leftBorderXMax &&
yOK &&
play.keyRight
) {
result = 1;
}
// Local player is on right side, pushing object left
if (
playerXCenter >= rightBorderXMin &&
playerXCenter <= rightBorderXMax &&
yOK &&
play.keyLeft
) {
result = -1;
}
// Bodyblock check:
// If another player is standing in the opposite side area,
// cancel the push before any movement happens.
if(result != 0) {
for(uint i = 0; i < 32; i++) {
if(!jjPlayers[i].isInGame || int(i) == play.playerID) continue;
jjPLAYER@ other = jjPlayers[i];
float otherXCenter = other.xPos;
bool otherYOK = playerOverlapsY(objTop, objBottom, other);
if(!otherYOK) continue;
bool otherOnLeft = otherXCenter >= leftBorderXMin && otherXCenter <= leftBorderXMax;
bool otherOnRight = otherXCenter >= rightBorderXMin && otherXCenter <= rightBorderXMax;
// pushing right, blocked by someone standing on right side
if(result == 1 && otherOnRight) {
result = 0;
break;
}
// pushing left, blocked by someone standing on left side
if(result == -1 && otherOnLeft) {
result = 0;
break;
}
}
}
if(DEBUG_ON) {
jjDrawRectangle(
int(leftBorderXMin),
int(obj.yPos + hotY),
int(leftBorderXMax - leftBorderXMin),
height,
result == 1 ? 24 : 12,
SPRITE::NORMAL,
0
);
jjDrawRectangle(
int(rightBorderXMin),
int(obj.yPos + hotY),
int(rightBorderXMax - rightBorderXMin),
height,
result == -1 ? 25 : 43,
SPRITE::NORMAL,
0
);
jjDrawRectangle(
int(playerXCenter),
int(play.yPos),
3,
32,
2,
SPRITE::NORMAL,
0
);
}
return result;
}
bool wouldHitWall(jjOBJ@ obj, int dir, int width, int hotX, int hotY, int height){
if(dir == 0) return false;
float nextX = obj.xPos + dir * BOX_PUSH_SPEED;
int checkX = dir > 0 ? int(nextX + hotX + width + 1) : int(nextX + hotX - 1);
int checkY = int(obj.yPos + hotY + 1);
int checkHeight = height - 2;
if(checkHeight < 1) checkHeight = height;
return jjMaskedVLine(checkX, checkY, checkHeight);
}
bool playerOverlapsY(float objTop, float objBottom, jjPLAYER@ play) {
float playerTop = play.yPos - 16;
float playerBottom = play.yPos + 16;
return playerBottom >= objTop && playerTop <= objBottom;
}
Jazz2Online © 1999-INFINITY (Site Credits). We have a Privacy Policy. Jazz Jackrabbit, Jazz Jackrabbit 2, Jazz Jackrabbit Advance and all related trademarks and media are ™ and © Epic Games. Lori Jackrabbit is © Dean Dodrill. J2O development powered by Loops of Fury and Chemical Beats.
Eat your lima beans, Johnny.