diff options
Diffstat (limited to 'sourcemod/scripting/gokz-core/map')
| -rw-r--r-- | sourcemod/scripting/gokz-core/map/buttons.sp | 138 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-core/map/end.sp | 155 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-core/map/mapfile.sp | 502 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-core/map/prefix.sp | 48 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-core/map/starts.sp | 219 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-core/map/triggers.sp | 855 | ||||
| -rw-r--r-- | sourcemod/scripting/gokz-core/map/zones.sp | 183 |
7 files changed, 2100 insertions, 0 deletions
diff --git a/sourcemod/scripting/gokz-core/map/buttons.sp b/sourcemod/scripting/gokz-core/map/buttons.sp new file mode 100644 index 0000000..8923fbd --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/buttons.sp @@ -0,0 +1,138 @@ +/* + Hooks between specifically named func_buttons and GOKZ. +*/ + + + +static Regex RE_BonusStartButton; +static Regex RE_BonusEndButton; + + + +// =====[ EVENTS ]===== + +void OnPluginStart_MapButtons() +{ + RE_BonusStartButton = CompileRegex(GOKZ_BONUS_START_BUTTON_NAME_REGEX); + RE_BonusEndButton = CompileRegex(GOKZ_BONUS_END_BUTTON_NAME_REGEX); +} + +void OnEntitySpawned_MapButtons(int entity) +{ + char buffer[32]; + + GetEntityClassname(entity, buffer, sizeof(buffer)); + if (!StrEqual("func_button", buffer, false)) + { + return; + } + + if (GetEntityName(entity, buffer, sizeof(buffer)) == 0) + { + return; + } + + int course = 0; + if (StrEqual(GOKZ_START_BUTTON_NAME, buffer, false)) + { + HookSingleEntityOutput(entity, "OnPressed", OnStartButtonPress); + RegisterCourseStart(course); + } + else if (StrEqual(GOKZ_END_BUTTON_NAME, buffer, false)) + { + HookSingleEntityOutput(entity, "OnPressed", OnEndButtonPress); + RegisterCourseEnd(course); + } + else if ((course = GetStartButtonBonusNumber(entity)) != -1) + { + HookSingleEntityOutput(entity, "OnPressed", OnBonusStartButtonPress); + RegisterCourseStart(course); + } + else if ((course = GetEndButtonBonusNumber(entity)) != -1) + { + HookSingleEntityOutput(entity, "OnPressed", OnBonusEndButtonPress); + RegisterCourseEnd(course); + } +} + +public void OnStartButtonPress(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + ProcessStartButtonPress(activator, 0); +} + +public void OnEndButtonPress(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + ProcessEndButtonPress(activator, 0); +} + +public void OnBonusStartButtonPress(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + int course = GetStartButtonBonusNumber(caller); + if (!GOKZ_IsValidCourse(course, true)) + { + return; + } + + ProcessStartButtonPress(activator, course); +} + +public void OnBonusEndButtonPress(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + int course = GetEndButtonBonusNumber(caller); + if (!GOKZ_IsValidCourse(course, true)) + { + return; + } + + ProcessEndButtonPress(activator, course); +} + + + +// =====[ PRIVATE ]===== + +static void ProcessStartButtonPress(int client, int course) +{ + if (GOKZ_StartTimer(client, course)) + { + // Only calling on success is intended behaviour (and prevents virtual button exploits) + OnStartButtonPress_Teleports(client, course); + OnStartButtonPress_VirtualButtons(client, course); + } +} + +static void ProcessEndButtonPress(int client, int course) +{ + GOKZ_EndTimer(client, course); + OnEndButtonPress_VirtualButtons(client, course); +} + +static int GetStartButtonBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartButton, 1); +} + +static int GetEndButtonBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndButton, 1); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/map/end.sp b/sourcemod/scripting/gokz-core/map/end.sp new file mode 100644 index 0000000..d119084 --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/end.sp @@ -0,0 +1,155 @@ +/* + Hooks between specifically named end destinations and GOKZ +*/ + + + +static Regex RE_BonusEndButton; +static Regex RE_BonusEndZone; +static CourseTimerType endType[GOKZ_MAX_COURSES]; +static float endOrigin[GOKZ_MAX_COURSES][3]; +static float endAngles[GOKZ_MAX_COURSES][3]; + + + +// =====[ EVENTS ]===== + +void OnPluginStart_MapEnd() +{ + RE_BonusEndButton = CompileRegex(GOKZ_BONUS_END_BUTTON_NAME_REGEX); + RE_BonusEndZone = CompileRegex(GOKZ_BONUS_END_ZONE_NAME_REGEX); +} + +void OnEntitySpawnedPost_MapEnd(int entity) +{ + char buffer[32]; + + GetEntityClassname(entity, buffer, sizeof(buffer)); + + if (StrEqual("trigger_multiple", buffer, false)) + { + bool isEndZone; + if (GetEntityName(entity, buffer, sizeof(buffer)) != 0) + { + if (StrEqual(GOKZ_END_ZONE_NAME, buffer, false)) + { + isEndZone = true; + StoreEnd(0, entity, CourseTimerType_ZoneNew); + } + else if (GetEndZoneBonusNumber(entity) != -1) + { + int course = GetEndZoneBonusNumber(entity); + if (GOKZ_IsValidCourse(course, true)) + { + isEndZone = true; + StoreEnd(course, entity, CourseTimerType_ZoneNew); + } + } + } + if (!isEndZone) + { + TimerButtonTrigger trigger; + if (IsTimerButtonTrigger(entity, trigger) && !trigger.isStartTimer) + { + StoreEnd(trigger.course, entity, CourseTimerType_ZoneLegacy); + } + } + } + else if (StrEqual("func_button", buffer, false)) + { + bool isEndButton; + if (GetEntityName(entity, buffer, sizeof(buffer)) != 0) + { + if (StrEqual(GOKZ_END_BUTTON_NAME, buffer, false)) + { + isEndButton = true; + StoreEnd(0, entity, CourseTimerType_Button); + } + else + { + int course = GetEndButtonBonusNumber(entity); + if (GOKZ_IsValidCourse(course, true)) + { + isEndButton = true; + StoreEnd(course, entity, CourseTimerType_Button); + } + } + } + if (!isEndButton) + { + TimerButtonTrigger trigger; + if (IsTimerButtonTrigger(entity, trigger) && !trigger.isStartTimer) + { + StoreEnd(trigger.course, entity, CourseTimerType_Button); + } + } + } +} + +void OnMapStart_MapEnd() +{ + for (int course = 0; course < GOKZ_MAX_COURSES; course++) + { + endType[course] = CourseTimerType_None; + } +} + +bool GetMapEndPosition(int course, float origin[3], float angles[3]) +{ + if (endType[course] == CourseTimerType_None) + { + return false; + } + + origin = endOrigin[course]; + angles = endAngles[course]; + + return true; +} + + + +// =====[ PRIVATE ]===== + +static void StoreEnd(int course, int entity, CourseTimerType type) +{ + // If StoreEnd is called, then there is at least an end position (even though it might not be a valid one) + if (endType[course] < CourseTimerType_Default) + { + endType[course] = CourseTimerType_Default; + } + + // Real zone is always better than "fake" zones which are better than buttons + // as the buttons found in a map with fake zones aren't meant to be visible. + if (endType[course] >= type) + { + return; + } + + float origin[3], distFromCenter[3]; + GetEntityPositions(entity, origin, endOrigin[course], endAngles[course], distFromCenter); + + // If it is a button or the center of the center of the zone is invalid + if (type == CourseTimerType_Button || !IsSpawnValid(endOrigin[course])) + { + // Attempt with various positions around the entity, pick the first valid one. + if (!FindValidPositionAroundTimerEntity(entity, endOrigin[course], endAngles[course], type == CourseTimerType_Button)) + { + endOrigin[course][2] -= 64.0; // Move the origin down so the eye position is directly on top of the button/zone. + return; + } + } + + // Only update the CourseTimerType if a valid position is found. + endType[course] = type; +} + +static int GetEndButtonBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndButton, 1); +} + +static int GetEndZoneBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndZone, 1); +} diff --git a/sourcemod/scripting/gokz-core/map/mapfile.sp b/sourcemod/scripting/gokz-core/map/mapfile.sp new file mode 100644 index 0000000..db60e7e --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/mapfile.sp @@ -0,0 +1,502 @@ +/* + Mapping API + + Reads data from the current map file. +*/ + +static Regex RE_BonusStartButton; +static Regex RE_BonusEndButton; + +// NOTE: 4 megabyte array for entity lump reading. +static char gEntityLump[4194304]; + +// =====[ PUBLIC ]===== + +void EntlumpParse(StringMap antiBhopTriggers, StringMap teleportTriggers, StringMap timerButtonTriggers, int &mappingApiVersion) +{ + char mapPath[512]; + GetCurrentMap(mapPath, sizeof(mapPath)); + Format(mapPath, sizeof(mapPath), "maps/%s.bsp", mapPath); + + // https://developer.valvesoftware.com/wiki/Source_BSP_File_Format + + File file = OpenFile(mapPath, "rb"); + if (file != INVALID_HANDLE) + { + int identifier; + file.ReadInt32(identifier); + + if (identifier == GOKZ_BSP_HEADER_IDENTIFIER) + { + // skip version number + file.Seek(4, SEEK_CUR); + + // the entity lump info is the first lump in the array, so we don't need to seek any further. + int offset; + int length; + file.ReadInt32(offset); + file.ReadInt32(length); + + // jump to the start of the entity lump + file.Seek(offset, SEEK_SET); + + int charactersRead = file.ReadString(gEntityLump, sizeof(gEntityLump), length); + delete file; + if (charactersRead >= sizeof(gEntityLump) - 1) + { + PushMappingApiError("ERROR: Entity lump: The map's entity lump is too big! Reduce the amount of entities in your map."); + return; + } + gEntityLump[length] = '\0'; + + int index = 0; + + StringMap entity = new StringMap(); + bool gotWorldSpawn = false; + while (EntlumpParseEntity(entity, gEntityLump, index)) + { + char classname[128]; + char targetName[GOKZ_ENTLUMP_MAX_VALUE]; + entity.GetString("classname", classname, sizeof(classname)); + + if (!gotWorldSpawn && StrEqual("worldspawn", classname, false)) + { + gotWorldSpawn = true; + char versionString[32]; + if (entity.GetString("climb_mapping_api_version", versionString, sizeof(versionString))) + { + if (StringToIntEx(versionString, mappingApiVersion) == 0) + { + PushMappingApiError("ERROR: Entity lump: Couldn't parse Mapping API version from map properties: \"%s\".", versionString); + mappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE; + } + } + else + { + // map doesn't have a mapping api version. + mappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE; + } + } + else if (StrEqual("trigger_multiple", classname, false)) + { + TriggerType triggerType; + if (!gotWorldSpawn || mappingApiVersion != GOKZ_MAPPING_API_VERSION_NONE) + { + if (entity.GetString("targetname", targetName, sizeof(targetName))) + { + // get trigger properties if applicable + triggerType = GetTriggerType(targetName); + if (triggerType == TriggerType_Antibhop) + { + AntiBhopTrigger trigger; + if (GetAntiBhopTriggerEntityProperties(trigger, entity)) + { + char key[32]; + IntToString(trigger.hammerID, key, sizeof(key)); + antiBhopTriggers.SetArray(key, trigger, sizeof(trigger)); + } + } + else if (triggerType == TriggerType_Teleport) + { + TeleportTrigger trigger; + if (GetTeleportTriggerEntityProperties(trigger, entity)) + { + char key[32]; + IntToString(trigger.hammerID, key, sizeof(key)); + teleportTriggers.SetArray(key, trigger, sizeof(trigger)); + } + } + } + } + + // Tracking legacy timer triggers that press the timer buttons upon triggered. + if (triggerType == TriggerType_Invalid) + { + char touchOutput[128]; + ArrayList value; + + if (entity.GetString("OnStartTouch", touchOutput, sizeof(touchOutput))) + { + TimerButtonTriggerCheck(touchOutput, sizeof(touchOutput), entity, timerButtonTriggers); + } + else if (entity.GetValue("OnStartTouch", value)) // If there are multiple outputs, we have to check for all of them. + { + for (int i = 0; i < value.Length; i++) + { + value.GetString(i, touchOutput, sizeof(touchOutput)); + TimerButtonTriggerCheck(touchOutput, sizeof(touchOutput), entity, timerButtonTriggers); + } + } + } + } + else if (StrEqual("func_button", classname, false)) + { + char pressOutput[128]; + ArrayList value; + + if (entity.GetString("OnPressed", pressOutput, sizeof(pressOutput))) + { + TimerButtonTriggerCheck(pressOutput, sizeof(pressOutput), entity, timerButtonTriggers); + } + else if (entity.GetValue("OnPressed", value)) // If there are multiple outputs, we have to check for all of them. + { + for (int i = 0; i < value.Length; i++) + { + value.GetString(i, pressOutput, sizeof(pressOutput)); + TimerButtonTriggerCheck(pressOutput, sizeof(pressOutput), entity, timerButtonTriggers); + } + } + } + // clear for next loop + entity.Clear(); + } + delete entity; + } + delete file; + } + else + { + // TODO: do something more elegant + SetFailState("Catastrophic extreme hyperfailure! Mapping API Couldn't open the map file for reading! %s. The map file might be gone or another program is using it.", mapPath); + } +} + + +// =====[ EVENTS ]===== + +void OnPluginStart_MapFile() +{ + char buffer[64]; + char press[8]; + FormatEx(press, sizeof(press), "%s%s", CHAR_ESCAPE, "Press"); + + buffer = GOKZ_BONUS_START_BUTTON_NAME_REGEX; + ReplaceStringEx(buffer, sizeof(buffer), "$", ""); + StrCat(buffer, sizeof(buffer), press); + RE_BonusStartButton = CompileRegex(buffer); + + buffer = GOKZ_BONUS_END_BUTTON_NAME_REGEX; + ReplaceStringEx(buffer, sizeof(buffer), "$", ""); + StrCat(buffer, sizeof(buffer), press); + RE_BonusEndButton = CompileRegex(buffer); +} + + +// =====[ PRIVATE ]===== + +static void EntlumpSkipAllWhiteSpace(char[] entityLump, int &index) +{ + while (IsCharSpace(entityLump[index]) && entityLump[index] != '\0') + { + index++; + } +} + +static int EntlumpGetString(char[] result, int maxLength, int copyCount, char[] entityLump, int entlumpIndex) +{ + int finalLength; + for (int i = 0; i < maxLength - 1 && i < copyCount; i++) + { + if (entityLump[entlumpIndex + i] == '\0') + { + break; + } + result[i] = entityLump[entlumpIndex + i]; + finalLength++; + } + + result[finalLength] = '\0'; + return finalLength; +} + +static EntlumpToken EntlumpGetToken(char[] entityLump, int &entlumpIndex) +{ + EntlumpToken result; + + EntlumpSkipAllWhiteSpace(entityLump, entlumpIndex); + + switch (entityLump[entlumpIndex]) + { + case '{': + { + result.type = EntlumpTokenType_OpenBrace; + EntlumpGetString(result.string, sizeof(result.string), 1, entityLump, entlumpIndex); + entlumpIndex++; + } + case '}': + { + result.type = EntlumpTokenType_CloseBrace; + EntlumpGetString(result.string, sizeof(result.string), 1, entityLump, entlumpIndex); + entlumpIndex++; + } + case '\0': + { + result.type = EntlumpTokenType_EndOfStream; + EntlumpGetString(result.string, sizeof(result.string), 1, entityLump, entlumpIndex); + entlumpIndex++; + } + case '\"': + { + result.type = EntlumpTokenType_Identifier; + int identifierLen; + entlumpIndex++; + for (int i = 0; i < sizeof(result.string) - 1; i++) + { + // NOTE: Unterminated strings can probably never happen, since the map has to be + // loaded by the game first and the engine will fail the load before we get to it. + if (entityLump[entlumpIndex + i] == '\0') + { + result.type = EntlumpTokenType_Unknown; + break; + } + if (entityLump[entlumpIndex + i] == '\"') + { + break; + } + result.string[i] = entityLump[entlumpIndex + i]; + identifierLen++; + } + + entlumpIndex += identifierLen + 1; // +1 to skip over last quotation mark + result.string[identifierLen] = '\0'; + } + default: + { + result.type = EntlumpTokenType_Unknown; + result.string[0] = entityLump[entlumpIndex]; + result.string[1] = '\0'; + } + } + + return result; +} + +static bool EntlumpParseEntity(StringMap result, char[] entityLump, int &entlumpIndex) +{ + EntlumpToken token; + token = EntlumpGetToken(entityLump, entlumpIndex); + if (token.type == EntlumpTokenType_EndOfStream) + { + return false; + } + + // NOTE: The following errors will very very likely never happen, since the entity lump has to be + // loaded by the game first and the engine will fail the load before we get to it. + // But if there's an obscure bug in this code, then we'll know!!! + for (;;) + { + token = EntlumpGetToken(entityLump, entlumpIndex); + switch (token.type) + { + case EntlumpTokenType_OpenBrace: + { + continue; + } + case EntlumpTokenType_Identifier: + { + EntlumpToken valueToken; + valueToken = EntlumpGetToken(entityLump, entlumpIndex); + if (valueToken.type == EntlumpTokenType_Identifier) + { + char tempString[GOKZ_ENTLUMP_MAX_VALUE]; + ArrayList values; + if (result.GetString(token.string, tempString, sizeof(tempString))) + { + result.Remove(token.string); + values = new ArrayList(ByteCountToCells(GOKZ_ENTLUMP_MAX_VALUE)); + values.PushString(tempString); + values.PushString(valueToken.string); + result.SetValue(token.string, values); + } + else if (result.GetValue(token.string, values)) + { + values.PushString(valueToken.string); + } + else + { + result.SetString(token.string, valueToken.string); + } + } + else + { + PushMappingApiError("ERROR: Entity lump: Unexpected token \"%s\".", valueToken.string); + return false; + } + } + case EntlumpTokenType_CloseBrace: + { + break; + } + case EntlumpTokenType_EndOfStream: + { + PushMappingApiError("ERROR: Entity lump: Unexpected end of entity lump! Entity lump parsing failed."); + return false; + } + default: + { + PushMappingApiError("ERROR: Entity lump: Invalid token \"%s\". Entity lump parsing failed.", token.string); + return false; + } + } + } + + return true; +} + +static bool GetHammerIDFromEntityStringMap(int &result, StringMap entity) +{ + char hammerID[32]; + if (!entity.GetString("hammerid", hammerID, sizeof(hammerID)) + || StringToIntEx(hammerID, result) == 0) + { + // if we don't have the hammer id, then we can't match the entity to an existing one! + char origin[64]; + entity.GetString("origin", origin, sizeof(origin)); + PushMappingApiError("ERROR: Failed to parse \"hammerid\" keyvalue on trigger! \"%i\" origin: %s.", result, origin); + return false; + } + return true; +} + +static bool GetAntiBhopTriggerEntityProperties(AntiBhopTrigger result, StringMap entity) +{ + if (!GetHammerIDFromEntityStringMap(result.hammerID, entity)) + { + return false; + } + + char time[32]; + if (!entity.GetString("climb_anti_bhop_time", time, sizeof(time)) + || StringToFloatEx(time, result.time) == 0) + { + result.time = GOKZ_ANTI_BHOP_TRIGGER_DEFAULT_DELAY; + } + + return true; +} + +static bool GetTeleportTriggerEntityProperties(TeleportTrigger result, StringMap entity) +{ + if (!GetHammerIDFromEntityStringMap(result.hammerID, entity)) + { + return false; + } + + char buffer[64]; + if (!entity.GetString("climb_teleport_type", buffer, sizeof(buffer)) + || StringToIntEx(buffer, view_as<int>(result.type)) == 0) + { + result.type = GOKZ_TELEPORT_TRIGGER_DEFAULT_TYPE; + } + + if (!entity.GetString("climb_teleport_destination", result.tpDestination, sizeof(result.tpDestination))) + { + // We don't want triggers without destinations dangling about, so we need to tell everyone about it!!! + PushMappingApiError("ERROR: Could not find \"climb_teleport_destination\" keyvalue on a climb_teleport trigger! hammer id \"%i\".", + result.hammerID); + return false; + } + + if (!entity.GetString("climb_teleport_delay", buffer, sizeof(buffer)) + || StringToFloatEx(buffer, result.delay) == 0) + { + result.delay = GOKZ_TELEPORT_TRIGGER_DEFAULT_DELAY; + } + + if (!entity.GetString("climb_teleport_use_dest_angles", buffer, sizeof(buffer)) + || StringToIntEx(buffer, result.useDestAngles) == 0) + { + result.useDestAngles = GOKZ_TELEPORT_TRIGGER_DEFAULT_USE_DEST_ANGLES; + } + + if (!entity.GetString("climb_teleport_reset_speed", buffer, sizeof(buffer)) + || StringToIntEx(buffer, result.resetSpeed) == 0) + { + result.resetSpeed = GOKZ_TELEPORT_TRIGGER_DEFAULT_RESET_SPEED; + } + + if (!entity.GetString("climb_teleport_reorient_player", buffer, sizeof(buffer)) + || StringToIntEx(buffer, result.reorientPlayer) == 0) + { + result.reorientPlayer = GOKZ_TELEPORT_TRIGGER_DEFAULT_REORIENT_PLAYER; + } + + if (!entity.GetString("climb_teleport_relative", buffer, sizeof(buffer)) + || StringToIntEx(buffer, result.relativeDestination) == 0) + { + result.relativeDestination = GOKZ_TELEPORT_TRIGGER_DEFAULT_RELATIVE_DESTINATION; + } + + // NOTE: Clamping + if (IsBhopTrigger(result.type)) + { + result.delay = FloatMax(result.delay, GOKZ_TELEPORT_TRIGGER_BHOP_MIN_DELAY); + } + else + { + result.delay = FloatMax(result.delay, 0.0); + } + + return true; +} + +static void TimerButtonTriggerCheck(char[] touchOutput, int size, StringMap entity, StringMap timerButtonTriggers) +{ + int course = 0; + char startOutput[128]; + char endOutput[128]; + FormatEx(startOutput, sizeof(startOutput), "%s%s%s", GOKZ_START_BUTTON_NAME, CHAR_ESCAPE, "Press"); + FormatEx(endOutput, sizeof(endOutput), "%s%s%s", GOKZ_END_BUTTON_NAME, CHAR_ESCAPE, "Press"); + if (StrContains(touchOutput, startOutput, false) != -1) + { + TimerButtonTrigger trigger; + if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity)) + { + trigger.course = 0; + trigger.isStartTimer = true; + } + char key[32]; + IntToString(trigger.hammerID, key, sizeof(key)); + timerButtonTriggers.SetArray(key, trigger, sizeof(trigger)); + } + else if (StrContains(touchOutput, endOutput, false) != -1) + { + TimerButtonTrigger trigger; + if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity)) + { + trigger.course = 0; + trigger.isStartTimer = false; + } + char key[32]; + IntToString(trigger.hammerID, key, sizeof(key)); + timerButtonTriggers.SetArray(key, trigger, sizeof(trigger)); + } + else if (RE_BonusStartButton.Match(touchOutput) > 0) + { + RE_BonusStartButton.GetSubString(1, touchOutput, sizeof(size)); + course = StringToInt(touchOutput); + TimerButtonTrigger trigger; + if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity)) + { + trigger.course = course; + trigger.isStartTimer = true; + } + char key[32]; + IntToString(trigger.hammerID, key, sizeof(key)); + timerButtonTriggers.SetArray(key, trigger, sizeof(trigger)); + } + else if (RE_BonusEndButton.Match(touchOutput) > 0) + { + RE_BonusEndButton.GetSubString(1, touchOutput, sizeof(size)); + course = StringToInt(touchOutput); + TimerButtonTrigger trigger; + if (GetHammerIDFromEntityStringMap(trigger.hammerID, entity)) + { + trigger.course = course; + trigger.isStartTimer = false; + } + char key[32]; + IntToString(trigger.hammerID, key, sizeof(key)); + timerButtonTriggers.SetArray(key, trigger, sizeof(trigger)); + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/map/prefix.sp b/sourcemod/scripting/gokz-core/map/prefix.sp new file mode 100644 index 0000000..3ecbf89 --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/prefix.sp @@ -0,0 +1,48 @@ +/* + Mapping API - Prefix + + Detects the map's prefix. +*/ + + + +static int currentMapPrefix; + + + +// =====[ PUBLIC ]===== + +int GetCurrentMapPrefix() +{ + return currentMapPrefix; +} + + + +// =====[ LISTENERS ]===== + +void OnMapStart_Prefix() +{ + char map[PLATFORM_MAX_PATH], mapPrefix[PLATFORM_MAX_PATH]; + GetCurrentMapDisplayName(map, sizeof(map)); + + // Get all characters before the first '_' character + for (int i = 0; i < sizeof(mapPrefix); i++) + { + if (map[i] == '\0' || map[i] == '_') + { + break; + } + + mapPrefix[i] = map[i]; + } + + if (StrEqual(mapPrefix[0], "kzpro", false)) + { + currentMapPrefix = MapPrefix_KZPro; + } + else + { + currentMapPrefix = MapPrefix_Other; + } +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/map/starts.sp b/sourcemod/scripting/gokz-core/map/starts.sp new file mode 100644 index 0000000..94d5b33 --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/starts.sp @@ -0,0 +1,219 @@ +/* + Hooks between start destinations and GOKZ. +*/ + + + +static Regex RE_BonusStart; +static bool startExists[GOKZ_MAX_COURSES]; +static float startOrigin[GOKZ_MAX_COURSES][3]; +static float startAngles[GOKZ_MAX_COURSES][3]; + +// Used for SearchStart +static Regex RE_BonusStartButton; +static Regex RE_BonusStartZone; +static CourseTimerType startType[GOKZ_MAX_COURSES]; +static float searchStartOrigin[GOKZ_MAX_COURSES][3]; +static float searchStartAngles[GOKZ_MAX_COURSES][3]; + +// =====[ EVENTS ]===== + +void OnPluginStart_MapStarts() +{ + RE_BonusStart = CompileRegex(GOKZ_BONUS_START_NAME_REGEX); + RE_BonusStartButton = CompileRegex(GOKZ_BONUS_START_BUTTON_NAME_REGEX); + RE_BonusStartZone = CompileRegex(GOKZ_BONUS_START_ZONE_NAME_REGEX); +} + +void OnEntitySpawned_MapStarts(int entity) +{ + char buffer[32]; + + GetEntityClassname(entity, buffer, sizeof(buffer)); + if (!StrEqual("info_teleport_destination", buffer, false)) + { + return; + } + + if (GetEntityName(entity, buffer, sizeof(buffer)) == 0) + { + return; + } + + if (StrEqual(GOKZ_START_NAME, buffer, false)) + { + StoreStart(0, entity); + } + else + { + int course = GetStartBonusNumber(entity); + if (GOKZ_IsValidCourse(course, true)) + { + StoreStart(course, entity); + } + } +} + +void OnEntitySpawnedPost_MapStarts(int entity) +{ + char buffer[32]; + GetEntityClassname(entity, buffer, sizeof(buffer)); + + if (StrEqual("trigger_multiple", buffer, false)) + { + bool isStartZone; + if (GetEntityName(entity, buffer, sizeof(buffer)) != 0) + { + if (StrEqual(GOKZ_START_ZONE_NAME, buffer, false)) + { + isStartZone = true; + StoreSearchStart(0, entity, CourseTimerType_ZoneNew); + } + else if (GetStartZoneBonusNumber(entity) != -1) + { + int course = GetStartZoneBonusNumber(entity); + if (GOKZ_IsValidCourse(course, true)) + { + isStartZone = true; + StoreSearchStart(course, entity, CourseTimerType_ZoneNew); + } + } + } + if (!isStartZone) + { + TimerButtonTrigger trigger; + if (IsTimerButtonTrigger(entity, trigger) && trigger.isStartTimer) + { + StoreSearchStart(trigger.course, entity, CourseTimerType_ZoneLegacy); + } + } + + } + else if (StrEqual("func_button", buffer, false)) + { + bool isStartButton; + if (GetEntityName(entity, buffer, sizeof(buffer)) != 0) + { + if (StrEqual(GOKZ_START_BUTTON_NAME, buffer, false)) + { + isStartButton = true; + StoreSearchStart(0, entity, CourseTimerType_Button); + } + else + { + int course = GetStartButtonBonusNumber(entity); + if (GOKZ_IsValidCourse(course, true)) + { + isStartButton = true; + StoreSearchStart(course, entity, CourseTimerType_Button); + } + } + } + if (!isStartButton) + { + TimerButtonTrigger trigger; + if (IsTimerButtonTrigger(entity, trigger) && trigger.isStartTimer) + { + StoreSearchStart(trigger.course, entity, CourseTimerType_Button); + } + } + } +} + +void OnMapStart_MapStarts() +{ + for (int course = 0; course < GOKZ_MAX_COURSES; course++) + { + startExists[course] = false; + startType[course] = CourseTimerType_None; + } +} + +bool GetMapStartPosition(int course, float origin[3], float angles[3]) +{ + if (!startExists[course]) + { + return false; + } + + origin = startOrigin[course]; + angles = startAngles[course]; + + return true; +} + +bool GetSearchStartPosition(int course, float origin[3], float angles[3]) +{ + if (startType[course] == CourseTimerType_None) + { + return false; + } + + origin = searchStartOrigin[course]; + angles = searchStartAngles[course]; + + return true; +} + +// =====[ PRIVATE ]===== + +static void StoreStart(int course, int entity) +{ + float origin[3], angles[3]; + GetEntPropVector(entity, Prop_Send, "m_vecOrigin", origin); + GetEntPropVector(entity, Prop_Data, "m_angRotation", angles); + angles[2] = 0.0; // Roll should always be 0.0 + + startExists[course] = true; + startOrigin[course] = origin; + startAngles[course] = angles; +} + +static void StoreSearchStart(int course, int entity, CourseTimerType type) +{ + // If StoreSearchStart is called, then there is at least an end position (even though it might not be a valid one) + if (startType[course] < CourseTimerType_Default) + { + startType[course] = CourseTimerType_Default; + } + + // Real zone is always better than "fake" zones which are better than buttons + // as the buttons found in a map with fake zones aren't meant to be visible. + if (startType[course] >= type) + { + return; + } + + float origin[3], distFromCenter[3]; + GetEntityPositions(entity, origin, searchStartOrigin[course], searchStartAngles[course], distFromCenter); + + // If it is a button or the center of the center of the zone is invalid + if (type == CourseTimerType_Button || !IsSpawnValid(searchStartOrigin[course])) + { + // Attempt with various positions around the entity, pick the first valid one. + if (!FindValidPositionAroundTimerEntity(entity, searchStartOrigin[course], searchStartAngles[course], type == CourseTimerType_Button)) + { + searchStartOrigin[course][2] -= 64.0; // Move the origin down so the eye position is directly on top of the button/zone. + return; + } + } + + // Only update the CourseTimerType if a valid position is found. + startType[course] = type; +} + + +static int GetStartBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusStart, 1); +} + +static int GetStartButtonBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartButton, 1); +} + +static int GetStartZoneBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartZone, 1); +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/map/triggers.sp b/sourcemod/scripting/gokz-core/map/triggers.sp new file mode 100644 index 0000000..2444493 --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/triggers.sp @@ -0,0 +1,855 @@ +/* + Mapping API - Triggers + + Implements trigger related features. +*/ + + + +static float lastTrigMultiTouchTime[MAXPLAYERS + 1]; +static float lastTrigTeleTouchTime[MAXPLAYERS + 1]; +static float lastTouchGroundOrLadderTime[MAXPLAYERS + 1]; +static int lastTouchSingleBhopEntRef[MAXPLAYERS + 1]; +static ArrayList lastTouchSequentialBhopEntRefs[MAXPLAYERS + 1]; +static int triggerTouchCount[MAXPLAYERS + 1]; +static int antiCpTriggerTouchCount[MAXPLAYERS + 1]; +static int antiPauseTriggerTouchCount[MAXPLAYERS + 1]; +static int antiJumpstatTriggerTouchCount[MAXPLAYERS + 1]; +static int mapMappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE; +static int bhopTouchCount[MAXPLAYERS + 1]; +static bool jumpedThisTick[MAXPLAYERS + 1]; +static float jumpOrigin[MAXPLAYERS + 1][3]; +static float jumpVelocity[MAXPLAYERS + 1][3]; +static ArrayList triggerTouchList[MAXPLAYERS + 1]; // arraylist of TouchedTrigger that the player is currently touching. this array won't ever get long (unless the mapper does something weird). +static StringMap triggerTouchCounts[MAXPLAYERS + 1]; // stringmap of int touch counts with key being a string of the entity reference. +static StringMap antiBhopTriggers; // stringmap of AntiBhopTrigger with key being a string of the m_iHammerID entprop. +static StringMap teleportTriggers; // stringmap of TeleportTrigger with key being a string of the m_iHammerID entprop. +static StringMap timerButtonTriggers; // stringmap of legacy timer zone triggers with key being a string of the m_iHammerID entprop. +static ArrayList parseErrorStrings; + + + +// =====[ PUBLIC ]===== + +bool BhopTriggersJustTouched(int client) +{ + // NOTE: This is slightly incorrect since we touch triggers in the air, but + // it doesn't matter since we can't checkpoint in the air. + if (bhopTouchCount[client] > 0) + { + return true; + } + // GetEngineTime return changes between calls. We only call it once at the beginning. + float engineTime = GetEngineTime(); + // If the player touches a teleport trigger, increase the delay required + if (engineTime - lastTouchGroundOrLadderTime[client] < GOKZ_MULT_NO_CHECKPOINT_TIME // Just touched ground or ladder + && engineTime - lastTrigMultiTouchTime[client] < GOKZ_MULT_NO_CHECKPOINT_TIME // Just touched trigger_multiple + || engineTime - lastTrigTeleTouchTime[client] < GOKZ_BHOP_NO_CHECKPOINT_TIME) // Just touched trigger_teleport + { + return true; + } + + return Movement_GetMovetype(client) == MOVETYPE_LADDER + && triggerTouchCount[client] > 0 + && engineTime - lastTrigTeleTouchTime[client] < GOKZ_LADDER_NO_CHECKPOINT_TIME; +} + +bool AntiCpTriggerIsTouched(int client) +{ + return antiCpTriggerTouchCount[client] > 0; +} + +bool AntiPauseTriggerIsTouched(int client) +{ + return antiPauseTriggerTouchCount[client] > 0; +} + +void PushMappingApiError(char[] format, any ...) +{ + char error[GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH]; + VFormat(error, sizeof(error), format, 2); + parseErrorStrings.PushString(error); +} + +TriggerType GetTriggerType(char[] targetName) +{ + TriggerType result = TriggerType_Invalid; + + if (StrEqual(targetName, GOKZ_ANTI_BHOP_TRIGGER_NAME)) + { + result = TriggerType_Antibhop; + } + else if (StrEqual(targetName, GOKZ_TELEPORT_TRIGGER_NAME)) + { + result = TriggerType_Teleport; + } + + return result; +} + +bool IsBhopTrigger(TeleportType type) +{ + return type == TeleportType_MultiBhop + || type == TeleportType_SingleBhop + || type == TeleportType_SequentialBhop; +} + +bool IsTimerButtonTrigger(int entity, TimerButtonTrigger trigger) +{ + char hammerID[32]; + bool gotHammerID = GetEntityHammerIDString(entity, hammerID, sizeof(hammerID)); + if (gotHammerID && timerButtonTriggers.GetArray(hammerID, trigger, sizeof(trigger))) + { + return true; + } + return false; +} + +// =====[ EVENTS ]===== + +void OnPluginStart_MapTriggers() +{ + parseErrorStrings = new ArrayList(ByteCountToCells(GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH)); + antiBhopTriggers = new StringMap(); + teleportTriggers = new StringMap(); + timerButtonTriggers = new StringMap(); +} + +void OnMapStart_MapTriggers() +{ + parseErrorStrings.Clear(); + antiBhopTriggers.Clear(); + teleportTriggers.Clear(); + timerButtonTriggers.Clear(); + mapMappingApiVersion = GOKZ_MAPPING_API_VERSION_NONE; + EntlumpParse(antiBhopTriggers, teleportTriggers, timerButtonTriggers, mapMappingApiVersion); + + if (mapMappingApiVersion > GOKZ_MAPPING_API_VERSION) + { + SetFailState("Map's mapping api version is too big! Maximum supported version is %i, but map has %i. If you're not on the latest GOKZ version, then update!", + GOKZ_MAPPING_API_VERSION, mapMappingApiVersion); + } +} + +void OnClientPutInServer_MapTriggers(int client) +{ + triggerTouchCount[client] = 0; + antiCpTriggerTouchCount[client] = 0; + antiPauseTriggerTouchCount[client] = 0; + antiJumpstatTriggerTouchCount[client] = 0; + + if (triggerTouchList[client] == null) + { + triggerTouchList[client] = new ArrayList(sizeof(TouchedTrigger)); + } + else + { + triggerTouchList[client].Clear(); + } + + if (triggerTouchCounts[client] == null) + { + triggerTouchCounts[client] = new StringMap(); + } + else + { + triggerTouchCounts[client].Clear(); + } + + bhopTouchCount[client] = 0; + + if (lastTouchSequentialBhopEntRefs[client] == null) + { + lastTouchSequentialBhopEntRefs[client] = new ArrayList(); + } + else + { + lastTouchSequentialBhopEntRefs[client].Clear(); + } +} + +void OnPlayerRunCmd_MapTriggers(int client, int &buttons) +{ + int flags = GetEntityFlags(client); + MoveType moveType = GetEntityMoveType(client); + + // if the player isn't touching any bhop triggers on ground/a ladder, then + // reset the singlebhop and sequential bhop state. + if ((flags & FL_ONGROUND || moveType == MOVETYPE_LADDER) + && bhopTouchCount[client] == 0) + { + ResetBhopState(client); + } + + if (antiJumpstatTriggerTouchCount[client] > 0) + { + if (GetFeatureStatus(FeatureType_Native, "GOKZ_JS_InvalidateJump") == FeatureStatus_Available) + { + GOKZ_JS_InvalidateJump(client); + } + } + + // Check if we're touching any triggers and act accordingly. + // NOTE: Read through the touch list in reverse order, so some + // trigger behaviours will be better. Trust me! + int triggerTouchListLength = triggerTouchList[client].Length; + for (int i = triggerTouchListLength - 1; i >= 0; i--) + { + TouchedTrigger touched; + triggerTouchList[client].GetArray(i, touched); + + if (touched.triggerType == TriggerType_Antibhop) + { + TouchAntibhopTrigger(client, touched, buttons, flags); + } + else if (touched.triggerType == TriggerType_Teleport) + { + // Sometimes due to lag or whatever, the player can be + // teleported twice by the same trigger. This fixes that. + if (TouchTeleportTrigger(client, touched, flags)) + { + RemoveTriggerFromTouchList(client, EntRefToEntIndex(touched.entRef)); + i--; + triggerTouchListLength--; + } + } + } + jumpedThisTick[client] = false; +} + +void OnPlayerSpawn_MapTriggers(int client) +{ + // Print trigger errors every time a player spawns so that + // mappers and testers can very easily spot mistakes in names + // and get them fixed asap. + if (parseErrorStrings.Length > 0) + { + char errStart[] = "ERROR: Errors detected when trying to load triggers!"; + CPrintToChat(client, "{red}%s", errStart); + PrintToConsole(client, "\n%s", errStart); + + int length = parseErrorStrings.Length; + for (int err = 0; err < length; err++) + { + char error[GOKZ_MAX_MAPTRIGGERS_ERROR_LENGTH]; + parseErrorStrings.GetString(err, error, sizeof(error)); + CPrintToChat(client, "{red}%s", error); + PrintToConsole(client, error); + } + CPrintToChat(client, "{red}If the errors get clipped off in the chat, then look in your developer console!\n"); + } +} + +public void OnPlayerJump_Triggers(int client) +{ + jumpedThisTick[client] = true; + GetClientAbsOrigin(client, jumpOrigin[client]); + Movement_GetVelocity(client, jumpVelocity[client]); +} + +void OnEntitySpawned_MapTriggers(int entity) +{ + char classname[32]; + GetEntityClassname(entity, classname, sizeof(classname)); + char name[64]; + GetEntityName(entity, name, sizeof(name)); + + bool triggerMultiple = StrEqual("trigger_multiple", classname); + if (triggerMultiple) + { + char hammerID[32]; + bool gotHammerID = GetEntityHammerIDString(entity, hammerID, sizeof(hammerID)); + + if (StrEqual(GOKZ_TELEPORT_TRIGGER_NAME, name)) + { + TeleportTrigger teleportTrigger; + if (gotHammerID && teleportTriggers.GetArray(hammerID, teleportTrigger, sizeof(teleportTrigger))) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnTeleportTrigTouchStart_MapTriggers); + HookSingleEntityOutput(entity, "OnEndTouch", OnTeleportTrigTouchEnd_MapTriggers); + } + else + { + PushMappingApiError("ERROR: Couldn't match teleport trigger's Hammer ID %s with any Hammer ID from the map.", hammerID); + } + } + else if (StrEqual(GOKZ_ANTI_BHOP_TRIGGER_NAME, name)) + { + AntiBhopTrigger antiBhopTrigger; + if (gotHammerID && antiBhopTriggers.GetArray(hammerID, antiBhopTrigger, sizeof(antiBhopTrigger))) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnAntiBhopTrigTouchStart_MapTriggers); + HookSingleEntityOutput(entity, "OnEndTouch", OnAntiBhopTrigTouchEnd_MapTriggers); + } + else + { + PushMappingApiError("ERROR: Couldn't match antibhop trigger's Hammer ID %s with any Hammer ID from the map.", hammerID); + } + } + else if (StrEqual(GOKZ_BHOP_RESET_TRIGGER_NAME, name)) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnBhopResetTouchStart_MapTriggers); + } + else if (StrEqual(GOKZ_ANTI_CP_TRIGGER_NAME, name, false)) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnAntiCpTrigTouchStart_MapTriggers); + HookSingleEntityOutput(entity, "OnEndTouch", OnAntiCpTrigTouchEnd_MapTriggers); + } + else if (StrEqual(GOKZ_ANTI_PAUSE_TRIGGER_NAME, name, false)) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnAntiPauseTrigTouchStart_MapTriggers); + HookSingleEntityOutput(entity, "OnEndTouch", OnAntiPauseTrigTouchEnd_MapTriggers); + } + else if (StrEqual(GOKZ_ANTI_JUMPSTAT_TRIGGER_NAME, name, false)) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnAntiJumpstatTrigTouchStart_MapTriggers); + HookSingleEntityOutput(entity, "OnEndTouch", OnAntiJumpstatTrigTouchEnd_MapTriggers); + } + else + { + // NOTE: SDKHook touch hooks bypass trigger filters. We want that only with + // non mapping api triggers because it prevents checkpointing on bhop blocks. + SDKHook(entity, SDKHook_StartTouchPost, OnTrigMultTouchStart_MapTriggers); + SDKHook(entity, SDKHook_EndTouchPost, OnTrigMultTouchEnd_MapTriggers); + } + } + else if (StrEqual("trigger_teleport", classname)) + { + SDKHook(entity, SDKHook_StartTouchPost, OnTrigTeleTouchStart_MapTriggers); + SDKHook(entity, SDKHook_EndTouchPost, OnTrigTeleTouchEnd_MapTriggers); + } +} + +public void OnAntiBhopTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + int touchCount = IncrementTriggerTouchCount(other, entity); + if (touchCount <= 0) + { + // The trigger has fired a matching endtouch output before + // the starttouch output, so ignore it. + return; + } + + if (jumpedThisTick[other]) + { + TeleportEntity(other, jumpOrigin[other], NULL_VECTOR, jumpVelocity[other]); + } + + AddTriggerToTouchList(other, entity, TriggerType_Antibhop); +} + +public void OnAntiBhopTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + DecrementTriggerTouchCount(other, entity); + RemoveTriggerFromTouchList(other, entity); +} + +public void OnTeleportTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + int touchCount = IncrementTriggerTouchCount(other, entity); + if (touchCount <= 0) + { + // The trigger has fired a matching endtouch output before + // the starttouch output, so ignore it. + return; + } + + char key[32]; + GetEntityHammerIDString(entity, key, sizeof(key)); + TeleportTrigger trigger; + if (teleportTriggers.GetArray(key, trigger, sizeof(trigger)) + && IsBhopTrigger(trigger.type)) + { + bhopTouchCount[other]++; + } + + AddTriggerToTouchList(other, entity, TriggerType_Teleport); +} + +public void OnTeleportTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + DecrementTriggerTouchCount(other, entity); + + char key[32]; + GetEntityHammerIDString(entity, key, sizeof(key)); + TeleportTrigger trigger; + if (teleportTriggers.GetArray(key, trigger, sizeof(trigger)) + && IsBhopTrigger(trigger.type)) + { + bhopTouchCount[other]--; + } + + RemoveTriggerFromTouchList(other, entity); +} + +public void OnBhopResetTouchStart_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + ResetBhopState(other); +} + +public void OnAntiCpTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + antiCpTriggerTouchCount[other]++; +} + +public void OnAntiCpTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + antiCpTriggerTouchCount[other]--; +} + +public void OnAntiPauseTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + antiPauseTriggerTouchCount[other]++; +} + +public void OnAntiPauseTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + antiPauseTriggerTouchCount[other]--; +} + +public void OnAntiJumpstatTrigTouchStart_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + antiJumpstatTriggerTouchCount[other]++; +} + +public void OnAntiJumpstatTrigTouchEnd_MapTriggers(const char[] output, int entity, int other, float delay) +{ + if (!IsValidClient(other)) + { + return; + } + + antiJumpstatTriggerTouchCount[other]--; +} + +public void OnTrigMultTouchStart_MapTriggers(int entity, int other) +{ + if (!IsValidClient(other)) + { + return; + } + + lastTrigMultiTouchTime[other] = GetEngineTime(); + triggerTouchCount[other]++; +} + +public void OnTrigMultTouchEnd_MapTriggers(int entity, int other) +{ + if (!IsValidClient(other)) + { + return; + } + + triggerTouchCount[other]--; +} + +public void OnTrigTeleTouchStart_MapTriggers(int entity, int other) +{ + if (!IsValidClient(other)) + { + return; + } + + lastTrigTeleTouchTime[other] = GetEngineTime(); + triggerTouchCount[other]++; +} + +public void OnTrigTeleTouchEnd_MapTriggers(int entity, int other) +{ + if (!IsValidClient(other)) + { + return; + } + + triggerTouchCount[other]--; +} + +void OnStartTouchGround_MapTriggers(int client) +{ + lastTouchGroundOrLadderTime[client] = GetEngineTime(); + + for (int i = 0; i < triggerTouchList[client].Length; i++) + { + TouchedTrigger touched; + triggerTouchList[client].GetArray(i, touched); + // set the touched tick to the tick that the player touches the ground. + touched.groundTouchTick = gI_TickCount[client]; + triggerTouchList[client].SetArray(i, touched); + } +} + +void OnStopTouchGround_MapTriggers(int client) +{ + for (int i = 0; i < triggerTouchList[client].Length; i++) + { + TouchedTrigger touched; + triggerTouchList[client].GetArray(i, touched); + + if (touched.triggerType == TriggerType_Teleport) + { + char key[32]; + GetEntityHammerIDString(touched.entRef, key, sizeof(key)); + TeleportTrigger trigger; + // set last touched triggers for single and sequential bhop. + if (teleportTriggers.GetArray(key, trigger, sizeof(trigger)) + && IsBhopTrigger(trigger.type)) + { + if (trigger.type == TeleportType_SequentialBhop) + { + lastTouchSequentialBhopEntRefs[client].Push(touched.entRef); + } + // NOTE: For singlebhops, we don't care which type of bhop we last touched, because + // otherwise jumping back and forth between a multibhop and a singlebhop wouldn't work. + if (i == 0 && IsBhopTrigger(trigger.type)) + { + // We only want to set this once in this loop. + lastTouchSingleBhopEntRef[client] = touched.entRef; + } + } + } + } +} + +void OnChangeMovetype_MapTriggers(int client, MoveType newMovetype) +{ + if (newMovetype == MOVETYPE_LADDER) + { + lastTouchGroundOrLadderTime[client] = GetEngineTime(); + } +} + + + +// =====[ PRIVATE ]===== + +static void AddTriggerToTouchList(int client, int trigger, TriggerType triggerType) +{ + int triggerEntRef = EntIndexToEntRef(trigger); + + TouchedTrigger touched; + touched.triggerType = triggerType; + touched.entRef = triggerEntRef; + touched.startTouchTick = gI_TickCount[client]; + touched.groundTouchTick = -1; + if (GetEntityFlags(client) & FL_ONGROUND) + { + touched.groundTouchTick = gI_TickCount[client]; + } + + triggerTouchList[client].PushArray(touched); +} + +static void RemoveTriggerFromTouchList(int client, int trigger) +{ + int triggerEntRef = EntIndexToEntRef(trigger); + for (int i = 0; i < triggerTouchList[client].Length; i++) + { + TouchedTrigger touched; + triggerTouchList[client].GetArray(i, touched); + if (touched.entRef == triggerEntRef) + { + triggerTouchList[client].Erase(i); + break; + } + } +} + +static int IncrementTriggerTouchCount(int client, int trigger) +{ + int entref = EntIndexToEntRef(trigger); + char szEntref[64]; + FormatEx(szEntref, sizeof(szEntref), "%i", entref); + + int value = 0; + triggerTouchCounts[client].GetValue(szEntref, value); + + value += 1; + triggerTouchCounts[client].SetValue(szEntref, value); + + return value; +} + +static void DecrementTriggerTouchCount(int client, int trigger) +{ + int entref = EntIndexToEntRef(trigger); + char szEntref[64]; + FormatEx(szEntref, sizeof(szEntref), "%i", entref); + + int value = 0; + triggerTouchCounts[client].GetValue(szEntref, value); + + value -= 1; + triggerTouchCounts[client].SetValue(szEntref, value); +} + +static void TouchAntibhopTrigger(int client, TouchedTrigger touched, int &newButtons, int flags) +{ + if (!(flags & FL_ONGROUND)) + { + // Disable jump when the player is in the air. + // This is a very simple way to fix jumpbugging antibhop triggers. + newButtons &= ~IN_JUMP; + return; + } + + if (touched.groundTouchTick == -1) + { + // The player hasn't touched the ground inside this trigger yet. + return; + } + + char key[32]; + GetEntityHammerIDString(touched.entRef, key, sizeof(key)); + AntiBhopTrigger trigger; + if (antiBhopTriggers.GetArray(key, trigger, sizeof(trigger))) + { + float touchTime = CalculateGroundTouchTime(client, touched); + if (trigger.time == 0.0 || touchTime <= trigger.time) + { + // disable jump + newButtons &= ~IN_JUMP; + } + } +} + +static bool TouchTeleportTrigger(int client, TouchedTrigger touched, int flags) +{ + bool shouldTeleport = false; + + char key[32]; + GetEntityHammerIDString(touched.entRef, key, sizeof(key)); + TeleportTrigger trigger; + if (!teleportTriggers.GetArray(key, trigger, sizeof(trigger))) + { + // Couldn't get the teleport trigger from the trigger array for some reason. + return shouldTeleport; + } + + bool isBhopTrigger = IsBhopTrigger(trigger.type); + // NOTE: Player hasn't touched the ground inside this trigger yet. + if (touched.groundTouchTick == -1 && isBhopTrigger) + { + return shouldTeleport; + } + + float destOrigin[3]; + float destAngles[3]; + bool gotDestOrigin; + bool gotDestAngles; + int destinationEnt = GetTeleportDestinationAndOrientation(trigger.tpDestination, destOrigin, destAngles, gotDestOrigin, gotDestAngles); + + float triggerOrigin[3]; + bool gotTriggerOrigin = GetEntityAbsOrigin(touched.entRef, triggerOrigin); + + // NOTE: We only use the trigger's origin if we're using a relative destination, so if + // we're not using a relative destination and don't have it, then it's fine. + if (!IsValidEntity(destinationEnt) || !gotDestOrigin + || (!gotTriggerOrigin && trigger.relativeDestination)) + { + PrintToConsole(client, "[KZ] Invalid teleport destination \"%s\" on trigger with hammerID %i.", trigger.tpDestination, trigger.hammerID); + return shouldTeleport; + } + + // NOTE: Find out if we should actually teleport. + if (isBhopTrigger && (flags & FL_ONGROUND)) + { + float touchTime = CalculateGroundTouchTime(client, touched); + if (touchTime > trigger.delay) + { + shouldTeleport = true; + } + else if (trigger.type == TeleportType_SingleBhop) + { + shouldTeleport = lastTouchSingleBhopEntRef[client] == touched.entRef; + } + else if (trigger.type == TeleportType_SequentialBhop) + { + int length = lastTouchSequentialBhopEntRefs[client].Length; + for (int j = 0; j < length; j++) + { + int entRef = lastTouchSequentialBhopEntRefs[client].Get(j); + if (entRef == touched.entRef) + { + shouldTeleport = true; + break; + } + } + } + } + else if (trigger.type == TeleportType_Normal) + { + float touchTime = CalculateStartTouchTime(client, touched); + shouldTeleport = touchTime > trigger.delay || (trigger.delay == 0.0); + } + + if (!shouldTeleport) + { + return shouldTeleport; + } + + bool shouldReorientPlayer = trigger.reorientPlayer + && gotDestAngles && (destAngles[1] != 0.0); + + float zAxis[3]; + zAxis = view_as<float>({0.0, 0.0, 1.0}); + + // NOTE: Work out finalOrigin. + float finalOrigin[3]; + if (trigger.relativeDestination) + { + float playerOrigin[3]; + Movement_GetOrigin(client, playerOrigin); + + float playerOffsetFromTrigger[3]; + SubtractVectors(playerOrigin, triggerOrigin, playerOffsetFromTrigger); + + if (shouldReorientPlayer) + { + // NOTE: rotate player offset by the destination trigger's yaw. + RotateVectorAxis(playerOffsetFromTrigger, zAxis, DegToRad(destAngles[1]), playerOffsetFromTrigger); + } + + AddVectors(destOrigin, playerOffsetFromTrigger, finalOrigin); + } + else + { + finalOrigin = destOrigin; + } + + // NOTE: Work out finalPlayerAngles. + float finalPlayerAngles[3]; + Movement_GetEyeAngles(client, finalPlayerAngles); + if (shouldReorientPlayer) + { + finalPlayerAngles[1] -= destAngles[1]; + + float velocity[3]; + Movement_GetVelocity(client, velocity); + + // NOTE: rotate velocity by the destination trigger's yaw. + RotateVectorAxis(velocity, zAxis, DegToRad(destAngles[1]), velocity); + Movement_SetVelocity(client, velocity); + } + else if (!trigger.reorientPlayer && trigger.useDestAngles) + { + finalPlayerAngles = destAngles; + } + + if (shouldTeleport) + { + TeleportPlayer(client, finalOrigin, finalPlayerAngles, gotDestAngles && trigger.useDestAngles, trigger.resetSpeed); + } + + return shouldTeleport; +} + +static float CalculateGroundTouchTime(int client, TouchedTrigger touched) +{ + float result = float(gI_TickCount[client] - touched.groundTouchTick) * GetTickInterval(); + return result; +} + +static float CalculateStartTouchTime(int client, TouchedTrigger touched) +{ + float result = float(gI_TickCount[client] - touched.startTouchTick) * GetTickInterval(); + return result; +} + +static void ResetBhopState(int client) +{ + lastTouchSingleBhopEntRef[client] = INVALID_ENT_REFERENCE; + lastTouchSequentialBhopEntRefs[client].Clear(); +} + +static bool GetEntityHammerIDString(int entity, char[] buffer, int maxLength) +{ + if (!IsValidEntity(entity)) + { + return false; + } + + if (!HasEntProp(entity, Prop_Data, "m_iHammerID")) + { + return false; + } + + int hammerID = GetEntProp(entity, Prop_Data, "m_iHammerID"); + IntToString(hammerID, buffer, maxLength); + + return true; +} + +// NOTE: returns an entity reference (possibly invalid). +static int GetTeleportDestinationAndOrientation(char[] targetName, float origin[3], float angles[3] = NULL_VECTOR, bool &gotOrigin = false, bool &gotAngles = false) +{ + // NOTE: We're not caching the teleport destination because it could change. + int destination = GOKZFindEntityByName(targetName, .ignorePlayers = true); + if (!IsValidEntity(destination)) + { + return destination; + } + + gotOrigin = GetEntityAbsOrigin(destination, origin); + + if (HasEntProp(destination, Prop_Data, "m_angAbsRotation")) + { + GetEntPropVector(destination, Prop_Data, "m_angAbsRotation", angles); + gotAngles = true; + } + else + { + gotAngles = false; + } + + return destination; +}
\ No newline at end of file diff --git a/sourcemod/scripting/gokz-core/map/zones.sp b/sourcemod/scripting/gokz-core/map/zones.sp new file mode 100644 index 0000000..684e42d --- /dev/null +++ b/sourcemod/scripting/gokz-core/map/zones.sp @@ -0,0 +1,183 @@ +/* + Hooks between specifically named trigger_multiples and GOKZ. +*/ + + + +static Regex RE_BonusStartZone; +static Regex RE_BonusEndZone; +static bool touchedGroundSinceTouchingStartZone[MAXPLAYERS + 1]; + + + +// =====[ EVENTS ]===== + +void OnPluginStart_MapZones() +{ + RE_BonusStartZone = CompileRegex(GOKZ_BONUS_START_ZONE_NAME_REGEX); + RE_BonusEndZone = CompileRegex(GOKZ_BONUS_END_ZONE_NAME_REGEX); +} + +void OnStartTouchGround_MapZones(int client) +{ + touchedGroundSinceTouchingStartZone[client] = true; +} + +void OnEntitySpawned_MapZones(int entity) +{ + char buffer[32]; + + GetEntityClassname(entity, buffer, sizeof(buffer)); + if (!StrEqual("trigger_multiple", buffer, false)) + { + return; + } + + if (GetEntityName(entity, buffer, sizeof(buffer)) == 0) + { + return; + } + + int course = 0; + if (StrEqual(GOKZ_START_ZONE_NAME, buffer, false)) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnStartZoneStartTouch); + HookSingleEntityOutput(entity, "OnEndTouch", OnStartZoneEndTouch); + RegisterCourseStart(course); + } + else if (StrEqual(GOKZ_END_ZONE_NAME, buffer, false)) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnEndZoneStartTouch); + RegisterCourseEnd(course); + } + else if ((course = GetStartZoneBonusNumber(entity)) != -1) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnBonusStartZoneStartTouch); + HookSingleEntityOutput(entity, "OnEndTouch", OnBonusStartZoneEndTouch); + RegisterCourseStart(course); + } + else if ((course = GetEndZoneBonusNumber(entity)) != -1) + { + HookSingleEntityOutput(entity, "OnStartTouch", OnBonusEndZoneStartTouch); + RegisterCourseEnd(course); + } +} + +public void OnStartZoneStartTouch(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + ProcessStartZoneStartTouch(activator, 0); +} + +public void OnStartZoneEndTouch(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + ProcessStartZoneEndTouch(activator, 0); +} + +public void OnEndZoneStartTouch(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + ProcessEndZoneStartTouch(activator, 0); +} + +public void OnBonusStartZoneStartTouch(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + int course = GetStartZoneBonusNumber(caller); + if (!GOKZ_IsValidCourse(course, true)) + { + return; + } + + ProcessStartZoneStartTouch(activator, course); +} + +public void OnBonusStartZoneEndTouch(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + int course = GetStartZoneBonusNumber(caller); + if (!GOKZ_IsValidCourse(course, true)) + { + return; + } + + ProcessStartZoneEndTouch(activator, course); +} + +public void OnBonusEndZoneStartTouch(const char[] name, int caller, int activator, float delay) +{ + if (!IsValidEntity(caller) || !IsValidClient(activator)) + { + return; + } + + int course = GetEndZoneBonusNumber(caller); + if (!GOKZ_IsValidCourse(course, true)) + { + return; + } + + ProcessEndZoneStartTouch(activator, course); +} + + + +// =====[ PRIVATE ]===== + +static void ProcessStartZoneStartTouch(int client, int course) +{ + touchedGroundSinceTouchingStartZone[client] = Movement_GetOnGround(client); + + GOKZ_StopTimer(client, false); + SetCurrentCourse(client, course); + + OnStartZoneStartTouch_Teleports(client, course); +} + +static void ProcessStartZoneEndTouch(int client, int course) +{ + if (!touchedGroundSinceTouchingStartZone[client]) + { + return; + } + + GOKZ_StartTimer(client, course, true); + GOKZ_ResetVirtualButtonPosition(client, true); +} + +static void ProcessEndZoneStartTouch(int client, int course) +{ + GOKZ_EndTimer(client, course); + GOKZ_ResetVirtualButtonPosition(client, false); +} + +static int GetStartZoneBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusStartZone, 1); +} + +static int GetEndZoneBonusNumber(int entity) +{ + return GOKZ_MatchIntFromEntityName(entity, RE_BonusEndZone, 1); +}
\ No newline at end of file |
