From e2e8a83493848e4ace0e69ac01c6c76b8f49b920 Mon Sep 17 00:00:00 2001 From: Hendrik Boll Date: Sun, 12 May 2019 15:00:14 +0200 Subject: [PATCH] merged the tmserver and xaseco repository and included docker-compose.yml --- docker-compose.yml | 4 +- Dockerfile => docker-tmserver/Dockerfile | 0 .../entrypoint-tmserver.sh | 0 .../tmserver}/CommandLine.html | 0 .../GameData/Config/Default.SystemConfig.Gbx | Bin .../tmserver}/GameData/Config/blacklist.txt | 0 .../tmserver}/GameData/Config/checksum.txt | 0 .../tmserver}/GameData/Config/config.txt | 0 .../tmserver}/GameData/Config/guestlist.txt | 0 .../tmserver}/GameData/Config/pkey.dat | Bin .../DedicatedTrackMania.TrackMania.gbx | Bin .../tmserver}/GameData/Game.FidCache.Gbx | Bin .../tmserver}/GameData/NationsList.xml | 0 .../Nations/Black/E01-Obstacle.Challenge.Gbx | Bin .../Nations/Black/E02-Endurance.Challenge.Gbx | Bin .../Nations/Black/E03-Endurance.Challenge.Gbx | Bin .../Nations/Black/E04-Obstacle.Challenge.Gbx | Bin .../Nations/Black/E05-Endurance.Challenge.Gbx | Bin .../Nations/Blue/C01-Race.Challenge.Gbx | Bin .../Nations/Blue/C02-Race.Challenge.Gbx | Bin .../Nations/Blue/C03-Acrobatic.Challenge.Gbx | Bin .../Nations/Blue/C04-Race.Challenge.Gbx | Bin .../Nations/Blue/C05-Endurance.Challenge.Gbx | Bin .../Nations/Blue/C06-Speed.Challenge.Gbx | Bin .../Nations/Blue/C07-Race.Challenge.Gbx | Bin .../Nations/Blue/C08-Obstacle.Challenge.Gbx | Bin .../Nations/Blue/C09-Race.Challenge.Gbx | Bin .../Nations/Blue/C10-Acrobatic.Challenge.Gbx | Bin .../Nations/Blue/C11-Race.Challenge.Gbx | Bin .../Nations/Blue/C12-Obstacle.Challenge.Gbx | Bin .../Nations/Blue/C13-Race.Challenge.Gbx | Bin .../Nations/Blue/C14-Endurance.Challenge.Gbx | Bin .../Nations/Blue/C15-Speed.Challenge.Gbx | Bin .../Nations/Green/B01-Race.Challenge.Gbx | Bin .../Nations/Green/B02-Race.Challenge.Gbx | Bin .../Nations/Green/B03-Race.Challenge.Gbx | Bin .../Nations/Green/B04-Acrobatic.Challenge.Gbx | Bin .../Nations/Green/B05-Race.Challenge.Gbx | Bin .../Nations/Green/B06-Obstacle.Challenge.Gbx | Bin .../Nations/Green/B07-Race.Challenge.Gbx | Bin .../Nations/Green/B08-Endurance.Challenge.Gbx | Bin .../Nations/Green/B09-Acrobatic.Challenge.Gbx | Bin .../Nations/Green/B10-Speed.Challenge.Gbx | Bin .../Nations/Green/B11-Race.Challenge.Gbx | Bin .../Nations/Green/B12-Race.Challenge.Gbx | Bin .../Nations/Green/B13-Obstacle.Challenge.Gbx | Bin .../Nations/Green/B14-Speed.Challenge.Gbx | Bin .../Nations/Green/B15-Race.Challenge.Gbx | Bin .../Nations/Red/D01-endurance.Challenge.Gbx | Bin .../Nations/Red/D02-Race.Challenge.Gbx | Bin .../Nations/Red/D03-Acrobatic.Challenge.Gbx | Bin .../Nations/Red/D04-Race.Challenge.Gbx | Bin .../Nations/Red/D05-Race.Challenge.Gbx | Bin .../Nations/Red/D06-Obstacle.Challenge.Gbx | Bin .../Nations/Red/D07-Race.Challenge.Gbx | Bin .../Nations/Red/D08-Speed.Challenge.Gbx | Bin .../Nations/Red/D09-obstacle.Challenge.Gbx | Bin .../Nations/Red/D10-Race.Challenge.Gbx | Bin .../Nations/Red/D11-Acrobatic.Challenge.Gbx | Bin .../Nations/Red/D12-Speed.Challenge.Gbx | Bin .../Nations/Red/D13-Race.Challenge.Gbx | Bin .../Nations/Red/D14-Endurance.Challenge.Gbx | Bin .../Nations/Red/D15-Endurance.Challenge.Gbx | Bin .../Nations/White/A01-Race.Challenge.Gbx | Bin .../Nations/White/A02-Race.Challenge.Gbx | Bin .../Nations/White/A03-Race.Challenge.Gbx | Bin .../Nations/White/A04-Acrobatic.Challenge.Gbx | Bin .../Nations/White/A05-Race.Challenge.Gbx | Bin .../Nations/White/A06-Obstacle.Challenge.Gbx | Bin .../Nations/White/A07-Race.Challenge.Gbx | Bin .../Nations/White/A08-Endurance.Challenge.Gbx | Bin .../Nations/White/A09-Race.Challenge.Gbx | Bin .../Nations/White/A10-Acrobatic.Challenge.Gbx | Bin .../Nations/White/A11-Race.Challenge.Gbx | Bin .../Nations/White/A12-Speed.Challenge.Gbx | Bin .../Nations/White/A13-Race.Challenge.Gbx | Bin .../Nations/White/A14-Race.Challenge.Gbx | Bin .../Nations/White/A15-Speed.Challenge.Gbx | Bin .../Platform/Black/PlatformE.Challenge.Gbx | Bin .../Platform/Blue/PlatformC1.Challenge.Gbx | Bin .../Platform/Blue/PlatformC2.Challenge.Gbx | Bin .../Platform/Blue/PlatformC3.Challenge.Gbx | Bin .../Platform/Blue/PlatformC4.Challenge.Gbx | Bin .../Platform/Blue/PlatformC5.Challenge.Gbx | Bin .../Platform/Green/PlatformB1.Challenge.Gbx | Bin .../Platform/Green/PlatformB2.Challenge.Gbx | Bin .../Platform/Green/PlatformB3.Challenge.Gbx | Bin .../Platform/Green/PlatformB4.Challenge.Gbx | Bin .../Platform/Green/PlatformB5.Challenge.Gbx | Bin .../Platform/Red/PlatformD1.Challenge.Gbx | Bin .../Platform/Red/PlatformD2.Challenge.Gbx | Bin .../Platform/Red/PlatformD3.Challenge.Gbx | Bin .../Platform/Red/PlatformD4.Challenge.Gbx | Bin .../Platform/Red/PlatformD5.Challenge.Gbx | Bin .../Platform/White/PlatformA1.Challenge.Gbx | Bin .../Platform/White/PlatformA2.Challenge.Gbx | Bin .../Platform/White/PlatformA3.Challenge.Gbx | Bin .../Platform/White/PlatformA4.Challenge.Gbx | Bin .../Platform/White/PlatformA5.Challenge.Gbx | Bin .../United/Puzzle/Black/PuzzleE.Challenge.Gbx | Bin .../United/Puzzle/Blue/PuzzleC1.Challenge.Gbx | Bin .../United/Puzzle/Blue/PuzzleC2.Challenge.Gbx | Bin .../United/Puzzle/Blue/PuzzleC3.Challenge.Gbx | Bin .../United/Puzzle/Blue/PuzzleC4.Challenge.Gbx | Bin .../United/Puzzle/Blue/PuzzleC5.Challenge.Gbx | Bin .../Puzzle/Green/PuzzleB1.Challenge.Gbx | Bin .../Puzzle/Green/PuzzleB2.Challenge.Gbx | Bin .../Puzzle/Green/PuzzleB3.Challenge.Gbx | Bin .../Puzzle/Green/PuzzleB4.Challenge.Gbx | Bin .../Puzzle/Green/PuzzleB5.Challenge.Gbx | Bin .../United/Puzzle/Red/PuzzleD1.Challenge.Gbx | Bin .../United/Puzzle/Red/PuzzleD2.Challenge.Gbx | Bin .../United/Puzzle/Red/PuzzleD3.Challenge.Gbx | Bin .../United/Puzzle/Red/PuzzleD4.Challenge.Gbx | Bin .../United/Puzzle/Red/PuzzleD5.Challenge.Gbx | Bin .../Puzzle/White/PuzzleA1.Challenge.Gbx | Bin .../Puzzle/White/PuzzleA2.Challenge.Gbx | Bin .../Puzzle/White/PuzzleA3.Challenge.Gbx | Bin .../Puzzle/White/PuzzleA4.Challenge.Gbx | Bin .../Puzzle/White/PuzzleA5.Challenge.Gbx | Bin .../United/Race/Bay/Black/BayE.Challenge.Gbx | Bin .../United/Race/Bay/Blue/BayC1.Challenge.Gbx | Bin .../United/Race/Bay/Blue/BayC2.Challenge.Gbx | Bin .../United/Race/Bay/Blue/BayC3.Challenge.Gbx | Bin .../United/Race/Bay/Blue/BayC4.Challenge.Gbx | Bin .../United/Race/Bay/Blue/BayC5.Challenge.Gbx | Bin .../United/Race/Bay/Green/BayB1.Challenge.Gbx | Bin .../United/Race/Bay/Green/BayB2.Challenge.Gbx | Bin .../United/Race/Bay/Green/BayB3.Challenge.Gbx | Bin .../United/Race/Bay/Green/BayB4.Challenge.Gbx | Bin .../United/Race/Bay/Green/BayB5.Challenge.Gbx | Bin .../United/Race/Bay/Red/BayD1.Challenge.Gbx | Bin .../United/Race/Bay/Red/BayD2.Challenge.Gbx | Bin .../United/Race/Bay/Red/BayD3.Challenge.Gbx | Bin .../United/Race/Bay/Red/BayD4.Challenge.Gbx | Bin .../United/Race/Bay/Red/BayD5.Challenge.Gbx | Bin .../United/Race/Bay/White/BayA1.Challenge.Gbx | Bin .../United/Race/Bay/White/BayA2.Challenge.Gbx | Bin .../United/Race/Bay/White/BayA3.Challenge.Gbx | Bin .../United/Race/Bay/White/BayA4.Challenge.Gbx | Bin .../United/Race/Bay/White/BayA5.Challenge.Gbx | Bin .../Race/Coast/Black/CoastE.Challenge.Gbx | Bin .../Race/Coast/Blue/CoastC1.Challenge.Gbx | Bin .../Race/Coast/Blue/CoastC2.Challenge.Gbx | Bin .../Race/Coast/Blue/CoastC3.Challenge.Gbx | Bin .../Race/Coast/Blue/CoastC4.Challenge.Gbx | Bin .../Race/Coast/Blue/CoastC5.Challenge.Gbx | Bin .../Race/Coast/Green/CoastB1.Challenge.Gbx | Bin .../Race/Coast/Green/CoastB2.Challenge.Gbx | Bin .../Race/Coast/Green/CoastB3.Challenge.Gbx | Bin .../Race/Coast/Green/CoastB4.Challenge.Gbx | Bin .../Race/Coast/Green/CoastB5.Challenge.Gbx | Bin .../Race/Coast/Red/CoastD1.Challenge.Gbx | Bin .../Race/Coast/Red/CoastD2.Challenge.Gbx | Bin .../Race/Coast/Red/CoastD3.Challenge.Gbx | Bin .../Race/Coast/Red/CoastD4.Challenge.Gbx | Bin .../Race/Coast/Red/CoastD5.Challenge.Gbx | Bin .../Race/Coast/White/CoastA1.Challenge.Gbx | Bin .../Race/Coast/White/CoastA2.Challenge.Gbx | Bin .../Race/Coast/White/CoastA3.Challenge.Gbx | Bin .../Race/Coast/White/CoastA4.Challenge.Gbx | Bin .../Race/Coast/White/CoastA5.Challenge.Gbx | Bin .../Race/Desert/Black/DesertE.Challenge.Gbx | Bin .../Race/Desert/Blue/DesertC1.Challenge.Gbx | Bin .../Race/Desert/Blue/DesertC2.Challenge.Gbx | Bin .../Race/Desert/Blue/DesertC3.Challenge.Gbx | Bin .../Race/Desert/Blue/DesertC4.Challenge.Gbx | Bin .../Race/Desert/Blue/DesertC5.Challenge.Gbx | Bin .../Race/Desert/Green/DesertB1.Challenge.Gbx | Bin .../Race/Desert/Green/DesertB2.Challenge.Gbx | Bin .../Race/Desert/Green/DesertB3.Challenge.Gbx | Bin .../Race/Desert/Green/DesertB4.Challenge.Gbx | Bin .../Race/Desert/Green/DesertB5.Challenge.Gbx | Bin .../Race/Desert/Red/DesertD1.Challenge.Gbx | Bin .../Race/Desert/Red/DesertD2.Challenge.Gbx | Bin .../Race/Desert/Red/DesertD3.Challenge.Gbx | Bin .../Race/Desert/Red/DesertD4.Challenge.Gbx | Bin .../Race/Desert/Red/DesertD5.Challenge.Gbx | Bin .../Race/Desert/White/DesertA1.Challenge.Gbx | Bin .../Race/Desert/White/DesertA2.Challenge.Gbx | Bin .../Race/Desert/White/DesertA3.Challenge.Gbx | Bin .../Race/Desert/White/DesertA4.Challenge.Gbx | Bin .../Race/Desert/White/DesertA5.Challenge.Gbx | Bin .../Race/Island/Black/IslandE.Challenge.Gbx | Bin .../Race/Island/Blue/IslandC1.Challenge.Gbx | Bin .../Race/Island/Blue/IslandC2.Challenge.Gbx | Bin .../Race/Island/Blue/IslandC3.Challenge.Gbx | Bin .../Race/Island/Blue/IslandC4.Challenge.Gbx | Bin .../Race/Island/Blue/IslandC5.Challenge.Gbx | Bin .../Race/Island/Green/IslandB1.Challenge.Gbx | Bin .../Race/Island/Green/IslandB2.Challenge.Gbx | Bin .../Race/Island/Green/IslandB3.Challenge.Gbx | Bin .../Race/Island/Green/IslandB4.Challenge.Gbx | Bin .../Race/Island/Green/IslandB5.Challenge.Gbx | Bin .../Race/Island/Red/IslandD1.Challenge.Gbx | Bin .../Race/Island/Red/IslandD2.Challenge.Gbx | Bin .../Race/Island/Red/IslandD3.Challenge.Gbx | Bin .../Race/Island/Red/IslandD4.Challenge.Gbx | Bin .../Race/Island/Red/IslandD5.Challenge.Gbx | Bin .../Race/Island/White/IslandA1.Challenge.Gbx | Bin .../Race/Island/White/IslandA2.Challenge.Gbx | Bin .../Race/Island/White/IslandA3.Challenge.Gbx | Bin .../Race/Island/White/IslandA4.Challenge.Gbx | Bin .../Race/Island/White/IslandA5.Challenge.Gbx | Bin .../Race/Rally/Black/RallyE.Challenge.Gbx | Bin .../Race/Rally/Blue/RallyC1.Challenge.Gbx | Bin .../Race/Rally/Blue/RallyC2.Challenge.Gbx | Bin .../Race/Rally/Blue/RallyC3.Challenge.Gbx | Bin .../Race/Rally/Blue/RallyC4.Challenge.Gbx | Bin .../Race/Rally/Blue/RallyC5.Challenge.Gbx | Bin .../Race/Rally/Green/RallyB1.Challenge.Gbx | Bin .../Race/Rally/Green/RallyB2.Challenge.Gbx | Bin .../Race/Rally/Green/RallyB3.Challenge.Gbx | Bin .../Race/Rally/Green/RallyB4.Challenge.Gbx | Bin .../Race/Rally/Green/RallyB5.Challenge.Gbx | Bin .../Race/Rally/Red/RallyD1.Challenge.Gbx | Bin .../Race/Rally/Red/RallyD2.Challenge.Gbx | Bin .../Race/Rally/Red/RallyD3.Challenge.Gbx | Bin .../Race/Rally/Red/RallyD4.Challenge.Gbx | Bin .../Race/Rally/Red/RallyD5.Challenge.Gbx | Bin .../Race/Rally/White/RallyA1.Challenge.Gbx | Bin .../Race/Rally/White/RallyA2.Challenge.Gbx | Bin .../Race/Rally/White/RallyA3.Challenge.Gbx | Bin .../Race/Rally/White/RallyA4.Challenge.Gbx | Bin .../Race/Rally/White/RallyA5.Challenge.Gbx | Bin .../Race/Snow/Black/SnowE.Challenge.Gbx | Bin .../Race/Snow/Blue/SnowC1.Challenge.Gbx | Bin .../Race/Snow/Blue/SnowC2.Challenge.Gbx | Bin .../Race/Snow/Blue/SnowC3.Challenge.Gbx | Bin .../Race/Snow/Blue/SnowC4.Challenge.Gbx | Bin .../Race/Snow/Blue/SnowC5.Challenge.Gbx | Bin .../Race/Snow/Green/SnowB1.Challenge.Gbx | Bin .../Race/Snow/Green/SnowB2.Challenge.Gbx | Bin .../Race/Snow/Green/SnowB3.Challenge.Gbx | Bin .../Race/Snow/Green/SnowB4.Challenge.Gbx | Bin .../Race/Snow/Green/SnowB5.Challenge.Gbx | Bin .../United/Race/Snow/Red/SnowD1.Challenge.Gbx | Bin .../United/Race/Snow/Red/SnowD2.Challenge.Gbx | Bin .../United/Race/Snow/Red/SnowD3.Challenge.Gbx | Bin .../United/Race/Snow/Red/SnowD4.Challenge.Gbx | Bin .../United/Race/Snow/Red/SnowD5.Challenge.Gbx | Bin .../Race/Snow/White/SnowA1.Challenge.Gbx | Bin .../Race/Snow/White/SnowA2.Challenge.Gbx | Bin .../Race/Snow/White/SnowA3.Challenge.Gbx | Bin .../Race/Snow/White/SnowA4.Challenge.Gbx | Bin .../Race/Snow/White/SnowA5.Challenge.Gbx | Bin .../Race/Stadium/Black/StadiumE.Challenge.Gbx | Bin .../Race/Stadium/Blue/StadiumC1.Challenge.Gbx | Bin .../Race/Stadium/Blue/StadiumC2.Challenge.Gbx | Bin .../Race/Stadium/Blue/StadiumC3.Challenge.Gbx | Bin .../Race/Stadium/Blue/StadiumC4.Challenge.Gbx | Bin .../Race/Stadium/Blue/StadiumC5.Challenge.Gbx | Bin .../Stadium/Green/StadiumB1.Challenge.Gbx | Bin .../Stadium/Green/StadiumB2.Challenge.Gbx | Bin .../Stadium/Green/StadiumB3.Challenge.Gbx | Bin .../Stadium/Green/StadiumB4.Challenge.Gbx | Bin .../Stadium/Green/StadiumB5.Challenge.Gbx | Bin .../Race/Stadium/Red/StadiumD1.Challenge.Gbx | Bin .../Race/Stadium/Red/StadiumD2.Challenge.Gbx | Bin .../Race/Stadium/Red/StadiumD3.Challenge.Gbx | Bin .../Race/Stadium/Red/StadiumD4.Challenge.Gbx | Bin .../Race/Stadium/Red/StadiumD5.Challenge.Gbx | Bin .../Stadium/White/StadiumA1.Challenge.Gbx | Bin .../Stadium/White/StadiumA2.Challenge.Gbx | Bin .../Stadium/White/StadiumA3.Challenge.Gbx | Bin .../Stadium/White/StadiumA4.Challenge.Gbx | Bin .../Stadium/White/StadiumA5.Challenge.Gbx | Bin .../United/Stunts/StuntA1.Challenge.Gbx | Bin .../United/Stunts/StuntA2.Challenge.Gbx | Bin .../United/Stunts/StuntA3.Challenge.Gbx | Bin .../United/Stunts/StuntA4.Challenge.Gbx | Bin .../United/Stunts/StuntA5.Challenge.Gbx | Bin .../United/Stunts/StuntB1.Challenge.Gbx | Bin .../United/Stunts/StuntB2.Challenge.Gbx | Bin .../United/Stunts/StuntB3.Challenge.Gbx | Bin .../United/Stunts/StuntB4.Challenge.Gbx | Bin .../United/Stunts/StuntB5.Challenge.Gbx | Bin .../United/Stunts/StuntC1.Challenge.Gbx | Bin .../United/Stunts/StuntC2.Challenge.Gbx | Bin .../United/Stunts/StuntC3.Challenge.Gbx | Bin .../United/Stunts/StuntC4.Challenge.Gbx | Bin .../United/Stunts/StuntC5.Challenge.Gbx | Bin .../United/Stunts/StuntD1.Challenge.Gbx | Bin .../United/Stunts/StuntD2.Challenge.Gbx | Bin .../United/Stunts/StuntD3.Challenge.Gbx | Bin .../United/Stunts/StuntD4.Challenge.Gbx | Bin .../United/Stunts/StuntD5.Challenge.Gbx | Bin .../United/Stunts/StuntE1.Challenge.Gbx | Bin .../Challenges/Nadeo/A01-Race.Challenge.Gbx | Bin .../Challenges/Nadeo/A02-Race.Challenge.Gbx | Bin .../Challenges/Nadeo/A03-Race.Challenge.Gbx | Bin .../Nadeo/A04-Acrobatic.Challenge.Gbx | Bin .../Challenges/Nadeo/A05-Race.Challenge.Gbx | Bin .../Nadeo/A06-Obstacle.Challenge.Gbx | Bin .../Challenges/Nadeo/A07-Race.Challenge.Gbx | Bin .../Nadeo/A08-Endurance.Challenge.Gbx | Bin .../Challenges/Nadeo/A09-Race.Challenge.Gbx | Bin .../Nadeo/A10-Acrobatic.Challenge.Gbx | Bin .../Challenges/Nadeo/A11-Race.Challenge.Gbx | Bin .../Challenges/Nadeo/A12-Speed.Challenge.Gbx | Bin .../Challenges/Nadeo/A13-Race.Challenge.Gbx | Bin .../Challenges/Nadeo/A14-Race.Challenge.Gbx | Bin .../Challenges/Nadeo/A15-Speed.Challenge.Gbx | Bin .../Challenges/Nadeo/B01-Race.Challenge.Gbx | Bin .../Challenges/Nadeo/B02-Race.Challenge.Gbx | Bin .../Challenges/Nadeo/B03-Race.Challenge.Gbx | Bin .../Nadeo/B04-Acrobatic.Challenge.Gbx | Bin .../Challenges/Nadeo/B05-Race.Challenge.Gbx | Bin .../Nadeo/B06-Obstacle.Challenge.Gbx | Bin .../Challenges/Nadeo/B07-Race.Challenge.Gbx | Bin .../Nadeo/B08-Endurance.Challenge.Gbx | Bin .../Nadeo/B09-Acrobatic.Challenge.Gbx | Bin .../Challenges/Nadeo/B10-Speed.Challenge.Gbx | Bin .../Challenges/Nadeo/B11-Race.Challenge.Gbx | Bin .../Challenges/Nadeo/B12-Race.Challenge.Gbx | Bin .../Nadeo/B13-Obstacle.Challenge.Gbx | Bin .../Challenges/Nadeo/B14-Speed.Challenge.Gbx | Bin .../Challenges/Nadeo/B15-Race.Challenge.Gbx | Bin .../Challenges/Nadeo/C01-Race.Challenge.Gbx | Bin .../Challenges/Nadeo/C02-Race.Challenge.Gbx | Bin .../Nadeo/C03-Acrobatic.Challenge.Gbx | Bin .../Challenges/Nadeo/C04-Race.Challenge.Gbx | Bin .../Nadeo/C05-Endurance.Challenge.Gbx | Bin .../Challenges/Nadeo/C06-Speed.Challenge.Gbx | Bin .../Challenges/Nadeo/C07-Race.Challenge.Gbx | Bin .../Nadeo/C08-Obstacle.Challenge.Gbx | Bin .../Challenges/Nadeo/C09-Race.Challenge.Gbx | Bin .../Nadeo/C10-Acrobatic.Challenge.Gbx | Bin .../Challenges/Nadeo/C11-Race.Challenge.Gbx | Bin .../Nadeo/C12-Obstacle.Challenge.Gbx | Bin .../Challenges/Nadeo/C13-Race.Challenge.Gbx | Bin .../Nadeo/C14-Endurance.Challenge.Gbx | Bin .../Challenges/Nadeo/C15-Speed.Challenge.Gbx | Bin .../Nadeo/D01-endurance.Challenge.Gbx | Bin .../Challenges/Nadeo/D02-Race.Challenge.Gbx | Bin .../Nadeo/D03-Acrobatic.Challenge.Gbx | Bin .../Challenges/Nadeo/D04-Race.Challenge.Gbx | Bin .../Challenges/Nadeo/D05-Race.Challenge.Gbx | Bin .../Nadeo/D06-Obstacle.Challenge.Gbx | Bin .../Challenges/Nadeo/D07-Race.Challenge.Gbx | Bin .../Challenges/Nadeo/D08-Speed.Challenge.Gbx | Bin .../Nadeo/D09-obstacle.Challenge.Gbx | Bin .../Challenges/Nadeo/D10-Race.Challenge.Gbx | Bin .../Nadeo/D11-Acrobatic.Challenge.Gbx | Bin .../Challenges/Nadeo/D12-Speed.Challenge.Gbx | Bin .../Challenges/Nadeo/D13-Race.Challenge.Gbx | Bin .../Nadeo/D14-Endurance.Challenge.Gbx | Bin .../Nadeo/D15-Endurance.Challenge.Gbx | Bin .../Nadeo/E01-Obstacle.Challenge.Gbx | Bin .../Nadeo/E02-Endurance.Challenge.Gbx | Bin .../Nadeo/E03-Endurance.Challenge.Gbx | Bin .../Nadeo/E04-Obstacle.Challenge.Gbx | Bin .../Nadeo/E05-Endurance.Challenge.Gbx | Bin .../MatchSettings/Nations/NationsBlue.txt | 0 .../MatchSettings/Nations/NationsGreen.txt | 0 .../MatchSettings/Nations/NationsRed.txt | 0 .../MatchSettings/Nations/NationsWhite.txt | 0 .../Tracks/MatchSettings/United/BayBlue.txt | 0 .../Tracks/MatchSettings/United/BayGreen.txt | 0 .../Tracks/MatchSettings/United/BayRed.txt | 0 .../Tracks/MatchSettings/United/BayWhite.txt | 0 .../Tracks/MatchSettings/United/CoastBlue.txt | 0 .../MatchSettings/United/CoastGreen.txt | 0 .../Tracks/MatchSettings/United/CoastRed.txt | 0 .../MatchSettings/United/CoastWhite.txt | 0 .../MatchSettings/United/DesertBlue.txt | 0 .../MatchSettings/United/DesertGreen.txt | 0 .../Tracks/MatchSettings/United/DesertRed.txt | 0 .../MatchSettings/United/DesertWhite.txt | 0 .../MatchSettings/United/IslandBlue.txt | 0 .../MatchSettings/United/IslandGreen.txt | 0 .../Tracks/MatchSettings/United/IslandRed.txt | 0 .../MatchSettings/United/IslandWhite.txt | 0 .../Tracks/MatchSettings/United/RallyBlue.txt | 0 .../MatchSettings/United/RallyGreen.txt | 0 .../Tracks/MatchSettings/United/RallyRed.txt | 0 .../MatchSettings/United/RallyWhite.txt | 0 .../Tracks/MatchSettings/United/SnowBlue.txt | 0 .../Tracks/MatchSettings/United/SnowGreen.txt | 0 .../Tracks/MatchSettings/United/SnowRed.txt | 0 .../Tracks/MatchSettings/United/SnowWhite.txt | 0 .../MatchSettings/United/StadiumBlue.txt | 0 .../MatchSettings/United/StadiumGreen.txt | 0 .../MatchSettings/United/StadiumRed.txt | 0 .../MatchSettings/United/StadiumWhite.txt | 0 .../Tracks/MatchSettings/playlist.txt | 0 .../tmserver}/ListCallbacks.html | 0 .../tmserver}/ListMethods.html | 0 .../tmserver}/Logs/ConsoleLog.1352.txt | 0 .../tmserver}/Readme_Dedicated.html | 0 .../PhpRemote/GbxRemote.bem.php | 0 .../PhpRemote/GbxRemote.inc.php | 0 .../PhpRemote/ListMethods.php | 0 .../PhpRemote/SpectatorUi.php | 0 .../RemoteControlExamples/PhpRemote/basic.php | 0 .../tmserver}/TrackmaniaServer | Bin .../tmserver}/bin/.xaseco_restart_pott.sh.swp | Bin .../tmserver}/bin/delete_cache.sh | 0 .../tmserver}/tmserver | 0 docker-xaseco/Dockerfile | 17 + docker-xaseco/entrypoint-xaseco.sh | 56 + docker-xaseco/xaseco/Aseco.sh | 3 + docker-xaseco/xaseco/DOCS/Features_080.html | 208 + docker-xaseco/xaseco/DOCS/Features_095.html | 890 +++ docker-xaseco/xaseco/DOCS/Features_103.html | 913 +++ docker-xaseco/xaseco/DOCS/Features_116.html | 895 +++ .../xaseco/DOCS/ListCallbacksForever.html | 149 + .../xaseco/DOCS/ListCallbacksNations.html | 88 + docker-xaseco/xaseco/DOCS/ListDedimania.html | 189 + .../xaseco/DOCS/ListMethodsForever.html | 717 ++ .../xaseco/DOCS/ListMethodsNations.html | 432 ++ .../xaseco/DOCS/OLD/Jfreu install.txt | 24 + .../xaseco/DOCS/OLD/Jfreu's plugin.txt | 121 + .../xaseco/DOCS/OLD/README-autotime.txt | 23 + docker-xaseco/xaseco/DOCS/OLD/README.txt | 144 + docker-xaseco/xaseco/DOCS/OLD/ReadMe.pdf | Bin 0 -> 215757 bytes .../xaseco/DOCS/OLD/plugin.sminfo.php | 119 + .../xaseco/DOCS/OLD/sminfofetcher.inc.php | 149 + .../xaseco/DOCS/admin_abilities.html | 194 + docker-xaseco/xaseco/DOCS/aseco_commands.doc | Bin 0 -> 69120 bytes docker-xaseco/xaseco/DOCS/aseco_commands.html | 498 ++ docker-xaseco/xaseco/DOCS/index.html | 421 ++ docker-xaseco/xaseco/DOCS/overview.html | 331 + docker-xaseco/xaseco/DOCS/quickstart_tmf.html | 254 + docker-xaseco/xaseco/DOCS/quickstart_tmn.html | 226 + docker-xaseco/xaseco/DOCS/repairnations.php | 432 ++ docker-xaseco/xaseco/DOCS/repairrecs.php | 128 + docker-xaseco/xaseco/DOCS/sm_hub.html | 112 + docker-xaseco/xaseco/DOCS/tm2_hub.html | 296 + docker-xaseco/xaseco/DOCS/tmf_hub.html | 127 + docker-xaseco/xaseco/DOCS/updatepanels.php | 76 + docker-xaseco/xaseco/DOCS/upgrades.html | 1128 ++++ docker-xaseco/xaseco/access.xml | 93 + docker-xaseco/xaseco/adminops.xml | 320 + docker-xaseco/xaseco/aseco.log | 5 + docker-xaseco/xaseco/aseco.php | 2562 ++++++++ docker-xaseco/xaseco/autotime.xml | 15 + docker-xaseco/xaseco/bannedips.xml | 6 + docker-xaseco/xaseco/config.xml | 219 + docker-xaseco/xaseco/dedimania.xml | 74 + docker-xaseco/xaseco/html.tpl | 28 + .../xaseco/includes/GbxRemote.bem.php | 891 +++ .../xaseco/includes/GbxRemote.inc.php | 854 +++ .../xaseco/includes/GbxRemote.response.php | 218 + docker-xaseco/xaseco/includes/basic.inc.php | 830 +++ .../xaseco/includes/gbxchallinfo.inc.php | 131 + .../xaseco/includes/gbxdatafetcher.inc.php | 1431 ++++ .../xaseco/includes/jfreu.config.php | 136 + .../xaseco/includes/manialinks.inc.php | 1404 ++++ .../xaseco/includes/ogg_comments.inc.php | 154 + docker-xaseco/xaseco/includes/rasp.funcs.php | 1949 ++++++ .../xaseco/includes/rasp.settings.php | 158 + .../xaseco/includes/tmndatafetcher.inc.php | 290 + .../xaseco/includes/tmxinfofetcher.inc.php | 297 + .../xaseco/includes/tmxinfosearcher.inc.php | 288 + docker-xaseco/xaseco/includes/types.inc.php | 511 ++ .../xaseco/includes/urlsafebase64.php | 26 + .../xaseco/includes/votes.config.php | 151 + .../xaseco/includes/web_access.inc.php | 1341 ++++ .../xaseco/includes/xmlparser.inc.php | 121 + .../xaseco/includes/xmlrpc_db.inc.php | 385 ++ docker-xaseco/xaseco/localdatabase.xml | 22 + docker-xaseco/xaseco/localdb/aseco.sql | 56 + docker-xaseco/xaseco/localdb/extra.sql | 18 + docker-xaseco/xaseco/localdb/rasp.sql | 47 + docker-xaseco/xaseco/logfile.txt | 4 + docker-xaseco/xaseco/matchsave.xml | 100 + docker-xaseco/xaseco/musicserver.xml | 55 + docker-xaseco/xaseco/panels/00README.txt | 34 + .../xaseco/panels/AdminAboveCPList.xml | 10 + .../xaseco/panels/AdminAboveCPListWide.xml | 10 + .../xaseco/panels/AdminAboveChat.xml | 10 + .../xaseco/panels/AdminAboveChatWide.xml | 10 + .../xaseco/panels/AdminAboveSpeed.xml | 10 + .../xaseco/panels/AdminAboveSpeed2.xml | 11 + .../xaseco/panels/AdminBelowChat.xml | 10 + .../xaseco/panels/AdminBottomCenter.xml | 10 + .../xaseco/panels/AdminBottomCenterWide.xml | 10 + docker-xaseco/xaseco/panels/AdminCallVote.xml | 10 + .../xaseco/panels/AdminCallVoteBlur.xml | 10 + docker-xaseco/xaseco/panels/AdminLeftEdge.xml | 10 + .../xaseco/panels/AdminRightEdge.xml | 10 + .../xaseco/panels/AdminTopCenter.xml | 10 + .../xaseco/panels/AdminTopCenterWide.xml | 10 + .../xaseco/panels/DonateBelowCPList.xml | 13 + .../xaseco/panels/DonateBelowCPListRM.xml | 13 + .../xaseco/panels/DonateLeftEdge.xml | 13 + .../xaseco/panels/DonateLeftEdge2.xml | 13 + .../xaseco/panels/DonateLeftSmall.xml | 13 + .../xaseco/panels/DonateRightEdge.xml | 13 + .../xaseco/panels/DonateRightEdge2.xml | 13 + .../xaseco/panels/DonateRightSmall.xml | 13 + docker-xaseco/xaseco/panels/DonateTopLeft.xml | 13 + .../xaseco/panels/RecordsLeftBlackBold.xml | 13 + .../xaseco/panels/RecordsLeftBlue.xml | 13 + .../xaseco/panels/RecordsLeftBlueBold.xml | 13 + .../xaseco/panels/RecordsLeftBlueLight.xml | 13 + .../xaseco/panels/RecordsLeftGray.xml | 13 + .../xaseco/panels/RecordsLeftGrayBold.xml | 13 + .../xaseco/panels/RecordsLeftItalic.xml | 13 + .../xaseco/panels/RecordsLeftOrange.xml | 13 + .../xaseco/panels/RecordsLeftSize1.xml | 13 + .../xaseco/panels/RecordsLeftSize1NoDedi.xml | 11 + .../xaseco/panels/RecordsLeftSize2.xml | 13 + .../xaseco/panels/RecordsLeftSize2NoDedi.xml | 11 + .../xaseco/panels/RecordsLeftWhite.xml | 13 + .../xaseco/panels/RecordsRightBottom.xml | 12 + .../panels/RecordsRightBottomNoDedi.xml | 10 + .../xaseco/panels/RecordsRightBottomRM.xml | 12 + docker-xaseco/xaseco/panels/StatsNations.xml | 18 + docker-xaseco/xaseco/panels/StatsUnited.xml | 20 + docker-xaseco/xaseco/panels/VoteBelowChat.xml | 7 + .../xaseco/panels/VoteBottomCenter.xml | 7 + .../xaseco/panels/VoteBottomCenterTransp.xml | 8 + docker-xaseco/xaseco/panels/VoteCallVote.xml | 7 + docker-xaseco/xaseco/panels/VoteTopCenter.xml | 7 + docker-xaseco/xaseco/plugins.xml | 44 + docker-xaseco/xaseco/plugins/chat.admin.php | 5772 +++++++++++++++++ .../xaseco/plugins/chat.dedimania.php | 941 +++ docker-xaseco/xaseco/plugins/chat.help.php | 32 + docker-xaseco/xaseco/plugins/chat.laston.php | 37 + docker-xaseco/xaseco/plugins/chat.lastwin.php | 40 + docker-xaseco/xaseco/plugins/chat.me.php | 31 + docker-xaseco/xaseco/plugins/chat.players.php | 141 + .../xaseco/plugins/chat.players2.php | 217 + docker-xaseco/xaseco/plugins/chat.records.php | 210 + .../xaseco/plugins/chat.records2.php | 888 +++ docker-xaseco/xaseco/plugins/chat.recrels.php | 306 + docker-xaseco/xaseco/plugins/chat.server.php | 424 ++ docker-xaseco/xaseco/plugins/chat.songmod.php | 63 + docker-xaseco/xaseco/plugins/chat.stats.php | 357 + docker-xaseco/xaseco/plugins/chat.wins.php | 23 + docker-xaseco/xaseco/plugins/jfreu.chat.php | 1866 ++++++ docker-xaseco/xaseco/plugins/jfreu.lite.php | 178 + docker-xaseco/xaseco/plugins/jfreu.plugin.php | 1791 +++++ .../xaseco/plugins/jfreu/jfreu.bans.xml | 8 + .../xaseco/plugins/jfreu/jfreu.config.xml | 29 + .../xaseco/plugins/jfreu/jfreu.vips.xml | 8 + .../xaseco/plugins/mistral.idlekick.php | 175 + .../xaseco/plugins/plugin.access.php | 448 ++ .../xaseco/plugins/plugin.autotime.php | 157 + .../xaseco/plugins/plugin.chatlog.php | 108 + .../xaseco/plugins/plugin.checkpoints.php | 809 +++ .../xaseco/plugins/plugin.dedimania.php | 1142 ++++ .../xaseco/plugins/plugin.donate.php | 258 + .../xaseco/plugins/plugin.localdatabase.php | 1023 +++ .../xaseco/plugins/plugin.matchsave.php | 1370 ++++ .../xaseco/plugins/plugin.ml_howto.php | 137 + .../xaseco/plugins/plugin.msglog.php | 106 + .../xaseco/plugins/plugin.musicserver.php | 851 +++ .../xaseco/plugins/plugin.muting.php | 247 + .../xaseco/plugins/plugin.panels.php | 898 +++ docker-xaseco/xaseco/plugins/plugin.rasp.php | 1065 +++ .../xaseco/plugins/plugin.rasp_chat.php | 472 ++ .../xaseco/plugins/plugin.rasp_irc.php | 166 + .../xaseco/plugins/plugin.rasp_jukebox.php | 1902 ++++++ .../xaseco/plugins/plugin.rasp_karma.php | 334 + .../xaseco/plugins/plugin.rasp_nextmap.php | 78 + .../xaseco/plugins/plugin.rasp_nextrank.php | 80 + .../xaseco/plugins/plugin.rasp_votes.php | 1158 ++++ .../xaseco/plugins/plugin.rounds.php | 133 + .../xaseco/plugins/plugin.rpoints.php | 262 + docker-xaseco/xaseco/plugins/plugin.style.php | 202 + .../xaseco/plugins/plugin.tmxinfo.php | 313 + docker-xaseco/xaseco/plugins/plugin.track.php | 184 + .../xaseco/plugins/plugin.uptodate.php | 147 + docker-xaseco/xaseco/rasp.xml | 81 + docker-xaseco/xaseco/styles/00README.txt | 24 + docker-xaseco/xaseco/styles/Black.xml | 31 + docker-xaseco/xaseco/styles/BlackBlur.xml | 31 + docker-xaseco/xaseco/styles/Blue.xml | 31 + docker-xaseco/xaseco/styles/BlueBlur.xml | 31 + docker-xaseco/xaseco/styles/Cyan.xml | 31 + docker-xaseco/xaseco/styles/CyanBlur.xml | 31 + docker-xaseco/xaseco/styles/DarkBlur.xml | 31 + docker-xaseco/xaseco/styles/DarkTransp.xml | 31 + docker-xaseco/xaseco/styles/Gray.xml | 31 + docker-xaseco/xaseco/styles/GrayBlur.xml | 31 + docker-xaseco/xaseco/styles/Green.xml | 31 + docker-xaseco/xaseco/styles/GreenBlur.xml | 31 + docker-xaseco/xaseco/styles/LightTransp.xml | 31 + docker-xaseco/xaseco/styles/LightTransp2.xml | 31 + docker-xaseco/xaseco/styles/LightTransp3.xml | 31 + docker-xaseco/xaseco/styles/Orange.xml | 31 + docker-xaseco/xaseco/styles/OrangeBlur.xml | 31 + docker-xaseco/xaseco/styles/ProgressBar.xml | 32 + docker-xaseco/xaseco/styles/WhiteCard.xml | 31 + docker-xaseco/xaseco/styles/WhiteRect.xml | 31 + docker-xaseco/xaseco/styles/WhiteRound.xml | 31 + docker-xaseco/xaseco/text.tpl | 11 + 590 files changed, 53526 insertions(+), 2 deletions(-) rename Dockerfile => docker-tmserver/Dockerfile (100%) rename entrypoint-tmserver.sh => docker-tmserver/entrypoint-tmserver.sh (100%) rename {tmserver => docker-tmserver/tmserver}/CommandLine.html (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Config/Default.SystemConfig.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Config/blacklist.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Config/checksum.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Config/config.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Config/guestlist.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Config/pkey.dat (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/DedicatedTrackMania.TrackMania.gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Game.FidCache.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/NationsList.xml (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Black/E01-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Black/E02-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Black/E03-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Black/E04-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Black/E05-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C01-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C02-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C03-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C04-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C05-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C06-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C07-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C08-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C09-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C10-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C11-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C12-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C13-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C14-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Blue/C15-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B01-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B02-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B03-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B04-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B05-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B06-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B07-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B08-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B09-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B10-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B11-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B12-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B13-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B14-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Green/B15-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D01-endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D02-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D03-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D04-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D05-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D06-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D07-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D08-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D09-obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D10-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D11-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D12-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D13-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D14-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/Red/D15-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A01-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A02-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A03-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A04-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A05-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A06-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A07-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A08-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A09-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A10-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A11-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A12-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A13-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A14-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/Nations/White/A15-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Black/PlatformE.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/White/PlatformA1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/White/PlatformA2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/White/PlatformA3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/White/PlatformA4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Platform/White/PlatformA5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Black/PuzzleE.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Black/BayE.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Black/CoastE.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Black/DesertE.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Black/IslandE.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Black/RallyE.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Black/SnowE.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Black/StadiumE.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntA1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntA2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntA3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntA4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntA5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntB1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntB2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntB3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntB4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntB5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntC1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntC2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntC3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntC4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntC5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntD1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntD2.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntD3.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntD4.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntD5.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Campaigns/United/Stunts/StuntE1.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A01-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A02-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A03-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A04-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A05-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A06-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A07-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A08-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A09-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A10-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A11-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A12-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A13-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A14-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/A15-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B01-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B02-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B03-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B04-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B05-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B06-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B07-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B08-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B09-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B10-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B11-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B12-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B13-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B14-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/B15-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C01-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C02-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C03-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C04-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C05-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C06-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C07-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C08-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C09-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C10-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C11-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C12-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C13-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C14-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/C15-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D01-endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D02-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D03-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D04-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D05-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D06-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D07-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D08-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D09-obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D10-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D11-Acrobatic.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D12-Speed.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D13-Race.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D14-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/D15-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/E01-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/E02-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/E03-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/E04-Obstacle.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/Challenges/Nadeo/E05-Endurance.Challenge.Gbx (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/Nations/NationsBlue.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/Nations/NationsGreen.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/Nations/NationsRed.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/Nations/NationsWhite.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/BayBlue.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/BayGreen.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/BayRed.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/BayWhite.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/CoastBlue.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/CoastGreen.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/CoastRed.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/CoastWhite.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/DesertBlue.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/DesertGreen.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/DesertRed.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/DesertWhite.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/IslandBlue.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/IslandGreen.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/IslandRed.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/IslandWhite.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/RallyBlue.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/RallyGreen.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/RallyRed.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/RallyWhite.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/SnowBlue.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/SnowGreen.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/SnowRed.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/SnowWhite.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/StadiumBlue.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/StadiumGreen.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/StadiumRed.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/United/StadiumWhite.txt (100%) rename {tmserver => docker-tmserver/tmserver}/GameData/Tracks/MatchSettings/playlist.txt (100%) rename {tmserver => docker-tmserver/tmserver}/ListCallbacks.html (100%) rename {tmserver => docker-tmserver/tmserver}/ListMethods.html (100%) rename {tmserver => docker-tmserver/tmserver}/Logs/ConsoleLog.1352.txt (100%) rename {tmserver => docker-tmserver/tmserver}/Readme_Dedicated.html (100%) rename {tmserver => docker-tmserver/tmserver}/RemoteControlExamples/PhpRemote/GbxRemote.bem.php (100%) rename {tmserver => docker-tmserver/tmserver}/RemoteControlExamples/PhpRemote/GbxRemote.inc.php (100%) rename {tmserver => docker-tmserver/tmserver}/RemoteControlExamples/PhpRemote/ListMethods.php (100%) rename {tmserver => docker-tmserver/tmserver}/RemoteControlExamples/PhpRemote/SpectatorUi.php (100%) rename {tmserver => docker-tmserver/tmserver}/RemoteControlExamples/PhpRemote/basic.php (100%) rename {tmserver => docker-tmserver/tmserver}/TrackmaniaServer (100%) rename {tmserver => docker-tmserver/tmserver}/bin/.xaseco_restart_pott.sh.swp (100%) rename {tmserver => docker-tmserver/tmserver}/bin/delete_cache.sh (100%) rename {tmserver => docker-tmserver/tmserver}/tmserver (100%) create mode 100644 docker-xaseco/Dockerfile create mode 100755 docker-xaseco/entrypoint-xaseco.sh create mode 100644 docker-xaseco/xaseco/Aseco.sh create mode 100644 docker-xaseco/xaseco/DOCS/Features_080.html create mode 100644 docker-xaseco/xaseco/DOCS/Features_095.html create mode 100644 docker-xaseco/xaseco/DOCS/Features_103.html create mode 100644 docker-xaseco/xaseco/DOCS/Features_116.html create mode 100644 docker-xaseco/xaseco/DOCS/ListCallbacksForever.html create mode 100644 docker-xaseco/xaseco/DOCS/ListCallbacksNations.html create mode 100644 docker-xaseco/xaseco/DOCS/ListDedimania.html create mode 100644 docker-xaseco/xaseco/DOCS/ListMethodsForever.html create mode 100644 docker-xaseco/xaseco/DOCS/ListMethodsNations.html create mode 100644 docker-xaseco/xaseco/DOCS/OLD/Jfreu install.txt create mode 100644 docker-xaseco/xaseco/DOCS/OLD/Jfreu's plugin.txt create mode 100644 docker-xaseco/xaseco/DOCS/OLD/README-autotime.txt create mode 100644 docker-xaseco/xaseco/DOCS/OLD/README.txt create mode 100644 docker-xaseco/xaseco/DOCS/OLD/ReadMe.pdf create mode 100644 docker-xaseco/xaseco/DOCS/OLD/plugin.sminfo.php create mode 100644 docker-xaseco/xaseco/DOCS/OLD/sminfofetcher.inc.php create mode 100644 docker-xaseco/xaseco/DOCS/admin_abilities.html create mode 100644 docker-xaseco/xaseco/DOCS/aseco_commands.doc create mode 100644 docker-xaseco/xaseco/DOCS/aseco_commands.html create mode 100644 docker-xaseco/xaseco/DOCS/index.html create mode 100644 docker-xaseco/xaseco/DOCS/overview.html create mode 100644 docker-xaseco/xaseco/DOCS/quickstart_tmf.html create mode 100644 docker-xaseco/xaseco/DOCS/quickstart_tmn.html create mode 100644 docker-xaseco/xaseco/DOCS/repairnations.php create mode 100644 docker-xaseco/xaseco/DOCS/repairrecs.php create mode 100644 docker-xaseco/xaseco/DOCS/sm_hub.html create mode 100644 docker-xaseco/xaseco/DOCS/tm2_hub.html create mode 100644 docker-xaseco/xaseco/DOCS/tmf_hub.html create mode 100644 docker-xaseco/xaseco/DOCS/updatepanels.php create mode 100644 docker-xaseco/xaseco/DOCS/upgrades.html create mode 100644 docker-xaseco/xaseco/access.xml create mode 100644 docker-xaseco/xaseco/adminops.xml create mode 100644 docker-xaseco/xaseco/aseco.log create mode 100644 docker-xaseco/xaseco/aseco.php create mode 100644 docker-xaseco/xaseco/autotime.xml create mode 100644 docker-xaseco/xaseco/bannedips.xml create mode 100644 docker-xaseco/xaseco/config.xml create mode 100644 docker-xaseco/xaseco/dedimania.xml create mode 100644 docker-xaseco/xaseco/html.tpl create mode 100644 docker-xaseco/xaseco/includes/GbxRemote.bem.php create mode 100644 docker-xaseco/xaseco/includes/GbxRemote.inc.php create mode 100644 docker-xaseco/xaseco/includes/GbxRemote.response.php create mode 100644 docker-xaseco/xaseco/includes/basic.inc.php create mode 100644 docker-xaseco/xaseco/includes/gbxchallinfo.inc.php create mode 100644 docker-xaseco/xaseco/includes/gbxdatafetcher.inc.php create mode 100644 docker-xaseco/xaseco/includes/jfreu.config.php create mode 100644 docker-xaseco/xaseco/includes/manialinks.inc.php create mode 100644 docker-xaseco/xaseco/includes/ogg_comments.inc.php create mode 100644 docker-xaseco/xaseco/includes/rasp.funcs.php create mode 100644 docker-xaseco/xaseco/includes/rasp.settings.php create mode 100644 docker-xaseco/xaseco/includes/tmndatafetcher.inc.php create mode 100644 docker-xaseco/xaseco/includes/tmxinfofetcher.inc.php create mode 100644 docker-xaseco/xaseco/includes/tmxinfosearcher.inc.php create mode 100644 docker-xaseco/xaseco/includes/types.inc.php create mode 100644 docker-xaseco/xaseco/includes/urlsafebase64.php create mode 100644 docker-xaseco/xaseco/includes/votes.config.php create mode 100644 docker-xaseco/xaseco/includes/web_access.inc.php create mode 100644 docker-xaseco/xaseco/includes/xmlparser.inc.php create mode 100644 docker-xaseco/xaseco/includes/xmlrpc_db.inc.php create mode 100644 docker-xaseco/xaseco/localdatabase.xml create mode 100644 docker-xaseco/xaseco/localdb/aseco.sql create mode 100644 docker-xaseco/xaseco/localdb/extra.sql create mode 100644 docker-xaseco/xaseco/localdb/rasp.sql create mode 100644 docker-xaseco/xaseco/logfile.txt create mode 100644 docker-xaseco/xaseco/matchsave.xml create mode 100644 docker-xaseco/xaseco/musicserver.xml create mode 100644 docker-xaseco/xaseco/panels/00README.txt create mode 100644 docker-xaseco/xaseco/panels/AdminAboveCPList.xml create mode 100644 docker-xaseco/xaseco/panels/AdminAboveCPListWide.xml create mode 100644 docker-xaseco/xaseco/panels/AdminAboveChat.xml create mode 100644 docker-xaseco/xaseco/panels/AdminAboveChatWide.xml create mode 100644 docker-xaseco/xaseco/panels/AdminAboveSpeed.xml create mode 100644 docker-xaseco/xaseco/panels/AdminAboveSpeed2.xml create mode 100644 docker-xaseco/xaseco/panels/AdminBelowChat.xml create mode 100644 docker-xaseco/xaseco/panels/AdminBottomCenter.xml create mode 100644 docker-xaseco/xaseco/panels/AdminBottomCenterWide.xml create mode 100644 docker-xaseco/xaseco/panels/AdminCallVote.xml create mode 100644 docker-xaseco/xaseco/panels/AdminCallVoteBlur.xml create mode 100644 docker-xaseco/xaseco/panels/AdminLeftEdge.xml create mode 100644 docker-xaseco/xaseco/panels/AdminRightEdge.xml create mode 100644 docker-xaseco/xaseco/panels/AdminTopCenter.xml create mode 100644 docker-xaseco/xaseco/panels/AdminTopCenterWide.xml create mode 100644 docker-xaseco/xaseco/panels/DonateBelowCPList.xml create mode 100644 docker-xaseco/xaseco/panels/DonateBelowCPListRM.xml create mode 100644 docker-xaseco/xaseco/panels/DonateLeftEdge.xml create mode 100644 docker-xaseco/xaseco/panels/DonateLeftEdge2.xml create mode 100644 docker-xaseco/xaseco/panels/DonateLeftSmall.xml create mode 100644 docker-xaseco/xaseco/panels/DonateRightEdge.xml create mode 100644 docker-xaseco/xaseco/panels/DonateRightEdge2.xml create mode 100644 docker-xaseco/xaseco/panels/DonateRightSmall.xml create mode 100644 docker-xaseco/xaseco/panels/DonateTopLeft.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsLeftBlackBold.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsLeftBlue.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsLeftBlueBold.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsLeftBlueLight.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsLeftGray.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsLeftGrayBold.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsLeftItalic.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsLeftOrange.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsLeftSize1.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsLeftSize1NoDedi.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsLeftSize2.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsLeftSize2NoDedi.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsLeftWhite.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsRightBottom.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsRightBottomNoDedi.xml create mode 100644 docker-xaseco/xaseco/panels/RecordsRightBottomRM.xml create mode 100644 docker-xaseco/xaseco/panels/StatsNations.xml create mode 100644 docker-xaseco/xaseco/panels/StatsUnited.xml create mode 100644 docker-xaseco/xaseco/panels/VoteBelowChat.xml create mode 100644 docker-xaseco/xaseco/panels/VoteBottomCenter.xml create mode 100644 docker-xaseco/xaseco/panels/VoteBottomCenterTransp.xml create mode 100644 docker-xaseco/xaseco/panels/VoteCallVote.xml create mode 100644 docker-xaseco/xaseco/panels/VoteTopCenter.xml create mode 100644 docker-xaseco/xaseco/plugins.xml create mode 100644 docker-xaseco/xaseco/plugins/chat.admin.php create mode 100644 docker-xaseco/xaseco/plugins/chat.dedimania.php create mode 100644 docker-xaseco/xaseco/plugins/chat.help.php create mode 100644 docker-xaseco/xaseco/plugins/chat.laston.php create mode 100644 docker-xaseco/xaseco/plugins/chat.lastwin.php create mode 100644 docker-xaseco/xaseco/plugins/chat.me.php create mode 100644 docker-xaseco/xaseco/plugins/chat.players.php create mode 100644 docker-xaseco/xaseco/plugins/chat.players2.php create mode 100644 docker-xaseco/xaseco/plugins/chat.records.php create mode 100644 docker-xaseco/xaseco/plugins/chat.records2.php create mode 100644 docker-xaseco/xaseco/plugins/chat.recrels.php create mode 100644 docker-xaseco/xaseco/plugins/chat.server.php create mode 100644 docker-xaseco/xaseco/plugins/chat.songmod.php create mode 100644 docker-xaseco/xaseco/plugins/chat.stats.php create mode 100644 docker-xaseco/xaseco/plugins/chat.wins.php create mode 100644 docker-xaseco/xaseco/plugins/jfreu.chat.php create mode 100644 docker-xaseco/xaseco/plugins/jfreu.lite.php create mode 100644 docker-xaseco/xaseco/plugins/jfreu.plugin.php create mode 100644 docker-xaseco/xaseco/plugins/jfreu/jfreu.bans.xml create mode 100644 docker-xaseco/xaseco/plugins/jfreu/jfreu.config.xml create mode 100644 docker-xaseco/xaseco/plugins/jfreu/jfreu.vips.xml create mode 100644 docker-xaseco/xaseco/plugins/mistral.idlekick.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.access.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.autotime.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.chatlog.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.checkpoints.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.dedimania.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.donate.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.localdatabase.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.matchsave.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.ml_howto.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.msglog.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.musicserver.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.muting.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.panels.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.rasp.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.rasp_chat.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.rasp_irc.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.rasp_jukebox.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.rasp_karma.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.rasp_nextmap.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.rasp_nextrank.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.rasp_votes.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.rounds.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.rpoints.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.style.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.tmxinfo.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.track.php create mode 100644 docker-xaseco/xaseco/plugins/plugin.uptodate.php create mode 100644 docker-xaseco/xaseco/rasp.xml create mode 100644 docker-xaseco/xaseco/styles/00README.txt create mode 100644 docker-xaseco/xaseco/styles/Black.xml create mode 100644 docker-xaseco/xaseco/styles/BlackBlur.xml create mode 100644 docker-xaseco/xaseco/styles/Blue.xml create mode 100644 docker-xaseco/xaseco/styles/BlueBlur.xml create mode 100644 docker-xaseco/xaseco/styles/Cyan.xml create mode 100644 docker-xaseco/xaseco/styles/CyanBlur.xml create mode 100644 docker-xaseco/xaseco/styles/DarkBlur.xml create mode 100644 docker-xaseco/xaseco/styles/DarkTransp.xml create mode 100644 docker-xaseco/xaseco/styles/Gray.xml create mode 100644 docker-xaseco/xaseco/styles/GrayBlur.xml create mode 100644 docker-xaseco/xaseco/styles/Green.xml create mode 100644 docker-xaseco/xaseco/styles/GreenBlur.xml create mode 100644 docker-xaseco/xaseco/styles/LightTransp.xml create mode 100644 docker-xaseco/xaseco/styles/LightTransp2.xml create mode 100644 docker-xaseco/xaseco/styles/LightTransp3.xml create mode 100644 docker-xaseco/xaseco/styles/Orange.xml create mode 100644 docker-xaseco/xaseco/styles/OrangeBlur.xml create mode 100644 docker-xaseco/xaseco/styles/ProgressBar.xml create mode 100644 docker-xaseco/xaseco/styles/WhiteCard.xml create mode 100644 docker-xaseco/xaseco/styles/WhiteRect.xml create mode 100644 docker-xaseco/xaseco/styles/WhiteRound.xml create mode 100644 docker-xaseco/xaseco/text.tpl diff --git a/docker-compose.yml b/docker-compose.yml index 5e6ad39..725ac22 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: ports: - "8080:80" tmserver: - build: . + build: ./docker-tmserver container_name: trackmania_tmserver depends_on: - db @@ -43,7 +43,7 @@ services: - "3450:3450" - "3450:3450/udp" xaseco: - build: ../xaseco + build: ./docker-xaseco container_name: trackmania_xaseco depends_on: - db diff --git a/Dockerfile b/docker-tmserver/Dockerfile similarity index 100% rename from Dockerfile rename to docker-tmserver/Dockerfile diff --git a/entrypoint-tmserver.sh b/docker-tmserver/entrypoint-tmserver.sh similarity index 100% rename from entrypoint-tmserver.sh rename to docker-tmserver/entrypoint-tmserver.sh diff --git a/tmserver/CommandLine.html b/docker-tmserver/tmserver/CommandLine.html similarity index 100% rename from tmserver/CommandLine.html rename to docker-tmserver/tmserver/CommandLine.html diff --git a/tmserver/GameData/Config/Default.SystemConfig.Gbx b/docker-tmserver/tmserver/GameData/Config/Default.SystemConfig.Gbx similarity index 100% rename from tmserver/GameData/Config/Default.SystemConfig.Gbx rename to docker-tmserver/tmserver/GameData/Config/Default.SystemConfig.Gbx diff --git a/tmserver/GameData/Config/blacklist.txt b/docker-tmserver/tmserver/GameData/Config/blacklist.txt similarity index 100% rename from tmserver/GameData/Config/blacklist.txt rename to docker-tmserver/tmserver/GameData/Config/blacklist.txt diff --git a/tmserver/GameData/Config/checksum.txt b/docker-tmserver/tmserver/GameData/Config/checksum.txt similarity index 100% rename from tmserver/GameData/Config/checksum.txt rename to docker-tmserver/tmserver/GameData/Config/checksum.txt diff --git a/tmserver/GameData/Config/config.txt b/docker-tmserver/tmserver/GameData/Config/config.txt similarity index 100% rename from tmserver/GameData/Config/config.txt rename to docker-tmserver/tmserver/GameData/Config/config.txt diff --git a/tmserver/GameData/Config/guestlist.txt b/docker-tmserver/tmserver/GameData/Config/guestlist.txt similarity index 100% rename from tmserver/GameData/Config/guestlist.txt rename to docker-tmserver/tmserver/GameData/Config/guestlist.txt diff --git a/tmserver/GameData/Config/pkey.dat b/docker-tmserver/tmserver/GameData/Config/pkey.dat similarity index 100% rename from tmserver/GameData/Config/pkey.dat rename to docker-tmserver/tmserver/GameData/Config/pkey.dat diff --git a/tmserver/GameData/DedicatedTrackMania.TrackMania.gbx b/docker-tmserver/tmserver/GameData/DedicatedTrackMania.TrackMania.gbx similarity index 100% rename from tmserver/GameData/DedicatedTrackMania.TrackMania.gbx rename to docker-tmserver/tmserver/GameData/DedicatedTrackMania.TrackMania.gbx diff --git a/tmserver/GameData/Game.FidCache.Gbx b/docker-tmserver/tmserver/GameData/Game.FidCache.Gbx similarity index 100% rename from tmserver/GameData/Game.FidCache.Gbx rename to docker-tmserver/tmserver/GameData/Game.FidCache.Gbx diff --git a/tmserver/GameData/NationsList.xml b/docker-tmserver/tmserver/GameData/NationsList.xml similarity index 100% rename from tmserver/GameData/NationsList.xml rename to docker-tmserver/tmserver/GameData/NationsList.xml diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Black/E01-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Black/E01-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Black/E01-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Black/E01-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Black/E02-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Black/E02-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Black/E02-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Black/E02-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Black/E03-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Black/E03-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Black/E03-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Black/E03-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Black/E04-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Black/E04-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Black/E04-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Black/E04-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Black/E05-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Black/E05-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Black/E05-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Black/E05-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C01-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C01-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C01-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C01-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C02-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C02-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C02-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C02-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C03-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C03-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C03-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C03-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C04-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C04-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C04-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C04-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C05-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C05-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C05-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C05-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C06-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C06-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C06-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C06-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C07-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C07-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C07-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C07-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C08-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C08-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C08-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C08-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C09-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C09-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C09-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C09-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C10-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C10-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C10-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C10-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C11-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C11-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C11-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C11-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C12-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C12-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C12-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C12-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C13-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C13-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C13-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C13-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C14-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C14-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C14-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C14-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C15-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C15-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Blue/C15-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Blue/C15-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B01-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B01-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B01-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B01-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B02-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B02-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B02-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B02-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B03-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B03-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B03-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B03-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B04-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B04-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B04-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B04-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B05-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B05-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B05-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B05-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B06-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B06-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B06-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B06-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B07-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B07-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B07-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B07-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B08-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B08-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B08-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B08-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B09-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B09-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B09-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B09-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B10-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B10-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B10-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B10-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B11-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B11-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B11-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B11-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B12-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B12-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B12-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B12-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B13-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B13-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B13-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B13-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B14-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B14-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B14-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B14-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Green/B15-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B15-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Green/B15-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Green/B15-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D01-endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D01-endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D01-endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D01-endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D02-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D02-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D02-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D02-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D03-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D03-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D03-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D03-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D04-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D04-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D04-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D04-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D05-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D05-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D05-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D05-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D06-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D06-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D06-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D06-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D07-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D07-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D07-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D07-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D08-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D08-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D08-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D08-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D09-obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D09-obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D09-obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D09-obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D10-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D10-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D10-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D10-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D11-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D11-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D11-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D11-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D12-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D12-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D12-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D12-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D13-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D13-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D13-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D13-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D14-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D14-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D14-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D14-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/Red/D15-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D15-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/Red/D15-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/Red/D15-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A01-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A01-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A01-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A01-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A02-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A02-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A02-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A02-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A03-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A03-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A03-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A03-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A04-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A04-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A04-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A04-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A05-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A05-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A05-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A05-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A06-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A06-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A06-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A06-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A07-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A07-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A07-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A07-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A08-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A08-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A08-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A08-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A09-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A09-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A09-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A09-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A10-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A10-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A10-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A10-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A11-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A11-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A11-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A11-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A12-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A12-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A12-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A12-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A13-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A13-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A13-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A13-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A14-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A14-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A14-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A14-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/Nations/White/A15-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A15-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/Nations/White/A15-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/Nations/White/A15-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Black/PlatformE.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Black/PlatformE.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Black/PlatformE.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Black/PlatformE.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Blue/PlatformC5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Green/PlatformB5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/Red/PlatformD5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Platform/White/PlatformA5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Black/PuzzleE.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Black/PuzzleE.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Black/PuzzleE.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Black/PuzzleE.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Blue/PuzzleC5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Green/PuzzleB5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/Red/PuzzleD5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Puzzle/White/PuzzleA5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Black/BayE.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Black/BayE.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Black/BayE.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Black/BayE.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Blue/BayC5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Green/BayB5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/Red/BayD5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Bay/White/BayA5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Black/CoastE.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Black/CoastE.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Black/CoastE.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Black/CoastE.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Blue/CoastC5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Green/CoastB5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/Red/CoastD5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Coast/White/CoastA5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Black/DesertE.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Black/DesertE.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Black/DesertE.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Black/DesertE.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Blue/DesertC5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Green/DesertB5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/Red/DesertD5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Desert/White/DesertA5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Black/IslandE.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Black/IslandE.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Black/IslandE.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Black/IslandE.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Blue/IslandC5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Green/IslandB5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/Red/IslandD5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Island/White/IslandA5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Black/RallyE.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Black/RallyE.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Black/RallyE.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Black/RallyE.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Blue/RallyC5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Green/RallyB5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/Red/RallyD5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Rally/White/RallyA5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Black/SnowE.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Black/SnowE.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Black/SnowE.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Black/SnowE.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Blue/SnowC5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Green/SnowB5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/Red/SnowD5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Snow/White/SnowA5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Black/StadiumE.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Black/StadiumE.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Black/StadiumE.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Black/StadiumE.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Blue/StadiumC5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Green/StadiumB5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/Red/StadiumD5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Race/Stadium/White/StadiumA5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntA5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntB5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntC5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD2.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD2.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD2.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD2.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD3.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD3.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD3.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD3.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD4.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD4.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD4.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD4.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD5.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD5.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD5.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntD5.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntE1.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntE1.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntE1.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Campaigns/United/Stunts/StuntE1.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A01-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A01-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A01-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A01-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A02-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A02-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A02-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A02-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A03-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A03-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A03-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A03-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A04-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A04-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A04-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A04-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A05-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A05-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A05-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A05-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A06-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A06-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A06-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A06-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A07-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A07-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A07-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A07-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A08-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A08-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A08-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A08-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A09-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A09-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A09-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A09-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A10-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A10-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A10-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A10-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A11-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A11-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A11-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A11-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A12-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A12-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A12-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A12-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A13-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A13-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A13-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A13-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A14-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A14-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A14-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A14-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/A15-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A15-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/A15-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/A15-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B01-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B01-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B01-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B01-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B02-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B02-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B02-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B02-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B03-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B03-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B03-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B03-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B04-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B04-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B04-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B04-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B05-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B05-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B05-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B05-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B06-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B06-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B06-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B06-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B07-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B07-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B07-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B07-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B08-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B08-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B08-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B08-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B09-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B09-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B09-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B09-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B10-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B10-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B10-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B10-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B11-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B11-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B11-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B11-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B12-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B12-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B12-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B12-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B13-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B13-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B13-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B13-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B14-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B14-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B14-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B14-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/B15-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B15-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/B15-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/B15-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C01-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C01-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C01-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C01-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C02-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C02-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C02-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C02-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C03-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C03-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C03-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C03-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C04-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C04-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C04-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C04-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C05-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C05-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C05-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C05-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C06-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C06-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C06-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C06-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C07-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C07-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C07-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C07-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C08-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C08-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C08-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C08-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C09-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C09-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C09-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C09-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C10-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C10-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C10-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C10-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C11-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C11-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C11-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C11-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C12-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C12-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C12-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C12-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C13-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C13-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C13-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C13-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C14-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C14-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C14-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C14-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/C15-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C15-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/C15-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/C15-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D01-endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D01-endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D01-endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D01-endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D02-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D02-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D02-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D02-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D03-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D03-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D03-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D03-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D04-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D04-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D04-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D04-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D05-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D05-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D05-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D05-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D06-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D06-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D06-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D06-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D07-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D07-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D07-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D07-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D08-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D08-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D08-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D08-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D09-obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D09-obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D09-obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D09-obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D10-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D10-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D10-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D10-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D11-Acrobatic.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D11-Acrobatic.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D11-Acrobatic.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D11-Acrobatic.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D12-Speed.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D12-Speed.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D12-Speed.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D12-Speed.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D13-Race.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D13-Race.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D13-Race.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D13-Race.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D14-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D14-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D14-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D14-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/D15-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D15-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/D15-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/D15-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/E01-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/E01-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/E01-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/E01-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/E02-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/E02-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/E02-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/E02-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/E03-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/E03-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/E03-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/E03-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/E04-Obstacle.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/E04-Obstacle.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/E04-Obstacle.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/E04-Obstacle.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/Challenges/Nadeo/E05-Endurance.Challenge.Gbx b/docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/E05-Endurance.Challenge.Gbx similarity index 100% rename from tmserver/GameData/Tracks/Challenges/Nadeo/E05-Endurance.Challenge.Gbx rename to docker-tmserver/tmserver/GameData/Tracks/Challenges/Nadeo/E05-Endurance.Challenge.Gbx diff --git a/tmserver/GameData/Tracks/MatchSettings/Nations/NationsBlue.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/Nations/NationsBlue.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/Nations/NationsBlue.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/Nations/NationsBlue.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/Nations/NationsGreen.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/Nations/NationsGreen.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/Nations/NationsGreen.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/Nations/NationsGreen.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/Nations/NationsRed.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/Nations/NationsRed.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/Nations/NationsRed.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/Nations/NationsRed.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/Nations/NationsWhite.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/Nations/NationsWhite.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/Nations/NationsWhite.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/Nations/NationsWhite.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/BayBlue.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/BayBlue.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/BayBlue.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/BayBlue.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/BayGreen.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/BayGreen.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/BayGreen.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/BayGreen.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/BayRed.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/BayRed.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/BayRed.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/BayRed.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/BayWhite.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/BayWhite.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/BayWhite.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/BayWhite.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/CoastBlue.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/CoastBlue.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/CoastBlue.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/CoastBlue.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/CoastGreen.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/CoastGreen.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/CoastGreen.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/CoastGreen.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/CoastRed.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/CoastRed.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/CoastRed.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/CoastRed.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/CoastWhite.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/CoastWhite.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/CoastWhite.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/CoastWhite.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/DesertBlue.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/DesertBlue.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/DesertBlue.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/DesertBlue.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/DesertGreen.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/DesertGreen.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/DesertGreen.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/DesertGreen.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/DesertRed.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/DesertRed.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/DesertRed.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/DesertRed.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/DesertWhite.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/DesertWhite.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/DesertWhite.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/DesertWhite.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/IslandBlue.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/IslandBlue.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/IslandBlue.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/IslandBlue.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/IslandGreen.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/IslandGreen.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/IslandGreen.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/IslandGreen.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/IslandRed.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/IslandRed.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/IslandRed.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/IslandRed.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/IslandWhite.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/IslandWhite.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/IslandWhite.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/IslandWhite.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/RallyBlue.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/RallyBlue.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/RallyBlue.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/RallyBlue.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/RallyGreen.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/RallyGreen.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/RallyGreen.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/RallyGreen.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/RallyRed.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/RallyRed.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/RallyRed.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/RallyRed.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/RallyWhite.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/RallyWhite.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/RallyWhite.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/RallyWhite.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/SnowBlue.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/SnowBlue.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/SnowBlue.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/SnowBlue.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/SnowGreen.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/SnowGreen.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/SnowGreen.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/SnowGreen.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/SnowRed.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/SnowRed.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/SnowRed.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/SnowRed.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/SnowWhite.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/SnowWhite.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/SnowWhite.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/SnowWhite.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/StadiumBlue.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/StadiumBlue.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/StadiumBlue.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/StadiumBlue.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/StadiumGreen.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/StadiumGreen.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/StadiumGreen.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/StadiumGreen.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/StadiumRed.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/StadiumRed.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/StadiumRed.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/StadiumRed.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/United/StadiumWhite.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/StadiumWhite.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/United/StadiumWhite.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/United/StadiumWhite.txt diff --git a/tmserver/GameData/Tracks/MatchSettings/playlist.txt b/docker-tmserver/tmserver/GameData/Tracks/MatchSettings/playlist.txt similarity index 100% rename from tmserver/GameData/Tracks/MatchSettings/playlist.txt rename to docker-tmserver/tmserver/GameData/Tracks/MatchSettings/playlist.txt diff --git a/tmserver/ListCallbacks.html b/docker-tmserver/tmserver/ListCallbacks.html similarity index 100% rename from tmserver/ListCallbacks.html rename to docker-tmserver/tmserver/ListCallbacks.html diff --git a/tmserver/ListMethods.html b/docker-tmserver/tmserver/ListMethods.html similarity index 100% rename from tmserver/ListMethods.html rename to docker-tmserver/tmserver/ListMethods.html diff --git a/tmserver/Logs/ConsoleLog.1352.txt b/docker-tmserver/tmserver/Logs/ConsoleLog.1352.txt similarity index 100% rename from tmserver/Logs/ConsoleLog.1352.txt rename to docker-tmserver/tmserver/Logs/ConsoleLog.1352.txt diff --git a/tmserver/Readme_Dedicated.html b/docker-tmserver/tmserver/Readme_Dedicated.html similarity index 100% rename from tmserver/Readme_Dedicated.html rename to docker-tmserver/tmserver/Readme_Dedicated.html diff --git a/tmserver/RemoteControlExamples/PhpRemote/GbxRemote.bem.php b/docker-tmserver/tmserver/RemoteControlExamples/PhpRemote/GbxRemote.bem.php similarity index 100% rename from tmserver/RemoteControlExamples/PhpRemote/GbxRemote.bem.php rename to docker-tmserver/tmserver/RemoteControlExamples/PhpRemote/GbxRemote.bem.php diff --git a/tmserver/RemoteControlExamples/PhpRemote/GbxRemote.inc.php b/docker-tmserver/tmserver/RemoteControlExamples/PhpRemote/GbxRemote.inc.php similarity index 100% rename from tmserver/RemoteControlExamples/PhpRemote/GbxRemote.inc.php rename to docker-tmserver/tmserver/RemoteControlExamples/PhpRemote/GbxRemote.inc.php diff --git a/tmserver/RemoteControlExamples/PhpRemote/ListMethods.php b/docker-tmserver/tmserver/RemoteControlExamples/PhpRemote/ListMethods.php similarity index 100% rename from tmserver/RemoteControlExamples/PhpRemote/ListMethods.php rename to docker-tmserver/tmserver/RemoteControlExamples/PhpRemote/ListMethods.php diff --git a/tmserver/RemoteControlExamples/PhpRemote/SpectatorUi.php b/docker-tmserver/tmserver/RemoteControlExamples/PhpRemote/SpectatorUi.php similarity index 100% rename from tmserver/RemoteControlExamples/PhpRemote/SpectatorUi.php rename to docker-tmserver/tmserver/RemoteControlExamples/PhpRemote/SpectatorUi.php diff --git a/tmserver/RemoteControlExamples/PhpRemote/basic.php b/docker-tmserver/tmserver/RemoteControlExamples/PhpRemote/basic.php similarity index 100% rename from tmserver/RemoteControlExamples/PhpRemote/basic.php rename to docker-tmserver/tmserver/RemoteControlExamples/PhpRemote/basic.php diff --git a/tmserver/TrackmaniaServer b/docker-tmserver/tmserver/TrackmaniaServer similarity index 100% rename from tmserver/TrackmaniaServer rename to docker-tmserver/tmserver/TrackmaniaServer diff --git a/tmserver/bin/.xaseco_restart_pott.sh.swp b/docker-tmserver/tmserver/bin/.xaseco_restart_pott.sh.swp similarity index 100% rename from tmserver/bin/.xaseco_restart_pott.sh.swp rename to docker-tmserver/tmserver/bin/.xaseco_restart_pott.sh.swp diff --git a/tmserver/bin/delete_cache.sh b/docker-tmserver/tmserver/bin/delete_cache.sh similarity index 100% rename from tmserver/bin/delete_cache.sh rename to docker-tmserver/tmserver/bin/delete_cache.sh diff --git a/tmserver/tmserver b/docker-tmserver/tmserver/tmserver similarity index 100% rename from tmserver/tmserver rename to docker-tmserver/tmserver/tmserver diff --git a/docker-xaseco/Dockerfile b/docker-xaseco/Dockerfile new file mode 100644 index 0000000..e846907 --- /dev/null +++ b/docker-xaseco/Dockerfile @@ -0,0 +1,17 @@ +FROM fanyx/php + +RUN mkdir /opt/xaseco + +COPY xaseco/ /opt/xaseco/ +COPY ./entrypoint-xaseco.sh / + +RUN apt update \ + && apt upgrade \ +RUN groupadd trackmania +RUN useradd -M -g trackmania trackmania +RUN chown -R trackmania:trackmania /opt/xaseco +RUN chown trackmania:trackmania /entrypoint-xaseco.sh + +USER trackmania +WORKDIR /opt/xaseco +CMD["/entrypoint-xaseco.sh"] diff --git a/docker-xaseco/entrypoint-xaseco.sh b/docker-xaseco/entrypoint-xaseco.sh new file mode 100755 index 0000000..f6502c7 --- /dev/null +++ b/docker-xaseco/entrypoint-xaseco.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# + +set -e + +if [[ -e /etc/xaseco/env ]] +then + . /etc/tmserver/env +fi + +# Evaluate the available environment variables +if [[ -z "${MASTERADMIN_LOGIN}" ]] +then + echo "No ingame MasterAdmin was specified." +fi +if [[ -z "${SERVER_SA_PASSWORD}" ]] +then + echo "No SuperAdmin password was specified. Xaseco cannot build a connection without this information." + exit 9 +fi +if [[ -z "${DB_HOST}" ]] +then + echo "No database host was specified. Defaulting to 'db' for docker-compose configuration." + DB_HOST="db" +fi +if [[ -z "${DB_LOGIN}" ]] +then + echo "No database user was specified. Defaulting to 'trackmania' for docker-compose configuration." + DB_LOGIN="trackmania" +fi +if [[ -z "${DB_LOGIN_PASSWORD}" ]] +then + echo "No database user password was specified. Please configure." + echo "The database connection cannot be established otherwise." +fi +if [[ -z "${DB_NAME}" ]] +then + echo "No database was specified. Defaulting to 'trackmania' for docker-compose configuration." + DB_NAME="trackmania" +fi + +#Evaluation over +#Commencing substition in config files + +#Xaseco Files + +sed -i -e "s/--\$MASTERADMIN_LOGIN--/$MASTERADMIN_LOGIN/" \ + -e "/--\$SERVER_SA_PASSWORD--/$SERVER_SA_PASSWORD/" \ + /opt/xaseco/config.xml +sed -i -e "s/--\$DB_HOST--/$DB_HOST/" \ + -e "s/--\$DB_LOGIN--/$DB_LOGIN/" \ + -e "s/--\$DB_LOGIN_PASSWORD--/$DB_LOGIN_PASSWORD/" \ + -e "s/--\$DB_NAME--/$DB_NAME/" \ + /opt/xaseco/localdatabase.xml + +exec "php" "opt/xaseco/aseco.php" "TMNF" "aseco.log" "2>&1" diff --git a/docker-xaseco/xaseco/Aseco.sh b/docker-xaseco/xaseco/Aseco.sh new file mode 100644 index 0000000..ce78be1 --- /dev/null +++ b/docker-xaseco/xaseco/Aseco.sh @@ -0,0 +1,3 @@ +#!/bin/sh +php aseco.php TMN aseco.log 2>&1 & +echo $! diff --git a/docker-xaseco/xaseco/DOCS/Features_080.html b/docker-xaseco/xaseco/DOCS/Features_080.html new file mode 100644 index 0000000..b66204d --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/Features_080.html @@ -0,0 +1,208 @@ + + + +TrackMania Nations - ASECO/RASP new features v0.8 + + + + + + + + + + +

+TrackMania Nations +


+ +

New features and other changes in the v0.8 release of ASECO/RASP:

+ + + +

Bug fixes in the v0.8 release of ASECO/RASP:

+ + + +
+
+Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 24-Oct-2007 +
+ + diff --git a/docker-xaseco/xaseco/DOCS/Features_095.html b/docker-xaseco/xaseco/DOCS/Features_095.html new file mode 100644 index 0000000..fd422d5 --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/Features_095.html @@ -0,0 +1,890 @@ + + + +TrackMania Nations - ASECO/RASP release notes v0.95 + + + + + + + + + + +

+TrackMania Nations +


+ +

New features and other changes in the v0.95 release of ASECO/RASP:

+ + + +

Bug fixes in the v0.95 release of ASECO/RASP:

+ + + +
+ +

New features and other changes in the v0.93 release of ASECO/RASP:

+ + + +

Bug fixes in the v0.93 release of ASECO/RASP:

+ + + +
+ +

New features and other changes in the v0.92b release of ASECO/RASP:

+ + + +

Bug fixes in the v0.92b release of ASECO/RASP:

+ + + +
+ +

New features and other changes in the v0.92 release of ASECO/RASP:

+ + + +

Bug fixes in the v0.92 release of ASECO/RASP:

+ + + +
+ +

New features and other changes in the v0.91 release of ASECO/RASP:

+ + + +

Bug fixes in the v0.91 release of ASECO/RASP:

+ + + +
+ +

New features and other changes in the v0.90 release of ASECO/RASP:

+ + + +

Bug fixes in the v0.90 release of ASECO/RASP:

+ + + +
+ +

New features and other changes in the v0.89 release of ASECO/RASP:

+ + + +

Bug fixes in the v0.89 release of ASECO/RASP:

+ + + +
+ +

New features and other changes in the v0.88 release of ASECO/RASP:

+ + + +

Bug fixes in the v0.88 release of ASECO/RASP:

+ + + +
+ +

New features and other changes in the v0.86 release of ASECO/RASP:

+ + + +

Bug fixes in the v0.86 release of ASECO/RASP:

+ + + +
+ +

New features and other changes in the v0.85 release of ASECO/RASP:

+ + + +

Bug fixes in the v0.85 release of ASECO/RASP:

+ + + +
+ +

New features and other changes in the v0.84 release of ASECO/RASP:

+ + + +

Bug fixes in the v0.84 release of ASECO/RASP:

+ + + +
+ +

New features and other changes in the v0.82 release of ASECO/RASP:

+ + + +

Bug fixes in the v0.82 release of ASECO/RASP:

+ + + +
+ +

New features and other changes in the v0.81 release of ASECO/RASP:

+ + + +

Bug fixes in the v0.81 release of ASECO/RASP:

+ + + +
+ +

Known problems in the v0.8+ releases of ASECO/RASP:

+ + + +
+ +

Initial release notes

+ +
+
+Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 27-Mar-2008 +
+ + diff --git a/docker-xaseco/xaseco/DOCS/Features_103.html b/docker-xaseco/xaseco/DOCS/Features_103.html new file mode 100644 index 0000000..5dd0bca --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/Features_103.html @@ -0,0 +1,913 @@ + + + +TrackMania Nations - XASECO release notes v1.03 + + + + + + + + + + +

+TrackMania Nations +


+ +

New features and other changes in the v1.03 release of XASECO:

+ + + +

Bug fixes in the v1.03 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.02 release of XASECO:

+ + + +

Bug fixes in the v1.02 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.01 release of XASECO:

+ + + +

Bug fixes in the v1.01 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.00 release of XASECO:

+ + + +

Bug fixes in the v1.00 release of XASECO:

+ + + +
+ +

New features and other changes in the v0.99b release of XASECO:

+ + + +

Bug fixes in the v0.99b release of XASECO:

+ + + +
+ +

New features and other changes in the v0.99 release of XASECO:

+ + + +

Bug fixes in the v0.99 release of XASECO:

+ + + +
+ +

New features and other changes in the v0.98 release of XASECO:

+ + + +

Bug fixes in the v0.98 release of XASECO:

+ + + +
+ +

New features and other changes in the v0.97 release of XASECO:

+ + + +

Bug fixes in the v0.97 release of XASECO:

+ + + +
+ +

New features and other changes in the v0.96b release of XASECO:

+ + + +

Bug fixes in the v0.96b release of XASECO:

+ + + +
+ +

New features and other changes in the v0.96 release of XASECO:

+ + + +

Bug fixes in the v0.96 release of XASECO:

+ + + +
+ +

Known issues in the v0.96+ release of XASECO:

+ + + +
+ +

Older release notes

+ +
+
+Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 19-Aug-2008 +
+ + diff --git a/docker-xaseco/xaseco/DOCS/Features_116.html b/docker-xaseco/xaseco/DOCS/Features_116.html new file mode 100644 index 0000000..e7ae48d --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/Features_116.html @@ -0,0 +1,895 @@ + + + +TrackMania Nations - XASECO release notes v1.16 + + + + + + + + + + +

+TrackMania Nations +


+ +

New features and other changes in the v1.16 release of XASECO:

+ + + +

Bug fixes in the v1.16 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.15b release of XASECO:

+ + + +

Bug fixes in the v1.15b release of XASECO:

+ + + +
+ +

New features and other changes in the v1.15 release of XASECO:

+ + + +

Bug fixes in the v1.15 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.14 release of XASECO:

+ + + +

Bug fixes in the v1.14 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.13 release of XASECO:

+ + + +

Bug fixes in the v1.13 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.12 release of XASECO:

+ + + +

Bug fixes in the v1.12 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.11 release of XASECO:

+ + + +

Bug fixes in the v1.11 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.10 release of XASECO:

+ + + +

Bug fixes in the v1.10 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.09 release of XASECO:

+ + + +

Bug fixes in the v1.09 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.08 release of XASECO:

+ + + +

Bug fixes in the v1.08 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.06 release of XASECO:

+ + + +

Bug fixes in the v1.06 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.05b release of XASECO:

+ + + +

Bug fixes in the v1.05b release of XASECO:

+ + + +
+ +

New features and other changes in the v1.05 release of XASECO:

+ + + +

Bug fixes in the v1.05 release of XASECO:

+ + + +
+ +

New features and other changes in the v1.04 release of XASECO:

+ + + +

Bug fixes in the v1.04 release of XASECO:

+ + + +
+ +

Older release notes

+ +
+
+Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 26-Jul-2013 +
+ + diff --git a/docker-xaseco/xaseco/DOCS/ListCallbacksForever.html b/docker-xaseco/xaseco/DOCS/ListCallbacksForever.html new file mode 100644 index 0000000..973767f --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/ListCallbacksForever.html @@ -0,0 +1,149 @@ + + + + +TrackMania Forever callbacks + + +

Available callbacks:

+ + + diff --git a/docker-xaseco/xaseco/DOCS/ListCallbacksNations.html b/docker-xaseco/xaseco/DOCS/ListCallbacksNations.html new file mode 100644 index 0000000..b6d43c6 --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/ListCallbacksNations.html @@ -0,0 +1,88 @@ + + + + +TrackMania Nations callbacks + + +

Available callbacks:

+ + + diff --git a/docker-xaseco/xaseco/DOCS/ListDedimania.html b/docker-xaseco/xaseco/DOCS/ListDedimania.html new file mode 100644 index 0000000..7e27934 --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/ListDedimania.html @@ -0,0 +1,189 @@ + + + +Dedimania Server Help Page + + + +

Dedimania Server Help Page

+ +

Site & Statistics:

+ + +

Usage:

+ +
Where to send queries ? + +Note that later the main url will reply only to dedimania.CheckConnection, dedimania.GetVersion, dedimania.Authenticate and dedimania.ValidateAccount methods ! +

+ + +
How to send a query ? + + + + +
+
The form of xmlrpc queries and replies + + + + +
+
What kind of records does Dedimania support ? + + + + +
+
What kind of records does Dedimania not support ? + + + + +
+
Sending xmlrpc request methods + + + + +
+
Raw query/reply examples (without http compression of course) + + + + +
+Note1:
+If you make a client script support, please notify it to Slig. First because I may open a new url port for your script, because the port 80 url use more resources, also because you should not use url used by other without telling it, and finally because i want to know what scripts are using Dedimania resources and on what port(s). +

+Note2:
+The url http://dedimania.net/RPC4/server.php must never be used, except eventually for testing or rescue (for users who have a server which can't use 80xx ports). To know what url you have to use, see note 1 ! +

+Note3: MaxRank
+The MaxRank work at several levels. Except special case where the player MaxRank is 0 because he is banned, a player +can always make a record on a server and challenge at the max rank determined by the max of ServerMaxRecords, his own MaxRank, and the MaxRank stored for his current record if he has one. +
- ServerMaxRecords is the server MaxRank, any player connected and not banned can make a record at least up to this value. +
- dedimania.PlayerArrive/MaxRank is the player MaxRank, the player can make a record up to this value also if ServerMaxRecords is smaller. +
- records MaxRank is the max of current player record and his general MaxRank, the record remain valid if not above that value (or ServerMaxRecords value if bigger). +

+ +

Available methods:

+
  • dedimania.CheckConnection
  • boolean + dedimania.CheckConnection()
    +Just reply true.
    +
  • dedimania.GetVersion
  • struct + dedimania.GetVersion()
    +Reply a struct {'Version': int, 'MaxRecords': int}.
    +
  • dedimania.Authenticate
  • boolean + dedimania.Authenticate(struct)
    +Allow user authentication by specifying a struct {'Game': string, 'Login': string, 'Password': string, 'Tool': string, 'Version': string, [Optionals: 'Packmask': string, 'Nation': string, 'ServerIP': string, 'ServerPort': int, 'XmlrpcPort': int, 'PlayersGame': boolean]}. Game can be 'TMF', 'TMUF', 'TMNF', 'TMU', 'TMO', 'TMS' or 'TMN' (for TMUF/TMNF servers, please send TMF or TMUF or TMNF, and the Packmask !). Packmask should be the value returned by GetServerPackMask. Nation can be 3 letters nation or TMF Path. If PlayersGame is set to true then a few methods will return more infos, mainly the game associated to the player login in records, and the player max rank (see general notes). Note: in case of error it returns a string with the error description.
    +
  • dedimania.ValidateAccount
  • struct + dedimania.ValidateAccount()
    +Reply a struct {'Status': boolean, 'Messages': array of struct {'Date': string, 'Text': string} }. Status is always true, else you get a not authenticated error.
    +Only if authenticated.

    +
  • dedimania.PlayerArrive
  • struct + dedimania.PlayerArrive(string, string, string, string, string, int, boolean, boolean)
    +Announce that a new player has arrived. Arguments are (Game, Login, Nickname, Nation, TeamName, LadderRanking, IsSpec, IsOff). Game can be 'TMF', 'TMUF', 'TMNF', 'TMU', 'TMO', 'TMS' or 'TMN'.
    +Reply a struct {['Game': string,] 'Login': string, 'TeamName': string, 'Nation': string, ['MaxRank': int, 'Status': int,] 'Options': array of struct {'Option': string, 'Value': string, 'Tool': string}, 'Aliases': array of struct {'Alias': string, 'Text': string, 'Tool': string} }. Game, MaxRank and Status are added only if PlayersGame is set in authenticate, Status bit 0 is 1 if banned (is which case MaxRank is 0). Note that MaxRank is the real max rank for the player (see general notes).
    +Only if authenticated.

    +
  • dedimania.PlayerLeave
  • struct + dedimania.PlayerLeave(string, string)
    +Announce that a player has left. Arguments are (Game,Login). Game can be 'TMF', 'TMUF', 'TMNF', 'TMU', 'TMO', 'TMS' or 'TMN'.
    +Reply a struct {'Login': string}.
    +Only if authenticated.

    +
  • dedimania.CurrentChallenge
  • struct + dedimania.CurrentChallenge(string, string, string, string, string, int, struct, int, array)
    +Set current challenge info and get records. Arguments are (Uid, Name, Environment, Author, Game, Mode, SrvInfo, MaxGetTimes, Players). Game is currently 'TMF', 'TMUF', 'TMNF', 'TMU', 'TMO', 'TMS' or 'TMN'. SrvInfo is a struct {'SrvName': string, 'Comment': string, 'Private': boolean, 'SrvIP': string, 'SrvPort': int, 'XmlrpcPort': int, 'NumPlayers': int, 'MaxPlayers': int, 'NumSpecs': int, 'MaxSpecs': int, 'LadderMode': int, 'NextFiveUID': string of next five uid separated with '/'}. Players is an array of struct {'Login': string, 'Nation': string, 'TeamName': string, 'TeamId': int, 'IsSpec': boolean, 'Ranking': int, 'IsOff': boolean}.
    +Reply a struct {'Uid': string, 'TotalRaces': int, 'TotalPlayers': int, 'TimeAttackRaces': int, 'TimeAttackPlayers': int, 'NumberOfChecks': int, 'ServerMaxRecords': int, 'Records': array of struct {['Game': string,] 'Login': string, 'NickName': string, 'Best': int, 'Rank': int, ['MaxRank': int,] 'Checks': array of int, 'Vote': int} }, NumberOfChecks per lap is 0 if unknown, Checks are the bestchecks of the associated record, Vote is 0 to 100 value, or -1 if player did not vote for the map. Game and MaxRank are added only if PlayersGame is set in authenticate (see general notes).
    +Only if authenticated.

    +
  • dedimania.ChallengeRaceTimes
  • struct + dedimania.ChallengeRaceTimes(string, string, string, string, string, int, int, int, array)
    +Set current challenge info and players' best times, and get the updated records. Arguments are (Uid, Name, Environment, Author, Game, Mode, NumberOfChecks, MaxGetTimes, Times). Game is currently 'TMF', 'TMUF', 'TMNF', 'TMU', 'TMO', 'TMS' or 'TMN'. Times is a sorted (by 'Best') array of struct {'Login': string, 'Best': int, 'Checks': array of int}. Checks are BestCheckpoints array of the best time of player (can also be sent as a comma separated list of int in a string, which is far smaller in xmlrpc). In case of time equality the order in the Times array is used.
    +Reply a struct {'Uid': string, 'TotalRaces': int, 'TotalPlayers': int, 'TimeAttackRaces': int, 'TimeAttackPlayers': int, 'NumberOfChecks': int, 'ServerMaxRecords': int, 'Records': array of struct {['Game': string,] 'Login': string, 'NickName': string, 'Best': int, 'Rank': int, ['MaxRank': int,] 'Checks': array of int, 'NewBest': boolean} }, NumberOfChecks per lap is 0 if unknown. Game and MaxRank are added only if PlayersGame is set in authenticate (see general notes).
    +Only if authenticated.

    +
  • dedimania.UpdateServerPlayers
  • boolean + dedimania.UpdateServerPlayers(string, int, struct, array)
    +Set current challenge and players info. Arguments are (Game, Mode, SrvInfo, Players). Game is currently 'TMF', 'TMUF', 'TMNF', 'TMU', 'TMO', 'TMS' or 'TMN'. SrvInfo is a struct {'SrvName': string, 'Comment': string, 'Private': boolean, 'SrvIP': string, 'SrvPort': int, 'XmlrpcPort': int, 'NumPlayers': int, 'MaxPlayers': int, 'NumSpecs': int, 'MaxSpecs': int, 'LadderMode': boolean, 'NextFiveUID': string of next five uid separated with '/'}. Players is an array of struct {'Login': string, 'Nation': string, 'TeamName': string, 'TeamId': int, 'IsSpec': boolean, 'Ranking': int, 'IsOff': boolean}.
    +Reply true.
    +Should be used every 4 minutes if no 'dedimania.CurrentChallenge' or 'dedimania.ChallengeRaceTimes' has been called, to keep the server and players 'On'.
    +Only if authenticated.

    +
  • dedimania.WarningsAndTTR
  • struct + dedimania.WarningsAndTTR()
    +Get warnings messages and TimeToRespond for all previous methods.
    +Reply a struct {'globalTTR': int, 'methods': array of struct {'methodName': string, 'errors': string, 'TTR': int}}.

    +
  • system.listMethods
  • array + system.listMethods()
    +This method lists all the methods that the XML-RPC server knows how to dispatch.
    +
  • system.methodHelp
  • string + system.methodHelp(string)
    +Returns help text if defined for the method passed, otherwise returns an empty string.
    +
  • system.methodSignature
  • array + system.methodSignature(string)
    +Returns an array of known signatures (an array of arrays) for the method name passed. If no signatures are known, returns a none-array (test for type != array to detect missing signature).
    +
  • system.multicall
  • array + system.multicall(array)
    +Boxcar multiple RPC calls in one request. See http://www.xmlrpc.com/discuss/msgReader$1208 and raw examples for details. Each array entry is a struct {'methodCall':string, 'params':array}.
    +
    + + diff --git a/docker-xaseco/xaseco/DOCS/ListMethodsForever.html b/docker-xaseco/xaseco/DOCS/ListMethodsForever.html new file mode 100644 index 0000000..d900c03 --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/ListMethodsForever.html @@ -0,0 +1,717 @@ + + + + +TrackMania Forever methods + + +

    Available methods:

    + + + diff --git a/docker-xaseco/xaseco/DOCS/ListMethodsNations.html b/docker-xaseco/xaseco/DOCS/ListMethodsNations.html new file mode 100644 index 0000000..eb95e8d --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/ListMethodsNations.html @@ -0,0 +1,432 @@ + + + + +TrackMania Nations methods + + +

    Available methods:

    + + + diff --git a/docker-xaseco/xaseco/DOCS/OLD/Jfreu install.txt b/docker-xaseco/xaseco/DOCS/OLD/Jfreu install.txt new file mode 100644 index 0000000..677cf5a --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/OLD/Jfreu install.txt @@ -0,0 +1,24 @@ +0/ Save "aseco/plugins.xml" somewhere + +1/ Copy "jfreu.config.php", "jfreu.plugin.php" , "chat.jfreu.php" + the "jfreu" directory in your aseco plugins directory (aseco/plugins/) + +2/ Add "jfreu.plugin.php" before "" + in "plugins.xml" in your aseco directory (aseco/plugins.xml) + +3/ Edit "jfreu.config.php" in your aseco plugins directory (aseco/plugins/) + change the server name, ranklimit,...(customise the plugin ^^) + +4/ Edit "jfreu/jfreu.lists.xml" and add VIP, team_vip & admins + +5/ Run Aseco and enjoy. ("/help" & "/jfreu help" for new commands avilable) + New admin commands are : "/jfreu [command]" (ex: "/jfreu novote ON") + + +Any problem or suggestion -> http://jfreu.servegame.com (forum) + + +?/ To uninstall the plugin delete "jfreu.config.php", "jfreu.plugin.php", + "chat.jfreu.php" and the "jfreu" directory in your aseco plugins directory + (aseco/plugins/) and overwrite modified file with your saved one + ("aseco/plugins.xml") or remove the "jfreu.plugin.php" line. diff --git a/docker-xaseco/xaseco/DOCS/OLD/Jfreu's plugin.txt b/docker-xaseco/xaseco/DOCS/OLD/Jfreu's plugin.txt new file mode 100644 index 0000000..2326af8 --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/OLD/Jfreu's plugin.txt @@ -0,0 +1,121 @@ +~~[ v0.13d ]~~ + +- Add : "/jfreu banfor X login" to ban a player for X min +- Fix : banFor X min ban for X min (not for X min - 30 sec) +- Fix : bug with non-included chat & settings files +- Upg : unspec command cancel the player's vote to unspec him. +- Upg : "unspec vote" ban the player for 5 min if vote result is NO. +- Upg : badwordsban ban the player for 10 min. +- Upg : in "unspec vote" +50% yes -> yes | +49% no -> no. + +~~[ v0.13c ]~~ + +- Fix : "/jfreu kickHiRank ON/OFF" fixed : kick worst ranked players + when the server is "full". +- Fix : "/jfreu kickWorst X" fixed : kick X wost players. + +~~[ v0.13b ]~~ + +- Add : the word between the server's name and the limit + (usually "Top") can be changed. +- Add : autoChangeName can be turned OFF + (server's name when the limit changes) +- Add : TEAM_VIP all team members are vip. +- Add : "/uptodate" command to know if the version of the + plugin is up to date. +- Add : NewPlayer message when a new player joins and ranklimit + is OFF. +- Add : "/jfreu removevip" & "/jfreu removeteamvip" to remove + a vip & a team_vip ingame (by an admin). +- Upg : Admins, VIP and TEAM_VIP added ingame are saved in a + xml file ("jfreu/jfreu.lists.xlm") +- Upg : All the plugin's admin commands are "jfreu commands" now, + to avoid the bug with "/admin help", now it is "/jfreu help" + the plugin's admin commands are "/jfreu [command]" +- Upg : Cancel custom vote ('unspec') when the player disconects. + +~~[ v0.13a ]~~ + +- Add : Admin command "addadmin {login}" to add an admin. +- Add : Admin private message to login : + /admin messagetologin {login} {message} +- Add : Hardlimit : players, VIP, specs over the hardlimit + are kicked (without message in the chat) +- Add : Admins can change the hardlimit ingame. +- Add : SpecOnly command unspec to launch an unspec vote. +- Add : If a player leaves the game his vote is canceled. +- Fix : novote can be turned OFF. +- Fix : fix bug in "clean_nick" function. +- Fix : No more timeOut crash +- Fix : PM fixed (no more server's login at the begining) +- Upg : badWord bot more powerfull. + ( "$zS$wHh$00FIII$wiiI$nT" is a badword ) +- Upg : Colors in the plugin are aseco's colors. +- Upg : Ranklimit & Info commands send a PM to the player. +- Upg : Unspec and VIP not in server average if autorankVIP ON. + + +~~[ v0.12d ]~~ + +- Add : Admin command "unspec" to allow a "SpecOnly" to join + the race. +- Add : custom maxBadWords +- Add : BadWordsBan (ON/OFF) ban when 2x maxBadWords +- Add : Admin command KickWorst to kick worst ranked players + except vip. +- Add : admin cancelvote to cancel current vote + (kick/ban/nextmap/restartmap) +- Add : novote (ON/OFF) to autocancel votes +- Upg : kickhirank function use KickWorst function. + +~~[ v0.12c ]~~ + +- Add : SpecOnly are kicked if they join the race +- Fix : bug with "admin help" +- Fix : bug with "autorankvip" +- Fix : bug with "autorankvip" default state + +~~[ v0.12b ]~~ + +- Add : "BadWords" -> 3 "badwords" = kicked (on/off) +- Add : random information message at the end of the race + (editable) (on/off) +- Add : players over ranklimit can join the game as spec +- Add : kick hiRank players if server is "full" + (custom maxplayers) (on/off) +- Fix : problem with "chat.admin" overwritten + +~~[ v0.11a ]~~ + +- Add : autoRankMinPlayer : autorank disabled when not enough + players then limit = Ranklimit. + autoRankMinPlayer value can be changed ingame (by admins) + Autorank limit = Ranklimit when not enough players +- Add : admin can add VIP ingame (deleted when aseco restarts) +- Add : admin can force Autorank limit (until a new player comes) + +~~[ v0.10b ]~~ + +- Add : admin command : /admin player {login} {message} +- Fix : bug with AutoRank Offset. +- Fix : bug timeOut crash. + +~~[ v0.09e ]~~ + +- Add : RankLimit can be turned on/off ingame (by an admin) +- Add : AutoRank offset value can be changed ingame (by an admin) +- Add : AutoRankLimit is changed when a player left the game + +~~[ v0.08 ]~~ + +- Rank limit (new player with a rank over the limit is kicked) +- AutoRankLimit (DynaRank) : server's rank + offset = ranklimit +- Change the server's name when the limit is changed : + [ServerName]Top[Limit] +- Server's messages in chat : "[ServerName] your message" +- fake rec : ">> XXX took the 1. rank with a time of... ! +- informations on account : IP/port/login/nick/score/rank +- AutoRankLimit can be turned on/off ingame (by an admin) +- Rank limit value can be changed ingame (by an admin) +- ... + diff --git a/docker-xaseco/xaseco/DOCS/OLD/README-autotime.txt b/docker-xaseco/xaseco/DOCS/OLD/README-autotime.txt new file mode 100644 index 0000000..a9168b4 --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/OLD/README-autotime.txt @@ -0,0 +1,23 @@ +//////////////////////////////////// +//Plugin Auto TimeLimit +//Changes Timelimit for TimeAttack dynamically depending on the next +// track's author time +// +//Written by ck|cyrus +//martin@die-webber.com +//www.chaoskrieger.com + +//////////////// +// Installation + +- Copy file "plugin.autotime.php" into your XAseco plugin directory +- Copy file "autotime.xml" into your XAseco main directory +- Add plugin.autotime.php at the END of your "plugins.xml" +- Open file "autotime.xml" and edit the variables to your liking +- Have fun and enjoy playing with nice timings! + +//////////////// +// SUPPORT: + +Visit: http://www.tm-forum.com/viewtopic.php?t=6563 +ICQ: 85020221 diff --git a/docker-xaseco/xaseco/DOCS/OLD/README.txt b/docker-xaseco/xaseco/DOCS/OLD/README.txt new file mode 100644 index 0000000..ccb5934 --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/OLD/README.txt @@ -0,0 +1,144 @@ +BEFORE ATTEMPTING TO INSTALL RASP, YOU MUST HAVE A FULLY WORKING VERSION OF ASECO 0.6.1b RUNNING ON A LOCAL DATABASE + + +Introduction +------------ +Rasp is a plugin pack for the aseco server script. The core rasp plugin is the ranks and stats system which heavily relies on aseco's records system. The other plugins are extra features which you can add/remove as you please and are completely independent of the core plugin. + +Features +-------- +Here is a list of the features: + +- In-built rank system which assigns a rank based on all records a player has made. +- Stats calculation which shows a player their personal best and average time on a track. +- Jukebox system which allows a player to select their favorite tracks to be played. +- Direct adding of tracks from http://nations.tm-exchange.com (Nations only) +- Karma system which works as a simple but effective replacement of the voting system. +- Built in IRC bot which links your server to a channel on an IRC server. +- /nextmap command which shows the track that will be played next on the server. +- /hi, /bye, /gg, /lol commands from FAST. +- /msg command which allows players to communicate privately via private message. +- Team match scoring & output +- New admin commands + + +Minimum Requirements +-------------------- +-Php5 or above (haven't tested with php4 but feel free to try) +-MySQL 5.1 or above +-Stable installation of Aseco 0.6.1b + + +Installation +------------ + +--CORE PLUGIN-- + +Before installing the core plugin you MUST ensure Aseco 0.6.1b is running stable on your server. Resolve any issues with aseco before attempting to install rasp. + +1. BACK UP your working ASECO files!!!!!!!!! All of them, including files in subdirectories!!!!! + +2. Un-rar/un-zip the rasp files and upload the following to the appropriate directory on your server. All files must be within the aseco root folder and in their correct directories. + - all but five of the files are in their correct directories in the current zip file; use winzip, or similar, and extract with folder/subdirectory names to the main aseco folder + - overwrite existing files with all of the ones that are in the zip file + - newinstall/rasp.settings.php has default values in it for a new install. Move it to the includes folder only if this is a brand new install of RASP and you don't yet have an includes/rasp.settings.php file. + - newinstall/*.xml goes in the main ASECO directory. If you are already running ASECO/RASP, you can leave these alone. + +3. Run the rasp.sql file on your aseco database, either with PhpMyAdmin or with console access to mysql. + +4, 5, & 6 apply to new installs only. Existing installs can skip these steps. + +4. Edit includes/rasp.settings.php as required. + +5. Edit plugins.xml to display the line plugin.rasp.php or copy the one out of newinstall and edit as needed. + +6. Edit rasp.xml, localdatabase.xml, publicdatabase.xml in the main ASECO directory as needed. + +7. If you had an existing RASP installation (prior to 1.1), open newinstall/localdatabase.xml and copy over the section to your existing localdatabase.xml. Do the same for publicdatabase.xml. + +8. If you had an existing RASP installation prior to 1.1, and are going to use the matchsave feature, edit matchsave.xml and make appropriate changes as necessary. If you don't want a continuous output file (append, instead of overwrite), you should be able to use nul: for Windows, and /dev/null for linux. If you don't want matchsave, don't add it to plugins.xml. + +9. Restart Aseco! + +Any problems go to http://www.tm-forum.com/viewtopic.php?t=3356 + + +--ADDITIONAL PLUGINS-- + +The extra plugins can be used independently or alongside the core plugin. If +used independently you must ensure rasp.settings.php has been copied over to +the includes directory. + +1. Place the plugin file inside the plugins folder for your aseco installation. + +2. Insert the line plugin.rasp_*plugin-name*.php in plugins.xml + +3. Modify includes/rasp.settings.php as required. + +3. Restart aseco. + + +plugin.rasp_jukebox.php +----------------------- +This plugin adds the jukebox function to your server, as well as the jukebox extension for requesting tracks directly from TM-Exchange. There are several variables inside rasp.settings.php which correspond to this plugin. + +To add a track to the jukebox type: +/jukebox + +To check what a track's ID is use /list. If you do /list xxx, you'll get a list of all tracks with "xxx" in their name, or author's name. + +To find out the best & worst rated tracks, use /list karma +# or -#. You'll get back a list of all tracks with # or higher value for +, and all tracks with # or lower for -. + +If an admin wants to clear the jukebox use /admin clearjukebox (requires the rasp version of chat.admin.php) + +To add a track from TM-Exchange use: +/add +The tmx_id is the number on the end of the external link which is at the top of a track's page in TMX. +Using the /add command successfuly will start a vote. You can specify the pass ratio in rasp.settings.php +If a player wants to vote for the track they must use /y. +If the vote is successful the track will be added to the jukebox. Once the track has been played it is removed from the map rotation. To add a track permenantly use /admin add (requires the rasp version of chat.admin.php) + + +plugin.rasp_chat.php +-------------------- +This plugin adds various chat commands to your server which were originally seen in FAST. These commands include /hi, /bye, /gg, /lol, /lool, /msg + +To use the hi/bye/gg commands you must use the following syntax (with /hi used as an example) + +/hi --displays the message "Hello all !" +/hi player_name --displays "Hello player_name !" + +The /msg command is used for sending private messages. +/msg player_login *some text* --will send *some text* to the player whose login name is player_login. + +/lol & /lool don't require any arguments. + +plugin.rasp_karma.php +--------------------- +This plugin adds a replacement to the built-in voting system for aseco. It is much easier to use and seems to get more usage than the original /vote command. Players type "/++" if they like a track and "/--" if they don't. The /karma command shows the track's current karma. + +plugin.rasp_irc.php +------------------- +If this plugin is added it will link your server to an IRC channel. You MUST specify the IRC variables in rasp.settings.php before adding this plugin. If successful you will see a bot in your chosen channel which will relay all chat from your server. It also parses the messages typed by other users in the channel and relays them to the TM-server. It is recommended you use a seperate channel for this bot, as putting it in an existing channel will make it virtually un-usable for anything else if you have a busy server. + +plugin.rasp_nextmap.php +----------------------- +This plugin is fairly simple. It adds one command to your server, /nextmap which shows the map to be played next on the server. + +--------------------------------------------- +Released under the GNU General Public License +Copyright (C) 2006 Iain Surgey + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. diff --git a/docker-xaseco/xaseco/DOCS/OLD/ReadMe.pdf b/docker-xaseco/xaseco/DOCS/OLD/ReadMe.pdf new file mode 100644 index 0000000000000000000000000000000000000000..008f8a2d952100d80646418ef2745e58e8660fc4 GIT binary patch literal 215757 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

    vTV&SWSTm_!i~g{qqyoD8>$ir+ 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%0AMqyMD?G!i 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%0DVn6!%s546S*UYD4~31s zBF+5z2$OuxP<(vB_}qiEz;t};V1D{XH?K&&4u(LzC=fJ!IVoelEinHonDs(!dzXT$ zsMMuo?+2ib77Ne(?00}$fc((7i8Jtxx4qycLR8t|&A1ARRNduSYnmqtpo>ptRqSzg zu@GE~*HQ;6Hwz^o>g#{ovIntw0nrxnrrLrPu!G2F0+meqDlQQm`kt9}XM??e7sBsm zts4l{^hKRfFfy0*MXw-Nc4OGE>JF%%&FQCFwo$>4{Q(c1jqKMbIt}_jgIL6 zdn2$CMZ!E(t2yXog>IHItqjEoiaG%*D4tp1c1SgRFYQm_Ydbz}wBZAPL{0y6Rlnp^ zpA>X_6gBf*nHY1?vKOTA1ri_L2_2$0d>Dbhe5{i?nu@3=z+gG~^|%ln2*LQ+0k){U zY!KwLBsw=oH&&kM(_>FiP}i5O&=nO@Dc0smbED*J6Errnpu7Ay#U)W0LHNGIPWbjX zicC(%{@np*^KoeSnnBl4KTEIGWre9o^HIrFpbnTEtnw4QRh3G63tSJk7y4WsmfZoC-30c!6v&JVp^2a%V3}y1 z^45F<@i=*{6uRDb(}SQ}=#$9wj_CM;=m8;%Ssz~0%mDfByGzPKC2to88*N z>>%v?fQDMo?NG2C@4FFWmF~Tv#s1Tc8SF5Lmhm1#bINg;{*?KUdDpQ$<`o6sOE(Vx z#?)^bNL^DbtAsm!2W1~)+R{Yljk(7<=p7;PegNK3g){H{l=D~z-VlvD4~IQ}$yI#8 zS^R)KPi;r|iCFZwm9eTIeSsrwRi|*~V9vtU;3RF86N9yX8Ax}Vj(*kO47rCHQP#|X z)*$!++d5DU#hF#sVftBoQglcR+GN_g)n4GgKeb=ic3_AlqvMcb>4Q=C-gIA

    =5W)w<}`m! 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 + $command['params'] = explode(' ', preg_replace('/ +/', ' ', $command['params'])); + + // check for optional Track/SM ID parameter + $id = $aseco->server->challenge->uid; + $name = $aseco->server->challenge->name; + if ($command['params'][0] != '') { + if (is_numeric($command['params'][0]) && $command['params'][0] > 0) { + $tid = ltrim($command['params'][0], '0'); + // check for possible track ID + if ($tid <= count($player->tracklist)) { + // find UID by given track ID + $tid--; + $id = $player->tracklist[$tid]['uid']; + $name = $player->tracklist[$tid]['name']; + } else { + // consider it an SM ID + $id = $tid; + $name = ''; + } + } else { + $message = '{#server}> {#highlite}' . $tid . '{#error} is not a valid Track/SM ID!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + return; + } + } + + // obtain SM info + $data = new SMInfoFetcher($id); + if (!$data->name) { + $message = '{#server}> {#highlite}' . ($name != '' ? stripColors($name) : $id) . + '{#error} is not a known SM track, or ShareMania is down!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + return; + } + $data->name = stripNewlines($data->name); + + // compile & send message + if ($aseco->server->getGame() == 'TMN') { + $stats = 'SM Info for: {#black}' . $data->name . '$z' . LF . LF; + $stats .= '$gSM ID : {#black}' . $data->id . LF; + $stats .= '$gUID : {#black}$n' . $data->uid . '$m' . LF; + $stats .= '$gAuthor : {#black}' . $data->author . LF; + $stats .= '$gUploaded : {#black}' . preg_replace('/^\d\d\d\d/', '\$n$0\$m', strftime('%Y-%m-%d %H:%M', $data->uploaded)) . LF; + if ($data->type == 'Stunts') + $stats .= '$gAuthorSc : {#black}' . $data->authorsc . LF; + else + $stats .= '$gAuthorTm : {#black}' . formatTime($data->authortm) . LF; + $stats .= '$gGame : {#black}' . $data->game . LF; + $stats .= '$gType : {#black}' . $data->type . LF; + $stats .= '$gEnviron : {#black}' . $data->envir . LF; + $stats .= '$gMood : {#black}' . $data->mood . LF; + $stats .= '$gNumLaps : {#black}' . $data->nblaps . LF; + $stats .= '$gCoppers : {#black}' . $data->coppers . LF; + $stats .= '$gRating : {#black}' . $data->rating . LF; + $stats .= '$gVotes : {#black}' . $data->votes . LF; + $stats .= '$gDownloads: {#black}' . $data->dnloads; + + // display popup message + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $aseco->formatColors($stats), 'OK', '', 0); + + } elseif ($aseco->server->getGame() == 'TMF') { + $header = 'SM Info for: {#black}' . $data->name; + $links = array($data->imageurl, true, + '$l[' . $data->pageurl . ']Visit SM Page', + '$l[' . $data->dloadurl . ']Download Track'); + $stats = array(); + $stats[] = array('SM ID', '{#black}' . $data->id, + 'Game', '{#black}' . $data->game); + $stats[] = array('UID', '{#black}$n' . $data->uid, + 'Type', '{#black}' . $data->type); + $stats[] = array('Author', '{#black}' . $data->author, + 'Environ', '{#black}' . $data->envir); + $stats[] = array('Uploaded', '{#black}' . strftime('%Y-%m-%d %H:%M', $data->uploaded), + 'Mood', '{#black}' . $data->mood); + if ($data->type == 'Stunts') + $stats[] = array('AuthorSc', '{#black}' . $data->authorsc, + 'NumLaps', '{#black}' . $data->nblaps); + else + $stats[] = array('AuthorTm', '{#black}' . formatTime($data->authortm), + 'NumLaps', '{#black}' . $data->nblaps); + $stats[] = array('Rating', '{#black}' . $data->rating, + 'Coppers', '{#black}' . $data->coppers); + $stats[] = array('Votes', '{#black}' . $data->votes, + 'Downloads', '{#black}' . $data->dnloads); + + // display custom ManiaLink message + display_manialink_track($login, $header, array('Icons64x64_1', 'Maximize', -0.01), $links, $stats, array(1.15, 0.2, 0.45, 0.2, 0.3), 'OK'); + + } else { // TMS/TMO + $stats = '{#server}ShareMania Info for: {#highlite}' . $data->name . '$z' . LF; + $stats .= '{#server}SM ID : {#highlite}' . $data->id . LF; + $stats .= '{#server}UID : {#highlite}' . $data->uid . LF; + $stats .= '{#server}Author : {#highlite}' . $data->author . LF; + $stats .= '{#server}Rating : {#highlite}' . $data->rating; + + // show chat message + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($stats), $login); + } +} // chat_sminfo +?> diff --git a/docker-xaseco/xaseco/DOCS/OLD/sminfofetcher.inc.php b/docker-xaseco/xaseco/DOCS/OLD/sminfofetcher.inc.php new file mode 100644 index 0000000..0e73a95 --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/OLD/sminfofetcher.inc.php @@ -0,0 +1,149 @@ + + * Based on TMXInfoFetcher & http://www.sharemania.eu/api_tutorial.php + * + * v1.2: Added magic __set_state function to support var_export() + * v1.1: Optimized get_file URL parsing + * v1.0: Initial release + */ +class SMInfoFetcher { + + public $uid, $id, + $name, $stname, $author, + $game, $type, $envir, $mood, $nblaps, $coppers, + $bronzetm, $silvertm, $goldtm, $authortm, $authorsc, + $rating, $votes, $dnloads, $uploaded, + $pageurl, $imageurl, $dloadurl; + + /** + * Fetches a hell of a lot of data about a SM track + * + * @param String $id + * The challenge UID to search for (if a 26/27-char alphanum string), + * otherwise the SM ID to search for (if a number) + * @return SMInfoFetcher + * If $name is empty, track was not found + */ + public function SMInfoFetcher($id) { + + // check for UID string + if (preg_match('/^\w{26,27}$/', $id)) { + $this->uid = $id; + $this->getData(true); + // check for SM ID + } elseif (is_numeric($id) && $id > 0) { + $this->id = floor($id); + $this->getData(false); + } + } // SMInfoFetcher + + public function __set_state($import) { + + $sm = new SMInfoFetcher(0); + + $sm->uid = $import['uid']; + $sm->id = $import['id']; + $sm->name = $import['name']; + $sm->stname = $import['stname']; + $sm->author = $import['author']; + $sm->game = $import['game']; + $sm->type = $import['type']; + $sm->envir = $import['envir']; + $sm->mood = $import['mood']; + $sm->nblaps = $import['nblaps']; + $sm->coppers = $import['coppers']; + $sm->bronzetm = $import['bronzetm']; + $sm->silvertm = $import['silvertm']; + $sm->goldtm = $import['goldtm']; + $sm->authortm = $import['authortm']; + $sm->authorsc = $import['authorsc']; + $sm->rating = $import['rating']; + $sm->votes = $import['votes']; + $sm->dnloads = $import['dnloads']; + $sm->uploaded = $import['uploaded']; + $sm->pageurl = $import['pageurl']; + $sm->imageurl = $import['imageurl']; + $sm->dloadurl = $import['dloadurl']; + + return $sm; + } // __set_state + + private function getData($isuid) { + + // get all track info + $file = $this->get_file('http://www.sharemania.eu/api.php?i&u&n&sn&a&gv&e&m&ty&nbl&c&t&p&pa&id=' . ($isuid ? $this->uid : $this->id)); + if ($file === false || $file == -1) + return false; + + // parse XML info + if (!$xml = @simplexml_load_string($file)) + return false; + + // extract all track info + if ($isuid) + $this->id = (string) $xml->header->i; + else + $this->uid = (string) $xml->header->u; + + $this->name = (string) $xml->header->n; + $this->stname = (string) $xml->header->sn; + $this->author = (string) $xml->header->a; + $this->type = (string) $xml->header->ty; + $this->game = (string) $xml->header->gv; + $this->envir = (string) $xml->header->e; + $this->mood = (string) $xml->header->m; + $this->nblaps = (string) $xml->header->nbl; + $this->coppers = (string) $xml->header->c; + $this->bronzetm = (string) $xml->times->b; + $this->silvertm = (string) $xml->times->s; + $this->goldtm = (string) $xml->times->g; + $this->authortm = (string) $xml->times->at; + $this->authorsc = (string) $xml->times->as; + $this->rating = (string) $xml->infos->r; + $this->votes = (string) $xml->infos->v; + $this->dnloads = (string) $xml->infos->d; + $this->uploaded = (string) $xml->infos->ud; + + $this->imageurl = (string) $xml->pic; + $this->pageurl = 'http://www.sharemania.eu/track.php?id=' . $this->id; + $this->dloadurl = 'http://www.sharemania.eu/download.php?id=' . $this->id; + } // getData + + // Simple HTTP Get function with timeout + // ok: return string || error: return false || timeout: return -1 + private function get_file($url) { + + $url = parse_url($url); + $port = isset($url['port']) ? $url['port'] : 80; + $query = isset($url['query']) ? "?" . $url['query'] : ""; + + $fp = @fsockopen($url['host'], $port, $errno, $errstr, 4); + if (!$fp) + return false; + + fwrite($fp, 'GET ' . $url['path'] . $query . " HTTP/1.0\r\n" . + 'Host: ' . $url['host'] . "\r\n\r\n"); + stream_set_timeout($fp, 2); + $res = ''; + $info['timed_out'] = false; + while (!feof($fp) && !$info['timed_out']) { + $res .= fread($fp, 512); + $info = stream_get_meta_data($fp); + } + fclose($fp); + + if ($info['timed_out']) { + return -1; + } else { + if (substr($res, 9, 3) != '200') + return false; + $page = explode("\r\n\r\n", $res, 2); + return trim($page[1]); + } + } // get_file +} // class SMInfoFetcher +?> diff --git a/docker-xaseco/xaseco/DOCS/admin_abilities.html b/docker-xaseco/xaseco/DOCS/admin_abilities.html new file mode 100644 index 0000000..1606fe8 --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/admin_abilities.html @@ -0,0 +1,194 @@ + + + +TrackMania Nations - XASECO administrator abilities + + + + + + + + + + +

    +TrackMania Nations +


    + +

    XASECO Administrator abilities:

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    AbilityDescriptionMasterAdminAdminOperator
    helpuse /admin helpXXX
    helpalluse /admin helpallXXX
    setservernameuse /admin setservernameX  
    setcommentuse /admin setcommentX  
    setpwduse /admin setpwdXX 
    setspecpwduse /admin setspecpwdXX 
    setrefpwd (TMF)use /admin setrefpwdXX 
    setmaxplayersuse /admin setmaxplayersX  
    setmaxspecsuse /admin setmaxspecsX  
    setgamemodeuse /admin setgamemodeXX 
    setrefmode (TMF)use /admin setrefmodeXX 
    nextmapuse /admin nextmapXXX
    nextuse /admin nextXXX
    skipmapuse /admin skipmapXXX
    skipuse /admin skipXXX
    previoususe /admin previousXXX
    prevuse /admin prevXXX
    nextenv (TMUF)use /admin nextenvXXX
    restartmapuse /admin restartmapXXX
    resuse /admin resXXX
    replaymapuse /admin replaymapXXX
    replayuse /admin replayXXX
    dropjukeboxuse /admin dropjukeboxXXX
    djbuse /admin djbXXX
    clearjukeboxuse /admin clearjukeboxXXX
    clearhistuse /admin clearhistXX 
    cjbuse /admin cjbXXX
    adduse /admin addXX 
    addthisuse /admin addthisXX 
    addlocaluse /admin addlocalXX 
    warnuse /admin warnXXX
    kickuse /admin kickXXX
    kickghostuse /admin kickghostXXX
    banuse /admin banXX 
    unbanuse /admin unbanXX 
    banipuse /admin banipXX 
    unbanipuse /admin unbanipXX 
    blackuse /admin blackXX 
    unblackuse /admin unblackXX 
    addguestuse /admin addguestXXX
    removeguestuse /admin removeguestXXX
    passuse /admin passXXX
    canceluse /admin cancelXXX
    canuse /admin canXXX
    endrounduse /admin endroundXXX
    eruse /admin erXXX
    playersuse /admin playersXXX
    showbanlistuse /admin showbanlistXXX
    listbansuse /admin listbansXXX
    showiplistuse /admin showiplistXXX
    listipsuse /admin listipsXXX
    showblacklistuse /admin showblacklistXXX
    listblacksuse /admin listblacksXXX
    showguestlistuse /admin showguestlistXXX
    listguestsuse /admin listguestsXXX
    writeiplistuse /admin writeiplistXX 
    readiplistuse /admin readiplistXX 
    writeblacklistuse /admin writeblacklistXX 
    readblacklistuse /admin readblacklistXX 
    writeguestlistuse /admin writeguestlistXX 
    readguestlistuse /admin readguestlistXX 
    cleanbanlistuse /admin cleanbanlistX  
    cleaniplistuse /admin cleaniplistX  
    cleanblacklistuse /admin cleanblacklistX  
    cleanguestlistuse /admin cleanguestlistX  
    mergegbluse /admin mergegblX  
    accessuse /admin accessX  
    writetracklistuse /admin writetracklistXX 
    readtracklistuse /admin readtracklistXX 
    shuffleuse /admin shuffleXX 
    shufflemapsuse /admin shufflemapsXX 
    listdupesuse /admin listdupesXX 
    removeuse /admin removeXX 
    eraseuse /admin eraseXX 
    removethisuse /admin removethisXX 
    erasethisuse /admin erasethisXX 
    muteuse /admin muteXXX
    ignoreuse /admin ignoreXXX
    unmuteuse /admin unmuteXXX
    unignoreuse /admin unignoreXXX
    mutelistuse /admin mutelistXXX
    ignorelistuse /admin ignorelistXXX
    listmutesuse /admin listmutesXXX
    listignoresuse /admin listignoresXXX
    cleanmutesuse /admin cleanmutesX  
    cleanignoresuse /admin cleanignoresX  
    addadminuse /admin addadminX  
    removeadminuse /admin removeadminX  
    addopuse /admin addopXX 
    removeopuse /admin removeopXX 
    listmastersuse /admin listmastersXXX
    listadminsuse /admin listadminsXXX
    listopsuse /admin listopsXXX
    adminabilityuse /admin adminabilityX  
    opabilityuse /admin opabilityX  
    listabilitiesuse /admin listabilitiesXXX
    writeabilitiesuse /admin writeabilitiesXX 
    readabilitiesuse /admin readabilitiesXX 
    walluse /admin wallXXX
    mtause /admin mtaXXX
    delrecuse /admin delrecX  
    prunerecsuse /admin prunerecsX  
    rpoints (TMF)use /admin rpointsXX 
    matchuse /admin matchXX 
    acdluse /admin acdlX  
    autotimeuse /admin autotimeXX 
    disablerespawn (TMF)use /admin disablerespawnXX 
    forceshowopp (TMF)use /admin forceshowoppXX 
    scorepanel (TMF)use /admin scorepanelXXX
    roundsfinish (TMF)use /admin roundsfinishXXX
    forceteam (TMF)use /admin forceteamXXX
    forcespec (TMF)use /admin forcespecXXX
    specfree (TMF)use /admin specfreeXXX
    panel (TMF)use /admin panelXXX
    style (TMF)use /admin styleX  
    admpanel (TMF)use /admin admpanelX  
    donpanel (TMUF)use /admin donpanelX  
    recpanel (TMF)use /admin recpanelX  
    votepanel (TMF)use /admin votepanelX  
    coppers (TMUF)use /admin coppersXX 
    pay (TMUF)use /admin payX  
    relays (TMF)use /admin relaysXXX
    serveruse /admin serverXX 
    pmuse /admin pmXX 
    pmloguse /admin pmlogXX 
    calluse /admin callX  
    unlockuse /admin unlockXXX
    debuguse /admin debugX  
    shutdownuse /admin shutdownX  
    shutdownalluse /admin shutdownallX  
    uptodateuse /admin uptodateXX 
    chat_pmause /pma to send a PM to player & adminsXX 
    chat_bestworst/best & /worst accept login/Player_IDXX 
    chat_statsip/stats includes IP addressXX 
    chat_summary/summary accepts login/Player_IDXX 
    chat_jukeboxuse /jukebox even if $feature_jukebox is falseXX 
    chat_jb_multi/jukebox adds more than one trackXX 
    chat_jb_recent/jukebox adds recently played trackXX 
    chat_add_trefuse /add trackref to write TMX trackref fileX  
    chat_matchuse /match to allow match controlX  
    chat_tc_listen/tc will copy team chat to adminsX  
    chat_jfreuuse all /jfreu commandsXX 
    chat_musicadmin (TMF)use /music admin commandsXX 
    noidlekick_playno idlekick when admin is playerX  
    noidlekick_specno idlekick when admin is spectatorXXX
    server_coppers (TMUF)view coppers amount in /serverXX 
    +
    +

    + +
    +
    +Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 08-Aug-2011 +
    + + diff --git a/docker-xaseco/xaseco/DOCS/aseco_commands.doc b/docker-xaseco/xaseco/DOCS/aseco_commands.doc new file mode 100644 index 0000000000000000000000000000000000000000..3c350db1b54d49045b53741397cfae4532ed2301 GIT binary patch literal 69120 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 + + +TrackMania Nations - XASECO commands + + + + + + + + + + +

    +TrackMania Nations +


    + + +

    Important XASECO admin commands:

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    /admin help | helpall
    /jfreu help | helpall
     
    /players
    /players <string>finds online player(s)
    /admin players
    /admin players <string>finds offline player(s)
    /admin players livelists online player(s)
     
    /pm <login/id> <message>
    /pma <login/id> <message>
    /pmlog
    /chatlog
    /msglog
     
    /admin pm <message>
    /admin pmlog
    /admin wall <message>
     
    /admin endround/admin er
    /admin resmap/admin res
    /admin nextmap/admin next
    /admin replaymap/admin replay
     
    /admin pass
    /admin cancel/admin can
    /admin dropjukebox <#>/admin djb <#>
    /admin clearjukebox/admin cjb
     
    /admin warn <login/id>
    /admin kick <login/id>
    /admin kickghost <login>
    /admin black <login/id>
    /admin ban <login/id>
    /admin banip <IP>
     
    /jfreu badword <login/id>
    /jfreu banfor <time> <login/id><time> = <xx> mins or <x>H hours
    /jfreu players
    /jfreu players <string>finds offline player(s)
    /jfreu players livelists online player(s)
    +
    + + +

    List of available user commands:

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    /adminProvides admin commands (see: /admin help)
    /helpShows all available commands
    /helpallDisplays help for available commands
    /recsDisplays all records on current track
    /recs help Displays this help information
    /recs pb Shows your personal best on current track
    /recs new Shows newly driven records
    /recs live Shows records of online players
    /recs first Shows first ranked record on current track
    /recs last Shows last ranked record on current track
    /recs next Shows next better ranked record to beat
    /recs diff Shows your difference to first ranked record
    /recs range Shows difference first to last ranked record
    /newrecsShows newly driven records
    /liverecsShows records of online players
    /bestDisplays your best records
    /worstDisplays your worst records
    /summaryShows summary of all your records
    /topsumsDisplays top 100 of top-3 record holders
    /toprecsDisplays top 100 ranked records holders
    /firstrecShows first ranked record on current track
    /lastrecShows last ranked record on current track
    /nextrecShows next better ranked record to beat
    /diffrecShows your difference to first ranked record
    /recrangeShows difference first to last ranked record
    /helpdedi | /dedihelpDisplays info about the Dedimania records system
    /dedirecsDisplays all Dedimania records on current track
    /dedirecs help Displays this help information
    /dedirecs pb Shows your personal best on current track
    /dedirecs new Shows newly driven records
    /dedirecs live Shows records of online players
    /dedirecs first Shows first ranked record on current track
    /dedirecs last Shows last ranked record on current track
    /dedirecs next Shows next better ranked record to beat
    /dedirecs diff Shows your difference to first ranked record
    /dedirecs range Shows difference first to last ranked record
    /dedinewShows newly driven Dedimania records
    /dediliveShows Dedimania records of online players
    /dedipbShows your Dedimania personal best on current track
    /dedifirstShows first Dedimania record on current track
    /dedilastShows last Dedimania record on current track
    /dedinextShows next better Dedimania record to beat
    /dedidiffShows your difference to first Dedimania record
    /dedirangeShows difference first to last Dedimania record
    /dedicpsSets Dedimania record checkpoints tracking
    /dedistatsDisplays Dedimania track statistics
    /dedicptmsDisplays all Dedimania records' checkpoints times
    /dedisectmsDisplays all Dedimania records' sector times
    /playersDisplays current list of nicks/logins
    /ranksDisplays list of online ranks/nicks
    /clansDisplays list of online clans/nicks
    /topclansDisplays top 10 best ranked clans
    /winsShows wins for current player
    /lastonShows when a player was last online
    /lastwinRe-opens the last closed multi-page window
    /statsDisplays statistics of current player
    /statsall (TMN)Displays world statistics of a player
    /settingsDisplays your personal settings
    /serverDisplays info about this server
    /xasecoDisplays info about this XASECO
    /pluginsDisplays list of active plugins
    /nationsDisplays top 10 most visiting nations
    /songShows filename of current track's song
    /modShows (file)name of current track's mod
    /meCan be used to express emotions
    /muteMute another player's chat messages
    /unmuteUnMute another player's chat messages
    /mutelistDisplay list of muted players
    /refreshRefresh chat window
    /tmxinfoDisplays TMX info {Track_ID/TMX_ID} {sec}
    /tmxrecsDisplays TMX records {Track_ID/TMX_ID} {sec}
    /trackShows info about the current track
    /playtimeShows time current track has been playing
    /timeShows current server time & date
    /cpsSets local record checkpoints tracking
    /cpsspecShows checkpoints of spectated player
    /cptmsDisplays all local records' checkpoint times
    /sectmsDisplays all local records' sector times
    /pbShows your personal best on current track
    /rankShows your current server rank
    /top10Displays top 10 best ranked players
    /top100Displays top 100 best ranked players
    /topwinsDisplays top 100 victorious players
    /activeDisplays top 100 most active players
    /listLists tracks currently on the server (see: /list help)
    /list help Displays this help information
    /list nofinish Tracks you don't have a rank on
    /list norank Tracks you haven't completed
    /list nogold Tracks you didn't beat gold time on
    /list noauthor Tracks you didn't beat author time on
    /list norecent Tracks you didn't play recently
    /list best | worst Tracks with your best / worst records
    /list longest | shortest The longest / shortest tracks
    /list newest | oldest <#> The newest / oldest # tracks (default: 50)
    /list <xxx> Where <xxx> is part of a track or author name
    /list env:<zzz> (TMUF) Where <zzz> is an environment from: stadium,
    bay,coast,island,snow/alpine,desert/speed,rally
    /list <xxx> env:<zzz> Combines the name and environment searches
    /list novote Tracks you didn't karma vote for
    /list karma <#> Display all tracks with karma >= or <= given value
    /jukeboxSets a track to be played next (see: /jukebox help)
    /jukebox help Displays this help information
    /jukebox list Shows upcoming tracks
    /jukebox display Displays upcoming tracks and requesters
    /jukebox drop Drops your currently added track
    /jukebox <#> Adds a track where <#> is track number from /list
    /autojukeJukeboxes track from /list (see: /autojuke help)
    /autojuke help Displays this help information
    /autojuke nofinish Tracks you don't have a rank on
    /autojuke norank Tracks you haven't completed
    /autojuke nogold Tracks you didn't beat gold time on
    /autojuke noauthor Tracks you didn't beat author time on
    /autojuke norecent Tracks you didn't play recently
    /autojuke longest | shortest The longest / shortest tracks
    /autojuke newest | oldest The newest / oldest # tracks
    /autojuke novote Tracks you didn't karma vote for
    /addAdds a track directly from TMX (<ID> {sec})
    /add <ID> <sec> (TMF)Adds a track from a specific TMX section
    /yVotes Yes for a TMX track or chat-based vote
    /historyShows the 10 most recently played tracks
    /xlistLists tracks on TMX (see: /xlist help)
    /xlist help Displays this help information
    /xlist recent Lists the 10 most recent tracks
    /xlist <xxx> Lists tracks matching (partial) name
    /xlist auth:<yyy> Lists tracks matching (partial) author
    /xlist env:<zzz> Where <zzz> is an environment from: stadium,
    bay,coast,island,snow/alpine,desert/speed,rally
    /xlist <xxx> auth:<yyy> env:<zzz> Combines the name, author and/or env searches
    /xlist <tmx> Where <tmx> is a TMX section from:
    TMO,TMS,TMN,TMNF,TMU
    Can be appended to any of the above searches
    /pmSends a private message to login or Player_ID
    /pmaSends a private message to player & admins (admin-only)
    /pmlogDisplays log of your recent private messages
    /hiSends a Hi message to everyone
    /byeSends a Bye message to everyone
    /thxSends a Thanks message to everyone
    /lolSends a Lol message to everyone
    /loolSends a Lool message to everyone
    /brbSends a Be Right Back message to everyone
    /afkSends an Away From Keyboard message to everyone
    /ggSends a Good Game message to everyone
    /grSends a Good Race message to everyone
    /n1Sends a Nice One message to everyone
    /bgmSends a Bad Game message to everyone
    /officialShows a helpful message ;-)
    /bootmeBoot yourself from the server
    /karmaShows karma for the current track
    /++Increases karma for the current track
    /--Decreases karma for the current track
    /nextmapShows name of the next challenge
    /nextrankShows the next better ranked player
    /helpvote | /votehelpDisplays info about the chat-based votes
    /endroundStarts a vote to end current round
    /ladderStarts a vote to restart track for ladder
    /replayStarts a vote to replay this track
    /skipStarts a vote to skip this track
    /ignoreStarts a vote to ignore a player
    /kickStarts a vote to kick a player
    /cancelCancels your current vote
    /chatlogDisplays log of recent chat messages
    /msglog (TMF)Displays log of recent system messages
    /style (TMF)Selects window style (see: /style help)
    /style help Displays this help information
    /style list Displays available styles
    /style default Resets style to server default
    /style off Disables TMF window style
    /style <xxx> Selects window style <xxx>
    /donpanel (TMUF)Selects donate panel (see: /donpanel help)
    /donpanel help Displays this help information
    /donpanel list Displays available panels
    /donpanel default Resets panel to server default
    /donpanel off Disables donate panel
    /donpanel <xxx> Selects donate panel <xxx>
    /recpanel (TMF)Selects records panel (see: /recpanel help)
    /recpanel help Displays this help information
    /recpanel list Displays available panels
    /recpanel default Resets panel to server default
    /recpanel off Disables records panel
    /recpanel <xxx> Selects records panel <xxx>
    /votepanel (TMF)Selects records panel (see: /votepanel help)
    /votepanel help Displays this help information
    /votepanel list Displays available panels
    /votepanel default Resets panel to server default
    /votepanel off Disables vote panel
    /votepanel <xxx> Selects vote panel <xxx>
    /donate (TMUF)Donates coppers to server
    /topdons (TMUF)Displays top 100 highest donators
    /music (TMF)Handles server music (see: /music help)
    /music help Displays this help information
    /music settings Displays current music settings
    /music list Displays all available songs
    /music list <xxx> Searches song names/tags for <xxx>
    /music current Shows the current song
    /music reload (admin) Reloads musicserver.xml config file
    /music next (admin) Skips to next song (upon next track)
    /music sort (admin) Sorts the song list
    /music shuffle (admin) Randomizes the song list
    /music override (admin) Changes track override setting
    /music autonext (admin) Changes automatic next song setting
    /music allowjb (admin) Changes allow jukebox setting
    /music stripdirs (admin) Changes strip subdirs setting
    /music stripexts (admin) Changes strip extensions setting
    /music off (admin) Disables music, auto next & jukebox
    /music jukebox Displays upcoming songs in jukebox
    /music drop Drops your currently added song
    /music <#> Adds a song to jukebox where <#> is song number
    /rpoints (TMF)Shows current Rounds points system
    /ranklimitShows the current rank limit
    /passwordShows server's player/spectator password
    /yesVotes Yes for unSpec
    /noVotes No for unSpec
    /unspecLaunches an unSpec vote
    /messageShows random informational message
    /jfreuJfreu admin commands (see: /jfreu help)
    /versionShows server version (dedicated built-in command)
    /serverlogin (TMF)Shows server login (dedicated built-in command)
    +
    + + +

    List of available /admin commands:

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    helpShows all available /admin commands
    helpallDisplays help for available /admin commands
    setservernameChanges the name of the server
    setcommentChanges the server comment
    setpwdChanges the player password
    setspecpwdChanges the spectator password
    setrefpwd (TMF)Changes the referee password
    setmaxplayersSets a new maximum of players
    setmaxspecsSets a new maximum of spectators
    setgamemodeSets next mode {ta,rounds,team,laps,stunts,cup}
    setrefmode (TMF)Sets referee mode {0=top3,1=all}
    nextmap | nextForces server to load next track
    skipmap | skipForces server to load next track
    previous | prevForces server to load previous track
    nextenv (TMUF)Loads next track in same environment
    restartmap | resRestarts currently running track
    replaymap | replayReplays current track (via jukebox)
    dropjukebox | djbDrops a track from the jukebox
    clearjukebox | cjbClears the entire jukebox
    clearhistClears (part of) track history
    addAdds tracks directly from TMX (<ID>... {sec})
    add <ID> <sec> (TMF)Adds a track from a specific TMX section
    addthisAdds current /add-ed track permanently
    addlocalAdds a local track (<filename>)
    warnSends a kick/ban warning to a player
    kickKicks a player from server
    kickghostKicks a ghost player from server
    banBans a player from server
    unbanUnBans a player from server
    banipBans an IP address from server
    unbanipUnBans an IP address from server
    blackBlacklists a player from server
    unblackUnBlacklists a player from server
    addguestAdds a guest player to server
    removeguestRemoves a guest player from server
    passPasses a chat-based or TMX /add vote
    cancel | canCancels any running vote
    endround | erForces end of current round
    playersDisplays list of known players {string}
    players liveDisplays list of online players
    showbanlist | listbansDisplays current ban list
    showiplist | listipsDisplays current banned IPs list
    showblacklist | listblacksDisplays current black list
    showguestlist | listguestsDisplays current guest list
    writeiplistSaves current banned IPs list (def: bannedips.xml)
    readiplistLoads current banned IPs list (def: bannedips.xml)
    writeblacklistSaves current black list (def: blacklist.txt)
    readblacklistLoads current black list (def: blacklist.txt)
    writeguestlistSaves current guest list (def: guestlist.txt)
    readguestlistLoads current guest list (def: guestlist.txt)
    cleanbanlistCleans current ban list
    cleaniplistCleans current banned IPs list
    cleanblacklistCleans current black list
    cleanguestlistCleans current guest list
    mergegblMerges a global black list {URL}
    accessHandles player access control (see: /admin acess help)
    access help Displays this help information
    access list Displays current access control settings
    access reload Reloads updated access control settings
    writetracklistSaves current track list (def: tracklist.txt)
    readtracklistLoads current track list (def: tracklist.txt)
    shuffle | shufflemapsRandomizes current track list
    listdupesDisplays list of duplicate tracks
    removeRemoves a track from rotation
    eraseRemoves a track from rotation & delete track file
    removethisRemoves this track from rotation
    erasethisRemoves this track from rotation & delete track file
    mute | ignoreAdds a player to global mute/ignore list
    unmute | unignoreRemoves a player from global mute/ignore list
    mutelist | listmutesDisplays global mute/ignore list
    ignorelist | listignoresDisplays global mute/ignore list
    cleanmutes | cleanignoresCleans global mute/ignore list
    addadminAdds a new admin
    removeadminRemoves an admin
    addopAdds a new operator
    removeopRemoves an operator
    listmastersDisplays current masteradmin list
    listadminsDisplays current admin list
    listopsDisplays current operator list
    adminabilityShows/changes admin ability {ON/OFF}
    opabilityShows/changes operator ability {ON/OFF}
    listabilitiesDisplays current abilities list
    writeabilitiesSaves current abilities list (def: adminops.xml)
    readabilitiesLoads current abilities list (def: adminops.xml)
    wall | mtaDisplays popup message to all players
    delrecDeletes specific record on current track
    prunerecsDeletes records for specified track
    rpoints (TMF)Sets custom Rounds points (see: /admin rpoints help)
    rpoints help Displays this help information
    rpoints list Displays available points systems
    rpoints show Shows current points system
    rpoints <xxx> Sets custom points system labelled <xxx>
    rpoints X,Y,...,Z Sets custom points system with specified values
    rpoints off Disables custom points system
    rpoints f1old Sets Formula 1 GP old points
    rpoints f1new Sets Formula 1 GP new points
    rpoints motogp Sets MotoGP points
    rpoints motogp5 Sets MotoGP + 5 points
    rpoints fet1 Sets Formula ET Season 1 points
    rpoints fet2 Sets Formula ET Season 2 points
    rpoints fet3 Sets Formula ET Season 3 points
    rpoints champcar Sets Champ Car World Series points
    rpoints superstars Sets Superstars points
    rpoints simple5 Sets Simple 5 points
    rpoints simple10 Sets Simple 10 points
    match{begin/end} to start/stop match tracking
    acdlSets AllowChallengeDownload {ON/OFF}
    autotimeSets Auto TimeLimit {ON/OFF}
    disablerespawn (TMF)Disable respawns at CPs {ON/OFF}
    forceshowopp (TMF)Forces to show opponents {##/ALL/OFF}
    scorepanel (TMF)Shows automatic scorepanel {ON/OFF}
    roundsfinish (TMF)Shows rounds panel upon first finish {ON/OFF}
    forceteam (TMF)Forces player into Blue or Red team
    forcespec (TMF)Forces player into free spectator
    specfree (TMF)Forces spectator into free mode
    panel (TMF)Selects admin panel (see: /admin panel help)
    panel help Displays this help information
    panel list Displays available panels
    panel default Resets panel to server default
    panel off Disables admin panel
    panel <xxx> Selects admin panel <xxx>
    style (TMF)Selects default window style
    admpanel (TMF)Selects default admin panel
    donpanel (TMUF)Selects default donate panel
    recpanel (TMF)Selects default records panel
    votepanel (TMF)Selects default vote panel
    coppers (TMUF)Shows server's coppers amount
    pay (TMUF)Pays server coppers to login
    relays (TMF)Displays relays list or shows relay master
    serverDisplays server's detailed settings
    pmSends private message to all available admins
    pmlogDisplays log of recent private admin messages
    callExecutes direct server call (see: /admin call help)
    unlock <pwd>Unlocks admin commands & features
    debugToggles debugging output
    shutdownShuts down XASECO
    shutdownallShuts down Server & XASECO
    uptodateChecks current version of XASECO
    +
    + + +

    List of available /jfreu commands:

    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    helpShows Jfreu commands
    helpallDisplays help for Jfreu commands
    autochangenameAuto change servername {ON/OFF}
    setrankSets ranklimiting {ON/OFF}
    setlimitSets ranklimit value
    autorankSets autoranking {ON/OFF}
    offsetSets autorank offset (-999 - 999)
    hardlimitSets hardlimit value
    autorankminplayersSets min players for autorank {0-50}
    autorankvipInclude VIPs in autorank {ON/OFF}
    maxplayersSets maxplayers for Kick HiRank
    kickhirankKick HiRank when server full {ON/OFF}
    listlimitsDisplays rank limit settings
    kickworstKicks worst players {count}
    playersDisplays list of known players {string}
    players liveDisplays list of online players
    unspecUnSpecs player {login/ID} (clear SpecOnly)
    addvipAdds a VIP {login/ID}
    removevipRemoves a VIP {login/ID}
    addvipteamAdds a VIP_Team {team}
    removevipteamRemoves VIP_Team {team}
    listvipsDisplays VIPs list
    listvipteamsDisplays VIP_Teams list
    writelistsSaves VIP/VIP_Team lists (def: jfreu.vips.xml)
    readlistsLoads VIP/VIP_Team lists (def: jfreu.vips.xml)
    badwordsSets badwords bot {ON/OFF}
    badwordsbanSets badwords ban {ON/OFF}
    badwordsnumSets badwords limit {count}
    badwordstimeSets banning period {mins}
    badwordGives extra badword warning {login/ID}
    banforBans player {mins/hoursH} {login/ID}
    unbanUnBans temporarily banned player
    listbansDisplays temporarily banned players
    messageFakes message from server {msg}
    playerFakes message from player {login/ID} {msg}
    nopfkickSets NoPfKick {map/time/OFF}
    cancelCancels current vote (kick/ban/nextmap/restart)
    novoteAuto-cancel CallVotes {ON/OFF}
    unspecvoteAllow /unspec votes {ON/OFF}
    infomessagesSets info messages {ON/OFF}
    writeconfigSaves Jfreu config (def: jfreu.config.xml)
    readconfigLoads Jfreu config (def: jfreu.config.xml)
    +
    +
    + +
    +
    +Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 02-Apr-2013 +
    + + diff --git a/docker-xaseco/xaseco/DOCS/index.html b/docker-xaseco/xaseco/DOCS/index.html new file mode 100644 index 0000000..029582d --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/index.html @@ -0,0 +1,421 @@ + + + +TrackMania Nations - XASECO + + + + + + + + + + +Welcome to this humble page for: + +

    +TrackMania Nations +


    + +

    Introduction:

    + +
    +
    +This is a simple hub page for TrackMania Nations ESWC. It aims to collect in one place useful information and references to official and community sites for this classic racing game by Nadeo. Here are relevant excerpts from the TMN ESWC press release: +

    +"For the first time in the history of eSport, a video game has been specially developed for the Electronic Sports World Cup and is being offered free of charge to the players of the entire planet." +

    +"This special version features a brand new environment, the Stadium, and a revolutionary new gameplay designed for Nadeo's own custom cars. Nadeo has pulled the technological rabbit from the hat with their own brand game engine, featuring the series' best graphics yet, finely tuned for the latest generation graphics cards, not forgetting simpler machines so that no one round the world has a technical freeze out on the race track. With the online in-game ladder, players can customise their cars and avatars and race in their nation's colours on the world's servers. Many Trackmania and Trackmania Sunrise players will already have access to the following exclusive features: peer 2 peer data exchange and skin and avatar customisation." +

    +
    + +

    XASECO:

    + +
    +
    +This page is also the initial home to XASECO, which stands +for Xymph's ASECO and is the new name for the system formerly +known as ASECO/RASP for classic TMN. It now supports TrackMania +Forever as well. ASECO is an abbreviation of "Automatic +SErver COntrol", a system for TrackMania Original, +Sunrise and (especially) Nations and Forever servers +to keep track of player records and provide useful player and admin +commands. RASP is a sizable set of plugins for ASECO that adds +server ranks, a track jukebox, a karma voting system, and lots more. + +

    +As evidenced by the many TrackMania (Nations) servers running it, +the ASECO/RASP system for TMO/TMS/TMN remained a very popular package +to keep track of records and offer players various useful commands +and features. But its original authors have moved on to TM United, +so I have been working at the system on my own since May 2007 to +improve and expand it. The number of changes is so large that I +decided (somewhat presumptuously perhaps) to call this system XASECO +version 0.8, subsequently updated to v1.16, many major +releases since ASECO 0.61b and RASP 1.5 that together combined into +version 0.7. TrackMania Forever is also supported since XASECO v0.96. + +

    +I don't believe there's a point in running a barebones (X)ASECO system +without at least the RASP plugins because those add so many basic +and useful features and commands which players enjoy, so they are +not released separately like before but as one combined system. The +remaining plugins included in this v1.16 release (Mistral's idle-kick, +and Jfreu's plugin) are however optional, as are the series of new +plugins I developed myself. My overall goal remained to stick to +'core functionality' as much as possible, rather than include into +the base system any of the variety of 'niche' plugins that exist out +there, while still providing a complete and integrated solution for +server control. + +

    +For a high-level outline of all of XASECO's features and plugins, +see the Overview page. For screenshots +of many of the TMF interface features, see this preview +thread. + +

    +For a comprehensive overview of the new stuff, see the v0.8 initial release notes, v0.81 - v0.95 oldest release notes, v0.96 - v1.03 older release notes, and +v1.04 - v1.16 current release notes. + +

    +And here is a complete overview of all available +commands in HTML and Word. + +

    +
    + + +

    Content:

    + + + +

    Installation:

    + +
      +
    • New installation of v1.16:

      +See the XASECO Installation page on XAseco.org, +or go directly to the classic TMN & XASECO quick start guide or TMF & XASECO quick start guide. +

      + +For historical info, you can consult the old ASECO v0.61b readme.pdf +and corresponding Tutorial, as well as the old RASP v1.5 readme.txt, +in Server Resources below. +

      + + +
    • Upgrading from v1.15b to v1.16:

      +See the XASECO Upgrade page on XAseco.org for general instructions. +

      + +The following files were updated in v1.16: aseco.php, dedimania.xml, +includes/GbxRemote.bem.php, GbxRemote.inc.php, basic.inc.php, +gbxdatafetcher.inc.php, rasp.funcs.php, rasp.settings.php, +tmxinfosearcher.inc.php, types.inc.php, jfreu.config.php, +plugins/chat.dedimania.php, plugin.checkpoints.php, +plugin.dedimania.php, plugin.localdatabase.php, +plugin.rasp_nextmap.php. + +

      + +Important: +
        +
      • To register your server with the central Dedimania +database, you must copy the login and password values in the +<masterserver_account> section from your +server's dedicated.cfg (TMN) or dedicated_cfg.txt (TMF) file into +the corresponding section of the dedimania.xml file, and add +the 3-character nation abbreviation. Instead of the password +you can also use the community code for your server by using +the server login/password on the official +site for your game (TMO/TMS/TMN) or on this page for +TMF. +
      • Open port 8002 on your firewall/router for communication +with the central Dedimania server. +
      • In the zip file, all *.XML and *.PHP config files are located +inside the newinstall/ directory. This means that you can (and +have to) unzip the download and replace all the PHP code files, without +worrying about overwriting your customized config files. However, for +every XML/PHP config file that was updated (see above), you must +replace your version with the one from the newinstall/ directory, or +compare them and add any new/changed configuration settings +to your version to insure the system remains working correctly. +
      • For a new installation, go into the newinstall/ directory and move +all *.XML files into the main directory (next to aseco.php), all *.PHP +files into the includes/ directory, and Aseco.bat|Aseco.sh|AsecoF.sh +also into the main directory. +
      +
      + +
    • Upgrading from v0.7 to v0.8+ and from v0.8-v1.14 to v1.15b:

      + +See the archived Upgrade notes. +
    + + +

    Configuration options:

    + + + + +

    Version Notes:

    + + + + +

    Download:

    + + + + +

    Feedback/questions:

    + + + +

    Thanks:

    + + + +

    Links:

    + + + +
    +
    +
    +Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 26-Jul-2013 +
    + + diff --git a/docker-xaseco/xaseco/DOCS/overview.html b/docker-xaseco/xaseco/DOCS/overview.html new file mode 100644 index 0000000..3459aae --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/overview.html @@ -0,0 +1,331 @@ + + + +TrackMania Nations - XASECO overview + + + + + + + + + + +

    +TrackMania Nations +


    + +

    XASECO Overview

    + +
      +
    1. Introduction:

      +XASECO is a server controller for TMF (both Nations and United) and TMN +written in PHP using a MySQL database. It should also still support +TMO and TMS but I'm unable to test that. The main XASECO program +aseco.php is merely a framework to handle communication with +the dedicated server, handle events, provide logging, and offer support +functionality. All user features and commands are provided by plugins. + +
    2. Plugins:

      +XASECO runs fine with an empty plugins.xml list, it just +won't do very much. To provide user functionality, add files from +plugins/ directory to the list. Some plugins are essential, such as +plugin.localdatabase.php for communication with the database, while +many others are optional. However, some plugins depend on others being +present (sometimes in the correct order in plugins.xml), so it's best +to preserve as much of the default plugins.xml list as needed. + +
    3. Configs:

      +Configuration files consist of .XML files in the main directory, as +well as some of the .PHP files in the includes/ directory. The most +important one is config.xml, containing many of the basic +settings, messages, and server and admin definitions. Other config +files are typically related to one specific plugin or set of plugins +(like RASP with rasp.xml and rasp.settings.php). + +
    4. Database:

      +Using the MySQL database requires the plugin.localdatabase.php +plugin configured via localdatabase.xml. It stores information +about tracks, players, local records, all finishes, karma (or old +style) votes, player ranks, and more. + +
    5. Help:

      +The main /help command provided by chat.help.php lists +all available chat commands in a chat message, while /helpall +generates a window that includes descriptions. Other plugins +sometimes include a help command (/helpdedi for Dedimania, +/helpvote for chat-based votes) and some commands include a +help option (e.g. /admin helpall, /list help, etc). + +
    6. Players:

      +Player connects and disconnects are handled by the main aseco.php +program and relayed to plugins via the onPlayerConnect and +onPlayerDisconnect events. The /players command is provided +by the chat.players.php plugin, while chat.players2.php +provides a couple of related commands. + +
    7. Local Records:

      +Local records are determined and stored by the +plugin.localdatabase.php plugin, and can be displayed +by the /recs command from the chat.records.php +plugin. Additional record rankings and listings are provided by +the chat.records2.php plugin, while chat.recrels.php +offers record relation commands. The maximum number of records +per track is defined in rasp.settings.php. + +
    8. Dedimania Records:

      +Dedimania records are determined and stored on the central Dedimania site +by the plugin.dedimania.php plugin, and can be displayed by the +/dedirecs command from the chat.dedimania.php plugin. +Additional record rankings and listings, as well as record relations, +are provided by the same plugin. All related configuration options +and messages are provided in dedimania.xml (the maximum number +of records per track should be kept at 30). Detailed information can +be found in the v0.95 release notes. + +
    9. Checkpoints:

      +Players can keep track of their relative performance at +each checkpoint with the /cps command, provided by the +plugin.checkpoints.php plugin. Normally it uses the player's +own (or any other) local record as reference, if checkpoints are +available for that record, and shows whether each crossing is faster +or slower in the CPS panel above the main racing time (TMF) or in +a brief pop-up window (TMN). If the Dedimania system is enabled, +the /dedicps command can be used to compare the times with the +player's own (or any other) Dedimania record instead. Both settings +are stored for all players, and reloaded upon every visit. More +information can be found in the v0.93, +v0.98 and v1.06 release notes. + +
    10. Anti-cheat checks:

      +When the plugin.checkpoints.php plugin is active, it also checks +checkpoints and finish times in several ways to reduce the chances of +cheated races resulting in local and Dedimania records. See the v1.05 release notes for more information. + +
    11. Rounds:

      +When the plugin.rounds.php plugin is active in Rounds, Team and +Cup (TMF) mode, a report with each player's finish time is shown after +each run. It serves no purpose in TimeAttack, Laps and Stunts mode. + +
    12. Track Lists & Jukebox:

      +The plugin.rasp_jukebox.php plugin provides the /list +command to list, search for and select from the available tracks +with a wide variety of options. Players can pick any track from the +list that hasn't been played recently, and add it to a jukebox of +tracks that's given precedence over tracks from the regular server +rotation. This is controlled by a number of configuration settings +in rasp.settings.php. + +
    13. TMX

      +Specific tracks can be downloaded and added directly from TMX with the +/add command (in plugin.rasp_jukebox.php) and /admin +add command (in chat.admin.php). Searching on TMX can be +done with /xlist, while the plugin.tmxinfo.php plugin +offers commands to display track information. More information +can be found in the v0.93 and v1.01 release notes. + +
    14. Karma:

      +Karma votes are an easy way for players to indicate via /++ +or /-- commands whether they like or don't like a track, +provided by the plugin.rasp_karma.php plugin and configured +by settings in rasp.settings.php. Some details can be found +in the v0.89 release notes. + +
    15. Ranks:

      +A player obtains a server rank by racing a minimum number (default 3, +configured in rasp.settings.php) of local ("ranked") records. +The average of a player's records over all server tracks determines +the ranking, managed by the plugin.rasp.php plugin with +plugin.rasp_nextrank.php providing a related command. + +
    16. Stats:

      +Player stats are displayed by the /stats command from +the chat.stats.php plugin, while server information is +provided by /server, /xaseco and /plugins from +chat.server.php. Related plugins chat.laston.php, +chat.songmod.php, chat.wins.php and +plugin.track.php offer associated commands. + +
    17. Tiered Admins:

      +The tiered admin system defines three ability levels: MasterAdmins +(specified in config.xml) who can run any admin command, and +Admins and Operators for whom the logins and abilities are specified +in adminops.xml. Typically, Operators are allowed to use +the fewest admin commands, and the abilities list extends beyond +the /admin command to special admin features of user commands +(such as jukeboxing tracks that were played too recently). Admins and +operators can be added and removed dynamically, or their abilities +updated, and adminops.xml will be automatically kept in sync. +See this table for the default list +of abilities, and detailed information can be found in the v0.88 release notes. + +
    18. Chat-based Votes:

      +Chat-based votes allow players to vote on actions that +regular CallVotes don't support, such as ending a round or +replaying a track. When enabled in rasp.settings.php, the +plugin.rasp_votes.php plugin disables CallVotes completely and +offers several voting commands that can be voted on with the /y +command (there is no /n counterpart), and on TMF also via the +vote panel provided by plugin.panels.php. Vote configuration +is done in votes.config.php, and detailed information can be +found in the v0.84 release notes. + +
    19. Public/Private Messages:

      +The plugin.rasp_chat.php and chat.me.php plugins +offer a wide variety of commands to quickly shout out a message to +other players. Players' regular chat lines are logged and displayed +by the plugin.chatlog.php plugin. Private messages from one +player to another can be send with the /pm command, from an +admin to a player (Cc-ing all other admins) with /pma, and +from one admin to all other admins with /admin pm. + +
    20. Muting/Ignoring:

      +Muting (the TMN term) or ignoring (TMF) implies that chat messages +from a specified player are suppressed and aren't readable for other +players in the chat window. Admins can maintain a global list of +muted/ignored players, which on TMF uses the built-in Ignore features +and on TMN a simulated suppression system. The latter system is +also used when an individual player mutes another player using the +commands provided in the plugin.muting.php plugin. See the v0.90 release notes for more information. + +
    21. Ban, Black- & Guestlist:

      +A number of /admin commands from chat.admin.php can be +used to warn (with a pop-up message) or kick players, and to punish +repeat offenders with a ban or blacklist. Bans work by IP address and +are tracked only by the dedicated server, and therefore get purged +by a server restart. Blacklists work by login and are stored in +the blacklist.txt file, and therefore persist across server +restarts. Logins on the guestlist can enter the server even when +the maximum number of players or spectators is reached or when the +server is passworded, and are preserved in the guestlist.txt +file. In addition to these built-in mechanisms, XASECO provides +a list of banned IP addresses in the bannedips.xml file +that also transcends server restarts, with more info in the v1.03 release notes. + +
    22. Player access

      +Besides the above local server mechanisms, player access can +also be managed with the global blacklist, and with +access control by nation (TMN) or zone (TMF). See the v1.09 release notes for details. Lastly, +the full Jfreu plugin (below) allows player limiting by rank. + +
    23. Admin Commands:

      +Other /admin commands control the game flow (ending a round, +skipping to the next or previous track, restarting or replaying +a track), affect the server settings (player/spectator limits, +TMF panels, etc), manage tracks and the jukebox, and much more. +Security of the admin commands and features against unauthorized +use can be warranted with a lock password and by specifying +authorized IP addresses for each admin login. See the v1.03 release notes for more information. + +
    24. Idlekick:

      +Inactive players and, optionally, spectators can be kicked +automatically at the end of each track after a specified number of +tracks by the mistral.idlekick.php plugin. Configuration +settings are included in the plugin itself. Some details can be +found in the v0.90 release notes. + +
    25. Auto TimeLimit:

      +With the plugin.autotime.php plugin enabled in TimeAttack mode, +the timelimit for each track is defined dynamically, based on the +track's author time and settings in autotime.xml. See the v1.04 release notes for details. + +
    26. Jfreu:

      +The Jfreu plugins jfreu.plugin.php and jfreu.chat.php +provide a variety of features, including player connect/disconnect +messages, informational messages, rank limiting (with VIP +exceptions), bad word filtering, and more. The first two features +are also available separately in the jfreu.lite.php +plugin in case the rest isn't needed. They are configured via +jfreu.config.php and changed options are automatically stored in +the jfreu/jfreu.config.xml file. More information is available +in the v0.91 release notes. + +
    27. TMF Music:

      +The plugin.musicserver.php plugin offers support for playing +server controlled music instead of the default game music and, +optionally, even overriding a track's own song. The /music +command includes a simple jukebox feature as well as admin options, +and .OGG comments (ID3 tags) can also be displayed. Configuration +is done in musicserver.xml, and more information can +be found in the v0.99, +v1.02 and v1.04 release notes. + +
    28. TMF Custom Rounds Points:

      +With the plugin.rpoints.php plugin enabled, admins can select +alternative points systems in Rounds mode instead of the standard +10,6,4,3,2,1. Various common systems (like Formula 1 GP and MotoGP) +are included, and completely custom systems can be defined as well. +See the v1.04 release notes for +details. + +
    29. TMF Styles & Panels:

      +The main output window can be dynamically customized with a large +number of style templates from the styles/ subdirectory, managed +by the plugin.style.php plugin. The plugin.panels.php +plugin provides four panels: the admin panel to easily activate a +handful of the most commonly used admin commands; the donate panel +to easily initiate a donation with a variety of copper amounts; the +records panel to display a track's relevant records; and a temporary +vote panel to easily respond to chat-based votes. Each panel +can be dynamically customized with a number of templates from the +panels/ subdirectory. All players' style and panel preferences +are stored, and reloaded upon every visit. Detailed information +can be found in the v1.00, +v1.01 and v1.06 release notes. + +
    30. TMF Stats Panels:

      +Instead of the rank chat messages at the end of each track, on TMF +a personal stats panel can be displayed for each player during the +scoreboard. The stats are: server rank, record average, records total, +wins total, session play time, and donation total (on TMUF servers). +Details are included in the v1.06 release +notes. + +
    31. TMF Message Window:

      +When enabled via plugin.msglog.php, the system message window +near the top of the screen can be used to temporarily display a wide +variety of global messages that normally flood the chat window. +These include new/improved/equalled records, record reports +before and after each track, rounds reports, Jfreu Info messages +and several more, all individually configured in their pertaining +config files. See the v1.02 and v1.03 release notes for more information. + +
    32. TMUF Coppers:

      +Copper transactions are managed by the plugin.donate.php +plugin, allowing players to donate coppers to the server, and +admins (with the appropriate ability) to pay coppers to any +other login (including another server). More information is +available in the v0.99 and v1.01 release notes. + +
    + +
    +
    +
    +Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 08-Aug-2010 +
    + + diff --git a/docker-xaseco/xaseco/DOCS/quickstart_tmf.html b/docker-xaseco/xaseco/DOCS/quickstart_tmf.html new file mode 100644 index 0000000..a9a7423 --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/quickstart_tmf.html @@ -0,0 +1,254 @@ + + + +TrackMania Forever - TMF & XASECO quick start guide + + + + + + + + + + +

    +TrackMania Forever +


    + +

    Quick Start Guide:

    + +This is step-by-step guide to setting up a TrackMania Forever (TMF) dedicated server with XASECO for those lost in or confused by the available readme's and tutorials, and those just too lazy to read them. ;-) + +

    +It's written for Linux but should largely apply to Windows too, and your system needs to have a working MySQL 4.x or 5.x and PHP 5.x setup. To manage the database, enter the MySQL commands in the mysql command prompt, PhpMyAdmin or another tool of your choice. You need to run the TMF server and XASECO on the same machine, running them on separate machines is beyond the scope of this guide. + +

    +First, the dedicated server: + +

      + +
    1. Ideally, the TMF server and XASECO run under a user account rather than root, so create a user e.g. "tmf" with home directory "/home/tmf". + +
    2. Login as (or switch to) user "tmf" so that all files created down the road receive the correct ownership and permissions. + +
    3. The TMF server zip doesn't have a top-level directory, so create one first to keep all the server files together, e.g.:
      +
      +    mkdir TMF
      +    cd TMF
      +
      + +
    4. Download and unzip the latest TMF server (TrackmaniaServer_2011-02-21.zip) into this TMF/ directory. + +
    5. Create a dedicated login: +
        +
      1. for TM Nations Forever, use your game client to create a new dedicated server login (note that this server won't be able to use coppers). +
      2. for TM United Forever, log into the TMF Player Page using your player login and password, and create a dedicated server login and password (different from your player password) after entering your full game key. +
      + +
    6. Edit TMF/GameData/Config/dedicated_cfg.txt: +
        +
      1. In the <authorization_levels> section, change all three passwords, but do not change the names (SuperAdmin, Admin, User). +
      2. In the <masterserver_account> section, enter the server <login> obtained above with its own <password>, and optionally the last three characters of your player key in <validation_key> if you want your TMUF server to be able to use coppers. +
      3. In the <server_options> section, give your server a <name> and configure the other options to your liking. +
      4. In the <system_config> section, set <connection_uploadrate> and <connection_downloadrate> to values closest to your connection's speed (in Kbps), and remember the three port numbers (default: 2350 for the server, 3450 for P2P and 5000 for XMLRPC; these can be changed to other free port numbers, but it's recommended to keep the defaults at least until your server comes online successfully). +
      5. For a TMNF server, <packmask> must be set to 'nations' or 'stadium'. For TMUF it can be empty or set to 'united', or to one environment like 'desert', 'island', etc, or to the three environments of the older games with 'original' or 'sunrise'. +
      + +
    7. Choose and edit a match settings file, e.g. TMF/GameData/Tracks/MatchSettings/Nations/NationsBlue.txt, to your liking (see the server readme for details). + +
    8. On your firewall/router, open the server port 2350 and the P2P port 3450 for both UDP and TCP traffic, but not the XMLRPC port 5000. For the Dedimania system in XASECO port 8002 needs to be open as well. For more information on this, PortForward.com may be useful. + +
    9. A standard startup script is no longer included in the zip, so create one looking like this in RunTrackmaniaNations.sh (and make it executable with chmod +x RunTrackmaniaNations.sh):
      +
      +    ./TrackmaniaServer /game_settings=MatchSettings/Nations/NationsBlue.txt /dedicated_cfg=dedicated_cfg.txt
      +
      + or in RunTrackmaniaNations.bat:
      +
      +    TrackmaniaServer.exe /game_settings=MatchSettings/Nations/NationsBlue.txt /dedicated_cfg=dedicated_cfg.txt
      +
      + Options /internet and /autoquit are now default, and the /game=... option (from TMN) is no longer needed due to the <packmask> setting. + +
    10. Start the server:
      +
      +    cd ~/TMF/
      +    ./RunTrackmaniaNations.sh (or RunTrackmaniaNations.bat)
      +
      + You should see output like the following:
      +
      +Starting TmForever v2011-02-21...
      +Initializing...
      +Configuration file : dedicated_cfg.txt
      +Loading system configuration...
      +...system configuration loaded
      +Loading cache...
      +...OK
      +Listening for xml-rpc commands on port 5000.
      +Trackmania server daemon started with pid=13512 (parent=13483).
      +
      + If you get a Segmentation Fault here, the server cannot create files/directories due to ownership/permission problems (perhaps you forgot step 2?). + +
    11. The first time the server starts, it creates a Default.SystemConfig.Gbx file as well as the blacklist.txt and guestlist.txt files in the GameData/Config/ directory, and a number of directories like Logs/, GameData/Cache/, GameData/Profiles/, GameData/Scores/, GameData/Tracks/Campaigns/, GameData/Tracks/Replays/, GameData/Tracks/Challenges/Downloaded/ and GameData/Tracks/Challenges/My Challenges/. The files in the Logs/ directory are useful to monitor your server's activity. + +
    12. Start your TMF client and check in the Internet server browser that the server is running in your zone with your chosen server name and the Nadeo Advanced tracks. + +
    13. Join your server, and have a friend join from elsewhere on the Internet, to verify that the server is accessible. + +
    14. Ignore the private network warning that is always logged in ConsoleLog.*.txt. + +
    15. To start & stop your TMF server on Linux more easily, you can use this start-up script. + +
    16. Collect your own tracks in GameData/Tracks/Challenges/My Challenges/, copy and edit a new match settings file in GameData/Tracks/MatchSettings/ that lists those tracks, use that file in RunTrackmaniaNations.sh (or RunTrackmaniaNations.bat), and restart your server. It should now be running your track selection. Congratulations. :-) + +
    + + +Secondly, the XASECO system: + +
      + +
    1. Create the XASECO database in MySQL (default "aseco" but any other name is okay too):
      +
      +    CREATE DATABASE aseco;
      +
      + Also create a separate user (e.g. "tmf") in MySQL with a password, and grant this user all rights to the "aseco" database. The basic MySQL commands are:
      +
      +    CREATE USER 'tmf'@'localhost';
      +    SET PASSWORD FOR 'tmf'@'localhost' = password('password');
      +    GRANT all ON aseco.* TO 'tmf'@'localhost'; +
      + +
    2. Login as (or switch to) user "tmf" so that all files created down the road receive the correct ownership and permissions. + +
    3. Download and unzip XASECO (latest version) into the "/home/tmf/" directory too, the default path will be xaseco/ so that future releases can be unpacked into the same directory tree.
      + In the zip file, all *.XML and config files are located inside the newinstall/ directory. Go into the newinstall/ directory and move all *.XML files into the main directory (next to aseco.php), and move all *.PHP files into the includes/ directory.
      + Also, move AsecoF.sh (on Linux) or Aseco.bat (on Windows) into the main directory and adjust it to your situation if necessary. + +
    4. Setting up the database tables in MySQL is done automatically the first time XASECO runs, but if you prefer you can do it manually in advance:
      +
      +    USE aseco;
      +    SOURCE /home/tmf/xaseco/localdb/aseco.sql;
      +    SOURCE /home/tmf/xaseco/localdb/rasp.sql;
      +    SOURCE /home/tmf/xaseco/localdb/extra.sql; +
      + +
    5. Edit xaseco/localdatabase.xml: +
        +
      1. Replace YOUR_MYSQL_LOGIN with the MySQL user you created above, e.g. tmf. +
      2. Replace YOUR_MYSQL_PASSWORD with the MySQL password you set above. +
      3. Use the same database name as you created above, e.g. aseco. +
      4. localhost is your own machine, so the server option is okay. +
      + +
    6. Edit xaseco/config.xml: +
        +
      1. In the <masteradmins> section, uncomment and replace YOUR_MASTERADMIN_LOGIN with your player login, and add further logins for other players you want to grant all XASECO admin rights. +
      2. In the <tmserver> section, replace YOUR_SUPERADMIN_PASSWORD with the password you chose for SuperAdmin in dedicated_cfg.txt (TMF step 6a above) but do not change the SuperAdmin login itself. +
      3. The <port> field should contain the same XMLRPC port number you chose in dedicated_cfg.txt (TMF step 6d above), default 5000. +
      4. IP 127.0.0.1 is your own machine again, so that option is okay too. +
      + +
    7. Edit xaseco/adminops.xml: +
        +
      1. In the <admins> section, uncomment and replace YOUR_ADMIN_LOGIN with an admin's login, and add further logins for other players you want to grant partial XASECO admin rights. Or leave the <tmlogin> entry commented out if there are none. +
      2. In the <operators> section, uncomment and replace YOUR_OPERATOR_LOGIN with an operator's login, and add further logins for other players you want to grant XASECO operator rights. Or leave the <tmlogin> entry commented out if there are none. +
      + +
    8. Edit xaseco/dedimania.xml if you want to use the Dedimania world records system: +
        +
      1. In the <masterserver_account> section, replace YOUR_SERVER_LOGIN and YOUR_SERVER_PASSWORD with the <login> and <password> values from your dedicated_cfg.txt file. +
      2. Also, replace YOUR_SERVER_NATION with your 3-character nation abbreviation. +
      3. Instead of the password you can also enter the community code for your server which can be obtained by using the server login/password on this page for TMF. +
      4. To disable Dedimania support, remove or comment out the chat.dedimania.php and plugin.dedimania.php entries in plugins.xml. +
      + +
    9. Start the XASECO system:
      +
      +    cd ~/xaseco/
      +    ./Aseco.sh (or Aseco.bat)
      +
      + You won't see output, but logfile.txt should contain something like the following:
      +
      +[XAseco] PHP Version is 5.3.x on Linux
      +[XAseco] Load settings [config.xml]
      +[XAseco] Load admin/ops lists [adminops.xml]
      +[XAseco] Load banned IPs list [bannedips.xml]
      +[XAseco] Load plugins list [plugins.xml]
      +[XAseco] Load plugin [plugin.localdatabase.php]
      +[XAseco] Load plugin [plugin.rounds.php]
      +[...snip plugins...]
      +[XAseco] Load plugin [jfreu.plugin.php]
      +[XAseco] Load plugin [mistral.idlekick.php]
      +[XAseco] Try to connect to TM dedicated server on 127.0.0.1:5000 timeout 180s
      +[XAseco] Try to authenticate with username 'SuperAdmin' and password 'PASSWORD'
      +[XAseco] Connection established successfully!
      +[Local DB] Load settings file
      +[Local DB] Try to connect to MySQL server on 'localhost' with database 'aseco'
      +[Local DB] MySQL Server Version is 5.1.56-log
      +[RASP] Cleaning up unused data
      +*-*-*-*-*-* RASP is running! *-*-*-*-*-*
      +|...Loading Settings
      +|...Loaded!
      +|...Checking database structure
      +|...Structure OK!
      +|...Calculating ranks
      +|...Done!
      +[04/xx,xx:11:17] Load default style [styles/DarkBlur.xml]
      +[04/xx,xx:11:17] Load default admin panel [panels/AdminBelowChat.xml]
      +[04/xx,xx:11:17] Load default donate panel [panels/DonateBelowCPList.xml]
      +[04/xx,xx:11:17] Load default records panel [panels/RecordsRightBottom.xml]
      +[04/xx,xx:11:17] Load default vote panel [panels/VoteBelowChat.xml]
      +[04/xx,xx:11:17] ************* (Dedimania) *************
      +[04/xx,xx:11:17] * Dataserver connection on Dedimania ...
      +[04/xx,xx:11:17] * Try connection on http://dedimania.net:8002/Dedimania ...
      +[04/xx,xx:11:18] Webaccess (dedimania.net:80): send: deflate, receive: gzip
      +[04/xx,xx:11:18] * Connection and status ok! :)
      +[04/xx,xx:11:18] * NEWS (Dedimania, 08/05): news
      +[04/xx,xx:11:18] ------------- (Dedimania) -------------
      +[04/xx,xx:11:18] Load stats panel [panels/StatsNations.xml]
      +[04/xx,xx:11:18] Load auto timelimit config [autotime.xml]
      +###############################################################################
      +  XASECO v1.1x running on 127.0.0.1:5000
      +  Name   : YOUR SERVER NAME - YOUR_SERVER_LOGIN
      +  Game   : TmForever United - Stadium - TimeAttack
      +  Version: 2.11.26 / 2011-02-21
      +  Authors: Florian Schnell & Assembler Maniac
      +  Re-Authored: Xymph
      +###############################################################################
      +Begin Race
      +[04/xx,xx:11:18] track changed [none] >> [Pro - 38~74~75~89~93]
      +[04/xx,xx:11:18] currently no record on Pro - 38~74~75~89~93
      +
      + If you get an RPC Permission Error here, there is an XMLRPC port mismatch or the dedicated server isn't running (anymore). + +
    10. To start & stop your XASECO on Linux more easily, you can use this start-up script. + +
    11. Edit the configuration options to your liking, and restart XASECO. It should now be ready to manage tracks, receive players, and record... er... records. Congratulations. :-) +
    + +Finally, to run another server on the same machine: + +
      + +
    1. Basically follow the same steps above, but use a second user account (e.g. "tmf2"), another new server login/password in dedicated_cfg.txt, a separate server name, a second set of ports (e.g. 2351, 3451 and 5001), a new database (e.g. "aseco2"), optionally a second MySQL account (e.g. "tmf2"), and the corresponding updates in localdatabase.xml and config.xml. + +
    2. Don't use symbolic links in the GameData/ directory tree (e.g. to symlink the tracks from the first server to the second one), as the TMF server will crash without an error message. + +
    + +And for the last time, read the readme's and tutorials completely for a more thorough understanding of the entire setup. +

    + +
    +
    +Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 08-May-2013 +
    + + diff --git a/docker-xaseco/xaseco/DOCS/quickstart_tmn.html b/docker-xaseco/xaseco/DOCS/quickstart_tmn.html new file mode 100644 index 0000000..9fb4bde --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/quickstart_tmn.html @@ -0,0 +1,226 @@ + + + +TrackMania Nations - TMN & XASECO quick start guide + + + + + + + + + + +

    +TrackMania Nations +


    + +

    Quick Start Guide:

    + +This is step-by-step guide to setting up a classic TrackMania Nations (TMN) dedicated server with XASECO for those lost in or confused by the available readme's and tutorials, and those just too lazy to read them. ;-) + +

    +It's written for Linux but should largely apply to Windows too, and your system needs to have a working MySQL 4.x or 5.x and PHP 5.x setup. To manage the database, enter the MySQL commands in the mysql command prompt, PhpMyAdmin or another tool of your choice. You need to run the TMN server and XASECO on the same machine, running them on separate machines is beyond the scope of this guide. + +

    +First, the dedicated server: + +

      + +
    1. Ideally, the TMN server and XASECO run under a user account rather than root, so create a user e.g. "tmn" with home directory "/home/tmn". + +
    2. Login as (or switch to) user "tmn" so that all files created down the road receive the correct ownership and permissions. + +
    3. Download and unzip the final TMN server (TmDedicatedServer_2006-05-30.zip) into this home directory, the default path will be TmDedicatedServer/. + +
    4. Edit TmDedicatedServer/dedicated.cfg: +
        +
      1. In the <authorization_levels> section, change all three passwords, but do not change the names (SuperAdmin, Admin, User). +
      2. In the <masterserver_account> section, enter a new server <login> (different from your player login) with its own <password> (again different) and an appropriate and valid three-letter <nation>. If you're not sure your chosen login is new, use your TMN client to test it first, in both cases the account will be registered automatically with the masterserver. +
      3. In the <server_options> section, give your server a <name> and configure the other options to your liking. +
      4. In the <system_config> section, set <connection_type> to a value closest to your connection's speed (see the server readme for correct values), and remember the three port numbers (default: 2350 for the server, 3450 for P2P and 5000 for XMLRPC; these can be changed to other free port numbers, but it's recommended to keep the defaults at least until your server comes online successfully). +
      + +
    5. Edit the default match settings file TmDedicatedServer/GameData/Tracks/MatchSettings/Internet/AdvancedTraining.txt to your liking (again, see the server readme for details). + +
    6. On your firewall/router, open the server port 2350 and the P2P port 3450 for both UDP and TCP traffic, but not the XMLRPC port 5000. For the Dedimania system in XASECO port 8002 needs to be open as well. For more information on this, PortForward.com may be useful. + +
    7. Start the server:
      +
      +    cd ~/TmDedicatedServer/
      +    ./RunTrackmaniaNations.sh (or RunTrackmaniaNations.bat)
      +
      + You should see output like the following:
      +
      +Starting TmNationsESWC v2006-05-30...
      +Unable to open '/home/tmn/TmDedicatedServer/Default.SystemConfig.GbxInitializing...
      +Configuration file : dedicated.cfg
      +Loading system configuration...
      +...system configuration loaded
      +Loading cache...
      +...OK
      +Listening for xml-rpc commands on port 5000.
      +Trackmania server daemon started with pid=28936 (parent=28935).
      +
      + If you get a Segmentation Fault here, the server cannot create files/directories due to ownership/permission problems (perhaps you forgot step 2?). + +
    8. The first time the server starts, it creates that Default.SystemConfig.Gbx file, as well as the blacklist.txt and guestlist.txt files and a number of directories like Logs/, GameData/Cache/, GameData/Profiles/, GameData/Scores/, GameData/Tracks/Campaigns/, GameData/Tracks/Replays/, GameData/Tracks/Challenges/Downloaded/ and GameData/Tracks/Challenges/My Challenges/. The files in the Logs/ directory are useful to monitor your server's activity. + +
    9. Start your TMN client and check in the Internet server browser that the server is running in your Nation with your chosen server name and the Nadeo Advanced tracks. + +
    10. Join your server, and have a friend join from elsewhere on the Internet, to verify that the server is accessible. + +
    11. Ignore the private network warning that is always logged in ConsoleLog.*.txt. + +
    12. To start & stop your TMN server on Linux more easily, you can use this start-up script. + +
    13. Collect your own tracks in GameData/Tracks/Challenges/My Challenges/, copy and edit a new match settings file in GameData/Tracks/MatchSettings/Internet/ that lists those tracks, use that file in RunTrackmaniaNations.sh (or RunTrackmaniaNations.bat), and restart your server. It should now be running your track selection. Congratulations. :-) + +
    + + +Secondly, the XASECO system: + +
      + +
    1. Create the XASECO database in MySQL (default "aseco" but any other name is okay too):
      +
      +    CREATE DATABASE aseco;
      +
      + Also create a separate user (e.g. "tmn") in MySQL with a password, and grant this user all rights to the "aseco" database. The basic MySQL commands are:
      +
      +    CREATE USER 'tmn'@'localhost';
      +    SET PASSWORD FOR 'tmn'@'localhost' = password('password');
      +    GRANT all ON aseco.* TO 'tmn'@'localhost'; +
      + +
    2. Login as (or switch to) user "tmn" so that all files created down the road receive the correct ownership and permissions. + +
    3. Download and unzip XASECO (latest version) into the "/home/tmn/" directory too, the default path will be xaseco/ so that future releases can be unpacked into the same directory tree.
      + In the zip file, all *.XML and config files are located inside the newinstall/ directory. Go into the newinstall/ directory and move all *.XML files into the main directory (next to aseco.php), and move all *.PHP files into the includes/ directory.
      + Also, move Aseco.sh (on Linux) or Aseco.bat (on Windows) into the main directory and adjust it to your situation if necessary. + +
    4. Setting up the database tables in MySQL is done automatically the first time XASECO runs, but if you prefer you can do it manually in advance:
      +
      +    USE aseco;
      +    SOURCE /home/tmn/xaseco/localdb/aseco.sql;
      +    SOURCE /home/tmn/xaseco/localdb/rasp.sql;
      +    SOURCE /home/tmn/xaseco/localdb/extra.sql; +
      + +
    5. Edit xaseco/localdatabase.xml: +
        +
      1. Replace YOUR_MYSQL_LOGIN with the MySQL user you created above, e.g. tmn. +
      2. Replace YOUR_MYSQL_PASSWORD with the MySQL password you set above. +
      3. Use the same database name as you created above, e.g. aseco. +
      4. localhost is your own machine, so the server option is okay. +
      + +
    6. Edit xaseco/config.xml: +
        +
      1. In the <masteradmins> section, uncomment and replace YOUR_MASTERADMIN_LOGIN with your player login, and add further logins for other players you want to grant all XASECO admin rights. +
      2. In the <tmserver> section, replace YOUR_SUPERADMIN_PASSWORD with the password you chose for SuperAdmin in dedicated.cfg (TMN step 4a above) but do not change the SuperAdmin login itself. +
      3. The <port> field should contain the same XMLRPC port number you chose in dedicated.cfg (TMN step 4d above), default 5000. +
      4. IP 127.0.0.1 is your own machine again, so that option is okay too. +
      + +
    7. Edit xaseco/adminops.xml: +
        +
      1. In the <admins> section, uncomment and replace YOUR_ADMIN_LOGIN with an admin's login, and add further logins for other players you want to grant partial XASECO admin rights. Or leave the <tmlogin> entry commented out if there are none. +
      2. In the <operators> section, uncomment and replace YOUR_OPERATOR_LOGIN with an operator's login, and add further logins for other players you want to grant XASECO operator rights. Or leave the <tmlogin> entry commented out if there are none. +
      + +
    8. Edit xaseco/dedimania.xml if you want to use the Dedimania world records system: +
        +
      1. In the <masterserver_account> section, replace YOUR_SERVER_LOGIN and YOUR_SERVER_PASSWORD with the <login> and <password> values from +your dedicated.cfg file. +
      2. Also, replace YOUR_SERVER_NATION with the <nation> value. +
      3. Instead of the password you can also enter the community code for your server which can be obtained by using the server login/password on the official site for your game (TMO/TMS/TMN). +
      4. To disable Dedimania support, remove or comment out the chat.dedimania.php and plugin.dedimania.php entries in plugins.xml. +
      + +
    9. Start the XASECO system:
      +
      +    cd ~/xaseco/
      +    ./Aseco.sh (or Aseco.bat)
      +
      + You won't see output, but logfile.txt should contain something like the following:
      +
      +[XAseco] PHP Version is 5.3.x on Linux
      +[XAseco] Load settings [config.xml]
      +[XAseco] Load admin/ops lists [adminops.xml]
      +[XAseco] Load banned IPs list [bannedips.xml]
      +[XAseco] Load plugins list [plugins.xml]
      +[XAseco] Load plugin [plugin.localdatabase.php]
      +[XAseco] Load plugin [plugin.rounds.php]
      +[...snip plugins...]
      +[XAseco] Load plugin [jfreu.plugin.php]
      +[XAseco] Load plugin [mistral.idlekick.php]
      +[XAseco] Try to connect to TM dedicated server on 127.0.0.1:5000 timeout 180s
      +[XAseco] Try to authenticate with username 'SuperAdmin' and password 'PASSWORD'
      +[XAseco] Connection established successfully!
      +[Local DB] Load settings file
      +[Local DB] Try to connect to MySQL server on 'localhost' with database 'aseco'
      +[Local DB] MySQL Server Version is 5.1.56-log
      +[RASP] Cleaning up unused data
      +*-*-*-*-*-* RASP is running! *-*-*-*-*-*
      +|...Loading Settings
      +|...Loaded!
      +|...Checking database structure
      +|...Structure OK!
      +|...Calculating ranks
      +|...Done!
      +[12/xx,xx:11:17] ************* (Dedimania) *************
      +[12/xx,xx:11:17] * Dataserver connection on Dedimania ...
      +[12/xx,xx:11:17] * Try connection on http://dedimania.net:8002/Dedimania ...
      +[12/xx,xx:11:18] Webaccess (dedimania.net:80): send: deflate, receive: gzip
      +[12/xx,xx:11:18] * Connection and status ok! :)
      +[12/xx,xx:11:18] * NEWS (Dedimania, 08/05): news
      +[12/xx,xx:11:18] ------------- (Dedimania) -------------
      +[12/xx,xx:11:18] Load auto timelimit config [autotime.xml]
      +###############################################################################
      +  XASECO v1.1x running on 127.0.0.1:5000
      +  Name   : YOUR SERVER NAME
      +  Game   : TmNationsESWC - TimeAttack
      +  Version: 0.1.7.4 / 2006-05-30
      +  Authors: Florian Schnell & Assembler Maniac
      +  Re-Authored: Xymph
      +###############################################################################
      +Begin Race
      +[12/xx,xx:11:18] track changed [none] >> [Pro - 38~74~75~89~93]
      +[12/xx,xx:11:18] currently no record on Pro - 38~74~75~89~93
      +
      + If you get an RPC Permission Error here, there is an XMLRPC port mismatch or the dedicated server isn't running (anymore). + +
    10. To start & stop your XASECO on Linux more easily, you can use this start-up script. + +
    11. Edit the configuration options to your liking, and restart XASECO. It should now be ready to manage tracks, receive players, and record... er... records. Congratulations. :-) +
    + +Finally, to run another server on the same machine: + +
      + +
    1. Basically follow the same steps above, but use a second user account (e.g. "tmn2"), another new server login/password in dedicated.cfg, a separate server name, a second set of ports (e.g. 2351, 3451 and 5001), a new database (e.g. "aseco2"), optionally a second MySQL account (e.g. "tmn2"), and the corresponding updates in localdatabase.xml and config.xml. + +
    2. Don't use symbolic links in the GameData/ directory tree (e.g. to symlink the tracks from the first server to the second one), as the TMN server will crash without an error message. + +
    + +And for the last time, read the readme's and tutorials completely for a more thorough understanding of the entire setup. +

    + +
    +
    +Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 08-May-2013 +
    + + diff --git a/docker-xaseco/xaseco/DOCS/repairnations.php b/docker-xaseco/xaseco/DOCS/repairnations.php new file mode 100644 index 0000000..ac95b26 --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/repairnations.php @@ -0,0 +1,432 @@ +#!/usr/bin/php -q + + + if (!mysql_connect('localhost','YOUR_MYSQL_LOGIN','YOUR_MYSQL_PASSWORD')) { + echo "could not connect\n"; + exit; + } + if (!mysql_select_db('aseco')) { + echo "could not select\n"; + exit; + } + + // get all unique Nation strings + $query = 'SELECT Nation FROM players'; + $resply = mysql_query($query); + + $nations = array(); + if (mysql_num_rows($resply) > 0) { + while ($rowply = mysql_fetch_object($resply)) { + $nations[] = $rowply->Nation; + } + mysql_free_result($resply); + } else { + echo "no players!\n"; + } + $uniques = array_unique($nations); + + echo 'Unique nations: ' . count($uniques) . "\n\n"; + + $count = 0; + foreach ($uniques as $oldnation) { + if (strlen($oldnation) == 0) + $newnation = 'OTH'; // default OTH(ER) for empty nations + // check for full, capitalized country name + elseif (strlen($oldnation) > 3) + $newnation = mapCountry($oldnation); + // check for trunctated, capitalized country name + elseif ($oldnation != strtoupper($oldnation)) + $newnation = mapAbbrev($oldnation); + else // already all-caps abbreviation + continue; + + // update Nation with 3-letter abbreviation + $query = 'UPDATE players + SET Nation="' . $newnation . '" + WHERE Nation="' . $oldnation . '"'; + $result = mysql_query($query); + $count++; + } + echo 'Updated nations: ' . $count . "\n"; + +/** + * Map country names to 3-letter Nation abbreviations + * Created by Xymph + * Based on http://en.wikipedia.org/wiki/List_of_IOC_country_codes + */ +function mapCountry($country) { + + $nations = array( + 'Afghanistan' => 'AFG', + 'Albania' => 'ALB', + 'Algeria' => 'ALG', + 'Andorra' => 'AND', + 'Angola' => 'ANG', + 'Argentina' => 'ARG', + 'Armenia' => 'ARM', + 'Aruba' => 'ARU', + 'Australia' => 'AUS', + 'Austria' => 'AUT', + 'Azerbaijan' => 'AZE', + 'Bahamas' => 'BAH', + 'Bahrain' => 'BRN', + 'Bangladesh' => 'BAN', + 'Barbados' => 'BAR', + 'Belarus' => 'BLR', + 'Belgium' => 'BEL', + 'Belize' => 'BIZ', + 'Benin' => 'BEN', + 'Bermuda' => 'BER', + 'Bhutan' => 'BHU', + 'Bolivia' => 'BOL', + 'Bosnia&Herzegovina' => 'BIH', + 'Botswana' => 'BOT', + 'Brazil' => 'BRA', + 'Brunei' => 'BRU', + 'Bulgaria' => 'BUL', + 'Burkina Faso' => 'BUR', + 'Burundi' => 'BDI', + 'Cambodia' => 'CAM', + 'Cameroon' => 'CAR', // actually CMR + 'Canada' => 'CAN', + 'Cape Verde' => 'CPV', + 'Central African Republic' => 'CAF', + 'Chad' => 'CHA', + 'Chile' => 'CHI', + 'China' => 'CHN', + 'Chinese Taipei' => 'TPE', + 'Colombia' => 'COL', + 'Congo' => 'CGO', + 'Costa Rica' => 'CRC', + 'Croatia' => 'CRO', + 'Cuba' => 'CUB', + 'Cyprus' => 'CYP', + 'Czech Republic' => 'CZE', + 'Czech republic' => 'CZE', + 'DR Congo' => 'COD', + 'Denmark' => 'DEN', + 'Djibouti' => 'DJI', + 'Dominica' => 'DMA', + 'Dominican Republic' => 'DOM', + 'Ecuador' => 'ECU', + 'Egypt' => 'EGY', + 'El Salvador' => 'ESA', + 'Eritrea' => 'ERI', + 'Estonia' => 'EST', + 'Ethiopia' => 'ETH', + 'Fiji' => 'FIJ', + 'Finland' => 'FIN', + 'France' => 'FRA', + 'Gabon' => 'GAB', + 'Gambia' => 'GAM', + 'Georgia' => 'GEO', + 'Germany' => 'GER', + 'Ghana' => 'GHA', + 'Greece' => 'GRE', + 'Grenada' => 'GRN', + 'Guam' => 'GUM', + 'Guatemala' => 'GUA', + 'Guinea' => 'GUI', + 'Guinea-Bissau' => 'GBS', + 'Guyana' => 'GUY', + 'Haiti' => 'HAI', + 'Honduras' => 'HON', + 'Hong Kong' => 'HKG', + 'Hungary' => 'HUN', + 'Iceland' => 'ISL', + 'India' => 'IND', + 'Indonesia' => 'INA', + 'Iran' => 'IRI', + 'Iraq' => 'IRQ', + 'Ireland' => 'IRL', + 'Israel' => 'ISR', + 'Italy' => 'ITA', + 'Ivory Coast' => 'CIV', + 'Jamaica' => 'JAM', + 'Japan' => 'JPN', + 'Jordan' => 'JOR', + 'Kazakhstan' => 'KAZ', + 'Kenya' => 'KEN', + 'Kiribati' => 'KIR', + 'Korea' => 'KOR', + 'Kuwait' => 'KUW', + 'Kyrgyzstan' => 'KGZ', + 'Laos' => 'LAO', + 'Latvia' => 'LAT', + 'Lebanon' => 'LIB', + 'Lesotho' => 'LES', + 'Liberia' => 'LBR', + 'Libya' => 'LBA', + 'Liechtenstein' => 'LIE', + 'Lithuania' => 'LTU', + 'Luxembourg' => 'LUX', + 'Macedonia' => 'MKD', + 'Malawi' => 'MAW', + 'Malaysia' => 'MAS', + 'Mali' => 'MLI', + 'Malta' => 'MLT', + 'Mauritania' => 'MTN', + 'Mauritius' => 'MRI', + 'Mexico' => 'MEX', + 'Moldova' => 'MDA', + 'Monaco' => 'MON', + 'Mongolia' => 'MGL', + 'Montenegro' => 'MNE', + 'Morocco' => 'MAR', + 'Mozambique' => 'MOZ', + 'Myanmar' => 'MYA', + 'Namibia' => 'NAM', + 'Nauru' => 'NRU', + 'Nepal' => 'NEP', + 'Netherlands' => 'NED', + 'New Zealand' => 'NZL', + 'Nicaragua' => 'NCA', + 'Niger' => 'NIG', + 'Nigeria' => 'NGR', + 'Norway' => 'NOR', + 'Oman' => 'OMA', + 'Other Countries' => 'OTH', + 'Pakistan' => 'PAK', + 'Palau' => 'PLW', + 'Palestine' => 'PLE', + 'Panama' => 'PAN', + 'Paraguay' => 'PAR', + 'Peru' => 'PER', + 'Philippines' => 'PHI', + 'Poland' => 'POL', + 'Portugal' => 'POR', + 'Puerto Rico' => 'PUR', + 'Qatar' => 'QAT', + 'Romania' => 'ROM', // actually ROU + 'Russia' => 'RUS', + 'Rwanda' => 'RWA', + 'Samoa' => 'SAM', + 'San Marino' => 'SMR', + 'Saudi Arabia' => 'KSA', + 'Senegal' => 'SEN', + 'Serbia' => 'SCG', // actually SRB + 'Sierra Leone' => 'SLE', + 'Singapore' => 'SIN', + 'Slovakia' => 'SVK', + 'Slovenia' => 'SLO', + 'Somalia' => 'SOM', + 'South Africa' => 'RSA', + 'Spain' => 'ESP', + 'Sri Lanka' => 'SRI', + 'Sudan' => 'SUD', + 'Suriname' => 'SUR', + 'Swaziland' => 'SWZ', + 'Sweden' => 'SWE', + 'Switzerland' => 'SUI', + 'Syria' => 'SYR', + 'Taiwan' => 'TWN', + 'Tajikistan' => 'TJK', + 'Tanzania' => 'TAN', + 'Thailand' => 'THA', + 'Togo' => 'TOG', + 'Tonga' => 'TGA', + 'Trinidad and Tobago' => 'TRI', + 'Tunisia' => 'TUN', + 'Turkey' => 'TUR', + 'Turkmenistan' => 'TKM', + 'Tuvalu' => 'TUV', + 'Uganda' => 'UGA', + 'Ukraine' => 'UKR', + 'United Arab Emirates' => 'UAE', + 'United Kingdom' => 'GBR', + 'United States of America' => 'USA', + 'Uruguay' => 'URU', + 'Uzbekistan' => 'UZB', + 'Vanuatu' => 'VAN', + 'Venezuela' => 'VEN', + 'Vietnam' => 'VIE', + 'Yemen' => 'YEM', + 'Zambia' => 'ZAM', + 'Zimbabwe' => 'ZIM', + ); + + if (array_key_exists($country, $nations)) { + $nation = $nations[$country]; + } else { + $nation = "OTH"; + } + return $nation; +} // end mapCountry + +/* + * Map truncated country names to 3-letter Nation abbreviations + */ +function mapAbbrev($abbrev) { + + $nations = array( + 'Afg' => 'AFG', + 'Alb' => 'ALB', + 'Alg' => 'ALG', + 'And' => 'AND', + 'Ang' => 'ANG', + 'Arg' => 'ARG', + 'Arm' => 'ARM', + 'Aru' => 'ARU', + 'Aus' => 'AUT', + 'Aze' => 'AZE', + 'Bah' => 'BAH', + 'Ban' => 'BAN', + 'Bar' => 'BAR', + 'Bel' => 'BEL', + 'Ben' => 'BEN', + 'Ber' => 'BER', + 'Bhu' => 'BHU', + 'Bol' => 'BOL', + 'Bos' => 'BIH', + 'Bot' => 'BOT', + 'Bra' => 'BRA', + 'Bru' => 'BRU', + 'Bul' => 'BUL', + 'Bur' => 'BDI', + 'Cam' => 'CAM', + 'Can' => 'CAN', + 'Cap' => 'CPV', + 'Cen' => 'CAF', + 'Cha' => 'CHA', + 'Chi' => 'CHN', + 'Col' => 'COL', + 'Con' => 'CGO', + 'Cos' => 'CRC', + 'Cro' => 'CRO', + 'Cub' => 'CUB', + 'Cyp' => 'CYP', + 'Cze' => 'CZE', + 'DR ' => 'COD', + 'Den' => 'DEN', + 'Dji' => 'DJI', + 'Dom' => 'DOM', + 'Ecu' => 'ECU', + 'Egy' => 'EGY', + 'El ' => 'ESA', + 'Eri' => 'ERI', + 'Est' => 'EST', + 'Eth' => 'ETH', + 'Fij' => 'FIJ', + 'Fin' => 'FIN', + 'Fra' => 'FRA', + 'Gab' => 'GAB', + 'Gam' => 'GAM', + 'Geo' => 'GEO', + 'Ger' => 'GER', + 'Gha' => 'GHA', + 'Gre' => 'GRE', + 'Gua' => 'GUA', + 'Gui' => 'GUI', + 'Guy' => 'GUY', + 'Hai' => 'HAI', + 'Hon' => 'HKG', + 'Hun' => 'HUN', + 'Ice' => 'ISL', + 'Ind' => 'IND', + 'Ira' => 'IRI', + 'Ire' => 'IRL', + 'Isr' => 'ISR', + 'Ita' => 'ITA', + 'Ivo' => 'CIV', + 'Jam' => 'JAM', + 'Jap' => 'JPN', + 'Jor' => 'JOR', + 'Kaz' => 'KAZ', + 'Ken' => 'KEN', + 'Kir' => 'KIR', + 'Kor' => 'KOR', + 'Kuw' => 'KUW', + 'Kyr' => 'KGZ', + 'Lao' => 'LAO', + 'Lat' => 'LAT', + 'Leb' => 'LIB', + 'Les' => 'LES', + 'Lib' => 'LBA', + 'Lie' => 'LIE', + 'Lit' => 'LTU', + 'Lux' => 'LUX', + 'Mac' => 'MKD', + 'Mal' => 'MAS', + 'Mau' => 'MTN', + 'Mex' => 'MEX', + 'Mol' => 'MDA', + 'Mon' => 'MNE', + 'Mor' => 'MAR', + 'Moz' => 'MOZ', + 'Mya' => 'MYA', + 'Nam' => 'NAM', + 'Nau' => 'NRU', + 'Nep' => 'NEP', + 'Net' => 'NED', + 'New' => 'NZL', + 'Nic' => 'NCA', + 'Nig' => 'NGR', + 'Nor' => 'NOR', + 'Oma' => 'OMA', + 'Oth' => 'OTH', + 'Pak' => 'PAK', + 'Pal' => 'PLE', + 'Pan' => 'PAN', + 'Par' => 'PAR', + 'Per' => 'PER', + 'Phi' => 'PHI', + 'Pol' => 'POL', + 'Por' => 'POR', + 'Pue' => 'PUR', + 'Qat' => 'QAT', + 'Rom' => 'ROM', + 'Rus' => 'RUS', + 'Rwa' => 'RWA', + 'Sam' => 'SAM', + 'San' => 'SMR', + 'Sau' => 'KSA', + 'Sen' => 'SEN', + 'Ser' => 'SCG', + 'Sie' => 'SLE', + 'Sin' => 'SIN', + 'Slo' => 'SVK', + 'Som' => 'SOM', + 'Sou' => 'RSA', + 'Spa' => 'ESP', + 'Sri' => 'SRI', + 'Sud' => 'SUD', + 'Sur' => 'SUR', + 'Swa' => 'SWZ', + 'Swe' => 'SWE', + 'Swi' => 'SUI', + 'Syr' => 'SYR', + 'Tai' => 'TWN', + 'Taj' => 'TJK', + 'Tan' => 'TAN', + 'Tha' => 'THA', + 'Tog' => 'TOG', + 'Ton' => 'TGA', + 'Tri' => 'TRI', + 'Tun' => 'TUN', + 'Tur' => 'TUR', + 'Tuv' => 'TUV', + 'Uga' => 'UGA', + 'Ukr' => 'UKR', + 'Uni' => 'USA', + 'Uru' => 'URU', + 'Uzb' => 'UZB', + 'Van' => 'VAN', + 'Ven' => 'VEN', + 'Vie' => 'VIE', + 'Yem' => 'YEM', + 'Zam' => 'ZAM', + 'Zim' => 'ZIM', + ); + + if (array_key_exists($abbrev, $nations)) { + $nation = $nations[$abbrev]; + } else { + $nation = "OTH"; + } + return $nation; +} // end mapAbbrev +?> diff --git a/docker-xaseco/xaseco/DOCS/repairrecs.php b/docker-xaseco/xaseco/DOCS/repairrecs.php new file mode 100644 index 0000000..84a2a3f --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/repairrecs.php @@ -0,0 +1,128 @@ +#!/usr/bin/php -q + +// Updated Apr 2012 by Xymph + + function stripColors($str) { + return + str_replace("\0", '$$', + preg_replace( + '/\\$(?:[0-9a-f]..|[g-z]|$)/iu', '', + str_replace('$$', "\0", $str) + ) + ) + ; + } + + date_default_timezone_set(@date_default_timezone_get()); + $maxrecs = 50; + + if (!mysql_connect('localhost','YOUR_MYSQL_LOGIN','YOUR_MYSQL_PASSWORD')) { + echo "could not connect\n"; + exit; + } + if (!mysql_select_db('aseco')) { + echo "could not select\n"; + exit; + } + + $query = 'SELECT id,name FROM challenges ORDER BY id'; + $reschl = mysql_query($query); + + if (mysql_num_rows($reschl) > 0) { + echo 'Selected challenges: ' . mysql_num_rows($reschl) . "\n\n"; + + $tracks = 0; + $trackid = 0; + $add = 0; + $upd = 0; + $list = array(); + while ($rowchl = mysql_fetch_object($reschl)) { + + $query = 'SELECT playerid,score,date FROM records WHERE ChallengeID=' . $rowchl->id . + ' ORDER BY score,date LIMIT ' . $maxrecs; + $resrec = mysql_query($query); + + $query = 'SELECT DISTINCT playerid,score FROM rs_times t1 WHERE challengeid=' . $rowchl->id . + ' AND score=(SELECT MIN(t2.score) FROM rs_times t2 WHERE challengeid=' . $rowchl->id . + ' AND t1.playerid=t2.playerid) ORDER BY score,date LIMIT ' . $maxrecs; + $restms = mysql_query($query); + + if (mysql_num_rows($resrec) > 0) { + $n = 1; + while ($rowrec = mysql_fetch_object($resrec)) { + $rowtms = mysql_fetch_object($restms); + if ($rowtms === false) { + printf("%3d : %32s\t-> rec %3d: no more rs_times entries - consistency error!\n", $rowchl->id, stripColors($rowchl->name), $n); + break; + } + + // consistency check + if ($rowrec->playerid != $rowtms->playerid || + $rowrec->score != $rowtms->score) { + // fetch corresponding date/time & checkpoints + $query = 'SELECT date,checkpoints FROM rs_times WHERE challengeid=' . $rowchl->id . + ' AND playerid=' . $rowtms->playerid . ' ORDER BY score,date LIMIT 1'; + $resdat = mysql_query($query); + $rowdat = mysql_fetch_object($resdat); + mysql_free_result($resdat); + $newdat = date('Y-m-d H:i:s', $rowdat->date); + +// printf("%3d : %32s\t-> rec %3d: %5d/%5d differs from %d/%5d\n", $rowchl->id, stripColors($rowchl->name), $n, $rowrec->playerid, $rowrec->score, $rowtms->playerid, $rowtms->score); + + $query = "INSERT INTO records + (ChallengeId, PlayerId, Score, Date, Checkpoints) + VALUES + (" . $rowchl->id . ", " . $rowtms->playerid . ", " . + $rowtms->score . ", '" . $newdat . "', '" . + $rowdat->checkpoints . "')"; + $result = mysql_query($query); + + // couldn't be inserted? then player had a record already + if (mysql_affected_rows() != 1) { + + $query = "UPDATE records + SET Score=" . $rowtms->score . ", Checkpoints='" . $rowdat->checkpoints . "', Date='" . $newdat . "' + WHERE ChallengeId=" . $rowchl->id . " AND PlayerId=" . $rowtms->playerid; + $result = mysql_query($query); + + // couldn't be updated? then something's going wrong + if (mysql_affected_rows() == -1) { + echo mysql_errno() . ': ' . mysql_error() . "\n"; + exit; + } elseif (mysql_affected_rows() == 0) { +// printf("%3d : %32s\t-> rec %3d: skipped %d5/%5d %s\n", $rowchl->id, stripColors($rowchl->name), $n, $rowtms->playerid, $rowtms->score, $newdat); + } else { // mysql_affected_rows() == 1 + printf("%3d : %32s\t-> rec %3d: updated %5d/%5d %s\n", $rowchl->id, stripColors($rowchl->name), $n, $rowtms->playerid, $rowtms->score, $newdat); + $upd++; + if ($trackid != $rowchl->id) { + $trackid = $rowchl->id; + $tracks++; + } + } + } else { // mysql_affected_rows() == 1 + printf("%3d : %32s\t-> rec %3d: added %5d/%5d %s\n", $rowchl->id, stripColors($rowchl->name), $n, $rowtms->playerid, $rowtms->score, $newdat); + $add++; + if ($trackid != $rowchl->id) { + $trackid = $rowchl->id; + $tracks++; + } + } + } + $n++; + } + } + + mysql_free_result($resrec); + mysql_free_result($restms); + } + + echo "\n" . $add . ' added & ' . $upd . ' updated records on ' . $tracks . " tracks\n"; + mysql_free_result($reschl); + } else { + echo "no challenges!\n"; + } +?> diff --git a/docker-xaseco/xaseco/DOCS/sm_hub.html b/docker-xaseco/xaseco/DOCS/sm_hub.html new file mode 100644 index 0000000..b365a88 --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/sm_hub.html @@ -0,0 +1,112 @@ + + + +ShootMania Storm + + + + + + + + + + +Welcome to this humble page for: + +

    +ShootMania Storm +


    + +

    Introduction:

    + +
    +
    +This is a simple hub page for ShootMania Storm. It aims to collect in one place useful information and references to official and community sites for this new multiplayer shooter game by Nadeo. Here are relevant excerpts from the Storm press releases: +

    +"ShootMania Storm is the second offering from ManiaPlanet, following the successful launch of TrackMania 2 in 2011. For the first time in its history, Nadeo have decided to bring to FPS what made TrackMania such a phenomenon: its unique competitive spirit and a sense of fun." +

    +"In conjunction with the online competitive multiplayer, ShootMania Storm will also feature extensive map editing capabilities, allowing players to customise, design and share their creations across the community, using the soon to be released, upgraded ManiaPlanet 2.0 gaming system." + +

    +
    + + +

    Content:

    + + + + + +

    Links:

    + + + +
    +
    +
    +Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 02-Mar-2013 +
    + + diff --git a/docker-xaseco/xaseco/DOCS/tm2_hub.html b/docker-xaseco/xaseco/DOCS/tm2_hub.html new file mode 100644 index 0000000..3941cce --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/tm2_hub.html @@ -0,0 +1,296 @@ + + + +TrackMania² - XASECO2 + + + + + + + + + + +Welcome to this humble page for: + +

    +TrackMania² +


    + +

    Introduction:

    + +
    +
    +This is a simple hub page for TrackMania² Canyon, Stadium and Valley. It aims to collect in one place useful information and references to official and community sites for these racing games by Nadeo. Here are relevant excerpts from their announcements: +

    +"TrackMania² Canyon will give players an adrenaline-filled experience, with insane jumps, controlled drifting and extreme speed. Driving in the Canyon is like running on a tightrope, only the fearless will succeed. It goes far beyond traditional driving games with a fully customisable world of tracks generated by the players themselves! With loops, wall rides, ramps and a multitude of other stunts, you're in for the ride of your life!" +

    +"With over 12 million players, Stadium is the all time favorite racer on PC. A classic needs to stay true to its roots. The acclaimed gameplay of Stadium remains identical: same speed, same controls, same blocks!
    +This core experience is improved by ManiaPlanet: better graphics and more tools for the community. Lots of competitions, new maps and game modes. The most creative community on PC can now benefit from ManiaPlanet cutting edge technologies." +

    +"The third game of the TrackMania² series is taking place in a lush valley with a rally gameplay. Reaching a level of detail never achieved on a TrackMania game, the valley environment will blow you away at first glance. The challenging gameplay will offer you hours and hours of fun, trying to shave milliseconds off every tight corner. Precision and control are key on this playground. Drifting is a last resort and will make you lose time." + +

    +
    + +

    XASECO2:

    + +
    +
    +This page is also the initial home to XASECO2, a port for +ManiaPlanet / TM² of the popular server controller for TM Forever +and previous TM games. + +

    +For a high-level outline of all of XASECO2's features and plugins, see +the Overview page. For a comprehensive +overview of the changes, see the v0.90 +initial release notes and the v0.93 - +v1.03 current release notes. + +

    +And here is a complete overview of all available commands +in HTML and Word. + +

    +
    + + +

    Content:

    + + + +

    TM² Downloads:

    + + + +

    TM² Versions:

    + + + +

    Installation:

    +
      +
    • New installation of v1.03:

      +See the XASECO2 Installation page on XAseco.org, +or go directly to the TM² & XASECO2 quick start guide. +

      + + +
    • Upgrading from v1.02 to v1.03:

      +See the XASECO2 Upgrade page on XAseco.org for general instructions. +

      + +The following files were updated in v1.03: xaseco2.php, dedimania.xml, +includes/gbxdatafetcher.inc.php, mxinfosearcher.inc.php, +rasp.funcs.php, rasp.settings.php, plugins/chat.dedimania.php, +plugin.checkpoints.php, plugin.dedimania.php, plugin.rasp.php. +

      + +Important: +
        +
      • To configure your server for the central Dedimania +database, you must copy the login value in the +<masterserver_account> section from +your server's dedicated_cfg.txt file into the corresponding +section of the dedimania.xml file. Further, register your +server with the Dedimania system, generate a DedimaniaCode, +and add that in dedimania.xml as well. +
      • Open port 8082 on your firewall/router for communication +with the central Dedimania server. +
      • In the zip file, all *.XML and *.PHP config files are located +inside the newinstall/ directory. This means that you can (and +have to) unzip the download and replace all the PHP code files, without +worrying about overwriting your customized config files. However, for +every XML/PHP config file that was updated (see above), you must +replace your version with the one from the newinstall/ directory, or +compare them and add any new/changed configuration settings +to your version to insure the system remains working correctly. +
      • For a new installation, go into the newinstall/ directory and move +all *.XML files into the main directory (next to xaseco2.php), all +*.PHP files into the includes/ directory, and XAseco2.bat|XAseco2.sh +also into the main directory. +
      +
      + +
    • Upgrading from v0.90 to v1.02:

      + +See the archived Upgrade notes. +
    + + +

    Configuration options:

    + + + +

    Versions:

    + + + +

    Download:

    + + + +

    Feedback/questions:

    + + + + +

    Links:

    + + + +
    +
    +
    +Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 29-Jun-2013 +
    + + diff --git a/docker-xaseco/xaseco/DOCS/tmf_hub.html b/docker-xaseco/xaseco/DOCS/tmf_hub.html new file mode 100644 index 0000000..34e5b4a --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/tmf_hub.html @@ -0,0 +1,127 @@ + + + +TrackMania Forever + + + + + + + + + + +Welcome to this humble page for: + +

    +TrackMania Forever +


    + +

    Introduction:

    + +
    +
    +This is a simple hub page for TrackMania Forever, both Nations and United. It aims to collect in one place useful information and references to official and community sites for these upgraded racing games by Nadeo. Here are relevant excerpts from the Forever press releases: +

    +"TrackMania Nations Forever offers a new complete 'Forever' version of the Stadium environment, a complete solo mode and 65 brand new, progressively difficult tracks. TrackMania Nations Forever will unite an even larger number of players thanks to its engaging multiplayer modes, innovative online functions and revolutionary interactivity between players." +

    +"The release of TrackMania Nations Forever will also allow the convergence of the huge TrackMania community. For the first time, players of the free versions will be able to play online with players from the retail version of TrackMania United on servers dedicated to the Stadium environment that is common to both games. Players of the TrackMania United retail version also gain a brilliant free extension named 'United Forever'. In addition to being compatible with Nations Forever, this extension offers many bonuses - including tracks that have never been seen before, new design blocks for the game environments and a spectacular graphics update for the three historic environments Desert, Snow and Rally." +

    +"The TrackMania Forever add-on also features a new 3D function that enhances the gameplay experience by optionally displaying the game in three dimensions when players wear special 3D glasses. Glasses will be provided in every new retail version of TrackMania United Forever. In order to offer even more interactivity, TrackMania United Forever lets player create and manage groups of friends in-game, much in the same way they would in Facebook or Myspace." +

    +
    + + +

    Content:

    + + + +

    TMF Downloads:

    + + + +

    TMF Versions:

    + + + +

    Links:

    + + + +
    +
    +
    +Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 26-Jul-2013 +
    + + diff --git a/docker-xaseco/xaseco/DOCS/updatepanels.php b/docker-xaseco/xaseco/DOCS/updatepanels.php new file mode 100644 index 0000000..1426299 --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/updatepanels.php @@ -0,0 +1,76 @@ +#!/usr/bin/php -q + + + $panelpath = '/home/tmf/aseco/panels'; + + if (!isset($argv[1]) || !isset($argv[2])) { + echo 'usage: ' . basename($argv[0]) . ' {admin|donate|records|vote} PanelName' . "\n"; + exit; + } + if (!file_exists($panelpath)) { + echo "Panel path '$panelpath' not found\n"; + exit; + } + if ($argv[1] != 'admin' && $argv[1] != 'donate' && + $argv[1] != 'records' && $argv[1] != 'vote') { + echo "unknown panel type\n"; + exit; + } + $panelpath = rtrim($panelpath, '/'); + if (!file_exists($panelpath . '/' . ucfirst($argv[1]) . $argv[2] . '.xml')) { + echo "unknown panel name\n"; + exit; + } + + if (!mysql_connect('localhost','YOUR_MYSQL_LOGIN','YOUR_MYSQL_PASSWORD')) { + echo "could not connect\n"; + exit; + } + if (!mysql_select_db('aseco')) { + echo "could not select\n"; + exit; + } + + $query = 'SELECT PlayerID,Panels FROM players_extra ORDER BY PlayerID'; + $resply = mysql_query($query); + + if (mysql_num_rows($resply) > 0) { + echo 'Updating players_extra entries: ' . mysql_num_rows($resply) . " ...\n"; + + while ($rowply = mysql_fetch_object($resply)) { + $panels = explode('/', $rowply->Panels); + switch ($argv[1]) { + case 'admin': + $panels[0] = ucfirst($argv[1]) . $argv[2]; + break; + case 'donate': + $panels[1] = ucfirst($argv[1]) . $argv[2]; + break; + case 'records': + $panels[2] = ucfirst($argv[1]) . $argv[2]; + break; + case 'vote': + $panels[3] = ucfirst($argv[1]) . $argv[2]; + break; + } + + $query = "UPDATE players_extra SET Panels = '" . implode('/', $panels) . "' WHERE PlayerID = " . $rowply->PlayerID; + $result = mysql_query($query); + if (mysql_affected_rows() == -1) { + mysql_free_result($resply); + echo "couldn't update panels for player ID " . $rowply->PlayerID . ":\n"; + echo mysql_error() . "\n"; + exit; + } + } + echo "Done\n"; + + mysql_free_result($resply); + } else { + echo "no players_extra!\n"; + } +?> diff --git a/docker-xaseco/xaseco/DOCS/upgrades.html b/docker-xaseco/xaseco/DOCS/upgrades.html new file mode 100644 index 0000000..a3abf5c --- /dev/null +++ b/docker-xaseco/xaseco/DOCS/upgrades.html @@ -0,0 +1,1128 @@ + + + +TrackMania Nations - XASECO upgrade history + + + + + + + + + + +

    +TrackMania Nations +


    + +

    Earlier upgrades:

    + +
      +
    • Upgrading from v1.15 to v1.15b:

      + +The following files were updated in v1.15b: aseco.php, +includes/gbxdatafetcher.inc.php, rasp.funcs.php, +tmxinfofetcher.inc.php, tmxinfosearcher.inc.php, +plugins/chat.songmod.php, plugin.autotime.php, plugin.track.php, +plugin.tmxinfo.php. +

      + +Important: +
        +
      • See the installation notes for upgrading to v1.15. +
      +
      + +
    • Upgrading from v1.14 to v1.15:

      + +The following files were updated in v1.15: aseco.php, config.xml, +dedimania.xml, rasp.xml, includes/GbxRemote.bem.php, GbxRemote.inc.php, +basic.inc.php, gbxdatafetcher.inc.php, jfreu.config.php, +manialinks.inc.php, ogg_comments.inc.php, rasp.funcs.php, +rasp.settings.php, replayparser.inc.php, tmndatafetcher.inc.php, +tmxinfofetcher.inc.php, tmxinfosearcher.inc.php, types.inc.php, +votes.config.php, web_access.inc.php, xmlparser.inc.php, +xmlrpc_db.inc.php, panels/VoteBottomCenterTransp.xml, +plugins/chat.admin.php, chat.dedimania.php, chat.players.php, +chat.players2.php, chat.records.php, chat.records2.php, +chat.recrels.php, chat.server.php, chat.stats.php, +plugin.checkpoints.php, plugin.dedimania.php, plugin.donate.php, +plugin.localdatabase.php, plugin.matchsave.php, plugin.panels.php, +plugin.rasp.php, plugin.rasp_chat.php, plugin.rasp_jukebox.php, +plugin.rasp_votes.php, plugin.rounds.php, plugin.rpoints.php, +plugin.style.php, plugin.tmxinfo.php, plugin.track.php. +

      + +Important: +
        +
      • To register your server with the central Dedimania +database, you must copy the login and password values in the +<masterserver_account> section from your +server's dedicated.cfg (TMN) or dedicated_cfg.txt (TMF) file into +the corresponding section of the dedimania.xml file, and add +the 3-character nation abbreviation. Instead of the password +you can also use the community code for your server by using +the server login/password on the official +site for your game (TMO/TMS/TMN) or on this page for +TMF. +
      • Open port 8002 on your firewall/router for communication +with the central Dedimania server. +
      • In the zip file, all *.XML and *.PHP config files are located +inside the newinstall/ directory. This means that you can (and +have to) unzip the download and replace all the PHP code files, without +worrying about overwriting your customized config files. However, for +every XML/PHP config file that was updated (see above), you must +replace your version with the one from the newinstall/ directory, or +compare them and add any new/changed configuration settings +to your version to insure the system remains working correctly. +
      • For a new installation, go into the newinstall/ directory and move +all *.XML files into the main directory (next to aseco.php), all *.PHP +files into the includes/ directory, and Aseco.bat|Aseco.sh|AsecoF.sh +also into the main directory. +
      +
      + +
    • Upgrading from v1.13 to v1.14:

      + +The following files were updated in v1.14: aseco.php, adminops.xml, +config.xml, dedimania.xml, includes/GbxRemote.bem.php, +GbxRemote.inc.php, manialinks.inc.php, rasp.funcs.php, +tmndatafetcher.inc.php, types.inc.php, xmlparser.inc.php, +xmlrpc_db.inc.php, plugins/chat.admin.php, chat.dedimania.php, +chat.records.php, chat.records2.php, chat.recrels.php, chat.server.php, +plugin.checkpoints.php, plugin.dedimania.php, plugin.localdatabase.php, +plugin.matchsave.php, plugin.rasp.php, plugin.rasp_chat.php, +plugin.rasp_irc.php, plugin.rasp_jukebox.php, plugin.rasp_karma.php, +plugin.rasp_nextmap.php, plugin.rasp_nextrank.php, plugin.rpoints.php, +plugin.track.php, plugin.uptodate.php, jfreu.chat.php, +mistral.idlekick.php. +

      + +Important: +
        +
      • See the installation notes for upgrading to v1.11. +
      +
      + +
    • Upgrading from v1.12 to v1.13:

      + +The following file was added in v1.13: chat.lastwin.php. +

      + +The following files were updated in v1.13: aseco.php, config.xml, +dedimania.xml, plugins.xml, rasp.xml, includes/GbxRemote.inc.php, +GbxRemote.bem.php, basic.inc.php, gbxdatafetcher.inc.php, +ogg_comments.inc.php, rasp.funcs.php, replayparser.inc.php, +tmndatafetcher.inc.php, tmxinfofetcher.inc.php, tmxinfosearcher.inc.php, +types.inc.php, votes.config.php, plugins/chat.admin.php, +chat.dedimania.php, chat.laston.php, chat.records2.php, chat.server.php, +chat.stats.php, plugin.checkpoints.php, plugin.localdatabase.php, +plugin.matchsave.php, plugin.musicserver.php, plugin.panels.php, +plugin.rasp.php, plugin.rasp_jukebox.php, plugin.rasp_karma.php, +plugin.rasp_votes.php, plugin.rounds.php, localdb/*.sql. +

      + +Important: +
        +
      • See the installation notes for upgrading to v1.11. +
      +
      + +
    • Upgrading from v1.11 to v1.12:

      + +The following files were updated in v1.12: aseco.php, +config.xml, dedimania.xml, rasp.xml, localdb/*.sql, +includes/GbxRemote.response.php, basic.inc.php, rasp.funcs.php, +rasp.settings.php, tmxinfofetcher.inc.php, tmxinfosearcher.inc.php, +types.inc.php, xmlrpc_db.inc.php, plugins/chat.admin.php, +chat.dedimania.php, chat.players.php, chat.players2.php, +chat.records.php, chat.records2.php, chat.recrels.php, chat.server.php, +chat.stats.php, plugin.access.php, plugin.autotime.php, +plugin.chatlog.php, plugin.checkpoints.php, plugin.dedimania.php, +plugin.donate.php, plugin.localdatabase.php, plugin.matchsave.php, +plugin.musicserver.php, plugin.muting.php, plugin.panels.php, +plugin.rasp.php, plugin.rasp_chat.php, plugin.rasp_jukebox.php, +plugin.rasp_karma.php, plugin.rasp_nextmap.php, +plugin.rasp_nextrank.php, plugin.rasp_votes.php, plugin.rpoints.php, +plugin.style.php, plugin.track.php, jfreu.chat.php. +

      + +The following files were removed from v1.12 (actually moved to the +DOCS/OLD/ directory for posterity): plugins/chat.vote.php, and the +votes.sql section of localdb/aseco.sql. +

      + +Important: +
        +
      • See the installation notes for upgrading to v1.11. +
      +
      + +
    • Upgrading from v1.10 to v1.11:

      + +The following file was renamed in v1.11: plugins/chat.song.php to +plugins/chat.songmod.php. +

      + +The following files were updated in v1.11: aseco.php, +config.xml, dedimania.xml, plugins.xml, includes/basic.inc.php, +gbxdatafetcher.inc.php, jfreu.config.php, manialinks.inc.php, +rasp.funcs.php, rasp.settings.php, tmndatafetcher.inc.php, +types.inc.php, web_access.inc.php, xmlrpc_db.inc.php, +plugins/chat.admin.php, chat.records2.php, chat.server.php, +chat.stats.php, plugin.autotime.php, plugin.chatlog.php, +plugin.checkpoints.php, plugin.dedimania.php, plugin.donate.php, +plugin.ml_howto.php, plugin.msglog.php, plugin.musicserver.php, +plugin.muting.php, plugin.panels.php, plugin.rasp.php, +plugin.rasp_chat.php, plugin.rasp_jukebox.php, plugin.rasp_karma.php, +plugin.rasp_votes.php, plugin.rpoints.php, plugin.style.php, +plugin.track.php, plugin.uptodate.php, jfreu.chat.php, jfreu.lite.php, +jfreu.plugin.php. +

      + +The following files were removed from v1.11 (actually moved to the +DOCS/OLD/ directory for posterity): includes/sminfofetcher.inc.php, +plugins/plugin.sminfo.php. +

      + +Important: +
        +
      • To register your server with the central Dedimania +database, you must copy the login and password values in the +<masterserver_account> section from your +server's dedicated.cfg (TMN) or dedicated_cfg.txt (TMF) file into +the corresponding section of the dedimania.xml file, and add +the 3-character nation abbreviation. Instead of the password +you can also use the community code for your server by using +the server login/password on the official +site for your game (TMO/TMS/TMN) or on this page for +TMF. +
      • Open port 8002 on your firewall/router for communication +with the central Dedimania server. +
      • In the zip file, all *.XML and *.PHP config files are now located +inside the newinstall/ directory. This means that you can (and +have to) unzip the download and replace all the PHP code files, without +worrying about overwriting your customized config files. However, for +every XML/PHP config file that was updated (see above), you must +replace your version with the one from the newinstall/ directory, +or compare them and add any new/changed configuration settings +to your version to insure the system remains working correctly. +
      • For a new installation, go into the newinstall/ directory and +move all *.XML files into the main directory, next to aseco.php, +and *.PHP files into the includes/ directory. +
      +
      + +
    • Upgrading from v1.09 to v1.10:

      + +The following files were added in v1.10: panels/DonateBelowCPListRM.xml, +RecordsRightBottomRM.xml. +

      + +The following files were updated in v1.10: aseco.php, adminops.xml, +config.xml, dedimania.xml, rasp.xml, includes/basic.inc.php, +rasp.funcs.php, rasp.settings.php, tmxinfofetcher.inc.php, +tmxinfosearcher.inc.php, types.inc.php, votes.config.php, +plugins/chat.admin.php, chat.dedimania.php, chat.records.php, +chat.server.php, chat.stats.php, plugin.checkpoints.php, +plugin.dedimania.php, plugin.donate.php, plugin.localdatabase.php, +plugin.panels.php, plugin.rasp.php, plugin.rasp_jukebox.php, +plugin.rasp_votes.php, plugin.rpoints.php, plugin.style.php, +plugin.tmxinfo.php, plugin.uptodate.php. +

      + +Important: +
        +
      • See the installation notes for upgrading to v1.05b. +
      +
      + +
    • Upgrading from v1.08 to v1.09:

      +The following files were added in v1.09: access.xml, +plugins/plugin.access.php. +

      + +The following files were updated in v1.09: aseco.php, +adminops.xml, config.xml, musicserver.xml, plugins.xml, +rasp.xml, includes/GbxRemote.bem.php, GbxRemote.inc.php, +basic.inc.php, gbxdatafetcher.inc.php, rasp.settings.php, +replayparser.inc.php, sminfofetcher.inc.php, tmxinfofetcher.inc.php, +types.inc.php, localdb/aseco.sql, rasp.sql, plugins/chat.admin.php, +plugin.autotime.php, plugin.checkpoints.php, plugin.dedimania.php, +plugin.localdatabase.php, plugin.musicserver.php, plugin.rasp.php, +plugin.rasp_chat.php, plugin.rasp_jukebox.php, plugin.rasp_votes.php, +plugin.rounds.php, plugin.track.php, plugin.uptodate.php, +jfreu.plugin.php, mistral.idlekick.php. +

      + +Important: +
        +
      • See the installation notes for upgrading to v1.05b. +
      +
      + +
    • Upgrading from v1.06 to v1.08:

      +The following file was added in v1.08: includes/replayparser.inc.php. +

      + +The following files were updated in v1.08: aseco.php, config.xml, +includes/GbxRemote.inc.php, GbxRemote.bem.php, basic.inc.php, +gbxdatafetcher.inc.php, ogg_comments.inc.php, rasp.funcs.php, +sminfofetcher.inc.php, tmxinfofetcher.inc.php, tmxinfosearcher.inc.php, +types.inc.php, web_access.inc.php, xmlparser.inc.php, +localdb/aseco.sql, plugins/chat.admin.php, chat.dedimania.php, +chat.players.php, chat.players2.php, chat.records.php, +chat.records2.php, chat.stats.php, plugin.autotime.php, +plugin.chatlog.php, plugin.checkpoints.php, plugin.dedimania.php, +plugin.donate.php, plugin.localdabase.php, plugin.matchsave.php, +plugin.musicserver.php, plugin.muting.php, plugin.panels.php, +plugin.rasp.php, plugin.rasp_chat.php, plugin.rasp_jukebox.php, +plugin.rasp_nextmap.php, plugin.rasp_votes.php, plugin.rounds.php, +plugin.sminfo.php, plugin.style.php, plugin.tmxinfo.php, +plugin.track.php, jfreu.chat.php, jfreu.lite.php, jfreu.plugin.php, +mistral.idlekick.php. +

      + +Important: +
        +
      • See the installation notes for upgrading to v1.05b. +
      +
      + +
    • Upgrading from v1.05b to v1.06:

      + +The following files were added in v1.06: localdb/extra.sql, +panels/StatsNations.xml, StatsUnited.xml. +

      + +The following files were updated in v1.06: aseco.php, +config.xml, includes/manialinks.inc.php, ogg_comments.inc.php, +types.inc.php, jfreu.config.php, localdb/aseco.sql, rasp.sql, +plugins/chat.admin.php, chat.dedimania.php, chat.server.php, +chat.stats.php, plugin.checkpoints.php, plugin.dedimania.php, +plugin.donate.php, plugin.localdatabase.php, plugin.matchsave.php, +plugin.musicserver.php, plugin.panels.php, plugin.rasp.php, +plugin.rasp_chat.php, plugin.rasp_jukebox.php, plugin.style.php, +jfreu.lite.php, jfreu.plugin.php. +

      + +Important: +
        +
      • See the installation notes for upgrading to v1.05b. +
      +
      + +
    • Upgrading from v1.05 to v1.05b:

      + +The following files were updated in v1.05b: aseco.php, dedimania.xml, +includes/basic.inc.php, plugins/plugin.checkpoints.php, +plugin.dedimania.php, plugin.msglog.php, plugin.musicserver.php, +jfreu.lite.php. +

      + +Important: +
        +
      • To register your server with the central Dedimania +database, you must copy the login and password values in the +<masterserver_account> section from your +server's dedicated.cfg (TMN) or dedicated_cfg.txt (TMF) file into +the corresponding section of the dedimania.xml file, and add +the 3-character nation abbreviation. Instead of the password +you can also use the community code for your server by using +the server login/password on the official +site for your game (TMO/TMS/TMN) or on this page for +TMF. +
      • Open ports 8003, 8006, 8007, 8011, 8012, 8013 and 8016 through +8021 on your firewall/router for communication with the central +Dedimania server (if that's not possible, the system falls back on +port 80). +
      • In the zip file, all *.XML and *.PHP config files are now located +inside the newinstall/ directory. This means that you can (and +have to) unzip the download and replace all the PHP code files, without +worrying about overwriting your customized config files. However, for +every XML/PHP config file that was updated (see above), you must +replace your version with the one from the newinstall/ directory, +or compare them and add any new/changed configuration settings +to your version to insure the system remains working correctly. +
      • For a new installation, go into the newinstall/ directory and +move all *.XML files into the main directory, next to aseco.php, +and *.PHP files into the includes/ directory. +
      +
      + +
    • Upgrading from v1.04 to v1.05:

      + +The following files were updated in v1.05: aseco.php, +adminops.xml, config.xml, musicserver.xml, includes/basic.inc.php, +gbxdatafetcher.inc.php, types.inc.php, votes.config.php, +xmlrpc_db.inc.php, plugins/chat.admin.php, chat.dedimania.php, +chat.players.php, plugin.autotime.php, plugin.checkpoints.php, +plugin.dedimania.php, plugin.donate.php, plugin.localdatabase.php, +plugin.musicserver.php, plugin.rasp_chat.php, plugin.rasp_jukebox.php, +plugin.rasp_votes.php, plugin.rpoints.php, plugin.uptodate.php, +jfreu.chat.php, jfreu.plugin.php. +

      + +Important: +
        +
      • See the installation notes for upgrading to v1.03. +
      +
      + +
    • Upgrading from v1.03 to v1.04:

      + +The following files were added in v1.04: autotime.xml, +plugins/plugin.autotime.php, plugin.rpoints.php. +

      + +The following files were updated in v1.04: aseco.php, +adminops.xml, config.xml, musicserver.xml, plugins.xml, rasp.xml, +includes/gbxdatafetcher.inc.php, manialinks.inc.php, rasp.funcs.php, +tmndatafetcher.inc.php, tmxinfofetcher.inc.php, types.inc.php, +votes.config.php, jfreu.config.php, plugins/chat.admin.php, +chat.dedimania.php, chat.laston.php, chat.players.php, +chat.records.php, chat.records2.php, chat.server.php, +chat.stats.php, plugin.chatlog.php, plugin.checkpoints.php, +plugin.dedimania.php, plugin.donate.php, plugin.localdatabase.php, +plugin.musicserver.php, plugin.muting.php, plugin.rasp.php, +plugin.rasp_chat.php, plugin.rasp_jukebox.php, plugin.rasp_karma.php, +plugin.rasp_nextmap.php, plugin.rasp_nextrank.php, +plugin.rasp_votes.php, plugin.rounds.php, plugin.sminfo.php, +plugin.tmxinfo.php, plugin.track.php, jfreu.lite.php, jfreu.chat.php, +jfreu.plugin.php, plugins/jfreu/jfreu.vips.xml. +

      + +Important: +
        +
      • See the installation notes for upgrading to v1.03. +
      +
      + +
    • Upgrading from v1.02 to v1.03:

      + +The following file was added in v1.03: bannedips.xml. +

      + +The following files were updated in v1.03: aseco.php, adminops.xml, +config.xml, dedimania.xml, rasp.xml, includes/basic.inc.php, +manialinks.inc.php, rasp.settings.php, votes.config.php, +types.inc.php, jfreu.config.php, plugins/chat.admin.php, +chat.players.php, chat.records2.php, chat.server.php, chat.stats.php, +plugin.dedimania.php, plugin.localdatabase.php, plugin.matchsave.php, +plugin.msglog.php, plugin.musicserver.php, plugin.muting.php, +plugin.panels.php, plugin.rasp.php, plugin.rasp_chat.php, +plugin.rasp_jukebox.php, plugin.rasp_votes.php, plugin.track.php, +plugin.uptodate.php, jfreu.chat.php, jfreu.lite.php, jfreu.plugin.php, +mistral.idlekick.php. +

      + +Important: +
        +
      • To register your server with the central Dedimania +database, you must copy the login and password values in the +<masterserver_account> section from your +server's dedicated.cfg (TMN) or dedicated_cfg.txt (TMF) file into +the corresponding section of the dedimania.xml file, and add +the 3-character nation abbreviation. Instead of the password +you can also use the community code for your server by using +the server login/password on the official +site for your game (TMO/TMS/TMN) or on this page for +TMF. +
      • Open ports 8003, 8006, 8007, 8011, 8012 and 8013 on your +firewall/router for communication with the central Dedimania server +(if that's not possible, the system falls back on port 80). +
      • In the zip file, all *.XML and *.PHP config files are now located +inside the newinstall/ directory. This means that you can (and +have to) unzip the download and replace all the PHP code files, without +worrying about overwriting your customized config files. However, for +every XML/PHP config file that was updated (see above), you must +replace your version with the one from the newinstall/ directory, +or compare them and add any new/changed configuration settings +to your version to insure the system remains working correctly. +
      • For a new installation, go into the newinstall/ directory and +move all *.XML files into the main directory, next to aseco.php, +and *.PHP files into the includes/ directory. +
      +
      + +
    • Upgrading from v1.01 to v1.02:

      + +The following files were added in v1.02: includes/ogg_comments.inc.php, +plugins/plugin.msglog.php, panels/AdminAboveSpeed2.xml, +DonateLeftSmall.xml, DonateRightSmall.xml, VoteBottomCenterTransp.xml, +styles/ProgressBar.xml. +

      + +The following files were updated in v1.02: aseco.php, +adminops.xml, config.xml, dedimania.xml, plugins.xml, rasp.xml, +includes/basic.inc.php, manialinks.inc.php, jfreu.config.php, +rasp.funcs.php, rasp.settings.php, votes.config.php, +sminfofetcher.inc.php, tmndatafetcher.inc.php, tmxinfofetcher.inc.php, +tmxinfosearcher.inc.php, plugins/chat.admin.php, chat.dedimania.php, +chat.help.php, chat.players.php, chat.players2.php, chat.records.php, +chat.records2.php, chat.recrels.php, chat.server.php, chat.stats.php, +plugin.chatlog.php, plugin.donate.php, plugin.matchsave.php, +plugin.ml_howto.php, plugin.musicserver.php, plugin.muting.php, +plugin.panels.php, plugin.rasp.php, plugin.rasp_chat.php, +plugin.rasp_irc.php, plugin.rasp_jukebox.php, plugin.rasp_votes.php, +plugin.rounds.php, plugin.sminfo.php, plugin.style.php, +plugin.tmxinfo.php, jfreu.chat.php, jfreu.lite.php, jfreu.plugin.php. +

      + +Important: +
        +
      • See the installation notes for upgrading to v1.01. +
      +
      + +
    • Upgrading from v1.00 to v1.01:

      + +The following files were added in v1.01: includes/sminfofetcher.inc.php, +tmxinfosearcher.inc.php, plugins/plugin.sminfo.php, panels/Donate*.xml. +

      + +The following files were updated in v1.01: aseco.php, +adminops.xml, config.xml, dedimania.xml, plugins.xml, +rasp.xml, includes/basic.inc.php, gbxdatafetcher.inc.php, +manialinks.inc.php, rasp.funcs.php, types.inc.php, +plugins/chat.admin.php, chat.records2.php, chat.server.php, +chat.stats.php, plugin.checkpoints.php, plugin.dedimania.php, +plugin.donate.php, plugin.localdatabase.php, plugin.musicserver.php, +plugin.muting.php, plugin.panels.php, plugin.rasp.php, +plugin.rasp_irc.php, plugin.rasp_jukebox.php, plugin.rasp_nextmap.php, +plugin.rasp_votes.php, plugin.style.php, plugin.tmxinfo.php, +jfreu.chat.php, jfreu.plugin.php, panels/00README.txt. +

      + +Important: +
        +
      • To register your server with the central Dedimania +database, you must copy the login and password values in the +<masterserver_account> section from your +server's dedicated.cfg (TMN) or dedicated_cfg.txt (TMF) file into +the corresponding section of the dedimania.xml file, and add +the 3-character nation abbreviation. Instead of the password +you can also use the community code for your server by using +the server login/password on the official +site for your game (TMO/TMS/TMN) or on this page for +TMF. +
      • Open ports 8003, 8006, 8007, 8011, 8012 and 8013 on your +firewall/router for communication with the central Dedimania server +(if that's not possible, the system falls back on port 80). +
      • In the zip file, all *.XML and *.PHP config files are now located +inside the newinstall/ directory. This means that you can (and +have to) unzip the download and replace all the PHP code files, without +worrying about overwriting your customized config files. However, for +every XML/PHP config file that was updated (see above), you must +replace your version with the one from the newinstall/ directory, +or compare them and add any new/changed configuration settings +to your version to insure the system remains working correctly. +
      • For a new installation, go into the newinstall/ directory and +move all *.XML files into the main directory, next to aseco.php, +and *.PHP files into the includes/ directory. +
      +
      + +
    • Upgrading from v0.99b to v1.00:

      + +The following directories were added in v1.00: panels/ and +styles/. +

      + +The following files were added in v1.00: panels/00README.txt, +panels/*.xml, style/00README.txt, styles/*.xml, +plugins/plugin.panels.php, plugin.style.php (TMN admins can remove +these from plugins.xml). +

      + +The following files were updated in v1.00: aseco.php, +adminops.xml, config.xml, plugins.xml, includes/basic.inc.php, +gbxdatafetcher.inc.php, jfreu.config.php, manialinks.inc.php, +rasp.funcs.php, types.inc.php, web_access.inc.php, +plugins/chat.admin.php, chat.dedimania.php, chat.players.php, +chat.records2.php, chat.recrels.php, chat.server.php, chat.song.php, +chat.stats.php, plugin.chatlog.php, plugin.checkpoints.php, +plugin.dedimania.php, plugin.localdatabase.php, plugin.musicserver.php, +plugin.muting.php, plugin.rasp.php, plugin.rasp_chat.php, +plugin.rasp_jukebox.php, plugin.rasp_karma.php, plugin.rasp_votes.php, +plugin.tmxinfo.php, plugin.track.php, plugin.uptodate.php, +jfreu.chat.php, jfreu.plugin.php, mistral.idlekick.php. +

      + +Important: +
        +
      • To register your server with the central Dedimania +database, you must copy the login and password values in the +<masterserver_account> section from your +server's dedicated.cfg (TMN) or dedicated_cfg.txt (TMF) file into +the corresponding section of the dedimania.xml file, and add +the 3-character nation abbreviation. Instead of the password +you can also use the community code for your server by using +the server login/password on the official +site for your game (TMO/TMS/TMN) or on this page for +TMF. +
      • Open ports 8003, 8006 and 8007 on your firewall/router for +communication with the central Dedimania server (if that's not +possible, the system falls back on port 80). +
      • In the zip file, all *.XML and *.PHP config files are now located +inside the newinstall/ directory. This means that you can (and +have to) unzip the download and replace all the PHP code files, without +worrying about overwriting your customized config files. However, for +every XML/PHP config file that was updated (see above), you must +replace your version with the one from the newinstall/ directory, +or compare them and add any new/changed configuration settings +to your version to insure the system remains working correctly. +
      • For a new installation, go into the newinstall/ directory and +move all *.XML files into the main directory, next to aseco.php, +and *.PHP files into the includes/ directory. +
      +
      + +
    • Upgrading from v0.99 to v0.99b:

      + +The following files were updated in v0.99b: aseco.php, adminops.xml, +config.xml, musicserver.xml, includes/manialinks.inc.php, +plugins/chat.admin.php, plugin.dedimania.php, plugin.localdatabase.php, +plugin.musicserver.php, plugin.rasp.php, plugin.uptodate.php. +

      + +Important: +
        +
      • See the installation notes for upgrading to v0.98. +
      +
      + +
    • Upgrading from v0.98 to v0.99:

      + +The following files were added in v0.99: musicserver.xml, +plugins/plugin.donate.php, plugin.musicserver.php. +

      + +The following files were updated in v0.99: aseco.php, adminops.xml, +config.xml, dedimania.xml, localdatabase.xml, plugins.xml, rasp.xml, +includes/GbxRemote.inc.php, GbxRemote.bem.php, basic.inc.php, +manialinks.inc.php, rasp.funcs.php, tmxinfofetcher.inc.php, +types.inc.php, xmlrpc_db.inc.php, plugins/chat.admin.php, +chat.dedimania.php, chat.records2.php, chat.server.php, chat.stats.php, +plugin.dedimania.php, plugin.localdatabase.php, plugin.matchsave.php, +plugin.muting.php, plugin.rasp.php, plugin.rasp_chat.php, +plugin.rasp_jukebox.php, plugin.rasp_karma.php, plugin.rasp_votes.php, +plugin.tmxinfo.php, plugin.track.php, jfreu.plugin.php. +

      + +Important: +
        +
      • See the installation notes for upgrading to v0.98. +
      +
      + +
    • Upgrading from v0.97 to v0.98:

      + +The following file was added in v0.98: plugins/plugin.ml_howto.php. +

      + +The following files were updated in v0.98: aseco.php, adminops.xml, +dedimania.xml, localdatabase.xml, includes/basic.inc.php, +manialinks.inc.php, rasp.funcs.php, types.inc.php, xmlparser.inc.php, +xmlrpc_db.inc.php, plugins/chat.admin.php, chat.dedimania.php, +chat.players.php, chat.players2.php, chat.records.php, +chat.records2.php, chat.recrels.php, chat.server.php, chat.stats.php, +plugin.chatlog.php, plugin.checkpoints.php, plugin.dedimania.php, +plugin.localdatabase.php, plugin.matchsave.php, plugin.muting.php, +plugin.rasp.php, plugin.rasp_chat.php, plugin.rasp_jukebox.php, +plugin.rasp_karma.php, plugin.track.php, plugin.uptodate.php, +jfreu.chat.php, jfreu.plugin.php. +

      + +Important: +
        +
      • To register your server with the central Dedimania +database, you must copy the login and password values in the +<masterserver_account> section from your +server's dedicated.cfg (TMN) or dedicated_cfg.txt (TMF) file into +the corresponding section of the dedimania.xml file, and add +the 3-character nation abbreviation. Instead of the password +you can also use the community code for your server by using +the server login/password on the official +site for your game (TMO/TMS/TMN) or on this page for +TMF. +
      • Open ports 8003, 8006 and 8007 on your firewall/router for +communication with the central Dedimania server (if that's not +possible, the system falls back on port 80). +
      • In the zip file, all *.XML and config files are now located inside +the newinstall/ directory. This means that you should be able +to unzip the download and replace all the code files (unless you made +changes to any) without worrying about overwriting your customized +config files. However, for every XML/config file that was updated +(see above), you must replace your version with the one from +the newinstall/ directory, or compare them and add any new/changed +configuration settings to your version to insure the system remains +working correctly. +
      • For a new installation, go into the newinstall/ directory and +move all *.XML files into the main directory, next to aseco.php, +and *.PHP files into the includes/ directory. +
      +
      + +
    • Upgrading from v0.96b to v0.97:

      + +The following files were updated in v0.97: aseco.php, adminops.xml, +config.xml, dedimania.xml, rasp.xml, includes/basic.inc.php, +manialinks.inc.php, rasp.funcs.php, types.inc.php, web_access.inc.php, +xmlrpc_db.inc.php, jfreu.config.php, plugins/chat.admin.php, +chat.dedimania.php, chat.players.php, chat.players2.php, +chat.records.php, chat.records2.php, chat.server.php, chat.stats.php, +plugin.checkpoints.php, plugin.dedimania.php, plugin.localdatabase.php, +plugin.matchsave.php, plugin.muting.php, plugin.rasp.php, +plugin.rasp_jukebox.php, plugin.rasp_votes.php, plugin.rounds.php, +plugin.tmxinfo.php, jfreu.chat.php, jfreu.plugin.php. +

      + +Important: +
        +
      • To register your server with the central Dedimania +database, you must copy the login and password values in the +<masterserver_account> section from your +server's dedicated.cfg (TMN) or dedicated_cfg.txt (TMF) file into +the corresponding section of the dedimania.xml file, and add +the 3-character nation abbreviation. Instead of the password +you can also use the community code for your server by using +the server login/password on the official +site for your game (TMO/TMS/TMN) or on this page for +TMF. +
      • Open port 8003 on your firewall/router for communication with the +central Dedimania server (if that's not possible, the system falls +back on port 80). +
      • In the zip file, all *.XML and config files are now located inside +the newinstall/ directory. This means that you should be able +to unzip the download and replace all the code files (unless you made +changes to any) without worrying about overwriting your customized +config files. However, for every XML/config file that was updated +(see above), you must replace your version with the one from +the newinstall/ directory, or compare them and add any new/changed +configuration settings to your version to insure the system remains +working correctly. +
      • For a new installation, go into the newinstall/ directory and +move all *.XML files into the main directory, next to aseco.php, +and *.PHP files into the includes/ directory. +
      +
      + +
    • Upgrading from v0.96 to v0.96b:

      + +The following files were updated in v0.96b: aseco.php, adminops.xml, +matchsave.xml, rasp.xml, includes/basic.inc.php, manialinks.inc.php, +rasp.settings.php, votes.config.php, plugins/chat.admin.php, +plugin.dedimania.php, plugin.localdatabase.php, plugin.matchsave.php, +plugin.rasp.php, plugin.rasp_jukebox.php, plugin.rasp_nextmap.php, +plugin.rasp_votes.php. +

      + +Important: +
        +
      • See the installation notes for upgrading to v0.96. +
      +
      + +
    • Upgrading from v0.95 to v0.96:

      + +The following file was added in v0.96: includes/manialinks.inc.php. +

      + +The following files were updated in v0.96: aseco.php, config.xml, +includes/basic.inc.php, rasp.funcs.php, rasp.settings.php, +tmxinfofetcher.inc.php, types.inc.php, web_access.inc.php, +jfreu.config.php, plugins/chat.admin.php, chat.dedimania.php, +chat.help.php, chat.players.php, chat.players2.php, chat.records.php, +chat.records2.php, chat.server.php, chat.stats.php, plugin.chatlog.php, +plugin.checkpoints.php, plugin.dedimania.php, plugin.localdatabase.php, +plugin.matchsave.php, plugin.muting.php, plugin.rasp.php, +plugin.rasp_chat.php plugin.rasp_jukebox.php, plugin.rasp_nextmap.php, +plugin.rasp_votes.php, plugin.rounds.php, plugin.tmxinfo.php, +plugin.track.php, plugin.uptodate.php, jfreu.chat.php. +

      + +The following files were removed from v0.96: publicdatabase.xml, +includes/dataexchanger.inc.php, plugins/plugin.publicdatabase.php +(the old public database ceased operations long ago, and has now been +superceded by Dedimania). +

      + +Important: +
        +
      • To register your server with the central Dedimania +database, you must copy the login and password values in the +<masterserver_account> section from your +server's dedicated.cfg (TMN) or dedicated_cfg.txt (TMF) file into +the corresponding section of the dedimania.xml file, and add +the 3-character nation abbreviation. Instead of the password +you can also use the community code for your server by using +the server login/password on the official +site for your game (TMO/TMS/TMN) or on this page for +TMF. +
      • Open port 8003 on your firewall/router for communication with the +central Dedimania server (if that's not possible, the system falls +back on port 80). +
      • In the zip file, all *.XML and config files are now located inside +the newinstall/ directory. This means that you should be able +to unzip the download and replace all the code files (unless you made +changes to any) without worrying about overwriting your customized +config files. However, for every XML/config file that was updated +(see above), you must replace your version with the one from +the newinstall/ directory, or compare them and add any new/changed +configuration settings to your version to insure the system remains +working correctly. +
      • For a new installation, go into the newinstall/ directory and +move all *.XML files into the main directory, next to aseco.php, +and *.PHP files into the includes/ directory. +
      +
      + +
    • Upgrading from v0.93 to v0.95:

      + +The following files were added in v0.95: dedimania.xml, +includes/GbxRemote.bem.php, GbxRemote.response.php, urlsafebase64.php, +web_access.inc.php, xmlrpc_db.inc.php, plugins/chat.dedimania.php, +plugin.dedimania.php. +

      + +The following files were updated in v0.95: aseco.php, +adminops.xml, config.xml, localdatabase.xml, plugins.xml, rasp.xml, +includes/GbxRemote.inc.php, basic.inc.php, gbxdatafetcher.inc.php, +rasp.funcs.php, tmndatafetcher.inc.php, tmxinfofetcher.inc.php, +types.inc.php, votes.config.php, xmlparser.inc.php, +plugins/chat.admin.php, chat.players2.php, chat.records2.php, +chat.server.php, chat.song.php, plugin.checkpoints.php, +plugin.localdatabase.php, plugin.rasp.php, plugin.rasp_jukebox.php, +plugin.rasp_nextrank.php, plugin.rasp_votes.php, plugin.tmxinfo.php, +plugin.track.php, jfreu.plugin.php. +

      + +Important: +
        +
      • To register your server with the central Dedimania database, you +must copy the three values in the <masterserver_account> +section from your server's dedicated.cfg file into the corresponding +section of the dedimania.xml file. Instead of the password you +can also use the community code for your server by using the server +login/password on the official site for your +game (TMO/TMS/TMN). +
      • Open port 8003 on your firewall/router for communication with the +central Dedimania server (if that's not possible, the system falls +back on port 80). +
      • In the zip file, all *.XML and config files are now located inside +the newinstall/ directory. This means that you should be able +to unzip the download and replace all the code files (unless you made +changes to any) without worrying about overwriting your customized +config files. However, for every XML/config file that was updated +(see above), you must replace your version with the one from +the newinstall/ directory, or compare them and add any new/changed +configuration settings to your version to insure the system remains +working correctly. +
      • For a new installation, go into the newinstall/ directory and +move all *.XML files into the main directory, next to aseco.php, +and *.PHP files into the includes/ directory. +
      +
      + +
    • Upgrading from v0.92b to v0.93:

      + +The following files were added in v0.93: +includes/gbxdatafetcher.inc.php, tmxinfofetcher.inc.php, +plugins/chat.song.php, plugin.checkpoints.php, plugin.tmxinfo.php. +

      + +The following file was renamed in v0.93: includes/datafetcher.inc.php +to includes/tmndatafetcher.inc.php. +

      + +The following files were updated in v0.93: aseco.php, +adminops.xml, config.xml, plugins.xml, includes/GbxRemote.inc.php, +rasp.funcs.php, jfreu.config.php, plugins/chat.admin.php, +chat.stats.php, plugin.localdatabase.php, plugin.matchsave.php, +plugin.publicdatabase.php, plugin.rasp.php, plugin.rasp_jukebox.php, +plugin.rasp_votes.php, plugin.rounds.php, jfreu.plugin.php. The +remaining code files were updated too, but only for the dependency +comments and occasional layout tweaks. +

      + +Important: +
        +
      • In the zip file, all *.XML and config files are now located inside +the newinstall/ directory. This means that you should be able +to unzip the download and replace all the code files (unless you made +changes to any) without worrying about overwriting your customized +config files. However, for every XML/config file that was updated +(see above), you must replace your version with the one from +the newinstall/ directory, or compare them and add any new/changed +configuration settings to your version to insure the system remains +working correctly. +
      • For a new installation, go into the newinstall/ directory and +move all *.XML files into the main directory, next to aseco.php, +and *.PHP files into the includes/ directory. +
      +
      + +
    • Upgrading from v0.92 to v0.92b:

      + +The following files were updated in v0.92b: aseco.php, +includes/jfreu.config.php, plugins/chat.stats.php, jfreu.chat.php, +jfreu.plugin.php. +

      + +
    • Upgrading from v0.91 to v0.92:

      + +The following file was added in v0.92: includes/datafetcher.inc.php. +

      + +The following files were updated in v0.92: aseco.php, rasp.xml, +includes/GbxRemote.inc.php, types.inc.php, rasp.funcs.php, +rasp.settings.php, jfreu.config.php, plugins/chat.admin.php, +chat.laston.php, chat.players2.php, chat.stats.php, +plugin.localdatabase.php, plugin.rasp.php, plugin.rasp_jukebox.php, +plugin.rasp_nextrank.php, plugin.uptodate.php, jfreu.chat.php, +jfreu.plugin.php. +

      + +
    • Upgrading from v0.90 to v0.91:

      + +The following file was moved in v0.91: plugins/jfreu.config.php +to includes/jfreu.config.php. +

      + +The following files were added in v0.91: includes/votes.config.php +(containing the configuration options previously embedded in +plugins/plugin.rasp_votes.php), plugins/chat.laston.php, and +plugins/jfreu/jfreu.config.xml (by default containing the same settings +as includes/jfreu.config.php). +

      + +The following file was removed in v0.91: plugins/jfreu.unspec.php +(it was integrated into plugins/jfreu.plugin.php). +

      + +The following files were updated in v0.91: aseco.php, adminops.xml, +config.xml, plugins.xml, rasp.xml, includes/jfreu.config.php, +rasp.funcs.php, rasp.settings.php, plugins/chat.admin.php, +chat.server.php, plugin.rasp.php, plugin.rasp_jukebox.php, +plugin.rasp_nextmap.php, plugin.rasp_votes.php, jfreu.chat.php, +jfreu.lite.php, jfreu.plugin.php. +

      + +Important: +
        +
      • Because jfreu.unspec.php was integrated into the main plugin, you +must remove the file from an existing installation and not include it +in plugins.xml anymore if you used it together with jfreu.plugin.php. + +
      • If you allow TMX /add votes ($feature_tmxadd +is true), you must created a new +'GameData/Tracks/Challenges/TMXtmp/' directory along the required +'GameData/Tracks/Challenges/TMX/' directory. Tracks downloaded via +/add are saved to the former directory, but if permanently +added to the server's track list via /admin addthis, they are +moved into the latter. +
      +
      + +
    • Upgrading from v0.89 to v0.90:

      + +The following file was added in v0.90 (although by default not enabled +in plugins.xml): plugins/plugin.muting.php. +

      + +All *.php files were updated in v0.90 for various reasons (if only to +add more comments), so it's strongly recommended to replace them all. +Further, adminops.xml, config.xml, localdatabase.xml, rasp.xml were +also updated. +

      + +Important: +
        +
      • If you use a blacklist or guestlist file (in the "GameData/" +directory), or a tracklist file (in "GameData/Tracks/MatchSettings/"), +rename their extensions from .xml back to .txt. +
      +
      + +
    • Upgrading from v0.88 to v0.89:

      + +The following file was renamed in v0.89: plugins/jfreu.player.php +to plugins/jfreu.lite.php. +

      + +Almost all *.php files were updated in v0.89 for various reasons, so +it's strongly recommended to replace them all. Further, config.xml, +rasp.xml and adminops.xml were also updated. +

      + +Important: +
        +
      • If you use a blacklist or guestlist file (in the "GameData/" +directory), rename its extension from .txt to .xml. + +
      • If you use a tracklist / match settings file, the default file +is now at "GameData/Tracks/MatchSettings/tracklist.xml" (previously +"GameData/Tracks/rasp-tracklist.txt"), and alternate files should +also be in the "GameData/Tracks/MatchSettings/" directory with the +.xml extension. +
      +
      + +
    • Upgrading from v0.86 to v0.88:

      + +The following file was added in v0.88: adminops.xml. +

      + +The following file was renamed in v0.88: plugins/jfreu/jfreu.lists.xml +into plugins/jfreu/jfreu.vips.xml. +

      + +The following files were updated in v0.88: aseco.php, config.xml, +rasp.xml, includes/types.inc.php, rasp.settings.php, rasp.funcs.php, +plugins/chat.admin.php, chat.players.php, chat.records2.php, +chat.server.php, chat.stats.php, plugin.localdatabase.php, +plugin.matchsave.php, plugin.rasp.php, plugin.rasp_chat.php, +plugin.rasp_irc.php, plugin.rasp_jukebox.php, plugin.rasp_karma.php, +plugin.rasp_nextmap.php, plugin.rasp_nextrank.php, +plugin.rasp_votes.php, jfreu.chat.php, jfreu.config.php, +jfreu.player.php, jfreu.plugin.php. +

      + +Important: +
        +
      • In config.xml the <admins> section has been +renamed to <masteradmins>, along with adding a +new <adminops_file> definition. If you use your +previous config.xml file, you must make these changes yourself. + +
      • The <masteradmins> section of config.xml should +contain only those logins you want to have all admin rights, +and it should also contain the server owner's LAN login (with IP and +port), if applicable. + +
      • Any other admin logins should be moved from config.xml into the +<admins> (or <operators>) +section of adminsops.xml, or they can be re-added later via the +/admin addadmin (or addop) command. + +
      • If you use Jfreu's vip/team_vip abilities, rename your existing +plugins/jfreu/jfreu.lists.xml file to plugins/jfreu/jfreu.vips.xml. + +
      • The <admin_list> section in jfreu.vips.xml +is obsolete, and its contents should also be moved into the +<admins> section of adminops.xml before +starting v0.88+, because it will no longer be written back into the +file after the next vip/team_vip change. +
      +
      + +
    • Upgrading from v0.85 to v0.86:

      + +The following file was renamed in v0.86: chat.jfreu.php into +jfreu.chat.php. +

      + +The following files were updated in v0.86: aseco.php, +config.xml, rasp.xml, includes/rasp.funcs.php, rasp.settings.php, +xmlparser.inc.php, plugins/chat.admin.php, chat.players.php, +chat.players2.php, chat.records.php, plugin.rasp.php, +plugin.rasp_jukebox.php, plugin.rasp_karma.php, plugin.rasp_votes.php, +plugin.uptodate.php, jfreu.config.php, jfreu.plugin.php, +mistral.idlekick.php. +

      + +
    • Upgrading from v0.84 to v0.85:

      + +The following file was added in v0.85: plugins/plugin.uptodate.php. +

      + +The following files were updated in v0.85: plugins.xml, +rasp.xml, localdatabase.xml, aseco.php, includes/rasp.settings.php, +rasp.funcs.php, plugins/chat.admin.php, chat.me.php, chat.records2.php, +chat.server.php, chat.stats.php, plugin.localdatabase.php, +plugin.matchsave.php, plugin.rasp.php, plugin.rasp_chat.php, +plugin.rasp_votes.php, jfreu.config.php, jfreu.plugin.php, +mistral.idlekick.php, plus layout tweaks in others. +

      + +
    • Upgrading from v0.82 to v0.84:

      + +The following file was added in v0.84: plugins/plugin.rasp_votes.php. +

      + +Once again, many other *.php files were updated for various reasons, so +it's strongly recommended to replace them all. Further, plugins.xml, +config.xml, localdatabase.xml and rasp.xml were also updated. +

      + +
    • Upgrading from v0.8 to v0.82:

      + +The following files were added in v0.81/0.82: +plugins/plugin.chatlog.php, plugin.rasp_nextrank.php, jfreu.player.php +and jfreu.unspec.php (previously known as jfreu.hack.php). +

      + +Almost all other *.php files were updated for the '&' +pass-by-reference fixes, so it's strongly recommended to replace them +all. Further, plugins.xml, config.xml and rasp.xml were also updated. +

      + +
    • Upgrading from v0.7 to v0.8+:

      +Because of the extent of the changes in v1.14, upgrading from v0.7 and +older versions is best done by renaming your ASECO directory to another +name, following the instructions for a new installation (above), +and then copying over your configuration settings into the pertaining +config files, as well as updating the new options to your liking. + +

      +Subsequently, you can port over any code changes and plugins specific +to your server, or let me know if you think +they might be of general interest, then I'll see whether they can be +incorporated in the next release. + +
    + +
    +
    +Note on upgrading from versions prior to v0.8, and v2.0 and beyond:
    +
    +This release, when first run, will automatically rename the +'trackID' column in the rs_times table in your ASECO database +to 'challengeID' for consistency with all the other tables. +This means that if you ever want to downgrade to v0.7, you'll +need to manually rename that column back by entering the following +command in your database (via PhpMyAdmin, the MySQL command prompt, +or similar):

    + +ALTER TABLE rs_times CHANGE challengeID trackID mediumint(9) NOT NULL default 0 +

    +Aseco/Rasp versions 2.0 and beyond include additional columns in some +tables, but they are ignored. +
    +
    + +
    +
    +
    +Copyright © 2007-2013 – Frans P. de Vries <tm@gamers.org> +            +Last updated 09-Mar-2013 +
    + + diff --git a/docker-xaseco/xaseco/access.xml b/docker-xaseco/xaseco/access.xml new file mode 100644 index 0000000..9b57d03 --- /dev/null +++ b/docker-xaseco/xaseco/access.xml @@ -0,0 +1,93 @@ + + + + Deny,Allow + + + all + + + + + + + + {#server}>> {#message}Player {#highlite}{1}$z$s{#message} denied access from {2} {#highlite}{3}{#message} [{#error}Kicked $z$s{#message}] + {#message}Access from zone {#highlite}{1} {#message}denied$z + {#server}> Player access control reloaded from {#highlite}access.xml + {#server}> {#highlite}access.xml {#error}config error, player access control disabled! + {#server}> {#error}Missing parameter, use {#highlite}$i /admin access help {#error}! + + diff --git a/docker-xaseco/xaseco/adminops.xml b/docker-xaseco/xaseco/adminops.xml new file mode 100644 index 0000000..b68e965 --- /dev/null +++ b/docker-xaseco/xaseco/adminops.xml @@ -0,0 +1,320 @@ + + + + MasterAdmin + Admin + Operator + + + + + + + + + + + + true + true + false + false + true + true + true + false + false + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + false + false + false + false + false + false + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + false + false + false + false + true + true + true + true + true + false + false + true + true + true + true + true + false + false + true + true + false + true + true + true + true + true + true + true + true + true + + false + false + false + false + true + false + true + true + true + true + false + true + false + false + false + true + true + true + true + true + true + true + true + true + false + false + false + true + true + false + true + true + + + + true + true + false + false + false + false + false + false + false + false + false + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + false + false + false + false + true + true + true + false + false + false + false + false + false + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + false + false + false + false + false + false + false + false + false + false + false + false + false + false + false + false + false + false + false + false + false + true + true + true + true + true + true + true + true + false + false + false + false + false + false + true + true + true + false + false + true + false + false + false + false + false + false + false + false + false + false + false + false + true + true + true + true + true + true + + false + false + false + false + false + false + true + false + false + false + false + true + false + false + false + false + false + false + false + true + false + false + false + false + false + false + false + false + false + false + true + false + + diff --git a/docker-xaseco/xaseco/aseco.log b/docker-xaseco/xaseco/aseco.log new file mode 100644 index 0000000..8cfa54c --- /dev/null +++ b/docker-xaseco/xaseco/aseco.log @@ -0,0 +1,5 @@ +# initialize XASECO ########################################################### +[XAseco] PHP Version is 5.6.40-7+0~20190503101815.14+stretch~1.gbp1a44f9 on Linux +[XAseco] Load settings [config.xml] +[XASECO Warning] [XML Error 80] Comment must not contain '--' (double-hyphen) on line 10 +[XASECO Fatal Error] Could not read/parse config file config.xml ! on line 459 in file /home/admin/docker/build/xaseco/xaseco/aseco.php diff --git a/docker-xaseco/xaseco/aseco.php b/docker-xaseco/xaseco/aseco.php new file mode 100644 index 0000000..c8525b9 --- /dev/null +++ b/docker-xaseco/xaseco/aseco.php @@ -0,0 +1,2562 @@ + + * + * Re-authored & copyright May 2007 - Jul 2013 by Xymph + * + * Visit the official site at http://www.xaseco.org/ + */ + +/** + * Include required classes + */ +require_once('includes/types.inc.php'); // contains classes to store information +require_once('includes/basic.inc.php'); // contains standard functions +require_once('includes/GbxRemote.inc.php'); // needed for dedicated server connections +require_once('includes/xmlparser.inc.php'); // provides an XML parser +require_once('includes/gbxdatafetcher.inc.php'); // provides access to GBX data +require_once('includes/tmndatafetcher.inc.php'); // provides access to TMN world stats +require_once('includes/rasp.settings.php'); // specific to the RASP plugins + +/** + * Runtime configuration definitions + */ + +// add abbreviations for some chat commands? +// /admin -> /ad, /jukebox -> /jb, /autojuke -> /aj +define('ABBREV_COMMANDS', false); +// disable local & Dedi record relations commands from help lists? +define('INHIBIT_RECCMDS', false); +// separate logs by month in logs/ dir? +define('MONTHLY_LOGSDIR', false); +// keep UTF-8 encoding in config.xml? +define('CONFIG_UTF8ENCODE', false); + +/** + * System definitions - no changes below this point + */ + +// current project version +define('XASECO_VERSION', '1.16'); +define('XASECO_TMN', 'http://www.gamers.org/tmn/'); +define('XASECO_TMF', 'http://www.gamers.org/tmf/'); +define('XASECO_TM2', 'http://www.gamers.org/tm2/'); +define('XASECO_ORG', 'http://www.xaseco.org/'); + +// required official dedicated server builds +define('TMN_BUILD', '2006-05-30'); +define('TMF_BUILD', '2011-02-21'); + +// check current operating system +if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + // on Win32/NT use: + define('CRLF', "\r\n"); +} else { + // on Unix use: + define('CRLF', "\n"); +} +if (!defined('LF')) { + define('LF', "\n"); +} + +/** + * Error function + * Report errors in a regular way. + */ +set_error_handler('displayError'); +function displayError($errno, $errstr, $errfile, $errline) { + global $aseco; + + // check for error suppression + if (error_reporting() == 0) return; + + switch ($errno) { + case E_USER_ERROR: + $message = "[XASECO Fatal Error] $errstr on line $errline in file $errfile" . CRLF; + echo $message; + doLog($message); + + // throw 'shutting down' event + $aseco->releaseEvent('onShutdown', null); + // clear all ManiaLinks + $aseco->client->query('SendHideManialinkPage'); + + if (function_exists('xdebug_get_function_stack')) + doLog(print_r(xdebug_get_function_stack()), true); + die(); + break; + case E_USER_WARNING: + $message = "[XASECO Warning] $errstr" . CRLF; + echo $message; + doLog($message); + break; + case E_ERROR: + $message = "[PHP Error] $errstr on line $errline in file $errfile" . CRLF; + echo $message; + doLog($message); + break; + case E_WARNING: + $message = "[PHP Warning] $errstr on line $errline in file $errfile" . CRLF; + echo $message; + doLog($message); + break; + default: + if (strpos($errstr, 'Function call_user_method') !== false) break; + //$message = "[PHP $errno] $errstr on line $errline in file $errfile" . CRLF; + //echo $message; + //doLog($message); + // do nothing, only treat known errors + } +} // displayError + +/** + * Here XASECO actually starts. + */ +class Aseco { + + /** + * Public fields + */ + var $client; + var $xml_parser; + var $script_timeout; + var $debug; + var $server; + var $command; + var $events; + var $rpc_calls; + var $rpc_responses; + var $chat_commands; + var $chat_colors; + var $chat_messages; + var $plugins; + var $settings; + var $style; + var $panels; + var $statspanel; + var $titles; + var $masteradmin_list; + var $admin_list; + var $adm_abilities; + var $operator_list; + var $op_abilities; + var $bannedips; + var $startup_phase; // XAseco start-up phase + var $warmup_phase; // warm-up phase + var $restarting; // restarting challenge (0 = not, 1 = instant, 2 = chattime) + var $changingmode; // changing game mode + var $currstatus; // server status changes + var $prevstatus; + var $currsecond; // server time changes + var $prevsecond; + var $uptime; // XAseco start-up time + + + /** + * Initializes the server. + */ + function Aseco($debug) { + global $maxrecs; // from rasp.settings.php + + echo '# initialize XASECO ###########################################################' . CRLF; + + // log php & mysql version info + $this->console_text('[XAseco] PHP Version is ' . phpversion() . ' on ' . PHP_OS); + + // initialize + $this->uptime = time(); + $this->chat_commands = array(); + $this->debug = $debug; + $this->client = new IXR_ClientMulticall_Gbx(); + $this->xml_parser = new Examsly(); + $this->server = new Server('127.0.0.1', 5000, 'SuperAdmin', 'SuperAdmin'); + $this->server->challenge = new Challenge(); + $this->server->players = new PlayerList(); + $this->server->records = new RecordList($maxrecs); + $this->server->mutelist = array(); + $this->plugins = array(); + $this->titles = array(); + $this->masteradmin_list = array(); + $this->admin_list = array(); + $this->adm_abilities = array(); + $this->operator_list = array(); + $this->op_abilities = array(); + $this->bannedips = array(); + $this->startup_phase = true; + $this->warmup_phase = false; + $this->restarting = 0; + $this->changingmode = false; + $this->currstatus = 0; + } // Aseco + + + /** + * Load settings and apply them on the current instance. + */ + function loadSettings($config_file) { + + if ($settings = $this->xml_parser->parseXml($config_file, true, CONFIG_UTF8ENCODE)) { + // read the XML structure into an array + $aseco = $settings['SETTINGS']['ASECO'][0]; + + // read settings and apply them + $this->chat_colors = $aseco['COLORS'][0]; + $this->chat_messages = $aseco['MESSAGES'][0]; + $this->masteradmin_list = $aseco['MASTERADMINS'][0]; + if (!isset($this->masteradmin_list) || !is_array($this->masteradmin_list)) + trigger_error('No MasterAdmin(s) configured in config.xml!', E_USER_ERROR); + + // check masteradmin list consistency + if (empty($this->masteradmin_list['IPADDRESS'])) { + // fill list to same length as list + if (($cnt = count($this->masteradmin_list['TMLOGIN'])) > 0) + $this->masteradmin_list['IPADDRESS'] = array_fill(0, $cnt, ''); + } else { + if (count($this->masteradmin_list['TMLOGIN']) != count($this->masteradmin_list['IPADDRESS'])) + trigger_error("MasterAdmin mismatch between 's and 's!", E_USER_WARNING); + } + + // set admin lock password + $this->settings['lock_password'] = $aseco['LOCK_PASSWORD'][0]; + // set cheater action + $this->settings['cheater_action'] = $aseco['CHEATER_ACTION'][0]; + // set script timeout + $this->settings['script_timeout'] = $aseco['SCRIPT_TIMEOUT'][0]; + // set minimum number of records to be displayed + $this->settings['show_min_recs'] = $aseco['SHOW_MIN_RECS'][0]; + // show records before start of track? + $this->settings['show_recs_before'] = $aseco['SHOW_RECS_BEFORE'][0]; + // show records after end of track? + $this->settings['show_recs_after'] = $aseco['SHOW_RECS_AFTER'][0]; + // show TMX world record? + $this->settings['show_tmxrec'] = $aseco['SHOW_TMXREC'][0]; + // show played time at end of track? + $this->settings['show_playtime'] = $aseco['SHOW_PLAYTIME'][0]; + // show current track at start of track? + $this->settings['show_curtrack'] = $aseco['SHOW_CURTRACK'][0]; + // set default filename for readtracklist/writetracklist + $this->settings['default_tracklist'] = $aseco['DEFAULT_TRACKLIST'][0]; + // set minimum number of ranked players in a clan to be included in /topclans + $this->settings['topclans_minplayers'] = $aseco['TOPCLANS_MINPLAYERS'][0]; + // set multiple of win count to show global congrats message + $this->settings['global_win_multiple'] = ($aseco['GLOBAL_WIN_MULTIPLE'][0] > 0 ? $aseco['GLOBAL_WIN_MULTIPLE'][0] : 1); + // timeout of the TMF message window in seconds + $this->settings['window_timeout'] = $aseco['WINDOW_TIMEOUT'][0]; + // set filename of admin/operator/ability lists file + $this->settings['adminops_file'] = $aseco['ADMINOPS_FILE'][0]; + // set filename of banned IPs list file + $this->settings['bannedips_file'] = $aseco['BANNEDIPS_FILE'][0]; + // set filename of blacklist file + $this->settings['blacklist_file'] = $aseco['BLACKLIST_FILE'][0]; + // set filename of guestlist file + $this->settings['guestlist_file'] = $aseco['GUESTLIST_FILE'][0]; + // set filename of track history file + $this->settings['trackhist_file'] = $aseco['TRACKHIST_FILE'][0]; + // set minimum admin client version + $this->settings['admin_client'] = $aseco['ADMIN_CLIENT_VERSION'][0]; + // set minimum player client version + $this->settings['player_client'] = $aseco['PLAYER_CLIENT_VERSION'][0]; + // set default rounds points system + $this->settings['default_rpoints'] = $aseco['DEFAULT_RPOINTS'][0]; + // set windows style (none = old TMN style) + $this->settings['window_style'] = $aseco['WINDOW_STYLE'][0]; + // set admin panel (none = no panel) + $this->settings['admin_panel'] = $aseco['ADMIN_PANEL'][0]; + // set donate panel (none = no panel) + $this->settings['donate_panel'] = $aseco['DONATE_PANEL'][0]; + // set records panel (none = no panel) + $this->settings['records_panel'] = $aseco['RECORDS_PANEL'][0]; + // set vote panel (none = no panel) + $this->settings['vote_panel'] = $aseco['VOTE_PANEL'][0]; + + // display welcome message as window ? + if (strtoupper($aseco['WELCOME_MSG_WINDOW'][0]) == 'TRUE') { + $this->settings['welcome_msg_window'] = true; + } else { + $this->settings['welcome_msg_window'] = false; + } + + // log all chat, not just chat commands ? + if (strtoupper($aseco['LOG_ALL_CHAT'][0]) == 'TRUE') { + $this->settings['log_all_chat'] = true; + } else { + $this->settings['log_all_chat'] = false; + } + + // show timestamps in /chatlog, /pmlog & /admin pmlog ? + if (strtoupper($aseco['CHATPMLOG_TIMES'][0]) == 'TRUE') { + $this->settings['chatpmlog_times'] = true; + } else { + $this->settings['chatpmlog_times'] = false; + } + + // show records range? + if (strtoupper($aseco['SHOW_RECS_RANGE'][0]) == 'TRUE') { + $this->settings['show_recs_range'] = true; + } else { + $this->settings['show_recs_range'] = false; + } + + // show records in message window? + if (strtoupper($aseco['RECS_IN_WINDOW'][0]) == 'TRUE') { + $this->settings['recs_in_window'] = true; + } else { + $this->settings['recs_in_window'] = false; + } + + // show round reports in message window? + if (strtoupper($aseco['ROUNDS_IN_WINDOW'][0]) == 'TRUE') { + $this->settings['rounds_in_window'] = true; + } else { + $this->settings['rounds_in_window'] = false; + } + + // add random filter to /admin writetracklist output + if (strtoupper($aseco['WRITETRACKLIST_RANDOM'][0]) == 'TRUE') { + $this->settings['writetracklist_random'] = true; + } else { + $this->settings['writetracklist_random'] = false; + } + + // add explanation to /help output + if (strtoupper($aseco['HELP_EXPLANATION'][0]) == 'TRUE') { + $this->settings['help_explanation'] = true; + } else { + $this->settings['help_explanation'] = false; + } + + // color nicknames in the various /top... etc lists? + if (strtoupper($aseco['LISTS_COLORNICKS'][0]) == 'TRUE') { + $this->settings['lists_colornicks'] = true; + } else { + $this->settings['lists_colornicks'] = false; + } + + // color tracknames in the various /lists... lists? + if (strtoupper($aseco['LISTS_COLORTRACKS'][0]) == 'TRUE') { + $this->settings['lists_colortracks'] = true; + } else { + $this->settings['lists_colortracks'] = false; + } + + // display checkpoints panel (TMF) or pop-up (TMN)? + if (strtoupper($aseco['DISPLAY_CHECKPOINTS'][0]) == 'TRUE') { + $this->settings['display_checkpoints'] = true; + } else { + $this->settings['display_checkpoints'] = false; + } + + // enable /cpsspec command (TMF-only)? + if (strtoupper($aseco['ENABLE_CPSSPEC'][0]) == 'TRUE') { + $this->settings['enable_cpsspec'] = true; + } else { + $this->settings['enable_cpsspec'] = false; + } + + // automatically enable /cps for new players? + if (strtoupper($aseco['AUTO_ENABLE_CPS'][0]) == 'TRUE') { + $this->settings['auto_enable_cps'] = true; + } else { + $this->settings['auto_enable_cps'] = false; + } + + // automatically enable /dedicps for new players? + if (strtoupper($aseco['AUTO_ENABLE_DEDICPS'][0]) == 'TRUE') { + $this->settings['auto_enable_dedicps'] = true; + } else { + $this->settings['auto_enable_dedicps'] = false; + } + + // automatically add IP for new admins/operators? + if (strtoupper($aseco['AUTO_ADMIN_ADDIP'][0]) == 'TRUE') { + $this->settings['auto_admin_addip'] = true; + } else { + $this->settings['auto_admin_addip'] = false; + } + + // automatically force spectator on player using /afk ? + if (strtoupper($aseco['AFK_FORCE_SPEC'][0]) == 'TRUE') { + $this->settings['afk_force_spec'] = true; + } else { + $this->settings['afk_force_spec'] = false; + } + + // provide clickable buttons in TMF lists? + if (strtoupper($aseco['CLICKABLE_LISTS'][0]) == 'TRUE') { + $this->settings['clickable_lists'] = true; + } else { + $this->settings['clickable_lists'] = false; + } + + // show logins in /recs on TMF? + if (strtoupper($aseco['SHOW_REC_LOGINS'][0]) == 'TRUE') { + $this->settings['show_rec_logins'] = true; + } else { + $this->settings['show_rec_logins'] = false; + } + + // display individual stats panels at TMF scoreboard? + if (strtoupper($aseco['SB_STATS_PANELS'][0]) == 'TRUE') { + $this->settings['sb_stats_panels'] = true; + } else { + $this->settings['sb_stats_panels'] = false; + } + + // read the XML structure into an array + $tmserver = $settings['SETTINGS']['TMSERVER'][0]; + + // read settings and apply them + $this->server->login = $tmserver['LOGIN'][0]; + $this->server->pass = $tmserver['PASSWORD'][0]; + $this->server->port = $tmserver['PORT'][0]; + $this->server->ip = $tmserver['IP'][0]; + if (isset($tmserver['TIMEOUT'][0])) { + $this->server->timeout = (int)$tmserver['TIMEOUT'][0]; + } else { + $this->server->timeout = null; + trigger_error('Server init timeout not specified in config.xml !', E_USER_WARNING); + } + + $this->style = array(); + $this->panels = array(); + $this->panels['admin'] = ''; + $this->panels['donate'] = ''; + $this->panels['records'] = ''; + $this->panels['vote'] = ''; + + if ($this->settings['admin_client'] != '' && + preg_match('/^2\.11\.[12][0-9]$/', $this->settings['admin_client']) != 1 || + $this->settings['admin_client'] == '2.11.10') + trigger_error('Invalid admin client version : ' . $this->settings['admin_client'] . ' !', E_USER_ERROR); + if ($this->settings['player_client'] != '' && + preg_match('/^2\.11\.[12][0-9]$/', $this->settings['player_client']) != 1 || + $this->settings['player_client'] == '2.11.10') + trigger_error('Invalid player client version: ' . $this->settings['player_client'] . ' !', E_USER_ERROR); + } else { + // could not parse XML file + trigger_error('Could not read/parse config file ' . $config_file . ' !', E_USER_ERROR); + } + } // loadSettings + + + /** + * Read Admin/Operator/Ability lists and apply them on the current instance. + */ + function readLists() { + + // get lists file name + $adminops_file = $this->settings['adminops_file']; + + if ($lists = $this->xml_parser->parseXml($adminops_file, true, true)) { + // read the XML structure into arrays + $this->titles = $lists['LISTS']['TITLES'][0]; + + if (is_array($lists['LISTS']['ADMINS'][0])) { + $this->admin_list = $lists['LISTS']['ADMINS'][0]; + // check admin list consistency + if (empty($this->admin_list['IPADDRESS'])) { + // fill list to same length as list + if (($cnt = count($this->admin_list['TMLOGIN'])) > 0) + $this->admin_list['IPADDRESS'] = array_fill(0, $cnt, ''); + } else { + if (count($this->admin_list['TMLOGIN']) != count($this->admin_list['IPADDRESS'])) + trigger_error("Admin mismatch between 's and 's!", E_USER_WARNING); + } + } + + if (is_array($lists['LISTS']['OPERATORS'][0])) { + $this->operator_list = $lists['LISTS']['OPERATORS'][0]; + // check operator list consistency + if (empty($this->operator_list['IPADDRESS'])) { + // fill list to same length as list + if (($cnt = count($this->operator_list['TMLOGIN'])) > 0) + $this->operator_list['IPADDRESS'] = array_fill(0, $cnt, ''); + } else { + if (count($this->operator_list['TMLOGIN']) != count($this->operator_list['IPADDRESS'])) + trigger_error("Operators mismatch between 's and 's!", E_USER_WARNING); + } + } + + $this->adm_abilities = $lists['LISTS']['ADMIN_ABILITIES'][0]; + $this->op_abilities = $lists['LISTS']['OPERATOR_ABILITIES'][0]; + + // convert strings to booleans + foreach ($this->adm_abilities as $ability => $value) { + if (strtoupper($value[0]) == 'TRUE') { + $this->adm_abilities[$ability][0] = true; + } else { + $this->adm_abilities[$ability][0] = false; + } + } + foreach ($this->op_abilities as $ability => $value) { + if (strtoupper($value[0]) == 'TRUE') { + $this->op_abilities[$ability][0] = true; + } else { + $this->op_abilities[$ability][0] = false; + } + } + return true; + } else { + // could not parse XML file + trigger_error('Could not read/parse adminops file ' . $adminops_file . ' !', E_USER_WARNING); + return false; + } + } // readLists + + /** + * Write Admin/Operator/Ability lists to save them for future runs. + */ + function writeLists() { + + // get lists file name + $adminops_file = $this->settings['adminops_file']; + + // compile lists file contents + $lists = "" . CRLF + . "" . CRLF + . "\t" . CRLF; + foreach ($this->titles as $title => $value) { + $lists .= "\t\t<" . strtolower($title) . ">" . + $value[0] + . "" . CRLF; + } + $lists .= "\t" . CRLF + . CRLF + . "\t" . CRLF; + $empty = true; + if (isset($this->admin_list['TMLOGIN'])) { + for ($i = 0; $i < count($this->admin_list['TMLOGIN']); $i++) { + if ($this->admin_list['TMLOGIN'][$i] != '') { + $lists .= "\t\t" . $this->admin_list['TMLOGIN'][$i] . "" + . " " . $this->admin_list['IPADDRESS'][$i] . "" . CRLF; + $empty = false; + } + } + } + if ($empty) { + $lists .= "" . CRLF; + } + $lists .= "\t" . CRLF + . CRLF + . "\t" . CRLF; + $empty = true; + if (isset($this->operator_list['TMLOGIN'])) { + for ($i = 0; $i < count($this->operator_list['TMLOGIN']); $i++) { + if ($this->operator_list['TMLOGIN'][$i] != '') { + $lists .= "\t\t" . $this->operator_list['TMLOGIN'][$i] . "" + . " " . $this->operator_list['IPADDRESS'][$i] . "" . CRLF; + $empty = false; + } + } + } + if ($empty) { + $lists .= "" . CRLF; + } + $lists .= "\t" . CRLF + . CRLF + . "\t" . CRLF; + foreach ($this->adm_abilities as $ability => $value) { + $lists .= "\t\t<" . strtolower($ability) . ">" . + ($value[0] ? "true" : "false") + . "" . CRLF; + } + $lists .= "\t" . CRLF + . CRLF + . "\t" . CRLF; + foreach ($this->op_abilities as $ability => $value) { + $lists .= "\t\t<" . strtolower($ability) . ">" . + ($value[0] ? "true" : "false") + . "" . CRLF; + } + $lists .= "\t" . CRLF + . "" . CRLF; + + // write out the lists file + if (!@file_put_contents($adminops_file, $lists)) { + trigger_error('Could not write adminops file ' . $adminops_file . ' !', E_USER_WARNING); + return false; + } else { + return true; + } + } // writeLists + + + /** + * Read Banned IPs list and apply it on the current instance. + */ + function readIPs() { + + // get banned IPs file name + $bannedips_file = $this->settings['bannedips_file']; + + if ($list = $this->xml_parser->parseXml($bannedips_file)) { + // read the XML structure into variable + if (isset($list['BAN_LIST']['IPADDRESS'])) + $this->bannedips = $list['BAN_LIST']['IPADDRESS']; + else + $this->bannedips = array(); + return true; + } else { + // could not parse XML file + trigger_error('Could not read/parse banned IPs file ' . $bannedips_file . ' !', E_USER_WARNING); + return false; + } + } // readIPs + + /** + * Write Banned IPs list to save it for future runs. + */ + function writeIPs() { + + // get banned IPs file name + $bannedips_file = $this->settings['bannedips_file']; + $empty = true; + + // compile banned IPs file contents + $list = "" . CRLF + . "" . CRLF; + for ($i = 0; $i < count($this->bannedips); $i++) { + if ($this->bannedips[$i] != '') { + $list .= "\t\t" . $this->bannedips[$i] . "" . CRLF; + $empty = false; + } + } + if ($empty) { + $list .= "" . CRLF; + } + $list .= "" . CRLF; + + // write out the list file + if (!@file_put_contents($bannedips_file, $list)) { + trigger_error('Could not write banned IPs file ' . $bannedips_file . ' !', E_USER_WARNING); + return false; + } else { + return true; + } + } // writeIPs + + + /** + * Loads files in the plugins directory. + */ + function loadPlugins() { + + // load and parse the plugins file + if ($plugins = $this->xml_parser->parseXml('plugins.xml')) { + if (!empty($plugins['ASECO_PLUGINS']['PLUGIN'])) { + // take each plugin tag + foreach ($plugins['ASECO_PLUGINS']['PLUGIN'] as $plugin) { + // log plugin message + $this->console_text('[XAseco] Load plugin [' . $plugin . ']'); + // include the plugin + require_once('plugins/' . $plugin); + $this->plugins[] = $plugin; + } + } + } else { + trigger_error('Could not read/parse plugins list plugins.xml !', E_USER_ERROR); + } + } // loadPlugins + + + /** + * Runs the server. + */ + function run($config_file) { + + // load new settings, if available + $this->console_text('[XAseco] Load settings [{1}]', $config_file); + $this->loadSettings($config_file); + + // load admin/operator/ability lists, if available + $this->console_text('[XAseco] Load admin/ops lists [{1}]', $this->settings['adminops_file']); + $this->readLists(); + + // load banned IPs list, if available + $this->console_text('[XAseco] Load banned IPs list [{1}]', $this->settings['bannedips_file']); + $this->readIPs(); + + // load plugins and register chat commands + $this->console_text('[XAseco] Load plugins list [plugins.xml]'); + $this->loadPlugins(); + + // connect to Trackmania Dedicated Server + if (!$this->connect()) { + // kill program with an error + trigger_error('Connection could not be established !', E_USER_ERROR); + } + + // log status message + $this->console('Connection established successfully !'); + // log admin lock message + if ($this->settings['lock_password'] != '') + $this->console_text("[XAseco] Locked admin commands & features with password '{1}'", $this->settings['lock_password']); + + // get basic server info + $this->client->query('GetVersion'); + $response['version'] = $this->client->getResponse(); + $this->server->game = $response['version']['Name']; + $this->server->version = $response['version']['Version']; + $this->server->build = $response['version']['Build']; + + // throw 'starting up' event + $this->releaseEvent('onStartup', null); + + // synchronize information with server + $this->serverSync(); + + // register all chat commands + if ($this->server->getGame() != 'TMF') { + $this->registerChatCommands(); + // set spectator not available outside TMF + if ($this->settings['cheater_action'] == 1) + $this->settings['cheater_action'] = 0; + } + + // make a visual header + $this->sendHeader(); + + // get current game infos if server loaded a track yet + if ($this->currstatus == 100) { + $this->console_text('[XAseco] Waiting for the server to start a challenge'); + } else { + $this->beginRace(false); + } + + // main loop + $this->startup_phase = false; + while (true) { + $starttime = microtime(true); + // get callbacks from the server + $this->executeCallbacks(); + + // sends calls to the server + $this->executeCalls(); + + // throw timing events + $this->releaseEvent('onMainLoop', null); + + $this->currsecond = time(); + if ($this->prevsecond != $this->currsecond) { + $this->prevsecond = $this->currsecond; + $this->releaseEvent('onEverySecond', null); + } + + // reduce CPU usage if main loop has time left + $endtime = microtime(true); + $delay = 200000 - ($endtime - $starttime) * 1000000; + if ($delay > 0) + usleep($delay); + // make sure the script does not timeout + @set_time_limit($this->settings['script_timeout']); + } + + // close the client connection + $this->client->Terminate(); + } // run + + + /** + * Authenticates XASECO at the server. + */ + function connect() { + + // only if logins are set + if ($this->server->ip && $this->server->port && $this->server->login && $this->server->pass) { + // log console message + $this->console('Try to connect to TM dedicated server on {1}:{2} timeout {3}s', + $this->server->ip, $this->server->port, + ($this->server->timeout !== null ? $this->server->timeout : 0)); + + // connect to the server + if (!$this->client->InitWithIp($this->server->ip, $this->server->port, $this->server->timeout)) { + trigger_error('[' . $this->client->getErrorCode() . '] InitWithIp - ' . $this->client->getErrorMessage(), E_USER_WARNING); + return false; + } + + // log console message + $this->console("Try to authenticate with login '{1}' and password '{2}'", + $this->server->login, $this->server->pass); + + // check login + if ($this->server->login != 'SuperAdmin') { + trigger_error("Invalid login '" . $this->server->login . "' - must be 'SuperAdmin' in config.xml !", E_USER_WARNING); + return false; + } + // check password + if ($this->server->pass == 'SuperAdmin') { + trigger_error("Insecure password '" . $this->server->pass . "' - should be changed in dedicated config and config.xml !", E_USER_WARNING); + } + + // log into the server + if (!$this->client->query('Authenticate', $this->server->login, $this->server->pass)) { + trigger_error('[' . $this->client->getErrorCode() . '] Authenticate - ' . $this->client->getErrorMessage(), E_USER_WARNING); + return false; + } + + // enable callback system + $this->client->query('EnableCallbacks', true); + + // wait for server to be ready + $this->waitServerReady(); + + // connection established + return true; + } else { + // connection failed + return false; + } + } // connect + + + /** + * Waits for the server to be ready (status 4, 'Running - Play') + */ + function waitServerReady() { + + $this->client->query('GetStatus'); + $status = $this->client->getResponse(); + if ($status['Code'] != 4) { + $this->console("Waiting for dedicated server to reach status 'Running - Play'..."); + $this->console('Status: ' . $status['Name']); + $timeout = 0; + $laststatus = $status['Name']; + while ($status['Code'] != 4) { + sleep(1); + $this->client->query('GetStatus'); + $status = $this->client->getResponse(); + if ($laststatus != $status['Name']) { + $this->console('Status: ' . $status['Name']); + $laststatus = $status['Name']; + } + if (isset($this->server->timeout) && $timeout++ > $this->server->timeout) + trigger_error('Timed out while waiting for dedicated server!', E_USER_ERROR); + } + } + } // waitServerReady + + /** + * Initializes the server and the player list. + * Reads a list of the players who are on the server already, + * and loads all server variables. + */ + function serverSync() { + + // check server build + if (strlen($this->server->build) == 0 || + ($this->server->getGame() != 'TMF' && strcmp($this->server->build, TMN_BUILD) < 0) || + ($this->server->getGame() == 'TMF' && strcmp($this->server->build, TMF_BUILD) < 0)) { + trigger_error("Obsolete server build '" . $this->server->build . "' - must be " . + ($this->server->getGame() == 'TMF' ? "at least '" . TMF_BUILD . "' !" : "'" . TMN_BUILD . "' !"), E_USER_ERROR); + } + + // get server id, login, nickname, zone & packmask + $this->server->id = 0; // on TMN/TMO/TMS + $this->server->rights = false; + $this->server->isrelay = false; + $this->server->relaymaster = null; + $this->server->relayslist = array(); + $this->server->gamestate = Server::RACE; + $this->server->packmask = ''; + if ($this->server->getGame() == 'TMF') { + $this->client->query('GetSystemInfo'); + $response['system'] = $this->client->getResponse(); + $this->server->serverlogin = $response['system']['ServerLogin']; + + $this->client->query('GetDetailedPlayerInfo', $this->server->serverlogin); + $response['info'] = $this->client->getResponse(); + $this->server->id = $response['info']['PlayerId']; + $this->server->nickname = $response['info']['NickName']; + $this->server->zone = substr($response['info']['Path'], 6); // strip 'World|' + $this->server->rights = ($response['info']['OnlineRights'] == 3); // United = true + + $this->client->query('GetLadderServerLimits'); + $response['ladder'] = $this->client->getResponse(); + $this->server->laddermin = $response['ladder']['LadderServerLimitMin']; + $this->server->laddermax = $response['ladder']['LadderServerLimitMax']; + + $this->client->query('IsRelayServer'); + $this->server->isrelay = ($this->client->getResponse() > 0); + if ($this->server->isrelay) { + $this->client->query('GetMainServerPlayerInfo', 1); + $this->server->relaymaster = $this->client->getResponse(); + } + + // TMNF packmask = 'Stadium' for 'nations' or 'stadium' + $this->client->query('GetServerPackMask'); + $this->server->packmask = $this->client->getResponse(); + + // clear possible leftover ManiaLinks + $this->client->query('SendHideManialinkPage'); + } + + // get mode & limits + $this->client->query('GetCurrentGameInfo', ($this->server->getGame() == 'TMF' ? 1 : 0)); + $response['gameinfo'] = $this->client->getResponse(); + $this->server->gameinfo = new Gameinfo($response['gameinfo']); + + // get status + $this->client->query('GetStatus'); + $response['status'] = $this->client->getResponse(); + $this->currstatus = $response['status']['Code']; + + // get game & trackdir + $this->client->query('GameDataDirectory'); + $this->server->gamedir = $this->client->getResponse(); + $this->client->query('GetTracksDirectory'); + $this->server->trackdir = $this->client->getResponse(); + + // get server name & options + $this->getServerOptions(); + + // throw 'synchronisation' event + $this->releaseEvent('onSync', null); + + // get current players/servers on the server (hardlimited to 300) + if ($this->server->getGame() == 'TMF') + $this->client->query('GetPlayerList', 300, 0, 2); + else + $this->client->query('GetPlayerList', 300, 0); + $response['playerlist'] = $this->client->getResponse(); + + // update players/relays lists + if (!empty($response['playerlist'])) { + foreach ($response['playerlist'] as $player) { + // fake it into thinking it's a connecting player: + // it gets team & ladder info this way & will also throw an + // onPlayerConnect event for players (not relays) to all plugins + $this->playerConnect(array($player['Login'], '')); + } + } + } // serverSync + + + /** + * Sends program header to console and ingame chat. + */ + function sendHeader() { + + $this->console_text('###############################################################################'); + $this->console_text(' XASECO v' . XASECO_VERSION . ' running on {1}:{2}', $this->server->ip, $this->server->port); + if ($this->server->getGame() == 'TMF') { + $this->console_text(' Name : {1} - {2}', stripColors($this->server->name, false), $this->server->serverlogin); + if ($this->server->isrelay) + $this->console_text(' Relays : {1} - {2}', stripColors($this->server->relaymaster['NickName'], false), $this->server->relaymaster['Login']); + $this->console_text(' Game : {1} {2} - {3} - {4}', $this->server->game, + ($this->server->rights ? 'United' : 'Nations'), + $this->server->packmask, $this->server->gameinfo->getMode()); + } else { + $this->console_text(' Name : {1}', stripColors($this->server->name, false)); + $this->console_text(' Game : {1} - {2}', $this->server->game, $this->server->gameinfo->getMode()); + } + $this->console_text(' Version: {1} / {2}', $this->server->version, $this->server->build); + $this->console_text(' Authors: Florian Schnell & Assembler Maniac'); + $this->console_text(' Re-Authored: Xymph'); + $this->console_text('###############################################################################'); + + // format the text of the message + $startup_msg = formatText($this->getChatMessage('STARTUP'), + XASECO_VERSION, + $this->server->ip, $this->server->port); + // show startup message + $this->client->query('ChatSendServerMessage', $this->formatColors($startup_msg)); + } // sendHeader + + + /** + * Gets callbacks from the TM Dedicated Server and reacts on them. + */ + function executeCallbacks() { + + // receive callbacks with a timeout (default: 2 ms) + $this->client->resetError(); + $this->client->readCB(); + + // now get the responses out of the 'buffer' + $calls = $this->client->getCBResponses(); + if ($this->client->isError()) { + trigger_error('ExecCallbacks XMLRPC Error [' . $this->client->getErrorCode() . '] - ' . $this->client->getErrorMessage(), E_USER_ERROR); + } + + if (!empty($calls)) { + while ($call = array_shift($calls)) { + switch ($call[0]) { + case 'TrackMania.PlayerConnect': // [0]=Login, [1]=IsSpectator + $this->playerConnect($call[1]); + break; + + case 'TrackMania.PlayerDisconnect': // [0]=Login + $this->playerDisconnect($call[1]); + break; + + case 'TrackMania.PlayerChat': // [0]=PlayerUid, [1]=Login, [2]=Text, [3]=IsRegistredCmd + $this->playerChat($call[1]); + $this->releaseEvent('onChat', $call[1]); + break; + + case 'TrackMania.PlayerServerMessageAnswer': // [0]=PlayerUid, [1]=Login, [2]=Answer + $this->playerServerMessageAnswer($call[1]); + break; + + case 'TrackMania.PlayerCheckpoint': // TMN: [0]=PlayerUid, [1]=Login, [2]=Time, [3]=Score, [4]=CheckpointIndex; TMF: [0]=PlayerUid, [1]=Login, [2]=TimeOrScore, [3]=CurLap, [4]=CheckpointIndex + if (!$this->server->isrelay) + $this->releaseEvent('onCheckpoint', $call[1]); + break; + + case 'TrackMania.PlayerFinish': // [0]=PlayerUid, [1]=Login, [2]=TimeOrScore + $this->playerFinish($call[1]); + break; + + case 'TrackMania.BeginRace': // [0]=Challenge + if ($this->server->getGame() != 'TMF') + $this->beginRace($call[1]); + break; + + case 'TrackMania.EndRace': // [0]=Rankings[], [1]=Challenge + if ($this->server->getGame() != 'TMF') + $this->endRace($call[1]); + break; + + case 'TrackMania.BeginRound': // none + $this->beginRound(); + break; + + case 'TrackMania.StatusChanged': // [0]=StatusCode, [1]=StatusName + // update status changes + $this->prevstatus = $this->currstatus; + $this->currstatus = $call[1][0]; + // if TMF mode Sync, check WarmUp state + if ($this->server->getGame() == 'TMF') { + if ($this->currstatus == 3 || $this->currstatus == 5) { + $this->client->query('GetWarmUp'); + $this->warmup_phase = $this->client->getResponse(); + } + } else { + $this->warmup_phase = false; + } + // on TMF, use real EndRound callback + if ($this->server->getGame() != 'TMF') { + // if change from Play (4) to Sync (3) or Finish (5), + // it's the end of a round + if ($this->prevstatus == 4 && ($this->currstatus == 3 || $this->currstatus == 5)) + $this->endRound(); + } + if ($this->currstatus == 4) { // Running - Play + $this->runningPlay(); + } + $this->releaseEvent('onStatusChangeTo' . $this->currstatus, $call[1]); + break; + + // new TMF callbacks: + + case 'TrackMania.EndRound': // none + $this->endRound(); + break; + + case 'TrackMania.BeginChallenge': // [0]=Challenge, [1]=WarmUp, [2]=MatchContinuation + $this->beginRace($call[1]); + break; + + case 'TrackMania.EndChallenge': // [0]=Rankings[], [1]=Challenge, [2]=WasWarmUp, [3]=MatchContinuesOnNextChallenge, [4]=RestartChallenge + $this->endRace($call[1]); + break; + + case 'TrackMania.PlayerManialinkPageAnswer': // [0]=PlayerUid, [1]=Login, [2]=Answer + $this->releaseEvent('onPlayerManialinkPageAnswer', $call[1]); + break; + + case 'TrackMania.BillUpdated': // [0]=BillId, [1]=State, [2]=StateName, [3]=TransactionId + $this->releaseEvent('onBillUpdated', $call[1]); + break; + + case 'TrackMania.ChallengeListModified': // [0]=CurChallengeIndex, [1]=NextChallengeIndex, [2]=IsListModified + $this->releaseEvent('onChallengeListModified', $call[1]); + break; + + case 'TrackMania.PlayerInfoChanged': // [0]=PlayerInfo + $this->playerInfoChanged($call[1][0]); + break; + + case 'TrackMania.PlayerIncoherence': // [0]=PlayerUid, [1]=Login + $this->releaseEvent('onPlayerIncoherence', $call[1]); + break; + + case 'TrackMania.TunnelDataReceived': // [0]=PlayerUid, [1]=Login, [2]=Data + $this->releaseEvent('onTunnelDataReceived', $call[1]); + break; + + case 'TrackMania.Echo': // [0]=Internal, [1]=Public + $this->releaseEvent('onEcho', $call[1]); + break; + + case 'TrackMania.ManualFlowControlTransition': // [0]=Transition + $this->releaseEvent('onManualFlowControlTransition', $call[1]); + break; + + case 'TrackMania.VoteUpdated': // [0]=StateName, [1]=Login, [2]=CmdName, [3]=CmdParam + $this->releaseEvent('onVoteUpdated', $call[1]); + break; + + default: + // do nothing + } + } + return $calls; + } else { + return false; + } + } // executeCallbacks + + + /** + * Adds calls to a multiquery. + * It's possible to set a callback function which + * will be executed on incoming response. + * You can also set an ID to read response later on. + */ + function addCall($call, $params = array(), $id = 0, $callback_func = false) { + + // adds call and registers a callback if needed + $index = $this->client->addCall($call, $params); + $rpc_call = new RPCCall($id, $index, $callback_func, array($call, $params)); + $this->rpc_calls[] = $rpc_call; + } // addCall + + + /** + * Executes a multicall and gets responses. + * Saves responses in array with IDs as keys. + */ + function executeCalls() { + + // clear responses + $this->rpc_responses = array(); + + // stop if there are no rpc calls in query + if (empty($this->client->calls)) { + return true; + } + + $this->client->resetError(); + $tmpcalls = $this->client->calls; // debugging code to find UTF-8 errors + // sends multiquery to the server and gets the response + if ($this->client->multiquery()) { + if ($this->client->isError()) { + $this->console_text(print_r($tmpcalls, true)); + trigger_error('ExecCalls XMLRPC Error [' . $this->client->getErrorCode() . '] - ' . $this->client->getErrorMessage(), E_USER_ERROR); + } + + // get new response from server + $responses = $this->client->getResponse(); + + // handle server responses + foreach ($this->rpc_calls as $call) { + // display error message if needed + $err = false; + if (isset($responses[$call->index]['faultString'])) { + $this->rpcErrorResponse($responses[$call->index]); + print_r($call->call); + $err = true; + } + + // if an id was set, then save the response under the specified id + if ($call->id) { + $this->rpc_responses[$call->id] = $responses[$call->index][0]; + } + + // if a callback function has been set, then execute it + if ($call->callback && !$err) { + if (function_exists($call->callback)) { + // callback the function with the response as parameter + call_user_func($call->callback, $responses[$call->index][0]); + } + + // if a function with the name of the callback wasn't found, then + // try to execute a method with its name + elseif (method_exists($this, $call->callback)) { + // callback the method with the response as parameter + call_user_func(array($this, $call->callback), $responses[$call->index][0]); + } + } + } + } + + // clear calls + $this->rpc_calls = array(); + } // executeCalls + + + /** + * Documents RPC Errors. + */ + function rpcErrorResponse($response) { + + $this->console_text('[RPC Error ' . $response['faultCode'] . '] ' . $response['faultString']); + } // rpcErrorResponse + + + /** + * Registers functions which are called on specific events. + */ + function registerEvent($event_type, $callback_func) { + + // registers a new event + $this->events[$event_type][] = $callback_func; + } // registerEvent + + /** + * Executes the functions which were registered for specified events. + */ + function releaseEvent($event_type, $func_param) { + + // executes registered event functions + // if there are any events for that type + if (!empty($this->events[$event_type])) { + // for each registered function of this type + foreach ($this->events[$event_type] as $func_name) { + // if function for the specified player connect event can be found + if (is_callable($func_name)) { + // ... execute it! + call_user_func($func_name, $this, $func_param); + } + } + } + } // releaseEvent + + + /** + * Stores a new user command. + */ + function addChatCommand($command_name, $command_help, $command_is_admin = false) { + + $chat_command = new ChatCommand($command_name, $command_help, $command_is_admin); + $this->chat_commands[] = $chat_command; + } // addChatCommand + + /** + * Registers all chat commands with the server. + */ + function registerChatCommands() { + + // clear the current list of chat commands + $this->client->query('CleanChatCommand'); + + if (isset($this->chat_commands)) { + foreach ($this->chat_commands as $command) { + // only if it's no admin command + if (!$command->isadmin) { + // log message if debug mode is set to true + if ($this->debug) { + $this->console_text('register chat command: ' . $command->name); + } + + // register chat command at server + $this->client->query('AddChatCommand', $command->name); + } + } + } + } // registerChatCommands + + + /** + * When a round is started, signal the event. + */ + function beginRound() { + + $this->console_text('Begin Round'); + $this->releaseEvent('onBeginRound', null); + } // beginRound + + /** + * When a round is ended, signal the event. + */ + function endRound() { + + $this->console_text('End Round'); + $this->releaseEvent('onEndRound', null); + } // endRound + + + /** + * When a TMF player's info changed, signal the event. Fields: + * Login, NickName, PlayerId, TeamId, SpectatorStatus, LadderRanking, Flags + */ + function playerInfoChanged($playerinfo) { + + // on relay, check for player from master server + if ($this->server->isrelay && floor($playerinfo['Flags'] / 10000) % 10 != 0) + return; + + // check for valid player + if (!$player = $this->server->players->getPlayer($playerinfo['Login'])) + return; + + // check ladder ranking + if ($playerinfo['LadderRanking'] > 0) { + $player->ladderrank = $playerinfo['LadderRanking']; + $player->isofficial = true; + } else { + $player->isofficial = false; + } + + // check spectator status (ignoring temporary changes) + $player->prevstatus = $player->isspectator; + if (($playerinfo['SpectatorStatus'] % 10) != 0) + $player->isspectator = true; + else + $player->isspectator = false; + + $this->releaseEvent('onPlayerInfoChanged', $playerinfo); + } // playerInfoChanged + + + /** + * When a new track is started we have to get information + * about the new track and so on. + */ + function runningPlay() { + // request information about the new challenge + // ... and callback to function newChallenge() + } // runningPlay + + + /** + * When a new race is started we have to get information + * about the new track and so on. + */ + function beginRace($race) { + // request information about the new challenge + // ... and callback to function newChallenge() + + // if TMF new challenge, check WarmUp state + if ($this->server->getGame() == 'TMF' && $race) + $this->warmup_phase = $race[1]; + + if (!$race) { + $this->addCall('GetCurrentChallengeInfo', array(), '', 'newChallenge'); + } else { + $this->newChallenge($race[0]); + } + } // beginRace + + + /** + * Reacts on new challenges. + * Gets record to current challenge etc. + */ + function newChallenge($challenge) { + + // log if not a restart + $this->server->gamestate = Server::RACE; + if ($this->restarting == 0) + $this->console_text('Begin Challenge'); + + // refresh game info + $this->client->query('GetCurrentGameInfo', ($this->server->getGame() == 'TMF' ? 1 : 0)); + $gameinfo = $this->client->getResponse(); + $this->server->gameinfo = new Gameinfo($gameinfo); + + // check for TMF restarting challenge + $this->changingmode = false; + if ($this->server->getGame() == 'TMF' && $this->restarting > 0) { + // check type of restart and signal an instant one + if ($this->restarting == 2) { + $this->restarting = 0; + } else { // == 1 + $this->restarting = 0; + // throw postfix 'restart challenge' event + $this->releaseEvent('onRestartChallenge2', $challenge); + return; + } + } + // refresh server name & options + $this->getServerOptions(); + + // reset record list + $this->server->records->clear(); + // reset player votings + //$this->server->players->resetVotings(); + + // create new challenge object + $challenge_item = new Challenge($challenge); + + // in TMF Rounds/Team/Cup mode if multilap track, get forced laps + if ($this->server->getGame() == 'TMF' && $challenge_item->laprace && + ($this->server->gameinfo->mode == Gameinfo::RNDS || + $this->server->gameinfo->mode == Gameinfo::TEAM || + $this->server->gameinfo->mode == Gameinfo::CUP)) { + $challenge_item->forcedlaps = $this->server->gameinfo->forcedlaps; + } + + // obtain challenge's GBX data, TMX info & records + $challenge_item->gbx = new GBXChallMapFetcher(true); + try + { + $challenge_item->gbx->processFile($this->server->trackdir . $challenge_item->filename); + } + catch (Exception $e) + { + trigger_error($e->getMessage(), E_USER_WARNING); + } + $challenge_item->tmx = findTMXdata($challenge_item->uid, $challenge_item->environment, $challenge_item->gbx->exeVer, true); + + // throw main 'begin challenge' event + $this->releaseEvent('onNewChallenge', $challenge_item); + + // log console message + $this->console('track changed [{1}] >> [{2}]', + stripColors($this->server->challenge->name, false), + stripColors($challenge_item->name, false)); + +/* disabled in favor of RASP's karma system - Xymph + // log track's score + if ($challenge_item->score && $challenge_item->votes) { + // calculate avarage score and display + $this->console('average score of this track is {1}', + $challenge_item->score/$challenge_item->votes); + } else { + // no votings + $this->console('no votings available for this track'); + } +disabled */ + + // check for relay server + if (!$this->server->isrelay) { + // check if record exists on new track + $cur_record = $this->server->records->getRecord(0); + if ($cur_record !== false && $cur_record->score > 0) { + $score = ($this->server->gameinfo->mode == Gameinfo::STNT ? + str_pad($cur_record->score, 5, ' ', STR_PAD_LEFT) : + formatTime($cur_record->score)); + + // log console message of current record + $this->console('current record on {1} is {2} and held by {3}', + stripColors($challenge_item->name, false), + trim($score), + stripColors($cur_record->player->nickname, false)); + + // replace parameters + $message = formatText($this->getChatMessage('RECORD_CURRENT'), + stripColors($challenge_item->name), + trim($score), + stripColors($cur_record->player->nickname)); + } else { + if ($this->server->gameinfo->mode == Gameinfo::STNT) + $score = ' ---'; + else + $score = ' --.--'; + + // log console message of no record + $this->console('currently no record on {1}', + stripColors($challenge_item->name, false)); + + // replace parameters + $message = formatText($this->getChatMessage('RECORD_NONE'), + stripColors($challenge_item->name)); + } + if (function_exists('setRecordsPanel')) + setRecordsPanel('local', $score); + + // if no trackrecs, show the original record message to all players + if (($this->settings['show_recs_before'] & 1) == 1) { + if (($this->settings['show_recs_before'] & 4) == 4 && function_exists('send_window_message')) + send_window_message($this, $message, false); + else + $this->client->query('ChatSendServerMessage', $this->formatColors($message)); + } + } + + // update the field which contains current challenge + $this->server->challenge = $challenge_item; + + // throw postfix 'begin challenge' event (various) + $this->releaseEvent('onNewChallenge2', $challenge_item); + + // show top-8 & records of all online players before track + if (($this->settings['show_recs_before'] & 2) == 2 && function_exists('show_trackrecs')) { + show_trackrecs($this, false, 1, $this->settings['show_recs_before']); // from chat.records2.php + } + } // newChallenge + + + /** + * End of current race. + * Write records to database and/or display final statistics. + */ + function endRace($race) { + + // check for TMF RestartChallenge flag + if ($this->server->getGame() == 'TMF' && $race[4]) { + $this->restarting = 1; + // check whether changing game mode or any player has a time/score, + // then there will be ChatTime, otherwise it's an instant restart + if ($this->changingmode) + $this->restarting = 2; + else + foreach ($race[0] as $pl) { + if ($pl['BestTime'] > 0 || $pl['Score'] > 0) { + $this->restarting = 2; + break; + } + } + // log type of restart and signal an instant one + if ($this->restarting == 2) { + $this->console_text('Restart Challenge (with ChatTime)'); + } else { // == 1 + $this->console_text('Restart Challenge (instant)'); + // throw main 'restart challenge' event + $this->releaseEvent('onRestartChallenge', $race); + return; + } + } + // log if not a restart + $this->server->gamestate = Server::SCORE; + if ($this->restarting == 0) + $this->console_text('End Challenge'); + + // show top-8 & all new records after track + if (($this->settings['show_recs_after'] & 2) == 2 && function_exists('show_trackrecs')) { + show_trackrecs($this, false, 3, $this->settings['show_recs_after']); // from chat.records2.php + } elseif (($this->settings['show_recs_after'] & 1) == 1) { + // fall back on old top-5 + $records = ''; + + if ($this->server->records->count() == 0) { + // display a no-new-record message + $message = formatText($this->getChatMessage('RANKING_NONE'), + stripColors($this->server->challenge->name), + 'after'); + } else { + // display new records set up this round + $message = formatText($this->getChatMessage('RANKING'), + stripColors($this->server->challenge->name), + 'after'); + + // go through each record + for ($i = 0; $i < 5; $i++) { + $cur_record = $this->server->records->getRecord($i); + + // if the record is set then display it + if ($cur_record !== false && $cur_record->score > 0) { + // replace parameters + $record_msg = formatText($this->getChatMessage('RANKING_RECORD_NEW'), + $i+1, + stripColors($cur_record->player->nickname), + ($this->server->gameinfo->mode == Gameinfo::STNT ? + $cur_record->score : formatTime($cur_record->score))); + $records .= $record_msg; + } + } + } + + // append the records if any + if ($records != '') { + $records = substr($records, 0, strlen($records)-2); // strip trailing ", " + $message .= LF . $records; + } + + // show ranking message to all players + if (($this->settings['show_recs_after'] & 4) == 4 && function_exists('send_window_message')) + send_window_message($this, $message, true); + else + $this->client->query('ChatSendServerMessage', $this->formatColors($message)); + } + + // get rankings and call endRaceRanking as soon as we have them + // $this->addCall('GetCurrentRanking', array(2, 0), false, 'endRaceRanking'); + if (!$this->server->isrelay) + $this->endRaceRanking($race[0]); + + // throw prefix 'end challenge' event (chat-based votes) + $this->releaseEvent('onEndRace1', $race); + // throw main 'end challenge' event + $this->releaseEvent('onEndRace', $race); + } // endRace + + + /** + * Check out who won the current track and increment his/her wins by one. + */ + function endRaceRanking($ranking) { + + // check for online login + if (isset($ranking[0]['Login']) && + ($player = $this->server->players->getPlayer($ranking[0]['Login'])) !== false) { + // check for winner if there's more than one player + if ($ranking[0]['Rank'] == 1 && count($ranking) > 1 && + ($this->server->gameinfo->mode == Gameinfo::STNT ? + ($ranking[0]['Score'] > 0) : ($ranking[0]['BestTime'] > 0))) { + // increase the player's wins + $player->newwins++; + + // log console message + $this->console('{1} won for the {2}. time!', + $player->login, $player->getWins()); + + if ($player->getWins() % $this->settings['global_win_multiple'] == 0) { + // replace parameters + $message = formatText($this->getChatMessage('WIN_MULTI'), + stripColors($player->nickname), $player->getWins()); + + // show chat message + $this->client->query('ChatSendServerMessage', $this->formatColors($message)); + } else { + // replace parameters + $message = formatText($this->getChatMessage('WIN_NEW'), + $player->getWins()); + + // show chat message + $this->client->query('ChatSendServerMessageToLogin', $this->formatColors($message), $player->login); + } + + // throw 'player wins' event + $this->releaseEvent('onPlayerWins', $player); + } + } + } // endRaceRanking + + + /** + * Handles connections of new players. + */ + function playerConnect($player) { + + // request information about the new player + // (removed callback mechanism here, as GetPlayerInfo occasionally + // returns no data and then the connecting login would be lost) + $login = $player[0]; + if ($this->server->getGame() == 'TMF') { + $this->client->query('GetDetailedPlayerInfo', $login); + $playerd = $this->client->getResponse(); + $this->client->query('GetPlayerInfo', $login, 1); + } else { // TMN/TMS/TMO + $this->client->query('GetPlayerInfo', $login); + } + $player = $this->client->getResponse(); + + // check for server + if (isset($player['Flags']) && floor($player['Flags'] / 100000) % 10 != 0) { + // register relay server + if (!$this->server->isrelay && $player['Login'] != $this->server->serverlogin) { + $this->server->relayslist[$player['Login']] = $player; + + // log console message + $this->console('<<< relay server {1} ({2}) connected', $player['Login'], + stripColors($player['NickName'], false)); + } + + // on relay, check for player from master server + } elseif ($this->server->isrelay && floor($player['Flags'] / 10000) % 10 != 0) { + ; // ignore + } else { + $ipaddr = isset($playerd['IPAddress']) ? preg_replace('/:\d+/', '', $playerd['IPAddress']) : ''; // strip port + + // if no data fetched, notify & kick the player + if (!isset($player['Login']) || $player['Login'] == '') { + $message = str_replace('{br}', LF, $this->getChatMessage('CONNECT_ERROR')); + $message = $this->formatColors($message); + $this->client->query('ChatSendServerMessageToLogin', str_replace(LF.LF, LF, $message), $login); + if ($this->server->getGame() == 'TMN') + $this->client->query('SendDisplayServerMessageToLogin', $login, $message, 'OK', '', 0); + sleep(5); // allow time to connect and see the notice + if ($this->server->getGame() == 'TMF') + $this->client->addCall('Kick', array($login, $this->formatColors($this->getChatMessage('CONNECT_DIALOG')))); + else + $this->client->addCall('Kick', array($login)); + // log console message + $this->console('GetPlayerInfo failed for ' . $login . ' -- notified & kicked'); + return; + + // if player IP in ban list, notify & kick the player + } elseif (!empty($this->bannedips) && in_array($ipaddr, $this->bannedips)) { + $message = str_replace('{br}', LF, $this->getChatMessage('BANIP_ERROR')); + $message = $this->formatColors($message); + $this->client->query('ChatSendServerMessageToLogin', str_replace(LF.LF, LF, $message), $login); + if ($this->server->getGame() == 'TMN') + $this->client->query('SendDisplayServerMessageToLogin', $login, $message, 'OK', '', 0); + sleep(5); // allow time to connect and see the notice + if ($this->server->getGame() == 'TMF') + $this->client->addCall('Ban', array($login, $this->formatColors($this->getChatMessage('BANIP_DIALOG')))); + else + $this->client->addCall('Ban', array($login)); + // log console message + $this->console('Player ' . $login . ' banned from ' . $ipaddr . ' -- notified & kicked'); + return; + + // client version checking on TMF + } elseif ($this->server->getGame() == 'TMF') { + // extract version number + $version = str_replace(')', '', preg_replace('/.*\(/', '', $playerd['ClientVersion'])); + if ($version == '') $version = '2.11.11'; + $message = str_replace('{br}', LF, $this->getChatMessage('CLIENT_ERROR')); + + // if invalid version, notify & kick the player + if ($this->settings['player_client'] != '' && + strcmp($version, $this->settings['player_client']) < 0) { + $this->client->query('ChatSendServerMessageToLogin', $this->formatColors($message), $login); + sleep(5); // allow time to connect and see the notice + $this->client->addCall('Kick', array($login, $this->formatColors($this->getChatMessage('CLIENT_DIALOG')))); + // log console message + $this->console('Obsolete player client version ' . $version . ' for ' . $login . ' -- notified & kicked'); + return; + } + + // if invalid version, notify & kick the admin + if ($this->settings['admin_client'] != '' && $this->isAnyAdminL($player['Login']) && + strcmp($version, $this->settings['admin_client']) < 0) { + $this->client->query('ChatSendServerMessageToLogin', $this->formatColors($message), $login); + sleep(5); // allow time to connect and see the notice + $this->client->addCall('Kick', array($login, $this->formatColors($this->getChatMessage('CLIENT_DIALOG')))); + // log console message + $this->console('Obsolete admin client version ' . $version . ' for ' . $login . ' -- notified & kicked'); + return; + } + } + + // if no TMN team, try again via world stats + if ($this->server->getGame() == 'TMN' && !isLANLogin($login) && + $player['LadderStats']['TeamName'] == '') { + $data = new TMNDataFetcher($login, false); + if ($data->teamname != '') { + $player['LadderStats']['TeamName'] = $data->teamname; + } + } + + // create player object + $player_item = new Player($this->server->getGame() == 'TMF' ? $playerd : $player); + // set default window style & panels + $player_item->style = $this->style; + $player_item->panels['admin'] = $this->panels['admin']; + $player_item->panels['donate'] = $this->panels['donate']; + $player_item->panels['records'] = $this->panels['records']; + $player_item->panels['vote'] = $this->panels['vote']; + + // adds a new player to the internal player list + $this->server->players->addPlayer($player_item); + + // log console message + $this->console('<< player {1} joined the game [{2} : {3} : {4} : {5} : {6}]', + $player_item->pid, + $player_item->login, + $player_item->nickname, + $player_item->nation, + $player_item->ladderrank, + $player_item->ip); + + // replace parameters + $message = formatText($this->getChatMessage('WELCOME'), + stripColors($player_item->nickname), + $this->server->name, XASECO_VERSION); + // hyperlink package name & version number on TMF + if ($this->server->getGame() == 'TMF') + $message = preg_replace('/XASECO.+' . XASECO_VERSION . '/', '$l[' . XASECO_TMN . ']$0$l', $message); + + // send welcome popup or chat message + if ($this->settings['welcome_msg_window']) { + if ($this->server->getGame() == 'TMF') { + $message = str_replace('{#highlite}', '{#message}', $message); + $message = preg_split('/{br}/', $this->formatColors($message)); + // repack all lines + foreach ($message as &$line) + $line = array($line); + display_manialink($player_item->login, '', + array('Icons64x64_1', 'Inbox'), $message, + array(1.2), 'OK'); + } else { // TMN + $message = str_replace('{br}', LF, $this->formatColors($message)); + $this->client->query('SendDisplayServerMessageToLogin', $player_item->login, $message, 'OK', '', 0); + } + } else { + $message = str_replace('{br}', LF, $this->formatColors($message)); + $this->client->query('ChatSendServerMessageToLogin', str_replace(LF.LF, LF, $message), $player_item->login); + } + + // if there's a record on current track + $cur_record = $this->server->records->getRecord(0); + if ($cur_record !== false && $cur_record->score > 0) { + // set message to the current record + $message = formatText($this->getChatMessage('RECORD_CURRENT'), + stripColors($this->server->challenge->name), + ($this->server->gameinfo->mode == Gameinfo::STNT ? + $cur_record->score : formatTime($cur_record->score)), + stripColors($cur_record->player->nickname)); + } else { // if there should be no record to display + // display a no-record message + $message = formatText($this->getChatMessage('RECORD_NONE'), + stripColors($this->server->challenge->name)); + } + + // show top-8 & records of all online players before track + if (($this->settings['show_recs_before'] & 2) == 2 && function_exists('show_trackrecs')) { + show_trackrecs($this, $player_item->login, 1, 0); // from chat.records2.php + } elseif (($this->settings['show_recs_before'] & 1) == 1) { + // or show original record message + $this->client->query('ChatSendServerMessageToLogin', $this->formatColors($message), $player_item->login); + } + + // throw main 'player connects' event + $this->releaseEvent('onPlayerConnect', $player_item); + // throw postfix 'player connects' event (access control) + $this->releaseEvent('onPlayerConnect2', $player_item); + } + } // playerConnect + + /** + * Handles disconnections of players. + */ + function playerDisconnect($player) { + + // check for relay server + if (!$this->server->isrelay && array_key_exists($player[0], $this->server->relayslist)) { + // log console message + $this->console('>>> relay server {1} ({2}) disconnected', $player[0], + stripColors($this->server->relayslist[$player[0]]['NickName'], false)); + + unset($this->server->relayslist[$player[0]]); + return; + } + + // delete player and put him into the player item + // ignore event if disconnect fluke after player already left, + // or on relay if player from master server (which wasn't added) + if (!$player_item = $this->server->players->removePlayer($player[0])) + return; + + // log console message + $this->console('>> player {1} left the game [{2} : {3} : {4}]', + $player_item->pid, + $player_item->login, + $player_item->nickname, + formatTimeH($player_item->getTimeOnline() * 1000, false)); + + // throw 'player disconnects' event + $this->releaseEvent('onPlayerDisconnect', $player_item); + } // playerDisconnect + + + /** + * Handles clicks on server messageboxes. + */ + function playerServerMessageAnswer($answer) { + + if ($answer[2]) { + // throw TMN 'click' event + $this->releaseEvent('onPlayerServerMessageAnswer', $answer); + } + } // playerServerMessageAnswer + + + /** + * Player reaches finish. + */ + function playerFinish($finish) { + + // if no track info, or if server 'finish', bail out immediately + if ($this->server->challenge->name == '' || $finish[0] == 0) + return; + + // if relay server or not in Play status, bail out immediately + if ($this->server->isrelay || $this->currstatus != 4) + return; + + // check for valid player + if ((!$player = $this->server->players->getPlayer($finish[1])) || + $player->login == '') + return; + + // build a record object with the current finish information + $finish_item = new Record(); + $finish_item->player = $player; + $finish_item->score = $finish[2]; + $finish_item->date = strftime('%Y-%m-%d %H:%M:%S'); + $finish_item->new = false; + $finish_item->challenge = clone $this->server->challenge; + unset($finish_item->challenge->gbx); // reduce memory usage + unset($finish_item->challenge->tmx); + + // throw prefix 'player finishes' event (checkpoints) + $this->releaseEvent('onPlayerFinish1', $finish_item); + // throw main 'player finishes' event + $this->releaseEvent('onPlayerFinish', $finish_item); + } // playerFinish + + + /** + * Receives chat messages and reacts on them. + * Reactions are done by the chat plugins. + */ + function playerChat($chat) { + + // verify login + if ($chat[1] == '' || $chat[1] == '???') { + trigger_error('playerUid ' . $chat[0] . 'has login [' . $chat[1] . ']!', E_USER_WARNING); + $this->console('playerUid {1} attempted to use chat command "{2}"', + $chat[0], $chat[2]); + return; + } + + // ignore master server messages on relay + if ($this->server->isrelay && $chat[1] == $this->server->relaymaster['Login']) + return; + + // check for chat command '/' prefix + $command = $chat[2]; + if ($command != '' && $command[0] == '/') { + // remove '/' prefix + $command = substr($command, 1); + + // split strings at spaces and add them into an array + $params = explode(' ', $command, 2); + $translated_name = str_replace('+', 'plus', $params[0]); + $translated_name = str_replace('-', 'dash', $translated_name); + + // check if the function and the command exist + if (function_exists('chat_' . $translated_name)) { + // insure parameter exists & is trimmed + if (isset($params[1])) + $params[1] = trim($params[1]); + else + $params[1] = ''; + + // get & verify player object + if (($author = $this->server->players->getPlayer($chat[1])) && + $author->login != '') { + // log console message + $this->console('player {1} used chat command "/{2} {3}"', + $chat[1], $params[0], $params[1]); + + // save circumstances in array + $chat_command = array(); + $chat_command['author'] = $author; + $chat_command['params'] = $params[1]; + + // call the function which belongs to the command + call_user_func('chat_' . $translated_name, $this, $chat_command); + } else { + trigger_error('Player object for \'' . $chat[1] . '\' not found!', E_USER_WARNING); + $this->console('player {1} attempted to use chat command "/{2} {3}"', + $chat[1], $params[0], $params[1]); + } + } elseif ($params[0] == 'version' || + ($params[0] == 'serverlogin' && $this->server->getGame() == 'TMF')) { + // log built-in commands + $this->console('player {1} used built-in command "/{2}"', + $chat[1], $command); + } else { + // optionally log bogus chat commands too + if ($this->settings['log_all_chat']) { + if ($chat[0] != $this->server->id) { + $this->console('({1}) {2}', $chat[1], stripColors($chat[2], false)); + } + } + } + } else { + // optionally log all normal chat too + if ($this->settings['log_all_chat']) { + if ($chat[0] != $this->server->id && $chat[2] != '') { + $this->console('({1}) {2}', $chat[1], stripColors($chat[2], false)); + } + } + } + } // playerChat + + + /** + * Gets the specified chat message out of the settings file. + */ + function getChatMessage($name) { + + return htmlspecialchars_decode($this->chat_messages[$name][0]); + } // getChatMessage + + + /** + * Checks if an admin is allowed to perform this ability + */ + function allowAdminAbility($ability) { + + // map to uppercase before checking list + $ability = strtoupper($ability); + if (isset($this->adm_abilities[$ability])) { + return $this->adm_abilities[$ability][0]; + } else { + return false; + } + } // allowAdminAbility + + /** + * Checks if an operator is allowed to perform this ability + */ + function allowOpAbility($ability) { + + // map to uppercase before checking list + $ability = strtoupper($ability); + if (isset($this->op_abilities[$ability])) { + return $this->op_abilities[$ability][0]; + } else { + return false; + } + } // allowOpAbility + + /** + * Checks if the given player is allowed to perform this ability + */ + function allowAbility($player, $ability) { + + // check for unlocked password + if ($this->settings['lock_password'] != '' && !$player->unlocked) + return false; + + // MasterAdmins can always do everything + if ($this->isMasterAdmin($player)) + return true; + + // check Admins & their abilities + if ($this->isAdmin($player)) + return $this->allowAdminAbility($ability); + + // check Operators & their abilities + if ($this->isOperator($player)) + return $this->allowOpAbility($ability); + + return false; + } // allowAbility + + + /** + * Checks if the given player IP matches the corresponding list IP, + * allowing for class C and B wildcards, and multiple comma-separated + * IPs / wildcards. + */ + function ip_match($playerip, $listip) { + + // check for offline player (removeadmin / removeop) + if ($playerip == '') + return true; + + $match = false; + // check all comma-separated IPs/wildcards + foreach (explode(',', $listip) as $ip) { + // check for complete list IP + if (preg_match('/^\d+\.\d+\.\d+\.\d+$/', $ip)) + $match = ($playerip == $ip); + // check class B wildcard + elseif (substr($ip, -4) == '.*.*') + $match = (preg_replace('/\.\d+\.\d+$/', '', $playerip) == substr($ip, 0, -4)); + // check class C wildcard + elseif (substr($ip, -2) == '.*') + $match = (preg_replace('/\.\d+$/', '', $playerip) == substr($ip, 0, -2)); + + if ($match) return true; + } + return false; + } + + /** + * Checks if the given player is in masteradmin list with, optionally, + * an authorized IP. + */ + function isMasterAdmin($player) { + + // check for masteradmin list entry + if (isset($player->login) && $player->login != '' && isset($this->masteradmin_list['TMLOGIN'])) + if (($i = array_search($player->login, $this->masteradmin_list['TMLOGIN'])) !== false) + // check for matching IP if set + if ($this->masteradmin_list['IPADDRESS'][$i] != '') + if (!$this->ip_match($player->ip, $this->masteradmin_list['IPADDRESS'][$i])) { + trigger_error("Attempt to use MasterAdmin login '" . $player->login . "' from IP " . $player->ip . " !", E_USER_WARNING); + return false; + } else + return true; + else + return true; + else + return false; + else + return false; + } // isMasterAdmin + + /** + * Checks if the given player is in admin list with, optionally, + * an authorized IP. + */ + function isAdmin($player) { + + // check for admin list entry + if (isset($player->login) && $player->login != '' && isset($this->admin_list['TMLOGIN'])) + if (($i = array_search($player->login, $this->admin_list['TMLOGIN'])) !== false) + // check for matching IP if set + if ($this->admin_list['IPADDRESS'][$i] != '') + if (!$this->ip_match($player->ip, $this->admin_list['IPADDRESS'][$i])) { + trigger_error("Attempt to use Admin login '" . $player->login . "' from IP " . $player->ip . " !", E_USER_WARNING); + return false; + } else + return true; + else + return true; + else + return false; + else + return false; + } // isAdmin + + /** + * Checks if the given player is in operator list with, optionally, + * an authorized IP. + */ + function isOperator($player) { + + // check for operator list entry + if (isset($player->login) && $player->login != '' && isset($this->operator_list['TMLOGIN'])) + if (($i = array_search($player->login, $this->operator_list['TMLOGIN'])) !== false) + // check for matching IP if set + if ($this->operator_list['IPADDRESS'][$i] != '') + if (!$this->ip_match($player->ip, $this->operator_list['IPADDRESS'][$i])) { + trigger_error("Attempt to use Operator login '" . $player->login . "' from IP " . $player->ip . " !", E_USER_WARNING); + return false; + } else + return true; + else + return true; + else + return false; + else + return false; + } // isOperator + + /** + * Checks if the given player is in any admin tier with, optionally, + * an authorized IP. + */ + function isAnyAdmin($player) { + + return ($this->isMasterAdmin($player) || $this->isAdmin($player) || $this->isOperator($player)); + } // isAnyAdmin + + + /** + * Checks if the given player login is in masteradmin list. + */ + function isMasterAdminL($login) { + + if ($login != '' && isset($this->masteradmin_list['TMLOGIN'])) { + return in_array($login, $this->masteradmin_list['TMLOGIN']); + } else { + return false; + } + } // isMasterAdminL + + /** + * Checks if the given player login is in admin list. + */ + function isAdminL($login) { + + if ($login != '' && isset($this->admin_list['TMLOGIN'])) { + return in_array($login, $this->admin_list['TMLOGIN']); + } else { + return false; + } + } // isAdminL + + /** + * Checks if the given player login is in operator list. + */ + function isOperatorL($login) { + + // check for operator list entry + if ($login != '' && isset($this->operator_list['TMLOGIN'])) + return in_array($login, $this->operator_list['TMLOGIN']); + else + return false; + } // isOperatorL + + /** + * Checks if the given player login is in any admin tier. + */ + function isAnyAdminL($login) { + + return ($this->isMasterAdminL($login) || $this->isAdminL($login) || $this->isOperatorL($login)); + } // isAnyAdminL + + + /** + * Checks if the given player is a spectator. + */ + function isSpectator($player) { + + // get current player status + if ($this->server->getGame() != 'TMF') { + $this->client->query('GetPlayerInfo', $player->login); + $info = $this->client->getResponse(); + if (isset($info['IsSpectator'])) + $player->isspectator = $info['IsSpectator']; + else + $player->isspectator = false; + } + return $player->isspectator; + } // isSpectator + + /** + * Handles cheating player. + */ + function processCheater($login, $checkpoints, $chkpt, $finish) { + + // collect checkpoints + $cps = ''; + foreach ($checkpoints as $cp) + $cps .= formatTime($cp) . '/'; + $cps = substr($cps, 0, strlen($cps)-1); // strip trailing '/' + + // report cheat + if ($finish == -1) + trigger_error('Cheat by \'' . $login . '\' detected! CPs: ' . $cps . ' Last: ' . formatTime($chkpt[2]) . ' index: ' . $chkpt[4], E_USER_WARNING); + else + trigger_error('Cheat by \'' . $login . '\' detected! CPs: ' . $cps . ' Finish: ' . formatTime($finish), E_USER_WARNING); + + // check for valid player + if (!$player = $this->server->players->getPlayer($login)) { + trigger_error('Player object for \'' . $login . '\' not found!', E_USER_WARNING); + return; + } + + switch ($this->settings['cheater_action']) { + + case 1: // set to spec (TMF only) + $rtn = $this->client->query('ForceSpectator', $login, 1); + if (!$rtn) { + trigger_error('[' . $this->client->getErrorCode() . '] ForceSpectator - ' . $this->client->getErrorMessage(), E_USER_WARNING); + } else { + // allow spectator to switch back to player + $rtn = $this->client->query('ForceSpectator', $login, 0); + } + // force free camera mode on spectator + $this->client->addCall('ForceSpectatorTarget', array($login, '', 2)); + // free up player slot + $this->client->addCall('SpectatorReleasePlayerSlot', array($login)); + + // log console message + $this->console('Cheater [{1} : {2}] forced into free spectator!', $login, stripColors($player->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}Cheater {#highlite}{1}$z$s{#admin} forced into spectator!', + str_ireplace('$w', '', $player->nickname)); + $this->client->query('ChatSendServerMessage', $this->formatColors($message)); + break; + + case 2: // kick + // log console message + $this->console('Cheater [{1} : {2}] kicked!', $login, stripColors($player->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}Cheater {#highlite}{1}$z$s{#admin} kicked!', + str_ireplace('$w', '', $player->nickname)); + $this->client->query('ChatSendServerMessage', $this->formatColors($message)); + + // kick the cheater + $this->client->query('Kick', $login); + break; + + case 3: // ban (& kick) + // log console message + $this->console('Cheater [{1} : {2}] banned!', $login, stripColors($player->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}Cheater {#highlite}{1}$z$s{#admin} banned!', + str_ireplace('$w', '', $player->nickname)); + $this->client->query('ChatSendServerMessage', $this->formatColors($message)); + + // update banned IPs file + $this->bannedips[] = $player->ip; + $this->writeIPs(); + + // ban the cheater and also kick him + $this->client->query('Ban', $player->login); + break; + + case 4: // blacklist & kick + // log console message + $this->console('Cheater [{1} : {2}] blacklisted!', $login, stripColors($player->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}Cheater {#highlite}{1}$z$s{#admin} blacklisted!', + str_ireplace('$w', '', $player->nickname)); + $this->client->query('ChatSendServerMessage', $this->formatColors($message)); + + // blacklist the cheater and then kick him + $this->client->query('BlackList', $player->login); + $this->client->query('Kick', $player->login); + + // update blacklist file + $filename = $this->settings['blacklist_file']; + $rtn = $this->client->query('SaveBlackList', $filename); + if (!$rtn) { + trigger_error('[' . $this->client->getErrorCode() . '] SaveBlackList (kick) - ' . $this->client->getErrorMessage(), E_USER_WARNING); + } + break; + + case 5: // blacklist & ban + // log console message + $this->console('Cheater [{1} : {2}] blacklisted & banned!', $login, stripColors($player->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}Cheater {#highlite}{1}$z$s{#admin} blacklisted & banned!', + str_ireplace('$w', '', $player->nickname)); + $this->client->query('ChatSendServerMessage', $this->formatColors($message)); + + // update banned IPs file + $this->bannedips[] = $player->ip; + $this->writeIPs(); + + // blacklist & ban the cheater + $this->client->query('BlackList', $player->login); + $this->client->query('Ban', $player->login); + + // update blacklist file + $filename = $this->settings['blacklist_file']; + $rtn = $this->client->query('SaveBlackList', $filename); + if (!$rtn) { + trigger_error('[' . $this->client->getErrorCode() . '] SaveBlackList (ban) - ' . $this->client->getErrorMessage(), E_USER_WARNING); + } + break; + + default: // ignore + } + } // processCheater + + + /** + * Finds a player ID from its login. + */ + function getPlayerId($login, $forcequery = false) { + + if (isset($this->server->players->player_list[$login]) && + $this->server->players->player_list[$login]->id > 0 && !$forcequery) { + $rtn = $this->server->players->player_list[$login]->id; + } else { + $query = 'SELECT id FROM players + WHERE login=' . quotedString($login); + $result = mysql_query($query); + if (mysql_num_rows($result) > 0) { + $row = mysql_fetch_row($result); + $rtn = $row[0]; + } else { + $rtn = 0; + } + mysql_free_result($result); + } + return $rtn; + } // getPlayerId + + /** + * Finds a player Nickname from its login. + */ + function getPlayerNick($login, $forcequery = false) { + + if (isset($this->server->players->player_list[$login]) && + $this->server->players->player_list[$login]->nickname != '' && !$forcequery) { + $rtn = $this->server->players->player_list[$login]->nickname; + } else { + $query = 'SELECT nickname FROM players + WHERE login=' . quotedString($login); + $result = mysql_query($query); + if (mysql_num_rows($result) > 0) { + $row = mysql_fetch_row($result); + $rtn = $row[0]; + } else { + $rtn = ''; + } + mysql_free_result($result); + } + return $rtn; + } // getPlayerNick + + + /** + * Finds an online player object from its login or Player_ID + * If $offline = true, search player database instead + * Returns false if not found + */ + function getPlayerParam($player, $param, $offline = false) { + + // if numeric param, find Player_ID from /players list (hardlimited to 300) + if (is_numeric($param) && $param >= 0 && $param < 300) { + if (empty($player->playerlist)) { + $message = '{#server}> {#error}Use {#highlite}$i/players {#error}first (optionally {#highlite}$i/players {#error})'; + $this->client->query('ChatSendServerMessageToLogin', $this->formatColors($message), $player->login); + return false; + } + $pid = ltrim($param, '0'); + $pid--; + // find player by given # + if (array_key_exists($pid, $player->playerlist)) { + $param = $player->playerlist[$pid]['login']; + // check online players list + $target = $this->server->players->getPlayer($param); + } else { + // try param as login string as yet + $target = $this->server->players->getPlayer($param); + if (!$target) { + $message = '{#server}> {#error}Player_ID not found! Type {#highlite}$i/players {#error}to see all players.'; + $this->client->query('ChatSendServerMessageToLogin', $this->formatColors($message), $player->login); + return false; + } + } + } else { // otherwise login string + // check online players list + $target = $this->server->players->getPlayer($param); + } + + // not found and offline allowed? + if (!$target && $offline) { + // check offline players database + $query = 'SELECT * FROM players + WHERE login=' . quotedString($param); + $result = mysql_query($query); + if (mysql_num_rows($result) > 0) { + $row = mysql_fetch_object($result); + // create dummy player object + $target = new Player(); + $target->id = $row->Id; + $target->login = $row->Login; + $target->nickname = $row->NickName; + $target->nation = $row->Nation; + $target->teamname = $row->TeamName; + $target->wins = $row->Wins; + $target->timeplayed = $row->TimePlayed; + } + mysql_free_result($result); + } + + // found anyone anywhere? + if (!$target) { + $message = '{#server}> {#highlite}' . $param . ' {#error}is not a valid player! Use {#highlite}$i/players {#error}to find the correct login or Player_ID.'; + $this->client->query('ChatSendServerMessageToLogin', $this->formatColors($message), $player->login); + } + return $target; + } // getPlayerParam + + + /** + * Finds a challenge ID from its UID. + */ + function getChallengeId($uid) { + + $query = 'SELECT Id FROM challenges + WHERE Uid=' . quotedString($uid); + $res = mysql_query($query); + if (mysql_num_rows($res) > 0) { + $row = mysql_fetch_row($res); + $rtn = $row[0]; + } else { + $rtn = 0; + } + mysql_free_result($res); + return $rtn; + } // getChallengeId + + /** + * Gets current servername + */ + function getServerName() { + + $this->client->query('GetServerName'); + $this->server->name = $this->client->getResponse(); + return $this->server->name; + } + + /** + * Gets current server name & options + */ + function getServerOptions() { + + $this->client->query('GetServerOptions'); + $options = $this->client->getResponse(); + $this->server->name = $options['Name']; + $this->server->maxplay = $options['CurrentMaxPlayers']; + $this->server->maxspec = $options['CurrentMaxSpectators']; + $this->server->votetime = $options['CurrentCallVoteTimeOut']; + $this->server->voterate = $options['CallVoteRatio']; + } + + + /** + * Formats aseco color codes in a string, + * for example '{#server} hello' will end up as '$ff0 hello'. + * It depends on what you've set in the config file. + */ + function formatColors($text) { + + // replace all chat colors + foreach ($this->chat_colors as $key => $value) { + $text = str_replace('{#'.strtolower($key).'}', $value[0], $text); + } + return $text; + } // formatColors + + + /** + * Outputs a formatted string without datetime. + */ + function console_text() { + + $args = func_get_args(); + $message = call_user_func_array('formatText', $args) . CRLF; + echo $message; + doLog($message); + flush(); + } // console_text + + /** + * Outputs a string to console with datetime prefix. + */ + function console() { + + $args = func_get_args(); + $message = '[' . date('m/d,H:i:s') . '] ' . call_user_func_array('formatText', $args) . CRLF; + echo $message; + doLog($message); + flush(); + } // console + +} // class Aseco + +// define process settings +if (function_exists('date_default_timezone_get') && function_exists('date_default_timezone_set')) + date_default_timezone_set(@date_default_timezone_get()); +$limit = ini_get('memory_limit'); +if (shorthand2bytes($limit) < 128 * 1048576) + ini_set('memory_limit', '128M'); +setlocale(LC_NUMERIC, 'C'); + +// create an instance of XASECO and run it +$aseco = new Aseco(false); +$aseco->run('config.xml'); +?> diff --git a/docker-xaseco/xaseco/autotime.xml b/docker-xaseco/xaseco/autotime.xml new file mode 100644 index 0000000..1ecf066 --- /dev/null +++ b/docker-xaseco/xaseco/autotime.xml @@ -0,0 +1,15 @@ + + + + + 6 + + 3.5 + + 7 + + 5 + + 1 + {#server}>> Set {1} timelimit for {#highlite}{2}{#server} : {#highlite}{3}{#server} (Author time: {#highlite}{4}{#server}) + diff --git a/docker-xaseco/xaseco/bannedips.xml b/docker-xaseco/xaseco/bannedips.xml new file mode 100644 index 0000000..c636898 --- /dev/null +++ b/docker-xaseco/xaseco/bannedips.xml @@ -0,0 +1,6 @@ + + + + diff --git a/docker-xaseco/xaseco/config.xml b/docker-xaseco/xaseco/config.xml new file mode 100644 index 0000000..e695055 --- /dev/null +++ b/docker-xaseco/xaseco/config.xml @@ -0,0 +1,219 @@ + + + + + + + + + + + + $f00$i + $f00 + $ff0 + $fff + $bbb + $0f3 + $fa0 + $d80 + $39f + $ff3 + $f8f + $ff0 + $f0f + $ff0 + $000 + $888 + $00f + $0c0 + $f00 + $ff0$i + $28b + $0b3 + + + + + {#server}*** XASECO {#highlite}v{1}{#server} running on {#highlite}{2}{#server}:{#highlite}{3}{#server} *** + {#welcome}Welcome {#highlite}{1}{#welcome} to {#highlite}{2}$z$s{br}{#welcome}This server uses {#highlite}XASECO v{3}{#welcome} to manage your records. + $s{#welcome}This is an administrative warning.{br}{br}$gWhatever you wrote is against our server's{br}policy. Not respecting other players, or{br}using offensive language might result in a{br}{#welcome}kick, or ban {#message}the next time.{br}{br}$gThe server administrators. + + + {#server}>> {#message}Current record on {#highlite}{1}{#message} is {#highlite}{2}{#message} by {#highlite}{3} + {#server}>> {#message}Currently no record on {#highlite}{1}{#message} ... + {#server}>> {#error}Could not get records from database... No records this round! + + + {#server}>> {#message}Local Record rankings on {#highlite}{1}{#message} {2} this round: + {#server}>> {#message}Local Record rankings on {#highlite}{1}{#message} {2} this round (range {#highlite}{3}{#message}): + {#server}>> {#message}Local Record rankings on {#highlite}{1}{#message} {2} this round ({#highlite}{3}{#message} new): + {#server}>> {#message}Local Record rankings on {#highlite}{1}{#message} {2} this round: none new so far + {#server}>> {#message}Local Record rankings on {#highlite}{1}{#message} {2} this round: no records! + + + {#rank}{1}{#message}.$i{#highlite}{2}{#message}[{#highlite}{3}{#message}]$i, + {#rank}{1}{#message}.{#highlite}{2}{#message}[{#highlite}{3}{#message}], + {#rank}{1}{#message}.$i{#timelite}{2}{#message}[{#timelite}{3}{#message}]$i, + {#rank}{1}{#message}.{#timelite}{2}{#message}[{#timelite}{3}{#message}], + {#rank}{1}{#message}.{#timelite}{2}{#message}, + + + {#server}> {#record}The first Local record is: + {#server}> {#record}The last Local record is: + {#server}> {#record}Difference between {1}{#record} and {2}{#record} is: {#highlite}{3} + {#server}> {#highlite}{1} $z$s{#record}has {#highlite}{2}{#record} Local record{3}, the top {4} being: + {#highlite}{1} {#record}rec{2} #{#rank}{3}{#record}, + + + {#server}> {#record}You have already won {#highlite}{1}{#record} race{2} + {#server}> {#record}Congratulations, you won your {#highlite}{1}{#record}. race! + {#server}>> {#record}Congratulations, {#highlite}{1}{#record} won his/her {#highlite}{2}{#record}. race! + + + {#server}> Player {#highlite}{1}$z$s{#server} is muted! + {#server}> Player {#highlite}{1}$z$s{#server} is unmuted! + {#server}> {#highlite}{1}{#error} disabled because you are on the global mute list! + + + {#donate} Donate {#highlite}{1}{#donate} coppers to {#highlite}{2}$z + {#server}>> {#highlite}{1}$z$s{#donate} received a donation of {#highlite}{2}{#donate} coppers from {#highlite}{3}$z$s{#donate}. Thank You! + {#server}> {#donate}You made a donation of {#highlite}{1}{#donate} coppers. Thank You! + {#server}> {#error}Minimum donation amount is {#highlite}$i {1}{#error} coppers! + {#server}> {#error}Use {#highlite}$i /donate <number>{#error} to donate coppers to the server + {#donate} Send {#highlite}{1}{#donate} coppers to {#highlite}{2}$z + {#server}> {#error}Insufficient server coppers: {#highlite}$i {1}{#error}! + {#server}> {#error}Cannot pay this server itself! + {#server}> {#donate}Payment of {#highlite}{1}{#donate} coppers to {#highlite}{2}{#donate} confirmed! Remaining coppers: {#highlite}{3} + {#server}> {#donate}Payment to {#highlite}{1}{#donate} cancelled! + {#server}> {#error}Use {#highlite}$i /admin pay <login> $m<number>{#error} to send server coppers to a login + + + {#server}> Current track {#highlite}{1}{#server} has been played for {#highlite}{2} + {#server}>> Current track {#highlite}{1}{#server} finished after {#highlite}{2} + {#server}({#highlite}{1}{#server} replay{2}, total {#highlite}{3}{#server}) + {#server}> Current track {#highlite}{1} {#server}by {#highlite}{2} {#server}Author: {#highlite}{3} {#server}Gold: {#highlite}{4} {#server}Silver: {#highlite}{5} {#server}Bronze: {#highlite}{6} {#server}Cost: {#highlite}{7} + {#server}>> Current track {#highlite}{1} {#server}by {#highlite}{2} {#server}Author: {#highlite}{3} + + + {#server}> {1}Custom points system set to {#highlite}{2}{3}: {#highlite}{4},... + {#server}> {1}Custom points system set to: {#highlite}{2},... + {#server}> {1}No custom Rounds points system defined! + + + {#server}> {#error}No relay servers connected + {#server}> This server relays master server: {#highlite}{1}{#server} ({#highlite}{2}{#server}) + {#server}> {#error}Command unavailable on relay server + + + {#server}>> {#message}This XASECO version {#highlite}{1}{#message} is up to date + {#server}>> {#message}New XASECO version {#highlite}{1}{#message} available from {#highlite}{2} + + + {#welcome}Your IP was banned from this server.$z + {#welcome}Could not connect:{br}{br}Your IP was banned from this server! + {#welcome}Obsolete client version, please $l[http://www.tm-forum.com/viewtopic.php?p=139752#p139752]upgrade$l.$z + {#welcome}Obsolete client version!{br}Please upgrade to the $l[http://www.tm-forum.com/viewtopic.php?p=139752#p139752]latest version$l. + {#welcome}Connection problem, please retry.$z + {#welcome}$sThis is an administrative notice.$z{br}{br}XASECO encountered a very rare player connection{br}problem. Please re-join the server to correct it.{br}Apologies for the inconvenience.{br}{br}$sThe server administrators. + + + {#server}>> IdleKick player {#highlite}{1}$z$s{#server} after {#highlite}{2}{#server} challenge{3}! + {#server}>> IdleSpec player {#highlite}{1}$z$s{#server} after {#highlite}{2}{#server} challenge{3} + {#server}>> IdleKick spectator {#highlite}{1}$z$s{#server} after {#highlite}{2}{#server} challenge{3}! + + + {#server}> Track {#highlite}{1} {#server}plays song: {#highlite}{2} + {#server}> Track {#highlite}{1} {#server}uses mod: {#highlite}{2} {#server}({#highlite}{3}{#server}) + {#server}> Server {#highlite}{1}$z$s {#server}owns {#highlite}{2} {#server}coppers! + + {#server}>> {#record}TMX World Record: {#highlite}{1}{#record} by {#highlite}{2} + $n{#message}R{#highlite}{1}{#message}> + {#server}> {#highlite}/cpsspec{#server} is not currently enabled on this server. + {#server}> {#error}You have to be in admin list to do that! + {#server}> Press the {#highlite}C{#server} key to see the whole list, and use {#highlite}/helpall{#server} for details + {#server}> {#error}This requires a TM United Forever {1}! + {#server}> {#error}Command only available on TM Forever! + + + False + + False + True + 0 + 60 + + 8 + + + + 2 + + + + 2 + True + + 1 + + 1 + + 0 + + tracklist.txt + True + False + True + True + 2 + 50 + True + False + True + False + True + adminops.xml + bannedips.xml + blacklist.txt + guestlist.txt + trackhistory.txt + + + 2.11.19 + + + True + True + True + + False + + False + + 6 + + + + False + + + + DarkBlur + + + AdminBelowChat + DonateBelowCPList + RecordsRightBottom + VoteBelowChat + + + + SuperAdmin + --$SERVER_SA_PASSWORD-- + tmserver + 5000 + 180 + + diff --git a/docker-xaseco/xaseco/dedimania.xml b/docker-xaseco/xaseco/dedimania.xml new file mode 100644 index 0000000..ad07c4d --- /dev/null +++ b/docker-xaseco/xaseco/dedimania.xml @@ -0,0 +1,74 @@ + + + + {#welcome}Welcome to the Dedimania world record system at www.dedimania.com - see {#highlite}/helpdedi + {#dedimsg}Dedimania system timed out - retrying in {#highlite}{1}{#dedimsg} minutes + Dedimania + + http://dedimania.net:8002/Dedimania + + True + + True + + + 8 + + + 1 + + + 1 + + True + + True + + True + + False + + + + 30 + + + + + + + + + + + + + + + + {#server}>> {#dedimsg}Dedimania Record rankings on {#highlite}{1}{#dedimsg} {2} this round: + {#server}>> {#dedimsg}Dedimania Record rankings on {#highlite}{1}{#dedimsg} {2} this round (range {#highlite}{3}{#dedimsg}): + {#server}>> {#dedimsg}Dedimania Record rankings on {#highlite}{1}{#dedimsg} {2} this round ({#highlite}{3}{#dedimsg} new): + {#server}>> {#dedimsg}Dedimania Record rankings on {#highlite}{1}{#dedimsg} {2} this round: none new so far + {#server}>> {#dedimsg}Dedimania Record rankings on {#highlite}{1}{#dedimsg} {2} this round: no records! + + + {#server}>> {#highlite}{1}{#dedirec} secured his/her {#rank}{2}{#dedirec}. Dedimania Record! {3}: {#highlite}{4}{#dedirec} $n({#rank}{5}{#highlite}{6}{#dedirec}) + {#server}>> {#highlite}{1}{#dedirec} equaled his/her {#rank}{2}{#dedirec}. Dedimania Record! {3}: {#highlite}{4} + {#server}>> {#highlite}{1}{#dedirec} gained the {#rank}{2}{#dedirec}. Dedimania Record! {3}: {#highlite}{4}{#dedirec} $n({#rank}{5}{#highlite}{6}{#dedirec}) + {#server}>> {#highlite}{1}{#dedirec} claimed the {#rank}{2}{#dedirec}. Dedimania Record! {3}: {#highlite}{4} + + + {#server}> {#dedirec}The first Dedimania record is: + {#server}> {#dedirec}The last Dedimania record is: + {#server}> {#dedirec}Difference between {1}{#dedirec} and {2}{#dedirec} is: {#highlite}{3} + + + {#server}> {#dedirec}Dedimania Personal Best: {#highlite}{1}{#dedirec}({#rank}{2}{#dedirec}) + {#server}> {#error}You don't have a Dedimania record on this track yet... + + + {#server}>> {#highlite}{1} {#server}({#highlite}{2}{#server}) {#error}is banned from Dedimania! + {#server}> {#error}Finish ignored by Dedimania as you are banned! + + diff --git a/docker-xaseco/xaseco/html.tpl b/docker-xaseco/xaseco/html.tpl new file mode 100644 index 0000000..2b9d9a3 --- /dev/null +++ b/docker-xaseco/xaseco/html.tpl @@ -0,0 +1,28 @@ + + + +Race Results for {DATE} - {TIME} on {TRACK} + +{HEADER} +

    +
    Track: {TRACK}{DATE} - {TIME}
    + + + + +
    RankNameTimeTeamPoints
    {RANK}{NICK}{TIME}{TEAM}{POINTS}

    {MATCHCELL} + +{MATCHPOINTS} + +
    TeamTotal
    {TEAM}{POINTS}

    diff --git a/docker-xaseco/xaseco/includes/GbxRemote.bem.php b/docker-xaseco/xaseco/includes/GbxRemote.bem.php new file mode 100644 index 0000000..3b8dafd --- /dev/null +++ b/docker-xaseco/xaseco/includes/GbxRemote.bem.php @@ -0,0 +1,891 @@ + htmlspecialchars) + Site: http://scripts.incutio.com/xmlrpc/ + Manual: http://scripts.incutio.com/xmlrpc/manual.php + Errors: http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php + Made available under the Artistic License: http://www.opensource.org/licenses/artistic-license.php + + Modified to support protocol 'GbxRemote 2' ('GbxRemote 1') + This version is for BigEndian machines. For LittleEndian (e.g. Intel PC) + machines use the original GbxRemote.inc.php instead. + + Release 2007-09-22 - Slig: + Modified to support >256KB received data (and now >2MB data produce a specific error message) + Modified readCB() to wait the initial timeout only before first read packet + Modified readCB() to return true if there is data to get with getCBResponses() + Modified to support amd64 (for $recvhandle) + Modified IXR_ClientMulticall_Gbx::addCall() to fit Aseco 0.6.1 + Added IXR_Client_Gbx::bytes_sent & bytes_received counters + Fix for a changed feature since php5.1.1 about reference parameter assignment (was used in stream_select) + Workaround for stream_select return value bug with amd64 + + Release 2008-01-20 - Slig / Xymph / Assembler Maniac: + Workaround for fread delay bug in some cases + Added IXR_Client_Gbx::resetError() method (by Xymph) + Some comments and strings code cleanup (by Xymph) + Fix stream_set_timeout($this->socket,...) (thx to CavalierDeVache) + Added a default timeout value to IXR_Client_Gbx::readCB($timeout) + Changed calls with timeout on a stream to use microseconds instead of seconds (by AM) + Removed IXR_Client_Gbx::bytes_sent & bytes_received counters - not used (by AM) + + Release 2008-02-05 - Slig: + Changed some socket read/write timeouts back to seconds to avoid 'transport error' + Changed max data received from 2MB to 4MB + + Release 2008-05-20 - Xymph: + Prevented unpack() warnings in IXR_Client_Gbx::query() when the connection dies + Changed IXR_Client_Gbx::resetError() to assign 'false' for correct isError() + Tweaked some 'transport error' messages + + Release 2009-04-08 - Gou1: + Added method IXR_Client_Gbx::queryIgnoreResult() + Added methods IXR_Client_Gbx::sendRequest() & IXR_Client_Gbx::getResult() + IXR_Client_Gbx::queryIgnoreResult checks if the request is larger than 512KB to avoid errors + If larger than 512KB and method is system.multicall, try to divide the request into + two separate requests with two separate IXR_Client_Gbx::queryIgnoreResult() calls + + Release 2009-06-03 - Xymph: + Suppress possible repetitive CRT warning at stream_select + + Release 2011-04-09 - Xymph / La beuze: + Added optional timeout mechanism to IXR_Client_Gbx::InitWithIp() + + Release 2011-05-22 - Xymph: + Added non-error (true) return status to IXR_Client_Gbx::queryIgnoreResult() + Updated status codes and messages for transport/endian errors + Prevented possible PHP warning in IXR_Client_Gbx::getErrorCode() and getErrorMessage() + + Release 2011-12-04 - Xymph: + Prevented possible PHP warning in IXR_Value::calculateType + + Release 2013-02-18 - Xymph: + Removed unnecessary breaks after returns in IXR_Value::getXml() switch +*/ + +if (!defined('LF')) { + define('LF', "\n"); +} + +class IXR_Value { + public $data; + public $type; + + function IXR_Value ($data, $type = false) { + $this->data = $data; + if (!$type) { + $type = $this->calculateType(); + } + $this->type = $type; + if ($type == 'struct') { + // Turn all the values in the array into new IXR_Value objects + foreach ($this->data as $key => $value) { + $this->data[$key] = new IXR_Value($value); + } + } + if ($type == 'array') { + for ($i = 0, $j = count($this->data); $i < $j; $i++) { + $this->data[$i] = new IXR_Value($this->data[$i]); + } + } + } + + function calculateType() { + if ($this->data === true || $this->data === false) { + return 'boolean'; + } + if (is_integer($this->data)) { + return 'int'; + } + if (is_double($this->data)) { + return 'double'; + } + // Deal with IXR object types base64 and date + if (is_object($this->data) && ($this->data instanceof IXR_Date)) { + return 'date'; + } + if (is_object($this->data) && ($this->data instanceof IXR_Base64)) { + return 'base64'; + } + // If it is a normal PHP object convert it into a struct + if (is_object($this->data)) { + $this->data = get_object_vars($this->data); + return 'struct'; + } + if (!is_array($this->data)) { + return 'string'; + } + // We have an array - is it an array or a struct? + if ($this->isStruct($this->data)) { + return 'struct'; + } else { + return 'array'; + } + } + + function getXml() { + // Return XML for this value + switch ($this->type) { + case 'boolean': + return '' . ($this->data ? '1' : '0') . ''; + case 'int': + return '' . $this->data . ''; + case 'double': + return '' . $this->data . ''; + case 'string': + return '' . htmlspecialchars($this->data) . ''; + case 'array': + $return = '' . LF; + foreach ($this->data as $item) { + $return .= ' ' . $item->getXml() . '' . LF; + } + $return .= ''; + return $return; + case 'struct': + $return = '' . LF; + foreach ($this->data as $name => $value) { + $return .= ' ' . $name . ''; + $return .= $value->getXml() . '' . LF; + } + $return .= ''; + return $return; + case 'date': + case 'base64': + return $this->data->getXml(); + } + return false; + } + + function isStruct($array) { + // Nasty function to check if an array is a struct or not + $expected = 0; + foreach ($array as $key => $value) { + if ((string)$key != (string)$expected) { + return true; + } + $expected++; + } + return false; + } +} + + +class IXR_Message { + public $message; + public $messageType; // methodCall / methodResponse / fault + public $faultCode; + public $faultString; + public $methodName; + public $params; + // Current variable stacks + protected $_arraystructs = array(); // Stack to keep track of the current array/struct + protected $_arraystructstypes = array(); // Stack to keep track of whether things are structs or array + protected $_currentStructName = array(); // A stack as well + protected $_param; + protected $_value; + protected $_currentTag; + protected $_currentTagContents; + // The XML parser + protected $_parser; + + function IXR_Message ($message) { + $this->message = $message; + } + + function parse() { + // first remove the XML declaration + $this->message = preg_replace('/<\?xml(.*)?\?'.'>/', '', $this->message); + if (trim($this->message) == '') { + return false; + } + $this->_parser = xml_parser_create(); + // Set XML parser to take the case of tags into account + xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false); + // Set XML parser callback functions + xml_set_object($this->_parser, $this); + xml_set_element_handler($this->_parser, 'tag_open', 'tag_close'); + xml_set_character_data_handler($this->_parser, 'cdata'); + if (!xml_parse($this->_parser, $this->message)) { + /* die(sprintf('GbxRemote XML error: %s at line %d', + xml_error_string(xml_get_error_code($this->_parser)), + xml_get_current_line_number($this->_parser))); */ + return false; + } + xml_parser_free($this->_parser); + // Grab the error messages, if any + if ($this->messageType == 'fault') { + $this->faultCode = $this->params[0]['faultCode']; + $this->faultString = $this->params[0]['faultString']; + } + return true; + } + + function tag_open($parser, $tag, $attr) { + $this->currentTag = $tag; + switch ($tag) { + case 'methodCall': + case 'methodResponse': + case 'fault': + $this->messageType = $tag; + break; + // Deal with stacks of arrays and structs + case 'data': // data is to all intents and purposes more interesting than array + $this->_arraystructstypes[] = 'array'; + $this->_arraystructs[] = array(); + break; + case 'struct': + $this->_arraystructstypes[] = 'struct'; + $this->_arraystructs[] = array(); + break; + } + } + + function cdata($parser, $cdata) { + $this->_currentTagContents .= $cdata; + } + + function tag_close($parser, $tag) { + $valueFlag = false; + switch ($tag) { + case 'int': + case 'i4': + $value = (int)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'double': + $value = (double)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'string': + $value = (string)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'dateTime.iso8601': + $value = new IXR_Date(trim($this->_currentTagContents)); + // $value = $iso->getTimestamp(); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'value': + // If no type is indicated, the type is string + if (trim($this->_currentTagContents) != '') { + $value = (string)$this->_currentTagContents; + $this->_currentTagContents = ''; + $valueFlag = true; + } + break; + case 'boolean': + $value = (boolean)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'base64': + $value = base64_decode($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + // Deal with stacks of arrays and structs + case 'data': + case 'struct': + $value = array_pop($this->_arraystructs); + array_pop($this->_arraystructstypes); + $valueFlag = true; + break; + case 'member': + array_pop($this->_currentStructName); + break; + case 'name': + $this->_currentStructName[] = trim($this->_currentTagContents); + $this->_currentTagContents = ''; + break; + case 'methodName': + $this->methodName = trim($this->_currentTagContents); + $this->_currentTagContents = ''; + break; + } + + if ($valueFlag) { + /* + if (!is_array($value) && !is_object($value)) { + $value = trim($value); + } + */ + if (count($this->_arraystructs) > 0) { + // Add value to struct or array + if ($this->_arraystructstypes[count($this->_arraystructstypes)-1] == 'struct') { + // Add to struct + $this->_arraystructs[count($this->_arraystructs)-1][$this->_currentStructName[count($this->_currentStructName)-1]] = $value; + } else { + // Add to array + $this->_arraystructs[count($this->_arraystructs)-1][] = $value; + } + } else { + // Just add as a paramater + $this->params[] = $value; + } + } + } +} + + +class IXR_Request { + public $method; + public $args; + public $xml; + + function IXR_Request($method, $args) { + $this->method = $method; + $this->args = $args; + $this->xml = '' . $this->method . ''; + foreach ($this->args as $arg) { + $this->xml .= ''; + $v = new IXR_Value($arg); + $this->xml .= $v->getXml(); + $this->xml .= '' . LF; + } + $this->xml .= ''; + } + + function getLength() { + return strlen($this->xml); + } + + function getXml() { + return $this->xml; + } +} + + +class IXR_Error { + public $code; + public $message; + + function IXR_Error($code, $message) { + $this->code = $code; + $this->message = $message; + } + + function getXml() { + $xml = << + + + + + faultCode + {$this->code} + + + faultString + {$this->message} + + + + + +EOD; + return $xml; + } +} + + +class IXR_Date { + public $year; + public $month; + public $day; + public $hour; + public $minute; + public $second; + + function IXR_Date($time) { + // $time can be a PHP timestamp or an ISO one + if (is_numeric($time)) { + $this->parseTimestamp($time); + } else { + $this->parseIso($time); + } + } + + function parseTimestamp($timestamp) { + $this->year = date('Y', $timestamp); + $this->month = date('Y', $timestamp); + $this->day = date('Y', $timestamp); + $this->hour = date('H', $timestamp); + $this->minute = date('i', $timestamp); + $this->second = date('s', $timestamp); + } + + function parseIso($iso) { + $this->year = substr($iso, 0, 4); + $this->month = substr($iso, 4, 2); + $this->day = substr($iso, 6, 2); + $this->hour = substr($iso, 9, 2); + $this->minute = substr($iso, 12, 2); + $this->second = substr($iso, 15, 2); + } + + function getIso() { + return $this->year.$this->month.$this->day.'T'.$this->hour.':'.$this->minute.':'.$this->second; + } + + function getXml() { + return ''.$this->getIso().''; + } + + function getTimestamp() { + return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year); + } +} + + +class IXR_Base64 { + public $data; + + function IXR_Base64($data) { + $this->data = $data; + } + + function getXml() { + return ''.base64_encode($this->data).''; + } +} + + +////////////////////////////////////////////////////////// +// Nadeo modifications // +// (many thanks to slig for adding callback support) // +////////////////////////////////////////////////////////// +class IXR_Client_Gbx { + public $socket; + public $message = false; + public $cb_message = array(); + public $reqhandle; + public $protocol = 0; + // Storage place for an error message + public $error = false; + + function bigEndianTest() { + list($endiantest) = array_values(unpack('L1L', pack('V', 1))); + if ($endiantest == 1) { + echo "Machine reports itself as LittleEndian, float handling will work correctly with unpack\r\n"; + echo "Use original GbxRemote.inc.php instead of GbxRemote.bem.php\r\n"; + die('App Terminated'); + return false; + } + return true; + } // bigEndianTest + + function IXR_Client_Gbx() { + $this->socket = false; + $this->reqhandle = 0x80000000; + } + + function InitWithIp($ip, $port, $timeout = null) { + + if (!$this->bigEndianTest()) { + $this->error = new IXR_Error(-31999, 'endian error - script doesn\'t match machine type'); + return false; + } + + // open connection, with timeout if specified + if (!isset($timeout)) { + $this->socket = @fsockopen($ip, $port, $errno, $errstr); + } else { + $init_time = microtime(true); + $init_timeout = 5; // retry every 5s + while (true) { + $this->socket = @fsockopen($ip, $port, $errno, $errstr, $init_timeout); + if ($this->socket || (microtime(true) - $init_time >= $timeout)) + break; + } + } + if (!$this->socket) { + $this->error = new IXR_Error(-32300, "transport error - could not open socket (error: $errno, $errstr)"); + return false; + } + // handshake + $array_result = big_endian_unpack('Vsize', fread($this->socket, 4)); + $size = $array_result['size']; + if ($size > 64) { + $this->error = new IXR_Error(-32300, 'transport error - wrong lowlevel protocol header'); + return false; + } + $handshake = fread($this->socket, $size); + if ($handshake == 'GBXRemote 1') { + $this->protocol = 1; + } else if ($handshake == 'GBXRemote 2') { + $this->protocol = 2; + } else { + $this->error = new IXR_Error(-32300, 'transport error - wrong lowlevel protocol version'); + return false; + } + return true; + } + + function Init($port) { + return $this->InitWithIp('localhost', $port); + } + + function Terminate() { + if ($this->socket) { + fclose($this->socket); + $this->socket = false; + } + } + + protected function sendRequest(IXR_Request $request) { + $xml = $request->getXml(); + + @stream_set_timeout($this->socket, 20); // timeout 20s (to write the request) + // send request + $this->reqhandle++; + if ($this->protocol == 1) { + $bytes = pack('Va*', strlen($xml), $xml); + } else { + $bytes = pack('VVa*', strlen($xml), $this->reqhandle, $xml); + } + + $bytes_to_write = strlen($bytes); + while ($bytes_to_write > 0) { + $r = @fwrite($this->socket, $bytes); + if ($r === false || $r == 0) { + // connection interrupted + return false; // or die? + } + + $bytes_to_write -= $r; + if ($bytes_to_write == 0) + break; + + $bytes = substr($bytes, $r); + } + + return true; + } + + protected function getResult() { + $contents = ''; + $contents_length = 0; + do { + $size = 0; + $recvhandle = 0; + @stream_set_timeout($this->socket, 20); // timeout 20s (to read the reply header) + // Get result + if ($this->protocol == 1) { + $contents = fread($this->socket, 4); + if (strlen($contents) == 0) { + $this->error = new IXR_Error(-32300, 'transport error - cannot read size'); + return false; + } + $array_result = big_endian_unpack('Vsize', $contents); + $size = $array_result['size']; + $recvhandle = $this->reqhandle; + } else { + $contents = fread($this->socket, 8); + if (strlen($contents) == 0) { + $this->error = new IXR_Error(-32300, 'transport error - cannot read size/handle'); + return false; + } + $array_result = big_endian_unpack('Vsize/Vhandle', $contents); + $size = $array_result['size']; + $recvhandle = $array_result['handle']; + // -- amd64 support -- + $bits = sprintf('%b', $recvhandle); + if (strlen($bits) == 64) { + $recvhandle = bindec(substr($bits, 32)); + } + } + + if ($recvhandle == 0 || $size == 0) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + if ($size > 4096*1024) { + $this->error = new IXR_Error(-32300, "transport error - response too large ($size)"); + return false; + } + + $contents = ''; + $contents_length = 0; + @stream_set_timeout($this->socket, 0, 10000); // timeout 10 ms (for successive reads until end) + while ($contents_length < $size) { + $contents .= fread($this->socket, $size-$contents_length); + $contents_length = strlen($contents); + } + + if (($recvhandle & 0x80000000) == 0) { + // this is a callback, not our answer! handle= $recvhandle, xml-rpc= $contents + // just add it to the message list for the user to read + $new_cb_message = new IXR_Message($contents); + if ($new_cb_message->parse() && $new_cb_message->messageType != 'fault') { + array_push($this->cb_message, array($new_cb_message->methodName, $new_cb_message->params)); + } + } + } while ((int)$recvhandle != (int)$this->reqhandle); + + $this->message = new IXR_Message($contents); + if (!$this->message->parse()) { + // XML error + $this->error = new IXR_Error(-32700, 'parse error. not well formed'); + return false; + } + // Is the message a fault? + if ($this->message->messageType == 'fault') { + $this->error = new IXR_Error($this->message->faultCode, $this->message->faultString); + return false; + } + // Message must be OK + return true; + } + + + function query() { + $args = func_get_args(); + $method = array_shift($args); + + if (!$this->socket || $this->protocol == 0) { + $this->error = new IXR_Error(-32300, 'transport error - client not initialized'); + return false; + } + + $request = new IXR_Request($method, $args); + + // Check if request is larger than 512 Kbytes + if (($size = $request->getLength()) > 512*1024-8) { + $this->error = new IXR_Error(-32300, "transport error - request too large ($size)"); + return false; + } + + // Send request + $ok = $this->sendRequest($request); + if (!$ok) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + + // Get result + return $this->getResult(); + } + + // Non-blocking query method: doesn't read the response + function queryIgnoreResult() { + $args = func_get_args(); + $method = array_shift($args); + + if (!$this->socket || $this->protocol == 0) { + $this->error = new IXR_Error(-32300, 'transport error - client not initialized'); + return false; + } + + $request = new IXR_Request($method, $args); + + // Check if the request is greater than 512 Kbytes to avoid errors + // If the method is system.multicall, make two calls (possibly recursively) + if (($size = $request->getLength()) > 512*1024-8) { + if ($method = 'system.multicall' && isset($args[0])) { + $count = count($args[0]); + // If count is 1, query cannot be reduced + if ($count < 2) { + $this->error = new IXR_Error(-32300, "transport error - request too large ($size)"); + return false; + } + $length = floor($count/2); + $args1 = array_slice($args[0], 0, $length); + $args2 = array_slice($args[0], $length, ($count-$length)); + + $res1 = $this->queryIgnoreResult('system.multicall', $args1); + $res2 = $this->queryIgnoreResult('system.multicall', $args2); + return ($res1 && $res2); + } + // If the method is not a multicall, just stop + else { + $this->error = new IXR_Error(-32300, "transport error - request too large ($size)"); + return false; + } + } + + // Send request + $ok = $this->sendRequest($request); + if (!$ok) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + return true; + } + + function readCB($timeout = 2000) { // timeout 2 ms + if (!$this->socket || $this->protocol == 0) { + $this->error = new IXR_Error(-32300, 'transport error - client not initialized'); + return false; + } + if ($this->protocol == 1) + return false; + + $something_received = count($this->cb_message)>0; + $contents = ''; + $contents_length = 0; + + @stream_set_timeout($this->socket, 0, 10000); // timeout 10 ms (to read available data) + // (assignment in arguments is forbidden since php 5.1.1) + $read = array($this->socket); + $write = NULL; + $except = NULL; + $nb = @stream_select($read, $write, $except, 0, $timeout); + // workaround for stream_select bug with amd64 + if ($nb !== false) + $nb = count($read); + + while ($nb !== false && $nb > 0) { + $timeout = 0; // we don't want to wait for the full time again, just flush the available data + + $size = 0; + $recvhandle = 0; + // Get result + $contents = fread($this->socket, 8); + if (strlen($contents) == 0) { + $this->error = new IXR_Error(-32300, 'transport error - cannot read size/handle'); + return false; + } + $array_result = big_endian_unpack('Vsize/Vhandle', $contents); + $size = $array_result['size']; + $recvhandle = $array_result['handle']; + + if ($recvhandle == 0 || $size == 0) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + if ($size > 4096*1024) { + $this->error = new IXR_Error(-32300, "transport error - response too large ($size)"); + return false; + } + + $contents = ''; + $contents_length = 0; + while ($contents_length < $size) { + $contents .= fread($this->socket, $size-$contents_length); + $contents_length = strlen($contents); + } + + if (($recvhandle & 0x80000000) == 0) { + // this is a callback. handle= $recvhandle, xml-rpc= $contents + //echo 'CALLBACK('.$contents_length.')[ '.$contents.' ]' . LF; + $new_cb_message = new IXR_Message($contents); + if ($new_cb_message->parse() && $new_cb_message->messageType != 'fault') { + array_push($this->cb_message, array($new_cb_message->methodName, $new_cb_message->params)); + } + $something_received = true; + } + + // (assignment in arguments is forbidden since php 5.1.1) + $read = array($this->socket); + $write = NULL; + $except = NULL; + $nb = @stream_select($read, $write, $except, 0, $timeout); + // workaround for stream_select bug with amd64 + if ($nb !== false) + $nb = count($read); + } + return $something_received; + } + + function getResponse() { + // methodResponses can only have one param - return that + return $this->message->params[0]; + } + + function getCBResponses() { + // (look at the end of basic.php for an example) + $messages = $this->cb_message; + $this->cb_message = array(); + return $messages; + } + + function isError() { + return is_object($this->error); + } + + function resetError() { + $this->error = false; + } + + function getErrorCode() { + if ($this->isError()) + return $this->error->code; + else + return 0; + } + + function getErrorMessage() { + if ($this->isError()) + return $this->error->message; + else + return ''; + } +} + + +class IXR_ClientMulticall_Gbx extends IXR_Client_Gbx { + public $calls = array(); + + function addCall($methodName, $args) { + $struct = array('methodName' => $methodName, 'params' => $args); + $this->calls[] = $struct; + + return (count($this->calls) - 1); + } + + function multiquery($ignoreResult = false) { + // Prepare multicall, then call the parent::query() (or queryIgnoreResult) method + if ($ignoreResult) { + $result = parent::queryIgnoreResult('system.multicall', $this->calls); + } else { + $result = parent::query('system.multicall', $this->calls); + } + $this->calls = array(); // reset for next calls + return $result; + } +} + +/** + * The following code is a workaround for php's unpack function which + * does not have the capability of unpacking double precision floats + * that were packed in the opposite byte order of the current machine. + */ +function big_endian_unpack($format, $data) { + $ar = unpack($format, $data); + $vals = array_values($ar); + $f = explode('/', $format); + $i = 0; + foreach ($f as $f_k => $f_v) { + $repeater = intval(substr($f_v, 1)); + if ($repeater == 0) + $repeater = 1; + if ($f_v{1} == '*') { + $repeater = count($ar) - $i; + } + if ($f_v{0} != 'd') { + $i += $repeater; + continue; + } + $j = $i + $repeater; + for ($a = $i; $a < $j; ++$a) { + $p = pack('d', $vals[$i]); + $p = strrev($p); + list($vals[$i]) = array_values(unpack('d1d', $p)); + ++$i; + } + } + $a = 0; + foreach ($ar as $ar_k => $ar_v) { + $ar[$ar_k] = $vals[$a]; + ++$a; + } + return $ar; +} +?> diff --git a/docker-xaseco/xaseco/includes/GbxRemote.inc.php b/docker-xaseco/xaseco/includes/GbxRemote.inc.php new file mode 100644 index 0000000..dbb3247 --- /dev/null +++ b/docker-xaseco/xaseco/includes/GbxRemote.inc.php @@ -0,0 +1,854 @@ + htmlspecialchars) + Site: http://scripts.incutio.com/xmlrpc/ + Manual: http://scripts.incutio.com/xmlrpc/manual.php + Errors: http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php + Made available under the Artistic License: http://www.opensource.org/licenses/artistic-license.php + + Modified to support protocol 'GbxRemote 2' ('GbxRemote 1') + This version is for LittleEndian (e.g. Intel PC) machines. For BegEndian + machines use GbxRemote.bem.php as GbxRemote.inc.php instead. + + Release 2007-09-22 - Slig: + Modified to support >256KB received data (and now >2MB data produce a specific error message) + Modified readCB() to wait the initial timeout only before first read packet + Modified readCB() to return true if there is data to get with getCBResponses() + Modified to support amd64 (for $recvhandle) + Modified IXR_ClientMulticall_Gbx::addCall() to fit Aseco 0.6.1 + Added IXR_Client_Gbx::bytes_sent & bytes_received counters + Fix for a changed feature since php5.1.1 about reference parameter assignment (was used in stream_select) + Workaround for stream_select return value bug with amd64 + + Release 2008-01-20 - Slig / Xymph / Assembler Maniac: + Workaround for fread delay bug in some cases + Added IXR_Client_Gbx::resetError() method (by Xymph) + Some comments and strings code cleanup (by Xymph) + Fix stream_set_timeout($this->socket,...) (thx to CavalierDeVache) + Added a default timeout value to IXR_Client_Gbx::readCB($timeout) + Changed calls with timeout on a stream to use microseconds instead of seconds (by AM) + Removed IXR_Client_Gbx::bytes_sent & bytes_received counters - not used (by AM) + + Release 2008-02-05 - Slig: + Changed some socket read/write timeouts back to seconds to avoid 'transport error' + Changed max data received from 2MB to 4MB + + Release 2008-05-20 - Xymph: + Prevented unpack() warnings in IXR_Client_Gbx::query() when the connection dies + Changed IXR_Client_Gbx::resetError() to assign 'false' for correct isError() + Tweaked some 'transport error' messages + + Release 2009-04-08 - Gou1: + Added method IXR_Client_Gbx::queryIgnoreResult() + Added methods IXR_Client_Gbx::sendRequest() & IXR_Client_Gbx::getResult() + IXR_Client_Gbx::queryIgnoreResult checks if the request is larger than 512KB to avoid errors + If larger than 512KB and method is system.multicall, try to divide the request into + two separate requests with two separate IXR_Client_Gbx::queryIgnoreResult() calls + + Release 2009-06-03 - Xymph: + Suppress possible repetitive CRT warning at stream_select + + Release 2011-04-09 - Xymph / La beuze: + Added optional timeout mechanism to IXR_Client_Gbx::InitWithIp() + + Release 2011-05-22 - Xymph: + Added non-error (true) return status to IXR_Client_Gbx::queryIgnoreResult() + Updated status codes and messages for transport/endian errors + Prevented possible PHP warning in IXR_Client_Gbx::getErrorCode() and getErrorMessage() + + Release 2011-12-04 - Xymph: + Prevented possible PHP warning in IXR_Value::calculateType + + Release 2013-02-18 - Xymph: + Removed unnecessary breaks after returns in IXR_Value::getXml() switch +*/ + +if (!defined('LF')) { + define('LF', "\n"); +} + +class IXR_Value { + public $data; + public $type; + + function IXR_Value ($data, $type = false) { + $this->data = $data; + if (!$type) { + $type = $this->calculateType(); + } + $this->type = $type; + if ($type == 'struct') { + // Turn all the values in the array into new IXR_Value objects + foreach ($this->data as $key => $value) { + $this->data[$key] = new IXR_Value($value); + } + } + if ($type == 'array') { + for ($i = 0, $j = count($this->data); $i < $j; $i++) { + $this->data[$i] = new IXR_Value($this->data[$i]); + } + } + } + + function calculateType() { + if ($this->data === true || $this->data === false) { + return 'boolean'; + } + if (is_integer($this->data)) { + return 'int'; + } + if (is_double($this->data)) { + return 'double'; + } + // Deal with IXR object types base64 and date + if (is_object($this->data) && ($this->data instanceof IXR_Date)) { + return 'date'; + } + if (is_object($this->data) && ($this->data instanceof IXR_Base64)) { + return 'base64'; + } + // If it is a normal PHP object convert it into a struct + if (is_object($this->data)) { + $this->data = get_object_vars($this->data); + return 'struct'; + } + if (!is_array($this->data)) { + return 'string'; + } + // We have an array - is it an array or a struct? + if ($this->isStruct($this->data)) { + return 'struct'; + } else { + return 'array'; + } + } + + function getXml() { + // Return XML for this value + switch ($this->type) { + case 'boolean': + return '' . ($this->data ? '1' : '0') . ''; + case 'int': + return '' . $this->data . ''; + case 'double': + return '' . $this->data . ''; + case 'string': + return '' . htmlspecialchars($this->data) . ''; + case 'array': + $return = '' . LF; + foreach ($this->data as $item) { + $return .= ' ' . $item->getXml() . '' . LF; + } + $return .= ''; + return $return; + case 'struct': + $return = '' . LF; + foreach ($this->data as $name => $value) { + $return .= ' ' . $name . ''; + $return .= $value->getXml() . '' . LF; + } + $return .= ''; + return $return; + case 'date': + case 'base64': + return $this->data->getXml(); + } + return false; + } + + function isStruct($array) { + // Nasty function to check if an array is a struct or not + $expected = 0; + foreach ($array as $key => $value) { + if ((string)$key != (string)$expected) { + return true; + } + $expected++; + } + return false; + } +} + + +class IXR_Message { + public $message; + public $messageType; // methodCall / methodResponse / fault + public $faultCode; + public $faultString; + public $methodName; + public $params; + // Current variable stacks + protected $_arraystructs = array(); // Stack to keep track of the current array/struct + protected $_arraystructstypes = array(); // Stack to keep track of whether things are structs or array + protected $_currentStructName = array(); // A stack as well + protected $_param; + protected $_value; + protected $_currentTag; + protected $_currentTagContents; + // The XML parser + protected $_parser; + + function IXR_Message ($message) { + $this->message = $message; + } + + function parse() { + // first remove the XML declaration + $this->message = preg_replace('/<\?xml(.*)?\?'.'>/', '', $this->message); + if (trim($this->message) == '') { + return false; + } + $this->_parser = xml_parser_create(); + // Set XML parser to take the case of tags into account + xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false); + // Set XML parser callback functions + xml_set_object($this->_parser, $this); + xml_set_element_handler($this->_parser, 'tag_open', 'tag_close'); + xml_set_character_data_handler($this->_parser, 'cdata'); + if (!xml_parse($this->_parser, $this->message)) { + /* die(sprintf('GbxRemote XML error: %s at line %d', + xml_error_string(xml_get_error_code($this->_parser)), + xml_get_current_line_number($this->_parser))); */ + return false; + } + xml_parser_free($this->_parser); + // Grab the error messages, if any + if ($this->messageType == 'fault') { + $this->faultCode = $this->params[0]['faultCode']; + $this->faultString = $this->params[0]['faultString']; + } + return true; + } + + function tag_open($parser, $tag, $attr) { + $this->currentTag = $tag; + switch ($tag) { + case 'methodCall': + case 'methodResponse': + case 'fault': + $this->messageType = $tag; + break; + // Deal with stacks of arrays and structs + case 'data': // data is to all intents and purposes more interesting than array + $this->_arraystructstypes[] = 'array'; + $this->_arraystructs[] = array(); + break; + case 'struct': + $this->_arraystructstypes[] = 'struct'; + $this->_arraystructs[] = array(); + break; + } + } + + function cdata($parser, $cdata) { + $this->_currentTagContents .= $cdata; + } + + function tag_close($parser, $tag) { + $valueFlag = false; + switch ($tag) { + case 'int': + case 'i4': + $value = (int)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'double': + $value = (double)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'string': + $value = (string)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'dateTime.iso8601': + $value = new IXR_Date(trim($this->_currentTagContents)); + // $value = $iso->getTimestamp(); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'value': + // If no type is indicated, the type is string + if (trim($this->_currentTagContents) != '') { + $value = (string)$this->_currentTagContents; + $this->_currentTagContents = ''; + $valueFlag = true; + } + break; + case 'boolean': + $value = (boolean)trim($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + case 'base64': + $value = base64_decode($this->_currentTagContents); + $this->_currentTagContents = ''; + $valueFlag = true; + break; + // Deal with stacks of arrays and structs + case 'data': + case 'struct': + $value = array_pop($this->_arraystructs); + array_pop($this->_arraystructstypes); + $valueFlag = true; + break; + case 'member': + array_pop($this->_currentStructName); + break; + case 'name': + $this->_currentStructName[] = trim($this->_currentTagContents); + $this->_currentTagContents = ''; + break; + case 'methodName': + $this->methodName = trim($this->_currentTagContents); + $this->_currentTagContents = ''; + break; + } + + if ($valueFlag) { + /* + if (!is_array($value) && !is_object($value)) { + $value = trim($value); + } + */ + if (count($this->_arraystructs) > 0) { + // Add value to struct or array + if ($this->_arraystructstypes[count($this->_arraystructstypes)-1] == 'struct') { + // Add to struct + $this->_arraystructs[count($this->_arraystructs)-1][$this->_currentStructName[count($this->_currentStructName)-1]] = $value; + } else { + // Add to array + $this->_arraystructs[count($this->_arraystructs)-1][] = $value; + } + } else { + // Just add as a paramater + $this->params[] = $value; + } + } + } +} + + +class IXR_Request { + public $method; + public $args; + public $xml; + + function IXR_Request($method, $args) { + $this->method = $method; + $this->args = $args; + $this->xml = '' . $this->method . ''; + foreach ($this->args as $arg) { + $this->xml .= ''; + $v = new IXR_Value($arg); + $this->xml .= $v->getXml(); + $this->xml .= '' . LF; + } + $this->xml .= ''; + } + + function getLength() { + return strlen($this->xml); + } + + function getXml() { + return $this->xml; + } +} + + +class IXR_Error { + public $code; + public $message; + + function IXR_Error($code, $message) { + $this->code = $code; + $this->message = $message; + } + + function getXml() { + $xml = << + + + + + faultCode + {$this->code} + + + faultString + {$this->message} + + + + + +EOD; + return $xml; + } +} + + +class IXR_Date { + public $year; + public $month; + public $day; + public $hour; + public $minute; + public $second; + + function IXR_Date($time) { + // $time can be a PHP timestamp or an ISO one + if (is_numeric($time)) { + $this->parseTimestamp($time); + } else { + $this->parseIso($time); + } + } + + function parseTimestamp($timestamp) { + $this->year = date('Y', $timestamp); + $this->month = date('Y', $timestamp); + $this->day = date('Y', $timestamp); + $this->hour = date('H', $timestamp); + $this->minute = date('i', $timestamp); + $this->second = date('s', $timestamp); + } + + function parseIso($iso) { + $this->year = substr($iso, 0, 4); + $this->month = substr($iso, 4, 2); + $this->day = substr($iso, 6, 2); + $this->hour = substr($iso, 9, 2); + $this->minute = substr($iso, 12, 2); + $this->second = substr($iso, 15, 2); + } + + function getIso() { + return $this->year.$this->month.$this->day.'T'.$this->hour.':'.$this->minute.':'.$this->second; + } + + function getXml() { + return ''.$this->getIso().''; + } + + function getTimestamp() { + return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year); + } +} + + +class IXR_Base64 { + public $data; + + function IXR_Base64($data) { + $this->data = $data; + } + + function getXml() { + return ''.base64_encode($this->data).''; + } +} + + +////////////////////////////////////////////////////////// +// Nadeo modifications // +// (many thanks to slig for adding callback support) // +////////////////////////////////////////////////////////// +class IXR_Client_Gbx { + public $socket; + public $message = false; + public $cb_message = array(); + public $reqhandle; + public $protocol = 0; + // Storage place for an error message + public $error = false; + + function bigEndianTest() { + list($endiantest) = array_values(unpack('L1L', pack('V', 1))); + if ($endiantest != 1) { + echo "Machine reports itself as BigEndian, float handling must be altered\r\n"; + echo "Overwrite GbxRemote.inc.php with GbxRemote.bem.php\r\n"; + die('App Terminated'); + return false; + } + return true; + } // bigEndianTest + + function IXR_Client_Gbx() { + $this->socket = false; + $this->reqhandle = 0x80000000; + } + + function InitWithIp($ip, $port, $timeout = null) { + + if (!$this->bigEndianTest()) { + $this->error = new IXR_Error(-31999, 'endian error - script doesn\'t match machine type'); + return false; + } + + // open connection, with timeout if specified + if (!isset($timeout)) { + $this->socket = @fsockopen($ip, $port, $errno, $errstr); + } else { + $init_time = microtime(true); + $init_timeout = 5; // retry every 5s + while (true) { + $this->socket = @fsockopen($ip, $port, $errno, $errstr, $init_timeout); + if ($this->socket || (microtime(true) - $init_time >= $timeout)) + break; + } + } + if (!$this->socket) { + $this->error = new IXR_Error(-32300, "transport error - could not open socket (error: $errno, $errstr)"); + return false; + } + // handshake + $array_result = unpack('Vsize', fread($this->socket, 4)); + $size = $array_result['size']; + if ($size > 64) { + $this->error = new IXR_Error(-32300, 'transport error - wrong lowlevel protocol header'); + return false; + } + $handshake = fread($this->socket, $size); + if ($handshake == 'GBXRemote 1') { + $this->protocol = 1; + } else if ($handshake == 'GBXRemote 2') { + $this->protocol = 2; + } else { + $this->error = new IXR_Error(-32300, 'transport error - wrong lowlevel protocol version'); + return false; + } + return true; + } + + function Init($port) { + return $this->InitWithIp('localhost', $port); + } + + function Terminate() { + if ($this->socket) { + fclose($this->socket); + $this->socket = false; + } + } + + protected function sendRequest(IXR_Request $request) { + $xml = $request->getXml(); + + @stream_set_timeout($this->socket, 20); // timeout 20s (to write the request) + // send request + $this->reqhandle++; + if ($this->protocol == 1) { + $bytes = pack('Va*', strlen($xml), $xml); + } else { + $bytes = pack('VVa*', strlen($xml), $this->reqhandle, $xml); + } + + $bytes_to_write = strlen($bytes); + while ($bytes_to_write > 0) { + $r = @fwrite($this->socket, $bytes); + if ($r === false || $r == 0) { + // connection interrupted + return false; // or die? + } + + $bytes_to_write -= $r; + if ($bytes_to_write == 0) + break; + + $bytes = substr($bytes, $r); + } + + return true; + } + + protected function getResult() { + $contents = ''; + $contents_length = 0; + do { + $size = 0; + $recvhandle = 0; + @stream_set_timeout($this->socket, 20); // timeout 20s (to read the reply header) + // Get result + if ($this->protocol == 1) { + $contents = fread($this->socket, 4); + if (strlen($contents) == 0) { + $this->error = new IXR_Error(-32300, 'transport error - cannot read size'); + return false; + } + $array_result = unpack('Vsize', $contents); + $size = $array_result['size']; + $recvhandle = $this->reqhandle; + } else { + $contents = fread($this->socket, 8); + if (strlen($contents) == 0) { + $this->error = new IXR_Error(-32300, 'transport error - cannot read size/handle'); + return false; + } + $array_result = unpack('Vsize/Vhandle', $contents); + $size = $array_result['size']; + $recvhandle = $array_result['handle']; + // -- amd64 support -- + $bits = sprintf('%b', $recvhandle); + if (strlen($bits) == 64) { + $recvhandle = bindec(substr($bits, 32)); + } + } + + if ($recvhandle == 0 || $size == 0) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + if ($size > 4096*1024) { + $this->error = new IXR_Error(-32300, "transport error - response too large ($size)"); + return false; + } + + $contents = ''; + $contents_length = 0; + @stream_set_timeout($this->socket, 0, 10000); // timeout 10 ms (for successive reads until end) + while ($contents_length < $size) { + $contents .= fread($this->socket, $size-$contents_length); + $contents_length = strlen($contents); + } + + if (($recvhandle & 0x80000000) == 0) { + // this is a callback, not our answer! handle= $recvhandle, xml-rpc= $contents + // just add it to the message list for the user to read + $new_cb_message = new IXR_Message($contents); + if ($new_cb_message->parse() && $new_cb_message->messageType != 'fault') { + array_push($this->cb_message, array($new_cb_message->methodName, $new_cb_message->params)); + } + } + } while ((int)$recvhandle != (int)$this->reqhandle); + + $this->message = new IXR_Message($contents); + if (!$this->message->parse()) { + // XML error + $this->error = new IXR_Error(-32700, 'parse error. not well formed'); + return false; + } + // Is the message a fault? + if ($this->message->messageType == 'fault') { + $this->error = new IXR_Error($this->message->faultCode, $this->message->faultString); + return false; + } + // Message must be OK + return true; + } + + + function query() { + $args = func_get_args(); + $method = array_shift($args); + + if (!$this->socket || $this->protocol == 0) { + $this->error = new IXR_Error(-32300, 'transport error - client not initialized'); + return false; + } + + $request = new IXR_Request($method, $args); + + // Check if request is larger than 512 Kbytes + if (($size = $request->getLength()) > 512*1024-8) { + $this->error = new IXR_Error(-32300, "transport error - request too large ($size)"); + return false; + } + + // Send request + $ok = $this->sendRequest($request); + if (!$ok) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + + // Get result + return $this->getResult(); + } + + // Non-blocking query method: doesn't read the response + function queryIgnoreResult() { + $args = func_get_args(); + $method = array_shift($args); + + if (!$this->socket || $this->protocol == 0) { + $this->error = new IXR_Error(-32300, 'transport error - client not initialized'); + return false; + } + + $request = new IXR_Request($method, $args); + + // Check if the request is greater than 512 Kbytes to avoid errors + // If the method is system.multicall, make two calls (possibly recursively) + if (($size = $request->getLength()) > 512*1024-8) { + if ($method = 'system.multicall' && isset($args[0])) { + $count = count($args[0]); + // If count is 1, query cannot be reduced + if ($count < 2) { + $this->error = new IXR_Error(-32300, "transport error - request too large ($size)"); + return false; + } + $length = floor($count/2); + $args1 = array_slice($args[0], 0, $length); + $args2 = array_slice($args[0], $length, ($count-$length)); + + $res1 = $this->queryIgnoreResult('system.multicall', $args1); + $res2 = $this->queryIgnoreResult('system.multicall', $args2); + return ($res1 && $res2); + } + // If the method is not a multicall, just stop + else { + $this->error = new IXR_Error(-32300, "transport error - request too large ($size)"); + return false; + } + } + + // Send request + $ok = $this->sendRequest($request); + if (!$ok) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + return true; + } + + function readCB($timeout = 2000) { // timeout 2 ms + if (!$this->socket || $this->protocol == 0) { + $this->error = new IXR_Error(-32300, 'transport error - client not initialized'); + return false; + } + if ($this->protocol == 1) + return false; + + $something_received = count($this->cb_message)>0; + $contents = ''; + $contents_length = 0; + + @stream_set_timeout($this->socket, 0, 10000); // timeout 10 ms (to read available data) + // (assignment in arguments is forbidden since php 5.1.1) + $read = array($this->socket); + $write = NULL; + $except = NULL; + $nb = @stream_select($read, $write, $except, 0, $timeout); + // workaround for stream_select bug with amd64 + if ($nb !== false) + $nb = count($read); + + while ($nb !== false && $nb > 0) { + $timeout = 0; // we don't want to wait for the full time again, just flush the available data + + $size = 0; + $recvhandle = 0; + // Get result + $contents = fread($this->socket, 8); + if (strlen($contents) == 0) { + $this->error = new IXR_Error(-32300, 'transport error - cannot read size/handle'); + return false; + } + $array_result = unpack('Vsize/Vhandle', $contents); + $size = $array_result['size']; + $recvhandle = $array_result['handle']; + + if ($recvhandle == 0 || $size == 0) { + $this->error = new IXR_Error(-32300, 'transport error - connection interrupted!'); + return false; + } + if ($size > 4096*1024) { + $this->error = new IXR_Error(-32300, "transport error - response too large ($size)"); + return false; + } + + $contents = ''; + $contents_length = 0; + while ($contents_length < $size) { + $contents .= fread($this->socket, $size-$contents_length); + $contents_length = strlen($contents); + } + + if (($recvhandle & 0x80000000) == 0) { + // this is a callback. handle= $recvhandle, xml-rpc= $contents + //echo 'CALLBACK('.$contents_length.')[ '.$contents.' ]' . LF; + $new_cb_message = new IXR_Message($contents); + if ($new_cb_message->parse() && $new_cb_message->messageType != 'fault') { + array_push($this->cb_message, array($new_cb_message->methodName, $new_cb_message->params)); + } + $something_received = true; + } + + // (assignment in arguments is forbidden since php 5.1.1) + $read = array($this->socket); + $write = NULL; + $except = NULL; + $nb = @stream_select($read, $write, $except, 0, $timeout); + // workaround for stream_select bug with amd64 + if ($nb !== false) + $nb = count($read); + } + return $something_received; + } + + function getResponse() { + // methodResponses can only have one param - return that + return $this->message->params[0]; + } + + function getCBResponses() { + // (look at the end of basic.php for an example) + $messages = $this->cb_message; + $this->cb_message = array(); + return $messages; + } + + function isError() { + return is_object($this->error); + } + + function resetError() { + $this->error = false; + } + + function getErrorCode() { + if ($this->isError()) + return $this->error->code; + else + return 0; + } + + function getErrorMessage() { + if ($this->isError()) + return $this->error->message; + else + return ''; + } +} + + +class IXR_ClientMulticall_Gbx extends IXR_Client_Gbx { + public $calls = array(); + + function addCall($methodName, $args) { + $struct = array('methodName' => $methodName, 'params' => $args); + $this->calls[] = $struct; + + return (count($this->calls) - 1); + } + + function multiquery($ignoreResult = false) { + // Prepare multicall, then call the parent::query() (or queryIgnoreResult) method + if ($ignoreResult) { + $result = parent::queryIgnoreResult('system.multicall', $this->calls); + } else { + $result = parent::query('system.multicall', $this->calls); + } + $this->calls = array(); // reset for next calls + return $result; + } +} +?> diff --git a/docker-xaseco/xaseco/includes/GbxRemote.response.php b/docker-xaseco/xaseco/includes/GbxRemote.response.php new file mode 100644 index 0000000..b5dfdbd --- /dev/null +++ b/docker-xaseco/xaseco/includes/GbxRemote.response.php @@ -0,0 +1,218 @@ + xml-rpc string) +// ----------------------------------------------------------------------- +// $method is the method name. +// $params should be an array containing the method parameters. +// For system.multicall, there should be one parameter, which is an array +// containing the methods structs {'methodName':string, 'params':array} +// ----------------------------------------------------------------------- + +function xmlrpc_request($method, $params = null) { + + // in case the first level array was forgotten in a multicall then add it... + if ($method == 'system.multicall' && isset($params[0]['methodName'])) + $params = array($params); + + $xml = '' + . "\n\n$method\n\n"; + + if (is_array($params)) { + foreach ($params as $param) { + $xml .= "\n"; + $v = new IXR_Value($param); + $xml .= $v->getXml(); + $xml .= "\n\n"; + } + } + $xml .= "\n"; + return $xml; +} + + +// ----------------------------------------------------------------------- +// Build a methodCall response in xml-rpc format from arrays args +// (array_args ==> xml-rpc string) +// ----------------------------------------------------------------------- +// $args should be an array containing the response +// ----------------------------------------------------------------------- + +function xmlrpc_response($args = null) { + + $xml = '' + . "\n\n\n\n"; + + if ($args !== null) { + $xml .= ''; + $v = new IXR_Value($args); + $xml .= $v->getXml(); + $xml .= "\n"; + } + $xml .= "\n\n"; + return $xml; +} + + +// ----------------------------------------------------------------------- +// Build an error response in xml-rpc format +// (error code + message ==> xml-rpc string) +// ----------------------------------------------------------------------- +// $code is the error code +// $message is the error string +// ----------------------------------------------------------------------- + +function xmlrpc_error($code, $message) { + + $xml = '' + . "\n\n\n" + . "\n" + . "faultCode$code\n\n" + . "faultString$message\n" + . "\n" + . "\n"; + return $xml; +} + + +// ----------------------------------------------------------------------- +// Build a method struct (array) usable for a method call in a multicall +// (method name + params array ==> method struct array +// ----------------------------------------------------------------------- +// $name is the method name +// $params is the params array +// ----------------------------------------------------------------------- + +function rpc_method($name, $params = null) { + + if (!is_array($params)) + $params = array(); + return array('methodName' => $name, 'params' => $params); +} + + +// ----------------------------------------------------------------------- +// Build a response struct (array) usable as a reply for a method in a multicall +// (method name + params array ==> method struct array +// ----------------------------------------------------------------------- +// $params is the methode response array +// ----------------------------------------------------------------------- + +function rpc_response($response = null) { + + if (!is_array($response)) + $response = array(); + return array($response); +} + + +// ----------------------------------------------------------------------- +// Build an error struct (array) usable as an error for a method in a multicall +// (error code + message ==> error struct array) +// ----------------------------------------------------------------------- +// $code is the error code +// $message is the error string +// ----------------------------------------------------------------------- + +function rpc_error($code, $message) { + + return array('faultCode' => $code, 'faultString' => $message); +} + + +// ----------------------------------------------------------------------- +// Build a data array from a text xml-rpc +// (xml-rpc string ==> array) +// ----------------------------------------------------------------------- +// $xml is the xml-rpc text to decode +// If there is an error then null is returned, you can then look infos +// in global $_xmlrpc_decode_obj +// ----------------------------------------------------------------------- + +function xml_decode_rpc($xml) { + global $_xmlrpc_decode_obj; + + $_xmlrpc_decode_obj = new IXR_Message($xml); + if (!$_xmlrpc_decode_obj->parse() || !isset($_xmlrpc_decode_obj->params[0])) + return null; + return $_xmlrpc_decode_obj->params[0]; +} + + +// ----------------------------------------------------------------------- +// use this class constructor to build a methodCall request +// ----------------------------------------------------------------------- + +class IXR_RequestStd { + var $method; + var $params; + var $xml; + + // see xmlrpc_request + function IXR_RequestStd($method, $params = null) { + + $this->method = $method; + // in case the first level array was forgotten in a multicall then add it... + if ($method == 'system.multicall' && isset($params[0]['methodName'])) + $this->params = array($params); + else + $this->params = $params; + + $this->xml = xmlrpc_request($this->method, $this->params); + } + + function getLength() { + + return strlen($this->xml); + } + + function getXml() { + + return $this->xml; + } +} + + +// ----------------------------------------------------------------------- +// use this class constructor to build a methodCall response +// ----------------------------------------------------------------------- +class IXR_ResponseStd { + var $args; + var $xml; + + // see xmlrpc_response + function IXR_ResponseStd($args) { + + $this->args = $args; + $this->xml = xmlrpc_response($this->args); + } + + function getLength() { + + return strlen($this->xml); + } + + function getXml() { + + return $this->xml; + } +} +?> diff --git a/docker-xaseco/xaseco/includes/basic.inc.php b/docker-xaseco/xaseco/includes/basic.inc.php new file mode 100644 index 0000000..2321002 --- /dev/null +++ b/docker-xaseco/xaseco/includes/basic.inc.php @@ -0,0 +1,830 @@ += $size) { + return false; + } + + // shift values down + for ($i = $size-1; $i >= $pos; $i--) { + $array[$i+1] = $array[$i]; + } + + // now put in the new element + $array[$pos] = $value; + return true; +} // insertArrayElement + +/** + * Removes an element from a specific position in an array. + * Decreases original size by one element. + */ +function removeArrayElement(&$array, $pos) { + + // get current size + $size = count($array); + + // if position is in array range + if ($pos < 0 && $pos >= $size) { + return false; + } + + // remove specified element + unset($array[$pos]); + // shift values up + $array = array_values($array); + return true; +} // removeArrayElement + +/** + * Moves an element from one position to the other. + * All items between are shifted down or up as needed. + */ +function moveArrayElement(&$array, $from, $to) { + + // get current size + $size = count($array); + + // destination and source have to be among the array borders! + if ($from < 0 || $from >= $size || $to < 0 || $to >= $size) { + return false; + } + + // backup the element we have to move + $moving_element = $array[$from]; + + if ($from > $to) { + // shift values between downwards + for ($i = $from-1; $i >= $to; $i--) { + $array[$i+1] = $array[$i]; + } + } else { // $from < $to + // shift values between upwards + for ($i = $from; $i <= $to; $i++) { + $array[$i] = $array[$i+1]; + } + } + + // now put in the element which was to move + $array[$to] = $moving_element; + return true; +} // moveArrayElement + +/** + * Formats a string from the format sssshh0 + * into the format mmm:ss.hh (or mmm:ss if $hsec is false) + */ +function formatTime($MwTime, $hsec = true) { + + if ($MwTime == -1) { + return '???'; + } else { + $minutes = floor($MwTime/(1000*60)); + $seconds = floor(($MwTime - $minutes*60*1000)/1000); + $hseconds = substr($MwTime, strlen($MwTime)-3, 2); + if ($hsec) { + $tm = sprintf('%02d:%02d.%02d', $minutes, $seconds, $hseconds); + } else { + $tm = sprintf('%02d:%02d', $minutes, $seconds); + } + } + if ($tm[0] == '0') { + $tm = substr($tm, 1); + } + return $tm; +} // formatTime + +/** + * Formats a string from the format sssshh0 + * into the format hh:mm:ss.hh (or hh:mm:ss if $hsec is false) + */ +function formatTimeH($MwTime, $hsec = true) { + + if ($MwTime == -1) { + return '???'; + } else { + $hseconds = substr($MwTime, strlen($MwTime)-3, 2); + $MwTime = substr($MwTime, 0, strlen($MwTime)-3); + $hours = floor($MwTime / 3600); + $MwTime = $MwTime - ($hours * 3600); + $minutes = floor($MwTime / 60); + $MwTime = $MwTime - ($minutes * 60); + $seconds = floor($MwTime); + if ($hsec) { + return sprintf('%02d:%02d:%02d.%02d', $hours, $minutes, $seconds, $hseconds); + } else { + return sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds); + } + } +} // formatTimeH + +/** + * Formats a text. + * Replaces parameters in the text which are marked with {n} + */ +function formatText($text) { + + // get all function's parameters + $args = func_get_args(); + + // first parameter is the text to format + $text = array_shift($args); + + // further parameters will be replaced in the text + $i = 1; + foreach ($args as $param) + $text = str_replace('{' . $i++ . '}', $param, $text); + + // and return the modified text + return $text; +} // formatText + +/** + * Make String for SQL use that single quoted & got special chars replaced. + */ +function quotedString($input) { + + return "'" . mysql_real_escape_string($input) . "'"; +} // quotedString + +/** + * Check login string for LAN postfix (pre/post v2.11.21). + */ +function isLANLogin($login) { + + $n="(25[0-5]|2[0-4]\d|[01]?\d\d|\d)"; + return (preg_match("/(\/{$n}\\.{$n}\\.{$n}\\.{$n}:\d+)$/", $login) || + preg_match("/(_{$n}\\.{$n}\\.{$n}\\.{$n}_\d+)$/", $login)); +} // isLANLogin + +/** + * Summary: Strips all display formatting from an input string, suitable for display + * within the game ('$$' escape pairs are preserved) and for logging + * Params : $input - The input string to strip formatting from + * $for_tm - Optional flag to double up '$' into '$$' (default, for TM) or not (for logs, etc) + * Returns: The content portions of $input without formatting + * Authors: Bilge/Assembler Maniac/Xymph/Slig + * + * "$af0Brat$s$fffwurst" will become "Bratwurst". + * 2007-08-27 Xymph - replaced with Bilge/AM's code (w/o the H&L tags bit) + * http://www.tm-forum.com/viewtopic.php?p=55867#p55867 + * 2008-04-24 Xymph - extended to handle the H/L/P tags for TMF + * http://www.tm-forum.com/viewtopic.php?p=112856#p112856 + * 2009-05-16 Slig - extended to emit non-TM variant & handle incomplete colors + * http://www.tm-forum.com/viewtopic.php?p=153368#p153368 + * 2010-10-05 Slig - updated to handle incomplete colors & tags better + * http://www.tm-forum.com/viewtopic.php?p=183410#p183410 + * 2010-10-09 Xymph - updated to handle $[ and $] properly + * http://www.tm-forum.com/viewtopic.php?p=183410#p183410 + */ +function stripColors($input, $for_tm = true) { + + return + //Replace all occurrences of a null character back with a pair of dollar + //signs for displaying in TM, or a single dollar for log messages etc. + str_replace("\0", ($for_tm ? '$$' : '$'), + //Replace links (introduced in TMU) + preg_replace( + '/ + #Strip TMF H, L & P links by stripping everything between each square + #bracket pair until another $H, $L or $P sequence (or EoS) is found; + #this allows a $H to close a $L and vice versa, as does the game + \\$[hlp](.*?)(?:\\[.*?\\](.*?))*(?:\\$[hlp]|$) + /ixu', + //Keep the first and third capturing groups if present + '$1$2', + //Replace various patterns beginning with an unescaped dollar + preg_replace( + '/ + #Match a single dollar sign and any of the following: + \\$ + (?: + #Strip color codes by matching any hexadecimal character and + #any other two characters following it (except $) + [0-9a-f][^$][^$] + #Strip any incomplete color codes by matching any hexadecimal + #character followed by another character (except $) + |[0-9a-f][^$] + #Strip any single style code (including an invisible UTF8 char) + #that is not an H, L or P link or a bracket ($[ and $]) + |[^][hlp] + #Strip the dollar sign if it is followed by [ or ], but do not + #strip the brackets themselves + |(?=[][]) + #Strip the dollar sign if it is at the end of the string + |$ + ) + #Ignore alphabet case, ignore whitespace in pattern & use UTF-8 mode + /ixu', + //Replace any matches with nothing (i.e. strip matches) + '', + //Replace all occurrences of dollar sign pairs with a null character + str_replace('$$', "\0", $input) + ) + ) + ) + ; +} // stripColors + +/** + * Strips only size tags from TM strings. + * "$w$af0Brat$n$fffwurst" will become "$af0Brat$fffwurst". + * 2009-03-27 Xymph - derived from stripColors above + * http://www.tm-forum.com/viewtopic.php?f=127&t=20602 + * 2009-05-16 Slig - extended to emit non-TM variant + * http://www.tm-forum.com/viewtopic.php?p=153368#p153368 + */ +function stripSizes($input, $for_tm = true) { + + return + //Replace all occurrences of a null character back with a pair of dollar + //signs for displaying in TM, or a single dollar for log messages etc. + str_replace("\0", ($for_tm ? '$$' : '$'), + //Replace various patterns beginning with an unescaped dollar + preg_replace( + '/ + #Match a single dollar sign and any of the following: + \\$ + (?: + #Strip any size code + [nwo] + #Strip the dollar sign if it is at the end of the string + |$ + ) + #Ignore alphabet case, ignore whitespace in pattern & use UTF-8 mode + /ixu', + //Replace any matches with nothing (i.e. strip matches) + '', + //Replace all occurrences of dollar sign pairs with a null character + str_replace('$$', "\0", $input) + ) + ) + ; +} // stripSizes + +/** + * Strips only newlines from TM strings. + */ +function stripNewlines($input) { + + return str_replace(array("\n\n", "\r", "\n"), + array(' ', '', ''), $input); +} // stripNewlines + + +/** + * Univeral show help for user, admin & Jfreu commands. + * Created by Xymph + * + * $width is the width of the first column in the ManiaLink window on TMF + */ +function showHelp($player, $chat_commands, $head, + $showadmin = false, $dispall = false, $width = 0.3) { + global $aseco; + + // display full help for TMN + if ($aseco->server->getGame() == 'TMN' && $dispall) { + $head = "Currently supported $head commands:" . LF; + + if (!empty($chat_commands)) { + // define admin or non-admin padding string + $pad = ($showadmin ? '$f00... ' : '$f00/'); + $help = ''; + $lines = 0; + $player->msgs = array(); + $player->msgs[0] = 1; + // create list of chat commands + foreach ($chat_commands as $cc) { + // collect either admin or non-admin commands + if ($cc->isadmin == $showadmin) { + $help .= $pad . $cc->name . ' $000' . $cc->help . LF; + if (++$lines > 14) { + $player->msgs[] = $head . $help; + $lines = 0; + $help = ''; + } + } + } + // add if last batch exists + if ($help != '') + $player->msgs[] = $head . $help; + + // display popup message + if (count($player->msgs) == 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $player->login, $player->msgs[1], 'OK', '', 0); + } else { // > 2 + $aseco->client->query('SendDisplayServerMessageToLogin', $player->login, $player->msgs[1], 'Close', 'Next', 0); + } + } + + // display full help for TMF + } elseif ($aseco->server->getGame() == 'TMF' && $dispall) { + $head = "Currently supported $head commands:"; + + if (!empty($chat_commands)) { + // define admin or non-admin padding string + $pad = ($showadmin ? '$f00... ' : '$f00/'); + $help = array(); + $lines = 0; + $player->msgs = array(); + $player->msgs[0] = array(1, $head, array(1.3, $width, 1.3 - $width), array('Icons64x64_1', 'TrackInfo', -0.01)); + // create list of chat commands + foreach ($chat_commands as $cc) { + // collect either admin or non-admin commands + if ($cc->isadmin == $showadmin) { + $help[] = array($pad . $cc->name, $cc->help); + if (++$lines > 14) { + $player->msgs[] = $help; + $lines = 0; + $help = array(); + } + } + } + // add if last batch exists + if (!empty($help)) + $player->msgs[] = $help; + + // display ManiaLink message + display_manialink_multi($player); + } + + // show help for TMS or TMO, and plain help for TMF/TMN + } else { + $head = "Currently supported $head commands:" . LF; + $help = $aseco->formatColors('{#interact}' . $head); + foreach ($chat_commands as $cc) { + // collect either admin or non-admin commands + if ($cc->isadmin == $showadmin) { + $help .= $cc->name . ', '; + } + } + // show chat message + $help = substr($help, 0, strlen($help) - 2); // strip trailing ", " + $aseco->client->query('ChatSendToLogin', $help, $player->login); + } +} // showHelp + +/** + * Map country names to 3-letter Nation abbreviations + * Created by Xymph + * Based on http://en.wikipedia.org/wiki/List_of_IOC_country_codes + * See also http://en.wikipedia.org/wiki/Comparison_of_IOC,_FIFA,_and_ISO_3166_country_codes + */ +function mapCountry($country) { + + $nations = array( + 'Afghanistan' => 'AFG', + 'Albania' => 'ALB', + 'Algeria' => 'ALG', + 'Andorra' => 'AND', + 'Angola' => 'ANG', + 'Argentina' => 'ARG', + 'Armenia' => 'ARM', + 'Aruba' => 'ARU', + 'Australia' => 'AUS', + 'Austria' => 'AUT', + 'Azerbaijan' => 'AZE', + 'Bahamas' => 'BAH', + 'Bahrain' => 'BRN', + 'Bangladesh' => 'BAN', + 'Barbados' => 'BAR', + 'Belarus' => 'BLR', + 'Belgium' => 'BEL', + 'Belize' => 'BIZ', + 'Benin' => 'BEN', + 'Bermuda' => 'BER', + 'Bhutan' => 'BHU', + 'Bolivia' => 'BOL', + 'Bosnia&Herzegovina' => 'BIH', + 'Botswana' => 'BOT', + 'Brazil' => 'BRA', + 'Brunei' => 'BRU', + 'Bulgaria' => 'BUL', + 'Burkina Faso' => 'BUR', + 'Burundi' => 'BDI', + 'Cambodia' => 'CAM', + 'Cameroon' => 'CAR', // actually CMR + 'Canada' => 'CAN', + 'Cape Verde' => 'CPV', + 'Central African Republic' => 'CAF', + 'Chad' => 'CHA', + 'Chile' => 'CHI', + 'China' => 'CHN', + 'Chinese Taipei' => 'TPE', + 'Colombia' => 'COL', + 'Congo' => 'CGO', + 'Costa Rica' => 'CRC', + 'Croatia' => 'CRO', + 'Cuba' => 'CUB', + 'Cyprus' => 'CYP', + 'Czech Republic' => 'CZE', + 'Czech republic' => 'CZE', + 'DR Congo' => 'COD', + 'Denmark' => 'DEN', + 'Djibouti' => 'DJI', + 'Dominica' => 'DMA', + 'Dominican Republic' => 'DOM', + 'Ecuador' => 'ECU', + 'Egypt' => 'EGY', + 'El Salvador' => 'ESA', + 'Eritrea' => 'ERI', + 'Estonia' => 'EST', + 'Ethiopia' => 'ETH', + 'Fiji' => 'FIJ', + 'Finland' => 'FIN', + 'France' => 'FRA', + 'Gabon' => 'GAB', + 'Gambia' => 'GAM', + 'Georgia' => 'GEO', + 'Germany' => 'GER', + 'Ghana' => 'GHA', + 'Greece' => 'GRE', + 'Grenada' => 'GRN', + 'Guam' => 'GUM', + 'Guatemala' => 'GUA', + 'Guinea' => 'GUI', + 'Guinea-Bissau' => 'GBS', + 'Guyana' => 'GUY', + 'Haiti' => 'HAI', + 'Honduras' => 'HON', + 'Hong Kong' => 'HKG', + 'Hungary' => 'HUN', + 'Iceland' => 'ISL', + 'India' => 'IND', + 'Indonesia' => 'INA', + 'Iran' => 'IRI', + 'Iraq' => 'IRQ', + 'Ireland' => 'IRL', + 'Israel' => 'ISR', + 'Italy' => 'ITA', + 'Ivory Coast' => 'CIV', + 'Jamaica' => 'JAM', + 'Japan' => 'JPN', + 'Jordan' => 'JOR', + 'Kazakhstan' => 'KAZ', + 'Kenya' => 'KEN', + 'Kiribati' => 'KIR', + 'Korea' => 'KOR', + 'Kuwait' => 'KUW', + 'Kyrgyzstan' => 'KGZ', + 'Laos' => 'LAO', + 'Latvia' => 'LAT', + 'Lebanon' => 'LIB', + 'Lesotho' => 'LES', + 'Liberia' => 'LBR', + 'Libya' => 'LBA', + 'Liechtenstein' => 'LIE', + 'Lithuania' => 'LTU', + 'Luxembourg' => 'LUX', + 'Macedonia' => 'MKD', + 'Malawi' => 'MAW', + 'Malaysia' => 'MAS', + 'Mali' => 'MLI', + 'Malta' => 'MLT', + 'Mauritania' => 'MTN', + 'Mauritius' => 'MRI', + 'Mexico' => 'MEX', + 'Moldova' => 'MDA', + 'Monaco' => 'MON', + 'Mongolia' => 'MGL', + 'Montenegro' => 'MNE', + 'Morocco' => 'MAR', + 'Mozambique' => 'MOZ', + 'Myanmar' => 'MYA', + 'Namibia' => 'NAM', + 'Nauru' => 'NRU', + 'Nepal' => 'NEP', + 'Netherlands' => 'NED', + 'New Zealand' => 'NZL', + 'Nicaragua' => 'NCA', + 'Niger' => 'NIG', + 'Nigeria' => 'NGR', + 'Norway' => 'NOR', + 'Oman' => 'OMA', + 'Other Countries' => 'OTH', + 'Pakistan' => 'PAK', + 'Palau' => 'PLW', + 'Palestine' => 'PLE', + 'Panama' => 'PAN', + 'Paraguay' => 'PAR', + 'Peru' => 'PER', + 'Philippines' => 'PHI', + 'Poland' => 'POL', + 'Portugal' => 'POR', + 'Puerto Rico' => 'PUR', + 'Qatar' => 'QAT', + 'Romania' => 'ROM', // actually ROU + 'Russia' => 'RUS', + 'Rwanda' => 'RWA', + 'Samoa' => 'SAM', + 'San Marino' => 'SMR', + 'Saudi Arabia' => 'KSA', + 'Senegal' => 'SEN', + 'Serbia' => 'SCG', // actually SRB + 'Sierra Leone' => 'SLE', + 'Singapore' => 'SIN', + 'Slovakia' => 'SVK', + 'Slovenia' => 'SLO', + 'Somalia' => 'SOM', + 'South Africa' => 'RSA', + 'Spain' => 'ESP', + 'Sri Lanka' => 'SRI', + 'Sudan' => 'SUD', + 'Suriname' => 'SUR', + 'Swaziland' => 'SWZ', + 'Sweden' => 'SWE', + 'Switzerland' => 'SUI', + 'Syria' => 'SYR', + 'Taiwan' => 'TWN', + 'Tajikistan' => 'TJK', + 'Tanzania' => 'TAN', + 'Thailand' => 'THA', + 'Togo' => 'TOG', + 'Tonga' => 'TGA', + 'Trinidad and Tobago' => 'TRI', + 'Tunisia' => 'TUN', + 'Turkey' => 'TUR', + 'Turkmenistan' => 'TKM', + 'Tuvalu' => 'TUV', + 'Uganda' => 'UGA', + 'Ukraine' => 'UKR', + 'United Arab Emirates' => 'UAE', + 'United Kingdom' => 'GBR', + 'United States of America' => 'USA', + 'Uruguay' => 'URU', + 'Uzbekistan' => 'UZB', + 'Vanuatu' => 'VAN', + 'Venezuela' => 'VEN', + 'Vietnam' => 'VIE', + 'Yemen' => 'YEM', + 'Zambia' => 'ZAM', + 'Zimbabwe' => 'ZIM', + ); + + if (array_key_exists($country, $nations)) { + $nation = $nations[$country]; + } else { + $nation = 'OTH'; + if ($country != '') + trigger_error('Could not map country: ' . $country, E_USER_WARNING); + } + return $nation; +} // mapCountry + +/** + * Find TMX data for the given track + * Created by Xymph + */ +require_once('includes/tmxinfofetcher.inc.php'); // provides access to TMX info +function findTMXdata($uid, $envir, $exever, $records = false) { + + // determine likely search order + if ($envir == 'Stadium') { + // check for old TMN + if (strcmp($exever, '0.1.8.0') < 0) + $sections = array('TMN', 'TMNF', 'TMU'); + // check for new TMF + elseif (strcmp($exever, '2.11.0') >= 0) + $sections = array('TMNF', 'TMU'); + else + $sections = array('TMU'); // TMNF section opened after TMF beta + } elseif ($envir == 'Bay' || $envir == 'Coast' || $envir == 'Island') { + // check for old TMS + if (strcmp($exever, '0.1.5.0') <= 0) + $sections = array('TMS', 'TMU'); + else + $sections = array('TMU'); // TMS section closed after TMU release + } else { // $envir == 'Alpine' || 'Snow' || 'Desert' || 'Speed' || 'Rally' + // check for old TMO + if (strcmp($exever, '0.1.5.0') <= 0) + $sections = array('TMO', 'TMU'); + else + $sections = array('TMU'); // TMO section closed after TMU release + } + + // search TMX for track + foreach ($sections as $section) { + $tmxdata = new TMXInfoFetcher($section, $uid, $records); + if ($tmxdata->name) { + return $tmxdata; + } + } + return false; +} // findTMXdata + +/** + * Simple HTTP Get function with timeout + * ok: return string || error: return false || timeout: return -1 + * if $openonly == true, don't read data but return true upon connect + */ +function http_get_file($url, $openonly = false) { + global $aseco; + + $url = parse_url($url); + $port = isset($url['port']) ? $url['port'] : 80; + $query = isset($url['query']) ? '?' . $url['query'] : ''; + + $fp = @fsockopen($url['host'], $port, $errno, $errstr, 4); + if (!$fp) + return false; + if ($openonly) { + fclose($fp); + return true; + } + + $uri = ''; + foreach (explode('/', $url['path']) as $subpath) + $uri .= rawurlencode($subpath) . '/'; + $uri = substr($uri, 0, strlen($uri)-1); // strip trailing '/' + + fwrite($fp, 'GET ' . $uri . $query . " HTTP/1.0\r\n" . + 'Host: ' . $url['host'] . "\r\n" . + 'User-Agent: XASECO-' . XASECO_VERSION . ' (' . PHP_OS . '; ' . + $aseco->server->game . ")\r\n\r\n"); + stream_set_timeout($fp, 2); + $res = ''; + $info['timed_out'] = false; + while (!feof($fp) && !$info['timed_out']) { + $res .= fread($fp, 512); + $info = stream_get_meta_data($fp); + } + fclose($fp); + + if ($info['timed_out']) { + return -1; + } else { + if (substr($res, 9, 3) != '200') + return false; + $page = explode("\r\n\r\n", $res, 2); + return $page[1]; + } +} // http_get_file + +/** + * Return valid UTF-8 string, replacing faulty byte values with a given string + * Created by (OoR-F)~fuckfish (fish@stabb.de) + * http://www.tm-forum.com/viewtopic.php?p=117639#p117639 + * Based on the original tm_substr function by Slig (slig@free.fr) + * Updated by Xymph; More info: http://en.wikipedia.org/wiki/UTF-8 + */ +function validateUTF8String($input, $invalidRepl = '') { + + $str = (string) $input; + $len = strlen($str); // byte string length + $pos = 0; // current byte pos in string + $new = ''; + + while ($pos < $len) { + $co = ord($str[$pos]); + + // 4-6 bytes UTF8 => unsupported + if ($co >= 240) { + // bad multibyte char + $new .= $invalidRepl; + $pos++; + + // 3 bytes UTF8 => 1110bbbb 10bbbbbb 10bbbbbb + } elseif ($co >= 224) { + if (($pos+2 < $len) && + (ord($str[$pos+1]) >= 128 && ord($str[$pos+1]) < 192) && + (ord($str[$pos+2]) >= 128 && ord($str[$pos+2]) < 192)) { + // ok, it was 1 character, increase counters + $new .= substr($str, $pos, 3); + $pos += 3; + } else { + // bad multibyte char + $new .= $invalidRepl; + $pos++; + } + + // 2 bytes UTF8 => 110bbbbb 10bbbbbb + } elseif ($co >= 194) { + if (($pos+1 < $len) && + (ord($str[$pos+1]) >= 128 && ord($str[$pos+1]) < 192)) { + // ok, it was 1 character, increase counters + $new .= substr($str, $pos, 2); + $pos += 2; + } else { + // bad multibyte char + $new .= $invalidRepl; + $pos++; + } + + // 2 bytes overlong encoding => unsupported + } elseif ($co >= 192) { + // bad multibyte char 1100000b + $new .= $invalidRepl; + $pos++; + + // 1 byte ASCII => 0bbbbbbb, or invalid => 10bbbbbb or 11111bbb + } else { // $co < 192 + // erroneous middle multibyte char? + if ($co >= 128 || $co == 0) + $new .= $invalidRepl; + else + $new .= $str[$pos]; + + $pos++; + } + } + return $new; +} // validateUTF8String + +/** + * Convert php.ini memory shorthand string to integer bytes + * http://www.php.net/manual/en/function.ini-get.php#96996 + */ +function shorthand2bytes($size_str) { + + switch (substr($size_str, -1)) { + case 'M': case 'm': return (int)$size_str * 1048576; + case 'K': case 'k': return (int)$size_str * 1024; + case 'G': case 'g': return (int)$size_str * 1073741824; + default: return (int)$size_str; + } +} // return_bytes + +/** + * Convert boolean value to text string + */ +function bool2text($boolval) { + + if ($boolval) + return 'True'; + else + return 'False'; +} // bool2text +?> diff --git a/docker-xaseco/xaseco/includes/gbxchallinfo.inc.php b/docker-xaseco/xaseco/includes/gbxchallinfo.inc.php new file mode 100644 index 0000000..8565002 --- /dev/null +++ b/docker-xaseco/xaseco/includes/gbxchallinfo.inc.php @@ -0,0 +1,131 @@ + + * + * v1.2: Allow getting info from tracks already on the server w/o removing them + * v1.1: Fix PHP notices about redefinition of constants + * v1.0: Initial release + */ + +class GBXChallengeInfo { + + public $name, $uid, $filename, $author, $envir, $mood, + $bronzetm, $silvertm, $goldtm, $authortm, + $coppers, $laprace, $nblaps, $nbcps, $nbrcps, $error; + + /** + * Fetches current ChallengeInfo for a GBX challenge + * Loads track into server, selects it, gets info, and removes it from server + * + * @param String $filename + * The challenge filename (must be a path below .../GameData/Tracks/) + * @return GBXChallengeInfo + * If $uid is empty, GBX data couldn't be extracted and $error contains + * an error message + */ + public function GBXChallengeInfo($filename) { + + $ip = 'localhost'; + $port = 5000; + $user = 'SuperAdmin'; + $pass = 'YOUR_SUPERADMIN_PASSWORD'; + + $this->uid = ''; + $this->error = ''; + $client = new IXR_Client_Gbx; + + // connect to the server + if (!$client->InitWithIp($ip, $port)) { + $this->error = 'Connection failed - Error ' . $client->getErrorCode() . ': ' . $client->getErrorMessage(); + return false; + } + + // log into the server + if (!$client->query('Authenticate', $user, $pass)) { + $this->error = 'Login failed - Error ' . $client->getErrorCode() . ': ' . $client->getErrorMessage(); + + } else { + // add the challenge + $ret = $client->query('AddChallenge', $filename); + $already = ($client->getErrorMessage() == 'Challenge already added.'); + if (!$ret && !$already) { + $this->error = 'AddChallenge failed - Error ' . $client->getErrorCode() . ': ' . $client->getErrorMessage(); + } else { + + // select the challenge + if (!$client->query('ChooseNextChallenge', $filename)) { + $this->error = 'ChooseNextChallenge failed - Error ' . $client->getErrorCode() . ': ' . $client->getErrorMessage(); + + // switch to our challenge + } elseif (!$client->query('NextChallenge')) { + $this->error = 'NextChallenge 1 failed - Error ' . $client->getErrorCode() . ': ' . $client->getErrorMessage(); + } else { + + // allow for challenge switch but time out after 5 seconds + $retry = 5; + while (true) { + sleep(1); + + // obtain challenge details + if ($client->query('GetCurrentChallengeInfo')) { + $info = $client->getResponse(); + // check for our challenge + if ($info['FileName'] == $filename) { + break; + } + } else { + $this->error = 'GetCurrentChallengeInfo failed - Error ' . $client->getErrorCode() . ': ' . $client->getErrorMessage(); + break; + } + if ($retry-- == 0) { + $this->error = 'GetCurrentChallengeInfo timed out after 5 seconds'; + break; + } + } + + // extract our challenge details + if ($this->error == '') { + $this->name = $info['Name']; + $this->uid = $info['UId']; + $this->filename = $info['FileName']; + $this->author = $info['Author']; + $this->envir = $info['Environnement']; + $this->mood = $info['Mood']; + $this->bronzetm = $info['BronzeTime']; + $this->silvertm = $info['SilverTime']; + $this->goldtm = $info['GoldTime']; + $this->authortm = $info['AuthorTime']; + $this->coppers = $info['CopperPrice']; + $this->laprace = $info['LapRace']; + $this->nblaps = $info['NbLaps']; + $this->nbcps = $info['NbCheckpoints']; + $this->nbrcps = $info['NbCheckpoints']; + + if ($this->laprace && $this->nblaps > 1) + $this->nbrcps *= $this->nblaps; + } + } + } + + // check if challenge wasn't already there + if (!$already) { + // remove the challenge + if (!$client->query('RemoveChallenge', $filename)) { + $this->error = 'RemoveChallenge failed - Error ' . $client->getErrorCode() . ': ' . $client->getErrorMessage(); + } + // switch away from our challenge + sleep(5); + if (!$client->query('NextChallenge')) { + $this->error = 'NextChallenge 2 failed - Error ' . $client->getErrorCode() . ': ' . $client->getErrorMessage(); + } + } + } + + $client->Terminate(); + } +} +?> diff --git a/docker-xaseco/xaseco/includes/gbxdatafetcher.inc.php b/docker-xaseco/xaseco/includes/gbxdatafetcher.inc.php new file mode 100644 index 0000000..9d6c0dd --- /dev/null +++ b/docker-xaseco/xaseco/includes/gbxdatafetcher.inc.php @@ -0,0 +1,1431 @@ + + * Thanks to Electron for additional input & prototyping + * Based on information at http://en.tm-wiki.org/wiki/GBX, + * http://www.tm-forum.com/viewtopic.php?p=192817#p192817 + * and http://en.tm-wiki.org/wiki/PAK#Header_versions_8.2B + * + * v2.5: Add lookback string Valley; strip optional digits from mood; fix empty + * thumbnail warning + * v2.4: Update GBXChallMapFetcher & GBXPackFetcher version-dependent processing; + * update lookback strings + * v2.3: Add UTF-8 decoding of parsed XML chunk's elements; strip UTF-8 BOM + * from various string fields + * v2.2: Add class GBXPackFetcher; limit loadGBXdata() to first 256KB + * v2.1: Add exception codes; add $thumbLen to GBXChallMapFetcher + * v2.0: Complete rewrite + */ + +/** + * @class GBXBaseFetcher + * @brief The base GBX class with all common functionality + */ +class GBXBaseFetcher +{ + public $parseXml, $xml, $xmlParsed; + + public $authorVer, $authorLogin, $authorNick, $authorZone, $authorEInfo; + + private $_gbxdata, $_gbxlen, $_gbxptr, $_debug, $_error, $_endianess, + $_lookbacks, $_parsestack; + + // supported class ID's + const GBX_CHALLENGE_TMF = 0x03043000; + const GBX_AUTOSAVE_TMF = 0x03093000; + const GBX_CHALLENGE_TM = 0x24003000; + const GBX_AUTOSAVE_TM = 0x2403F000; + const GBX_REPLAY_TM = 0x2407E000; + + const MACHINE_ENDIAN_ORDER = 0; + const LITTLE_ENDIAN_ORDER = 1; + const BIG_ENDIAN_ORDER = 2; + + const LOAD_LIMIT = 256; // KBs to read in loadGBXdata() + + // initialise new instance + public function __construct() + { + $this->parseXml = false; + $this->xml = ''; + $this->xmlParsed = array(); + $this->_debug = false; + $this->_error = ''; + + list($endiantest) = array_values(unpack('L1L', pack('V', 1))); + if ($endiantest != 1) + $this->_endianess = self::BIG_ENDIAN_ORDER; + else + $this->_endianess = self::LITTLE_ENDIAN_ORDER; + + $this->clearGBXdata(); + $this->clearLookbacks(); + $this->_parsestack = array(); + + $this->authorVer = 0; + $this->authorLogin = ''; + $this->authorNick = ''; + $this->authorZone = ''; + $this->authorEInfo = ''; + } + + // enable debug logging + protected function enableDebug() + { + $this->_debug = true; + } + + // disable debug logging + protected function disableDebug() + { + $this->_debug = false; + } + + // print message to stderr if debugging + protected function debugLog($msg) + { + if ($this->_debug) + fwrite(STDERR, $msg."\n"); + } + + // set error message prefix + protected function setError($prefix) + { + $this->_error = (string)$prefix; + } + + // exit with error exception + protected function errorOut($msg, $code = 0) + { + $this->clearGBXdata(); + throw new Exception($this->_error . $msg, $code); + } + + // read in raw GBX data + protected function loadGBXdata($filename) + { + $gbxdata = @file_get_contents($filename, false, null, 0, self::LOAD_LIMIT * 1024); + if ($gbxdata !== false) + $this->storeGBXdata($gbxdata); + else + $this->errorOut('Unable to read GBX data from ' . $filename, 1); + } + + // store raw GBX data + protected function storeGBXdata($gbxdata) + { + $this->_gbxdata = & $gbxdata; + $this->_gbxlen = strlen($gbxdata); + $this->_gbxptr = 0; + } + + // clear GBX data (to avoid print_r problems & reduce memory usage) + protected function clearGBXdata() + { + $this->storeGBXdata(''); + } + + // get GBX pointer + protected function getGBXptr() + { + return $this->_gbxptr; + } + + // set GBX pointer + protected function setGBXptr($ptr) + { + $this->_gbxptr = (int)$ptr; + } + + // move GBX pointer + protected function moveGBXptr($len) + { + $this->_gbxptr += (int)$len; + } + + // read $len bytes from GBX data + protected function readData($len) + { + if ($this->_gbxptr + $len > $this->_gbxlen) + $this->errorOut(sprintf('Insufficient data for %d bytes at pos 0x%04X', + $len, $this->_gbxptr), 2); + $data = ''; + while ($len-- > 0) + $data .= $this->_gbxdata[$this->_gbxptr++]; + return $data; + } + + // read signed byte from GBX data + protected function readInt8() + { + $data = $this->readData(1); + list(, $int8) = unpack('c*', $data); + return $int8; + } + + // read signed short from GBX data + protected function readInt16() + { + $data = $this->readData(2); + if ($this->_endianess == self::BIG_ENDIAN_ORDER) + $data = strrev($data); + list(, $int16) = unpack('s*', $data); + return $int16; + } + + // read signed long from GBX data + protected function readInt32() + { + $data = $this->readData(4); + if ($this->_endianess == self::BIG_ENDIAN_ORDER) + $data = strrev($data); + list(, $int32) = unpack('l*', $data); + return $int32; + } + + // read string from GBX data + protected function readString() + { + $gbxptr = $this->getGBXptr(); + $len = $this->readInt32(); + $len &= 0x7FFFFFFF; + if ($len <= 0 || $len >= 0x12000) { // for large XML & Data blocks + if ($len != 0) + $this->errorOut(sprintf('Invalid string length %d (0x%04X) at pos 0x%04X', + $len, $len, $gbxptr), 3); + } + $data = $this->readData($len); + return $data; + } + + // strip UTF-8 BOM from string + protected function stripBOM($str) + { + return str_replace("\xEF\xBB\xBF", '', $str); + } + + // clear lookback strings + protected function clearLookbacks() + { + $this->_lookbacks = array(); + } + + // read lookback string from GBX data + protected function readLookbackString() + { + if (empty($this->_lookbacks)) { + $version = $this->readInt32(); + if ($version != 3) + $this->errorOut('Unknown lookback strings version: ' . $version, 4); + } + + // check index + $index = $this->readInt32(); + if ($index == -1) { + // unassigned (empty) string + $str = ''; + } elseif (($index & 0xC0000000) == 0) { + // use external reference string + switch ($index) { + case 11: $str = 'Valley'; + break; + case 12: $str = 'Canyon'; + break; + case 17: $str = 'TMCommon'; + break; + case 202: $str = 'Storm'; + break; + case 299: $str = 'SMCommon'; + break; + case 10003: $str = 'Common'; + break; + default: $str = 'UNKNOWN'; + } + } elseif (($index & 0x3FFFFFFF) == 0) { + // read string & add to lookbacks + $str = $this->readString(); + $this->_lookbacks[] = $str; + } else { + // use string from lookbacks + $str = $this->_lookbacks[($index & 0x3FFFFFFF) - 1]; + } + + return $str; + } + + // XML parser functions + private function startTag($parser, $name, $attribs) + { + foreach ($attribs as $key => &$val) + $val = utf8_decode($val); + //echo 'startTag: ' . $name . "\n"; print_r($attribs); + array_push($this->_parsestack, $name); + if ($name == 'DEP') { + $this->xmlParsed['DEPS'][] = $attribs; + } elseif (count($this->_parsestack) <= 2) { + // HEADER, IDENT, DESC, TIMES, CHALLENGE/MAP, DEPS, CHECKPOINTS + $this->xmlParsed[$name] = $attribs; + } + } + + private function charData($parser, $data) + { + //echo 'charData: ' . $data . "\n"; + if (count($this->_parsestack) == 3) + $this->xmlParsed[$this->_parsestack[1]][$this->_parsestack[2]] = $data; + elseif (count($this->_parsestack) > 3) + $this->debugLog('XML chunk nested too deeply: ', print_r($this->_parsestack, true)); + } + + private function endTag($parser, $name) + { + //echo 'endTag: ' . $name . "\n"; + array_pop($this->_parsestack); + } + + protected function parseXMLstring() + { + // define a dedicated parser to handle the attributes + $xml_parser = xml_parser_create(); + xml_set_object($xml_parser, $this); + xml_set_element_handler($xml_parser, 'startTag', 'endTag'); + xml_set_character_data_handler($xml_parser, 'charData'); + + // escape '&' characters unless already a known entity + $xml = preg_replace('/&(?!(?:amp|quot|apos|lt|gt);)/', '&', $this->xml); + + if (!xml_parse($xml_parser, utf8_encode($xml), true)) + $this->errorOut(sprintf('XML chunk parse error: %s at line %d', + xml_error_string(xml_get_error_code($xml_parser)), + xml_get_current_line_number($xml_parser)), 12); + + xml_parser_free($xml_parser); + } + + /** + * Check GBX header, main class ID & header block + * @param Array $classes + * The main class IDs accepted for this GBX + * @return Size of GBX header block + */ + protected function checkHeader(array $classes) + { + // check magic header + $data = $this->readData(3); + $version = $this->readInt16(); + if ($data != 'GBX' || $version != 6) + $this->errorOut('No magic GBX header', 5); + + // check header block (un)compression + $data = $this->readData(4); + if ($data[1] != 'U') + $this->errorOut('Compressed GBX header block not supported', 6); + + // check main class ID + $mainClass = $this->readInt32(); + if (!in_array($mainClass, $classes)) + $this->errorOut(sprintf('Main class ID %08X not supported', $mainClass), 7); + $this->debugLog(sprintf('GBX main class ID: %08X', $mainClass)); + + // get header size + $headerSize = $this->readInt32(); + if ($headerSize == 0) + $this->errorOut('No GBX header block', 8); + + $this->debugLog(sprintf('GBX header block size: %d (%.1f KB)', + $headerSize, $headerSize / 1024)); + return $headerSize; + } // checkHeader + + /** + * Get list of chunks from GBX header block + * @param Int $headerSize + * Size of header block (chunks list & chunks data) + * @param array $chunks + * List of chunk IDs & names + * @return List of chunk offsets & sizes + */ + protected function getChunksList($headerSize, array $chunks) + { + // get number of chunks + $numChunks = $this->readInt32(); + if ($numChunks == 0) + $this->errorOut('No GBX header chunks', 9); + + $this->debugLog('GBX number of header chunks: ' . $numChunks); + $chunkStart = $this->getGBXptr(); + $this->debugLog(sprintf('GBX start of chunk list: 0x%04X', $chunkStart)); + $chunkOffset = $chunkStart + $numChunks * 8; + + // get list of all chunks + $chunksList = array(); + for ($i = 0; $i < $numChunks; $i++) + { + $chunkId = $this->readInt32(); + $chunkSize = $this->readInt32(); + + $chunkId &= 0x00000FFF; + $chunkSize &= 0x7FFFFFFF; + + if (array_key_exists($chunkId, $chunks)) { + $name = $chunks[$chunkId]; + $chunksList[$name] = array( + 'off' => $chunkOffset, + 'size' => $chunkSize + ); + } else { + $name = 'UNKNOWN'; + } + $this->debugLog(sprintf('GBX chunk %2d %-8s Id 0x%03X Offset 0x%06X Size %6d', + $i, $name, $chunkId, $chunkOffset, $chunkSize)); + $chunkOffset += $chunkSize; + } + + //$this->debugLog(print_r($chunksList, true)); + $totalSize = $chunkOffset - $chunkStart + 4; // numChunks + if ($headerSize != $totalSize) + $this->errorOut(sprintf('Chunk list size mismatch: %d <> %d', + $headerSize, $totalSize), 10); + + return $chunksList; + } // getChunksList + + /** + * Initialize for a new chunk + * @param int $offset + */ + protected function initChunk($offset) + { + $this->setGBXptr($offset); + $this->clearLookbacks(); + } + + /** + * Get XML chunk from GBX header block & optionally parse it + * @param array $chunksList + * List of chunk offsets & sizes + */ + protected function getXMLChunk(array $chunksList) + { + if (!isset($chunksList['XML'])) return; + + $this->initChunk($chunksList['XML']['off']); + $this->xml = $this->readString(); + + if ($chunksList['XML']['size'] != strlen($this->xml) + 4) + $this->errorOut(sprintf('XML chunk size mismatch: %d <> %d', + $chunksList['XML']['size'], strlen($this->xml) + 4), 11); + + if ($this->parseXml && $this->xml != '') + $this->parseXMLstring(); + } // getXMLChunk + + /** + * Get Author fields from GBX header block + */ + protected function getAuthorFields() + { + $this->authorVer = $this->readInt32(); + $this->authorLogin = $this->readString(); + $this->authorNick = $this->stripBOM($this->readString()); + $this->authorZone = $this->stripBOM($this->readString()); + $this->authorEInfo = $this->readString(); + } // getAuthorFields + + /** + * Get Author chunk from GBX header block + * @param array $chunksList + * List of chunk offsets & sizes + */ + protected function getAuthorChunk(array $chunksList) + { + if (!isset($chunksList['Author'])) return; + + $this->initChunk($chunksList['Author']['off']); + $version = $this->readInt32(); + $this->debugLog('GBX Author chunk version: ' . $version); + + $this->getAuthorFields(); + } // getAuthorChunk + +} // class GBXBaseFetcher + + +/** + * @class GBXChallMapFetcher + * @brief The class that fetches all GBX challenge/map info + */ +class GBXChallMapFetcher extends GBXBaseFetcher +{ + public $tnImage; + + public $headerVersn, $bronzeTime, $silverTime, $goldTime, $authorTime, + $cost, $multiLap, $type, $typeName, $authorScore, $simpleEdit, + $nbChecks, $nbLaps; + public $uid, $envir, $author, $name, $kind, $kindName, $password, + $mood, $envirBg, $authorBg, $mapType, $mapStyle, $lightmap, $titleUid; + public $xmlVer, $exeVer, $exeBld, $validated, $songFile, $songUrl, + $modName, $modFile, $modUrl; + public $thumbLen, $thumbnail, $comment; + + const IMAGE_FLIP_HORIZONTAL = 1; + const IMAGE_FLIP_VERTICAL = 2; + const IMAGE_FLIP_BOTH = 3; + + /** + * Mirror (flip) an image across horizontal, vertical or both axis + * Source: http://www.php.net/manual/en/function.imagecopy.php#85992 + * @param String $imgsrc + * Source image data + * @param Int $dir + * Flip direction from the constants above + * @return Flipped image data if successful, otherwise source image data + */ + private function imageFlip($imgsrc, $dir) + { + $width = imagesx($imgsrc); + $height = imagesy($imgsrc); + $src_x = 0; + $src_y = 0; + $src_width = $width; + $src_height = $height; + + switch ((int)$dir) { + case self::IMAGE_FLIP_HORIZONTAL: + $src_y = $height; + $src_height = -$height; + break; + case self::IMAGE_FLIP_VERTICAL: + $src_x = $width; + $src_width = -$width; + break; + case self::IMAGE_FLIP_BOTH: + $src_x = $width; + $src_y = $height; + $src_width = -$width; + $src_height = -$height; + break; + default: + return $imgsrc; + } + + $imgdest = imagecreatetruecolor($width, $height); + if (imagecopyresampled($imgdest, $imgsrc, 0, 0, $src_x, $src_y, + $width, $height, $src_width, $src_height)) { + return $imgdest; + } + return $imgsrc; + } // imageFlip + + /** + * Instantiate GBX challenge/map fetcher + * + * @param Boolean $parsexml + * If true, the fetcher also parses the XML block + * @param Boolean $tnimage + * If true, the fetcher also extracts the thumbnail image; + * if GD/JPEG libraries are present, image will be flipped upright, + * otherwise it will be in the original upside-down format + * Warning: this is binary data in JPEG format, 256x256 pixels for + * TMU/TMF or 512x512 pixels for MP + * @param Boolean $debug + * If true, the fetcher prints debug logging to stderr + * @return GBXChallMapFetcher + * If GBX data couldn't be extracted, an Exception is thrown with + * the error message & code + */ + public function __construct($parsexml = false, $tnimage = false, $debug = false) + { + parent::__construct(); + + $this->headerVersn = 0; + $this->bronzeTime = 0; + $this->silverTime = 0; + $this->goldTime = 0; + $this->authorTime = 0; + + $this->cost = 0; + $this->multiLap = false; + $this->type = 0; + $this->typeName = ''; + + $this->authorScore = 0; + $this->simpleEdit = false; + $this->nbChecks = 0; + $this->nbLaps = 0; + + $this->uid = ''; + $this->envir = ''; + $this->author = ''; + $this->name = ''; + $this->kind = 0; + $this->kindName = ''; + + $this->password = ''; + $this->mood = ''; + $this->envirBg = ''; + $this->authorBg = ''; + + $this->mapType = ''; + $this->mapStyle = ''; + $this->lightmap = 0; + $this->titleUid = ''; + + $this->xmlVer = ''; + $this->exeVer = ''; + $this->exeBld = ''; + $this->validated = false; + $this->songFile = ''; + $this->songUrl = ''; + $this->modName = ''; + $this->modFile = ''; + $this->modUrl = ''; + + $this->thumbLen = 0; + $this->thumbnail = ''; + $this->comment = ''; + + $this->parseXml = (bool)$parsexml; + $this->tnImage = (bool)$tnimage; + if ((bool)$debug) + $this->enableDebug(); + + $this->setError('GBX map error: '); + } // __construct + + /** + * Process GBX challenge/map file + * + * @param String $filename + * The challenge filename + */ + public function processFile($filename) + { + $this->loadGBXdata((string)$filename); + + $this->processGBX(); + } // processFile + + /** + * Process GBX challenge/map data + * + * @param String $gbxdata + * The challenge/map data + */ + public function processData($gbxdata) + { + $this->storeGBXdata((string)$gbxdata); + + $this->processGBX(); + } // processData + + // process GBX data + private function processGBX() + { + // supported challenge/map class IDs + $challclasses = array( + self::GBX_CHALLENGE_TMF, + self::GBX_CHALLENGE_TM, + ); + + $headerSize = $this->checkHeader($challclasses); + $headerStart = $headerEnd = $this->getGBXptr(); + + // desired challenge/map chunk IDs + $chunks = array( + 0x002 => 'Info', // TM, MP + 0x003 => 'String', // TM, MP + 0x004 => 'Version', // TM, MP + 0x005 => 'XML', // TM, MP + 0x007 => 'Thumbnl', // TM, MP + 0x008 => 'Author', // MP + ); + + $chunksList = $this->getChunksList($headerSize, $chunks); + + $this->getInfoChunk($chunksList); + $headerEnd = max($headerEnd, $this->getGBXptr()); + + $this->getStringChunk($chunksList); + $headerEnd = max($headerEnd, $this->getGBXptr()); + + $this->getVersionChunk($chunksList); + $headerEnd = max($headerEnd, $this->getGBXptr()); + + $this->getXMLChunk($chunksList); + $headerEnd = max($headerEnd, $this->getGBXptr()); + + $this->getThumbnlChunk($chunksList); + $headerEnd = max($headerEnd, $this->getGBXptr()); + + $this->getAuthorChunk($chunksList); + $headerEnd = max($headerEnd, $this->getGBXptr()); + + if ($headerSize != $headerEnd - $headerStart) + $this->errorOut(sprintf('Header size mismatch: %d <> %d', + $headerSize, $headerEnd - $headerStart), 16); + + if ($this->parseXml) { + if (isset($this->xmlParsed['HEADER']['VERSION'])) + $this->xmlVer = $this->xmlParsed['HEADER']['VERSION']; + if (isset($this->xmlParsed['HEADER']['EXEVER'])) + $this->exeVer = $this->xmlParsed['HEADER']['EXEVER']; + if (isset($this->xmlParsed['HEADER']['EXEBUILD'])) + $this->exeBld = $this->xmlParsed['HEADER']['EXEBUILD']; + if ($this->lightmap == 0 && isset($this->xmlParsed['HEADER']['LIGHTMAP'])) + $this->lightmap = (int)$this->xmlParsed['HEADER']['LIGHTMAP']; + if ($this->authorZone == '' && isset($this->xmlParsed['IDENT']['AUTHORZONE'])) + $this->authorZone = $this->xmlParsed['IDENT']['AUTHORZONE']; + if ($this->envir == 'UNKNOWN' && isset($this->xmlParsed['DESC']['ENVIR'])) + $this->envir = $this->xmlParsed['DESC']['ENVIR']; + if ($this->nbLaps == 0 && isset($this->xmlParsed['DESC']['NBLAPS'])) + $this->nbLaps = (int)$this->xmlParsed['DESC']['NBLAPS']; + if (isset($this->xmlParsed['DESC']['VALIDATED'])) + $this->validated = (bool)$this->xmlParsed['DESC']['VALIDATED']; + if (isset($this->xmlParsed['DESC']['MOD'])) + $this->modName = $this->xmlParsed['DESC']['MOD']; + + // extract optional song & mod filenames + if (!empty($this->xmlParsed['DEPS'])) { + for ($i = 0; $i < count($this->xmlParsed['DEPS']); $i++) { + if (preg_match('/ChallengeMusics\\\\(.+)/', $this->xmlParsed['DEPS'][$i]['FILE'], $path)) { + $this->songFile = $path[1]; + if (isset($this->xmlParsed['DEPS'][$i]['URL'])) + $this->songUrl = $this->xmlParsed['DEPS'][$i]['URL']; + } elseif (preg_match('/.+\\\\Mod\\\\.+/', $this->xmlParsed['DEPS'][$i]['FILE'], $path)) { + $this->modFile = $path[0]; + if (isset($this->xmlParsed['DEPS'][$i]['URL'])) + $this->modUrl = $this->xmlParsed['DEPS'][$i]['URL']; + } + } + } + } + + $this->clearGBXdata(); + } // processGBX + + /** + * Get Info chunk from GBX header block + * @param array $chunksList + * List of chunk offsets & sizes + */ + protected function getInfoChunk(array $chunksList) + { + if (!isset($chunksList['Info'])) return; + + $this->initChunk($chunksList['Info']['off']); + $version = $this->readInt8(); + $this->debugLog('GBX Info chunk version: ' . $version); + + if ($version < 3) { + $this->uid = $this->readLookbackString(); + + $this->envir = $this->readLookbackString(); + $this->author = $this->readLookbackString(); + + $this->name = $this->stripBOM($this->readString()); + } + $this->moveGBXptr(4); // skip bool 0 + + if ($version >= 1) { + $this->bronzeTime = $this->readInt32(); + + $this->silverTime = $this->readInt32(); + + $this->goldTime = $this->readInt32(); + + $this->authorTime = $this->readInt32(); + + if ($version == 2) + $this->moveGBXptr(1); // skip unknown byte + + if ($version >= 4) { + $this->cost = $this->readInt32(); + + if ($version >= 5) { + $this->multiLap = (bool)$this->readInt32(); + + if ($version == 6) + $this->moveGBXptr(4); // skip unknown bool + + if ($version >= 7) { + $this->type = $this->readInt32(); + switch ($this->type) { + case 0: $this->typeName = 'Race'; + break; + case 1: $this->typeName = 'Platform'; + break; + case 2: $this->typeName = 'Puzzle'; + break; + case 3: $this->typeName = 'Crazy'; + break; + case 4: $this->typeName = 'Shortcut'; + break; + case 5: $this->typeName = 'Stunts'; + break; + case 6: $this->typeName = 'Script'; + break; + default: $this->typeName = 'UNKNOWN'; + } + + if ($version >= 9) { + $this->moveGBXptr(4); // skip int32 0 + + if ($version >= 10) { + $this->authorScore = $this->readInt32(); + + if ($version >= 11) { + $this->simpleEdit = (bool)$this->readInt32(); + + if ($version >= 12) { + $this->moveGBXptr(4); // skip bool 0 + + if ($version >= 13) { + $this->nbChecks = $this->readInt32(); + + $this->nbLaps = $this->readInt32(); + } + } + } + } + } + } + } + } + } + } // getInfoChunk + + /** + * Get String chunk from GBX header block + * @param array $chunksList + * List of chunk offsets & sizes + */ + protected function getStringChunk(array $chunksList) + { + if (!isset($chunksList['String'])) return; + + $this->initChunk($chunksList['String']['off']); + $version = $this->readInt8(); + $this->debugLog('GBX String chunk version: ' . $version); + + $this->uid = $this->readLookbackString(); + + $this->envir = $this->readLookbackString(); + $this->author = $this->readLookbackString(); + + $this->name = $this->stripBOM($this->readString()); + + $this->kind = $this->readInt8(); + switch ($this->kind) { + case 0: $this->kindName = '(internal)EndMarker'; + break; + case 1: $this->kindName = '(old)Campaign'; + break; + case 2: $this->kindName = '(old)Puzzle'; + break; + case 3: $this->kindName = '(old)Retro'; + break; + case 4: $this->kindName = '(old)TimeAttack'; + break; + case 5: $this->kindName = '(old)Rounds'; + break; + case 6: $this->kindName = 'InProgress'; + break; + case 7: $this->kindName = 'Campaign'; + break; + case 8: $this->kindName = 'Multi'; + break; + case 9: $this->kindName = 'Solo'; + break; + case 10: $this->kindName = 'Site'; + break; + case 11: $this->kindName = 'SoloNadeo'; + break; + case 12: $this->kindName = 'MultiNadeo'; + break; + default: $this->kindName = 'UNKNOWN'; + } + + if ($version >= 1) { + $this->moveGBXptr(4); // skip locked + + $this->password = $this->readString(); + + if ($version >= 2) { + $this->mood = $this->readLookbackString(); + $this->mood = preg_replace('/([A-Za-z]+)\d*/', '\1', $this->mood); + + $this->envirBg = $this->readLookbackString(); + $this->authorBg = $this->readLookbackString(); + + if ($version >= 3) { + $this->moveGBXptr(8); // skip mapOrigin + + if ($version >= 4) { + $this->moveGBXptr(8); // skip mapTarget + + if ($version >= 5) { + $this->moveGBXptr(16); // skip unknown int128 + + if ($version >= 6) { + $this->mapType = $this->readString(); + $this->mapStyle = $this->readString(); + + if ($version <= 8) + $this->moveGBXptr(4); // skip unknown bool + + if ($version >= 8) { + $this->moveGBXptr(8); // skip lightmapCacheUID + + if ($version >= 9) { + $this->lightmap = $this->readInt8(); + + if ($version >= 11) { + $this->titleUid = $this->readLookbackString(); + } + } + } + } + } + } + } + } + } + } // getStringChunk + + /** + * Get Version chunk from GBX header block + * @param array $chunksList + * List of chunk offsets & sizes + */ + protected function getVersionChunk(array $chunksList) + { + if (!isset($chunksList['Version'])) return; + + $this->initChunk($chunksList['Version']['off']); + $this->headerVersn = $this->readInt32(); + } // getVersionChunk + + /** + * Get Thumbnail/Comments chunk from GBX header block + * @param array $chunksList + * List of chunk offsets & sizes + */ + protected function getThumbnlChunk(array $chunksList) + { + if (!isset($chunksList['Thumbnl'])) return; + + $this->initChunk($chunksList['Thumbnl']['off']); + $version = $this->readInt32(); + $this->debugLog('GBX Thumbnail chunk version: ' . $version); + + if ($version == 1) { + $thumbSize = $this->readInt32(); + $this->debugLog(sprintf('GBX Thumbnail size: %d (%.1f KB)', + $thumbSize, $thumbSize / 1024)); + + $this->moveGBXptr(strlen('')); + $this->thumbnail = $this->readData($thumbSize); + $this->thumbLen = strlen($this->thumbnail); + $this->moveGBXptr(strlen('')); + + $this->moveGBXptr(strlen('')); + $this->comment = $this->stripBOM($this->readString()); + $this->moveGBXptr(strlen('')); + + // return extracted thumbnail image? + if ($this->tnImage && $this->thumbLen > 0) { + // check for GD/JPEG libraries + if (function_exists('imagecreatefromjpeg') && + function_exists('imagecopyresampled')) { + // flip thumbnail via temporary file + $tmp = tempnam(sys_get_temp_dir(), 'gbxflip'); + if (@file_put_contents($tmp, $this->thumbnail) !== false) { + if ($tn = @imagecreatefromjpeg($tmp)) { + $tn = $this->imageFlip($tn, self::IMAGE_FLIP_HORIZONTAL); + if (@imagejpeg($tn, $tmp)) { + if (($tn = @file_get_contents($tmp)) !== false) { + $this->thumbnail = $tn; + } + } + } + unlink($tmp); + } + } + } else { + $this->thumbnail = ''; + } + } + } // getThumbnlChunk + +} // class GBXChallMapFetcher + + +/** + * @class GBXChallengeFetcher + * @brief Wrapper class for backwards compatibility with the old GBXChallengeFetcher + * @deprecated Do not use for new development, use GBXChallMapFetcher instead + */ +class GBXChallengeFetcher extends GBXChallMapFetcher +{ + public $authortm, $goldtm, $silvertm, $bronzetm, $ascore, $azone, $multi, $editor, + $pub, $nblaps, $parsedxml, $xmlver, $exever, $exebld, $songfile, $songurl, + $modname, $modfile, $modurl; + + /** + * Fetches a hell of a lot of data about a GBX challenge + * + * @param String $filename + * The challenge filename (must include full path) + * @param Boolean $parsexml + * If true, the script also parses the XML block + * @param Boolean $tnimage + * If true, the script also extracts the thumbnail image; if GD/JPEG + * libraries are present, image will be flipped upright, otherwise + * it will be in the original upside-down format + * Warning: this is binary data in JPEG format, 256x256 pixels for + * TMU/TMF or 512x512 pixels for MP + * @return GBXChallengeFetcher + * If $uid is empty, GBX data couldn't be extracted + */ + public function __construct($filename, $parsexml = false, $tnimage = false) + { + parent::__construct($parsexml, $tnimage, false); + + try + { + $this->processFile($filename); + + $this->authortm = $this->authorTime; + $this->goldtm = $this->goldTime; + $this->silvertm = $this->silverTime; + $this->bronzetm = $this->bronzeTime; + $this->ascore = $this->authorScore; + $this->azone = $this->authorZone; + $this->multi = $this->multiLap; + $this->editor = $this->simpleEdit; + $this->pub = $this->authorBg; + $this->nblaps = $this->nbLaps; + $this->parsedxml = $this->xmlParsed; + $this->xmlver = $this->xmlVer; + $this->exever = $this->exeVer; + $this->exebld = $this->exeBld; + $this->songfile = $this->songFile; + $this->songurl = $this->songUrl; + $this->modname = $this->modName; + $this->modfile = $this->modFile; + $this->modurl = $this->modUrl; + } + catch (Exception $e) + { + $this->uid = ''; + } + } + +} // class GBXChallengeFetcher + + +/** + * @class GBXReplayFetcher + * @brief The class that fetches all GBX replay info + * @note The interface for GBXReplayFetcher has changed compared to the old class, + * but there is no wrapper because no third-party XASECO[2] plugins used that + */ +class GBXReplayFetcher extends GBXBaseFetcher +{ + public $uid, $envir, $author, $replay, $nickname, $login, $titleUid; + public $xmlVer, $exeVer, $exeBld, $respawns, $stuntScore, $validable, + $cpsCur, $cpsLap; + + /** + * Instantiate GBX replay fetcher + * + * @param Boolean $parsexml + * If true, the fetcher also parses the XML block + * @param Boolean $debug + * If true, the fetcher prints debug logging to stderr + * @return GBXReplayFetcher + * If GBX data couldn't be extracted, an Exception is thrown with + * the error message & code + */ + public function __construct($parsexml = false, $debug = false) + { + parent::__construct(); + + $this->uid = ''; + $this->envir = ''; + $this->author = ''; + $this->replay = 0; + $this->nickname = ''; + $this->login = ''; + $this->titleUid = ''; + + $this->xmlVer = ''; + $this->exeVer = ''; + $this->exeBld = ''; + $this->respawns = 0; + $this->stuntScore = 0; + $this->validable = false; + $this->cpsCur = 0; + $this->cpsLap = 0; + + $this->parseXml = (bool)$parsexml; + if ((bool)$debug) + $this->enableDebug(); + + $this->setError('GBX replay error: '); + } // __construct + + /** + * Process GBX replay file + * + * @param String $filename + * The replay filename + */ + public function processFile($filename) + { + $this->loadGBXdata((string)$filename); + + $this->processGBX(); + } // processFile + + /** + * Process GBX replay data + * + * @param String $gbxdata + * The replay data + */ + public function processData($gbxdata) + { + $this->storeGBXdata((string)$gbxdata); + + $this->processGBX(); + } // processData + + // process GBX data + private function processGBX() + { + // supported replay class IDs + $replayclasses = array( + self::GBX_AUTOSAVE_TMF, + self::GBX_AUTOSAVE_TM, + self::GBX_REPLAY_TM, + ); + + $headerSize = $this->checkHeader($replayclasses); + $headerStart = $headerEnd = $this->getGBXptr(); + + // desired replay chunk IDs + $chunks = array( + 0x000 => 'String', // TM, MP + 0x001 => 'XML', // TM, MP + 0x002 => 'Author', // MP + ); + + $chunksList = $this->getChunksList($headerSize, $chunks); + + $this->getStringChunk($chunksList); + $headerEnd = max($headerEnd, $this->getGBXptr()); + + $this->getXMLChunk($chunksList); + $headerEnd = max($headerEnd, $this->getGBXptr()); + + $this->getAuthorChunk($chunksList); + $headerEnd = max($headerEnd, $this->getGBXptr()); + + if ($headerSize != $headerEnd - $headerStart) + $this->errorOut(sprintf('Header size mismatch: %d <> %d', + $headerSize, $headerEnd - $headerStart), 20); + + if ($this->parseXml) { + if (isset($this->xmlParsed['HEADER']['VERSION'])) + $this->xmlVer = $this->xmlParsed['HEADER']['VERSION']; + if (isset($this->xmlParsed['HEADER']['EXEVER'])) + $this->exeVer = $this->xmlParsed['HEADER']['EXEVER']; + if (isset($this->xmlParsed['HEADER']['EXEBUILD'])) + $this->exeBld = $this->xmlParsed['HEADER']['EXEBUILD']; + if (isset($this->xmlParsed['TIMES']['RESPAWNS'])) + $this->respawns = (int)$this->xmlParsed['TIMES']['RESPAWNS']; + if (isset($this->xmlParsed['TIMES']['STUNTSCORE'])) + $this->stuntScore = (int)$this->xmlParsed['TIMES']['STUNTSCORE']; + if (isset($this->xmlParsed['TIMES']['VALIDABLE'])) + $this->validable = (bool)$this->xmlParsed['TIMES']['VALIDABLE']; + if (isset($this->xmlParsed['CHECKPOINTS']['CUR'])) + $this->cpsCur = (int)$this->xmlParsed['CHECKPOINTS']['CUR']; + if (isset($this->xmlParsed['CHECKPOINTS']['ONELAP'])) + $this->cpsLap = (int)$this->xmlParsed['CHECKPOINTS']['ONELAP']; + } + + $this->clearGBXdata(); + } // processGBX + + /** + * Get String chunk from GBX header block + * @param array $chunksList + * List of chunk offsets & sizes + */ + protected function getStringChunk(array $chunksList) + { + if (!isset($chunksList['String'])) return; + + $this->initChunk($chunksList['String']['off']); + $version = $this->readInt32(); + $this->debugLog('GBX String chunk version: ' . $version); + + if ($version >= 2) { + $this->uid = $this->readLookbackString(); + + $this->envir = $this->readLookbackString(); + $this->author = $this->readLookbackString(); + + $this->replay = $this->readInt32(); + + $this->nickname = $this->stripBOM($this->readString()); + + if ($version >= 6) { + $this->login = $this->readString(); + + if ($version >= 8) { + $this->moveGBXptr(1); // skip unknown byte + + $this->titleUid = $this->readLookbackString(); + } + } + } + } // getStringChunk + +} // class GBXReplayFetcher + + +/** + * @class GBXPackFetcher + * @brief The class that fetches all GBX pack info + */ +class GBXPackFetcher extends GBXBaseFetcher +{ + public $headerVersn, $flags, $infoMlUrl, $creatDate, $comment, $titleId, + $usageSubdir, $buildInfo, $authorUrl, $exeVer, $exeBld, $xmlDate; + + /** + * Read Windows FileTime and convert to Unix timestamp + * Filetime = 64-bit value with the number of 100-nsec intervals since Jan 1, 1601 (UTC) + * Based on http://www.mysqlperformanceblog.com/2007/03/27/integers-in-php-running-with-scissors-and-portability/ + * @return Unix timestamp, or -1 on error + */ + private function readFiletime() + { + // Unix epoch (1970-01-01) - Windows epoch (1601-01-01) in 100ns units + $EPOCHDIFF = '116444735995904000'; + $UINT32MAX = '4294967296'; + $USEC2SEC = 1000000; + + $lo = $this->readInt32(); + $hi = $this->readInt32(); + + // check for 64-bit platform + if (PHP_INT_SIZE >= 8) { + // use native math + if ($lo < 0) $lo += (1 << 32); + $date = ($hi << 32) + $lo; + $this->debugLog(sprintf('PAK CreationDate source: %016x', $date)); + if ($date == 0) return -1; + + // convert to Unix timestamp in usec + $stamp = ($date - (int)$EPOCHDIFF) / 10; + $this->debugLog(sprintf('PAK CreationDate 64-bit: %u.%06u', + $stamp / $USEC2SEC, $stamp % $USEC2SEC)); + return (int)($stamp / $USEC2SEC); + + // check for 32-bit platform + } elseif (PHP_INT_SIZE >= 4) { + $this->debugLog(sprintf('PAK CreationDate source: %08x%08x', $hi, $lo)); + if ($lo == 0 && $hi == 0) return -1; + + // workaround signed/unsigned braindamage on x32 + $lo = sprintf('%u', $lo); + $hi = sprintf('%u', $hi); + + // try and use GMP + if (function_exists('gmp_mul')) { + $date = gmp_add(gmp_mul($hi, $UINT32MAX), $lo); + // convert to Unix timestamp in usec + $stamp = gmp_div(gmp_sub($date, $EPOCHDIFF), 10); + $stamp = gmp_div_qr($stamp, $USEC2SEC); + $this->debugLog(sprintf('PAK CreationDate GNU MP: %u.%06u', + gmp_strval($stamp[0]), gmp_strval($stamp[1]))); + return (int)gmp_strval($stamp[0]); + } + + // try and use BC Math + if (function_exists('bcmul')) { + $date = bcadd(bcmul($hi, $UINT32MAX), $lo); + // convert to Unix timestamp in usec + $stamp = bcdiv(bcsub($date, $EPOCHDIFF), 10, 0); + $this->debugLog(sprintf('PAK CreationDate BCMath: %u.%06u', + bcdiv($stamp, $USEC2SEC), bcmod($stamp, $USEC2SEC))); + return (int)bcdiv($stamp, $USEC2SEC); + } + + // compute everything manually + $a = substr($hi, 0, -5); + $b = substr($hi, -5); + // hope that float precision is enough + $ac = $a * 42949; + $bd = $b * 67296; + $adbc = $a * 67296 + $b * 42949; + $r4 = substr($bd, -5) + substr($lo, -5); + $r3 = substr($bd, 0, -5) + substr($adbc, -5) + substr($lo, 0, -5); + $r2 = substr($adbc, 0, -5) + substr($ac, -5); + $r1 = substr($ac, 0, -5); + while ($r4 >= 100000) { $r4 -= 100000; $r3++; } + while ($r3 >= 100000) { $r3 -= 100000; $r2++; } + while ($r2 >= 100000) { $r2 -= 100000; $r1++; } + $date = ltrim(sprintf('%d%05d%05d%05d', $r1, $r2, $r3, $r4), '0'); + + // convert to Unix timestamp in usec + $r3 = substr($date, -6) - substr($EPOCHDIFF, -6); + $r2 = substr($date, -12, 6) - substr($EPOCHDIFF, -12, 6); + $r1 = substr($date, -18, 6) - substr($EPOCHDIFF, -18, 6); + if ($r3 < 0) { $r3 += 1000000; $r2--; } + if ($r2 < 0) { $r2 += 1000000; $r1--; } + $stamp = substr(sprintf('%d%06d%06d', $r1, $r2, $r3), 0, -1); + $this->debugLog(sprintf('PAK CreationDate manual: %s.%s', + substr($stamp, 0, -6), substr($stamp, -6))); + return (int)substr($stamp, 0, -6); + } else { + return -1; + } + } + + /** + * Instantiate GBX pack fetcher + * + * @param Boolean $parsexml + * If true, the fetcher also parses the XML block + * @param Boolean $debug + * If true, the fetcher prints debug logging to stderr + * @return GBXPackFetcher + * If GBX data couldn't be extracted, an Exception is thrown with + * the error message & code + */ + public function __construct($parsexml = false, $debug = false) + { + parent::__construct(); + + $this->headerVersn = 0; + $this->flags = 0; + $this->infoMlUrl = ''; + $this->creatDate = -1; + $this->comment = ''; + $this->titleId = ''; + $this->usageSubdir = ''; + $this->buildInfo = ''; + $this->authorUrl = ''; + $this->exeVer = ''; + $this->exeBld = ''; + $this->xmlDate = ''; + + $this->parseXml = (bool)$parsexml; + if ((bool)$debug) + $this->enableDebug(); + + $this->setError('GBX pack error: '); + } // __construct + + /** + * Process GBX pack file + * + * @param String $filename + * The pack filename + */ + public function processFile($filename) + { + $this->loadGBXdata((string)$filename); + + $this->processGBX(); + } // processFile + + /** + * Process GBX pack data + * + * @param String $gbxdata + * The pack data + */ + public function processData($gbxdata) + { + $this->storeGBXdata((string)$gbxdata); + + $this->processGBX(); + } // processData + + // process GBX data + private function processGBX() + { + // check magic header + $data = $this->readData(8); + if ($data != 'NadeoPak') + $this->errorOut('No magic NadeoPak header', 5); + + $this->headerVersn = $this->readInt32(); + if ($this->headerVersn < 6) + $this->errorOut(sprintf('Pack version %d not supported', $this->headerVersn), 24); + + $this->moveGBXptr(32); // skip ContentsChecksum + + $this->flags = $this->readInt32(); + + if ($this->headerVersn >= 7) { + $this->getAuthorFields(); + + if ($this->headerVersn < 9) { + $this->comment = $this->stripBOM($this->readString()); + + $this->moveGBXptr(16); // skip unused uint128 + + if ($this->headerVersn >= 8) { + $this->buildInfo = $this->readString(); + + $this->authorUrl = $this->readString(); + } + + } else { // >= 9 + $this->infoMlUrl = $this->readString(); + + $this->creatDate = $this->readFiletime(); + + $this->comment = $this->stripBOM($this->readString()); + + if ($this->headerVersn >= 12) { + $this->xml = $this->readString(); + $this->titleId = $this->readString(); + + if ($this->parseXml && $this->xml != '') { + $this->parseXMLstring(); + + if (isset($this->xmlParsed['HEADER']['EXEVER'])) + $this->exeVer = $this->xmlParsed['HEADER']['EXEVER']; + if (isset($this->xmlParsed['HEADER']['EXEBUILD'])) + $this->exeBld = $this->xmlParsed['HEADER']['EXEBUILD']; + if (isset($this->xmlParsed['IDENT']['CREATIONDATE'])) + $this->xmlDate = $this->xmlParsed['IDENT']['CREATIONDATE']; + } + } + + $this->usageSubdir = $this->readString(); + + $this->buildInfo = $this->readString(); + + $this->moveGBXptr(16); // skip unused uint128 + // if ($this->headerVersn >= 10) // skip encrypted IncludedPacks + } + } + + $this->clearGBXdata(); + } // processGBX + +} // class GBXPackFetcher +?> diff --git a/docker-xaseco/xaseco/includes/jfreu.config.php b/docker-xaseco/xaseco/includes/jfreu.config.php new file mode 100644 index 0000000..a737c15 --- /dev/null +++ b/docker-xaseco/xaseco/includes/jfreu.config.php @@ -0,0 +1,136 @@ + paths to config, vip/vip_team & bans files + $conf_file = 'plugins/jfreu/jfreu.config.xml'; + $vips_file = 'plugins/jfreu/jfreu.vips.xml'; + $bans_file = 'plugins/jfreu/jfreu.bans.xml'; + + //-> Server's base name: (ex: '$000Jfreu') + // Max. length: 26 chars (incl. colors & tags, and optional "TopXXX") + $servername = 'YOUR SERVER NAME'; + //-> Word between the servername and the limit (usually " Top") + $top = ' $449TOP'; + //-> Change the servername when the limit changes: "Servername TopXXX" (0 = OFF, 1 = ON) + $autochangename = 0; + + //-> ranklimit: ranklimiting default state (0 = OFF, 1 = ON) + $ranklimit = 0; + + //-> limit: ranklimit default value (when autorank is OFF) + $limit = 500000; + + //-> spec ranklimit + $hardlimit = 1000000; + + //-> autorank: autorank default state (0 = OFF, 1 = ON) + $autorank = 0; + + //-> offset (average + offset = Auto-RankLimit) + $offset = 999; + + //-> autorankminplayers (autorank disabled when not enough players) + $autorankminplayers = 10; + //-> autorankvip: include VIP/unSpec in autorank calculation (0 = OFF, 1 = ON) + $autorankvip = 0; + + //-> kick hirank when server is full and new player arrives (0 = OFF, 1 = ON) + $kickhirank = 0; + //-> maxplayers value for kickhirank (must be less than server's ) + $maxplayers = 20; + + //-> allow user /unspec vote (0 = OFF, 1 = ON) + $unspecvote = 1; + + //-> player join/leave messages + $player_join = '{#server}>> {1}: {#highlite}{2}$z$s{#message} Nation: {#highlite}{3}{#message} Ladder: {#highlite}{4}'; + $player_joins = '{#server}>> {1}: {#highlite}{2}$z$s{#message} Nation: {#highlite}{3}{#message} Ladder: {#highlite}{4}{#message} Server: {#highlite}{5}'; + $player_left = '{#server}>> {#highlite}{1}$z$s{#message} has left the game. Played: {#highlite}{2}'; + + //-> random info messages at the end of the race (0 = OFF, 1 = in chat, 2 = in TMF message window) + $infomessages = 1; + //-> prefix for info messages + $message_start = '$z$s$ff0>> [$f00INFO$ff0] $fff'; + + //-> random information messages (if you add a message don't forget to change the number) (999 messages max :-P) + // $message1 = 'Jfreu\'s plugin: "http://reload.servegame.com/plugin/"'; + $message1 = 'Information about and download of this XASECO on ' . XASECO_TMN; + $message2 = 'Use "/list" -> "/jukebox ##" to add a track in the jukebox.'; + $message3 = 'Please don\'t sound your horn throughout the entire track.'; + $message4 = 'When going AFK, please set your car to Spectator mode.'; + $message5 = 'Don\'t use Enter to skip intros - instead use Space & Enter'; + $message6 = 'For player & server info use the "/stats" and "/server" commands.'; + $message7 = 'Looking for the name of this server? Use the "/server" command.'; + $message8 = 'Use "/list nofinish" to find tracks you haven\'t completed yet, then /jukebox them!'; + $message9 = 'Use "/list norank" to find tracks you aren\'t ranked on, then /jukebox them!'; + $message10 = 'Can you beat the Gold time on all tracks? Use "/list nogold" to find out!'; + $message11 = 'Can you beat the Author time on all tracks? Use "/list noauthor" to find out!'; + $message12 = 'Wondering which tracks you haven\'t played recently? Use "/list norecent" to find out!'; + $message13 = 'Use the "/best" & "/worst" commands to find your best and worst records!'; + $message14 = 'Use the "/clans" & "/topclans" commands to see clan members and ranks!'; + $message15 = 'Use the "/ranks" commands to see the server ranks of all online players!'; + $message16 = 'Who is the most victorious player? Use "/topwins" to find out!'; + $message17 = 'Who has the most ranked records? Use "/toprecs" to find out!'; + $message18 = 'Wondering what tracks were played recently? Use the "/history" command.'; + $message19 = 'Looking for the next better ranked record to beat? Use "/nextrec"!'; + $message20 = 'Find the difference between your personal best and the track record with the "/diffrec" command!'; + $message21 = 'Check how many records were driven on the current track with the "/newrecs" command!'; + $message22 = 'Check how many records, and the 3 best ones, you have with the "/summary" command!'; + $message23 = 'Who has the most top-3 ranked records? Use "/topsums" to find out!'; + $message24 = 'Jukeboxed the wrong track? Use "/jukebox drop" to remove it!'; + $message25 = 'Forgot what someone said? Use "/chatlog" to check the chat history!'; + $message26 = 'Forgot what someone pm-ed you? Use "/pmlog" to check your PM history!'; + $message27 = 'Looking for the next better ranked player to beat? Use "/nextrank"!'; + $message28 = 'Use "/list newest <#>" to find the newest tracks added to the server, then /jukebox them!'; + $message29 = 'Find the longest and shortest tracks with the "/list longest / shortest" commands!'; + $message30 = 'Use "/mute" and "/unmute" to mute / unmute other players, and "/mutelist" to list them!'; + $message31 = 'Wondering when a player was last online? Use "/laston " to find out!'; + $message32 = 'Looking for any player\'s world stats? Use the "/statsall " command!'; + $message33 = 'Use checkpoints tracking in Rounds/Team/Cup modes with the "/cps" command!'; + $message34 = 'Find the TMX info & records for a track with the "/tmxinfo" & "/tmxrecs" commands!'; + $message35 = 'Looking for the name of the current track\'s song? Use the "/song" command!'; + $message36 = 'Looking for the name of the current track\'s mod? Use the "/mod" command!'; + $message37 = 'Use the "/style" command to select your personal window style!'; + $message38 = 'Use the "/recpanel" command to select your personal records panel!'; + $message39 = 'Use the "/votepanel" command to select your personal vote panel!'; + $message40 = 'Find out all about the Dedimania world records system with "/helpdedi"!'; + $message41 = 'Check out the XASECO[2] site at ' . XASECO_ORG . ' !'; + global $feature_votes; + if ($feature_votes) { + $message42 = 'Find out all about the chat-based voting commands with "/helpvote"!'; + } + if (function_exists('send_window_message')) { + $message43 = 'Missed a system message? Use "/msglog" to check the message history!'; + } + + //-> Badwords checking (0 = OFF, 1 = ON) + $badwords = 0; + //-> Badwords banning (0 = OFF, 1 = ON) + $badwordsban = 0; + //-> Number of badwords allowed + $badwordsnum = 3; + //-> Banning period (minutes) + $badwordstime = 10; + + //-> Badwords to check for + $badwordslist = array( + 'putain','ptain','klote','kIote','kanker','kenker', + 'arschl','wichs','fick','fikk','salop','siktirgit','gvd', + 'hitler','nutte','dick','cock','faitchier','bordel','shit', + 'encul','sucks','a.q','conerie','scheise','scheiße','scheis', + 'baskasole','cocugu','kodugumun','cazo','hoer','bitch', + 'penis','fotze','maul','frese','pizda','gay','fuck','tyfus', + 'sugi','cacat','pisat','labagiu','gaozar','muist','orospu', + 'pédé','cunt','godve','godfe','kut','kudt','lul','iui'); + + //-> novote (auto-cancel votes) (0 = OFF, 1 = ON) + $novote = 0; +?> diff --git a/docker-xaseco/xaseco/includes/manialinks.inc.php b/docker-xaseco/xaseco/includes/manialinks.inc.php new file mode 100644 index 0000000..dccfe8b --- /dev/null +++ b/docker-xaseco/xaseco/includes/manialinks.inc.php @@ -0,0 +1,1404 @@ + block fields & records panel +global $ml_custom_ui, $ml_records; +$ml_custom_ui = array('global' => true, + 'notice' => true, + 'challenge_info' => true, + 'net_infos' => true, + 'chat' => true, + 'checkpoint_list' => true, + 'round_scores' => true, + 'scoretable' => true + ); +$ml_records = array('local' => ' --.--', 'dedi' => ' --.--', 'tmx' => ' --.--'); + +/** + * Displays a single ManiaLink window to a player + * + * $login : player login to send window to + * $header: string + * $icon : array( $style, $substyle {, $sizechg} ) + * $data : array( $line1=array($col1, $col2, ...), $line2=array(...) ) + * $widths: array( $overal, $col1, $col2, ...) + * $button: string + * + * A $line with one $col will occupy the full window width, + * otherwise all $line's must have the same number of columns, + * as should $widths (+1 for $overall). + * line height=".046" is required minimum to prevent alignment glitches + * due to large characters in some cells. + * If $colX is an array, it contains the string and the button's action id. + */ +function display_manialink($login, $header, $icon, $data, $widths, $button) { + global $aseco; + + $player = $aseco->server->players->getPlayer($login); + $style = $player->style; + + // check for old TMN-style window + if (empty($style)) { + + $tsp = 'B'; // 'F' = solid, '0' = invisible + $txt = '333' . $tsp; // dark grey + $bgd = 'FFF' . $tsp; // white + $spc = 'DDD' . $tsp; // light grey + + // build manialink header + $xml = '' . + '' . LF . + '' . LF; + + // add header + $xml .= ' $o' . htmlspecialchars(validateUTF8String($header)) . '' . LF; + + // add spacer + $xml .= '' . LF; + $xml .= '$' . LF; + + // add lines with optional columns + foreach ($data as $line) { + $xml .= ''; + if (!empty($line)) { + if (count($line) > 1) { + for ($i = 0; $i < count($widths)-1; $i++) { + if (is_array($line[$i])) { + $xml .= ' $o' . htmlspecialchars(validateUTF8String($line[$i][0])) . ''; + } else { + $xml .= ' $o' . htmlspecialchars(validateUTF8String($line[$i])) . ''; + } + } + } else { + $xml .= ' $o' . htmlspecialchars(validateUTF8String($line[0])) . ''; + } + } else { // spacer + $xml .= '$'; + } + $xml .= '' . LF; + } + + // add spacer, button (action "0" = close) & footer + $xml .= '$' . LF; + $xml .= '$o' . $button . ''; + + } else { // TMF-style window + $hsize = $style['HEADER'][0]['TEXTSIZE'][0]; + $bsize = $style['BODY'][0]['TEXTSIZE'][0]; + $lines = count($data); + + // build manialink header & window + $xml = '' . + '' . LF; + + // add header and optional icon + $xml .= '' . LF; + if (is_array($icon)) { + $isize = $hsize; + if (isset($icon[2])) + $isize += $icon[2]; + $xml .= '' . LF; + $xml .= ''; + $xml = str_replace('{#black}', $style['WINDOW'][0]['BLACKCOLOR'][0], $xml); + } + + //$aseco->console_text($xml); + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($login, $aseco->formatColors($xml), 0, true)); +} // display_manialink + + +/** + * Displays custom TMX track ManiaLink window to a player + * + * $login : player login to send window to + * $header: string + * $icon : array( $style, $substyle {, $sizechg} ) + * $links : array( $image, $square, $page, $download ) + * $data : array( $line1=array($col1, $col2, ...), $line2=array(...) ) + * $widths: array( $overal, $col1, $col2, ...) + * $button: string + * + * A $line with one $col will occupy the full window width, + * otherwise all $line's must have the same number of columns, + * as should $widths (+1 for $overall). + * line height=".046" is required minimum to prevent alignment glitches + * due to large characters in some cells. + */ +function display_manialink_track($login, $header, $icon, $links, $data, $widths, $button) { + global $aseco; + + $player = $aseco->server->players->getPlayer($login); + $style = $player->style; + $square = $links[1]; + + // check for old TMN-style window + if (empty($style)) { + + $tsp = 'B'; // 'F' = solid, '0' = invisible + $txt = '333' . $tsp; // dark grey + $bgd = 'FFF' . $tsp; // white + $spc = 'DDD' . $tsp; // light grey + + // build manialink header + $xml = '' . + '' . LF . + '' . LF; + + // add header + $xml .= ' $o' . htmlspecialchars(validateUTF8String($header)) . '' . LF; + + // add spacers & image + $xml .= '' . LF; + $xml .= '$' . LF; + $xml .= '' . htmlspecialchars($links[0]) . '' . LF; + $xml .= '$' . LF; + + // add lines with optional columns + foreach ($data as $line) { + $xml .= ''; + if (!empty($line)) { + for ($i = 0; $i < count($widths)-1; $i++) { + $xml .= ' $o' . htmlspecialchars(validateUTF8String($line[$i])) . ''; + } + } else { // spacer + $xml .= '$'; + } + $xml .= '' . LF; + } + + // add spacer & links + $xml .= '$' . LF; + $xml .= '' . LF; + $xml .= '$o' . htmlspecialchars($links[2]) . '' . + '$o' . htmlspecialchars($links[3]) . '' . LF; + + // add spacer, button (action "0" = close) & footer + $xml .= '' . LF; + $xml .= '$' . LF; + $xml .= '$o' . $button . ''; + + } else { // TMF-style window + $hsize = $style['HEADER'][0]['TEXTSIZE'][0]; + $bsize = $style['BODY'][0]['TEXTSIZE'][0]; + $lines = count($data); + + // build manialink header & window + $xml = '' . + '' . LF; + + // add header + $xml .= '' . LF; + if (is_array($icon)) { + $isize = $hsize; + if (isset($icon[2])) + $isize += $icon[2]; + $xml .= '' . LF; + $xml .= ''; + $xml = str_replace('{#black}', $style['WINDOW'][0]['BLACKCOLOR'][0], $xml); + } + + //$aseco->console_text($xml); + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($login, $aseco->formatColors($xml), 0, true)); +} // display_manialink_track + + +/** + * Displays a multipage ManiaLink window to a player + * + * $player: player object to send windows to + * ->msgs: array( array( $ptr, $header, $widths, $icon ), + * page1: array( $line1=array($col1, $col2, ...), $line2=array(...) ), + * 2: array( $line1=array($col1, $col2, ...), $line2=array(...) ), + * ... ) + * $header: string + * $widths: array( $overal, $col1, $col2, ...) + * $icon : array( $style, $substyle {, $sizechg} ) + * + * A $line with one $col will occupy the full window width, + * otherwise all $line's must have the same number of columns, + * as should $widths (+1 for $overall). + * line height=".046" is required minimum to prevent alignment glitches + * due to large characters in some cells. + * If $colX is an array, it contains the string and the button's action id. + */ +function display_manialink_multi($player) { + global $aseco; + + // fake current page event + event_manialink($aseco, array(0, $player->login, 1)); +} // display_manialink_multi + +// called @ onPlayerManialinkPageAnswer +// Handles all ManiaLink main system responses, +// as well as multi-page ManiaLink windows +// [0]=PlayerUid, [1]=Login, [2]=Answer +function event_manialink($aseco, $answer) { + global $donation_values; + + // leave actions outside -6 - 36 to other handlers + if ($answer[2] < -6 || $answer[2] > 36) + return; + + // get player + $login = $answer[1]; + $player = $aseco->server->players->getPlayer($login); + + // check player answer + switch ($answer[2]) { + case 0: + // close main pop-up window + mainwindow_off($aseco, $login); + return; + + // /stats fields + case -5: + // log clicked command + $aseco->console('player {1} clicked command "/active "', $player->login); + // /stats field Time Played + $command = array(); + $command['author'] = $player; + chat_active($aseco, $command); + return; + case -6: + // log clicked command + $aseco->console('player {1} clicked command "/top100 "', $player->login); + // /stats field Server Rank + $command = array(); + $command['author'] = $player; + chat_top100($aseco, $command); + return; + case 5: + // log clicked command + $aseco->console('player {1} clicked command "/toprecs "', $player->login); + // /stats field Records + $command = array(); + $command['author'] = $player; + chat_toprecs($aseco, $command); + return; + case 6: + // log clicked command + $aseco->console('player {1} clicked command "/topwins "', $player->login); + // /stats field Races Won + $command = array(); + $command['author'] = $player; + chat_topwins($aseco, $command); + return; + + // Records panel fields + case 7: + // log clicked command + $aseco->console('player {1} clicked command "/topsums "', $player->login); + // records panel PB field + $command = array(); + $command['author'] = $player; + chat_topsums($aseco, $command); + return; + case 8: + // log clicked command + $aseco->console('player {1} clicked command "/recs "', $player->login); + // records panel Local field + $command = array(); + $command['author'] = $player; + $command['params'] = ''; + chat_recs($aseco, $command); + return; + case 9: + // log clicked command + $aseco->console('player {1} clicked command "/dedirecs "', $player->login); + // records panel Dedi field + $command = array(); + $command['author'] = $player; + $command['params'] = ''; + if (function_exists('chat_dedirecs')) chat_dedirecs($aseco, $command); + return; + case 10: + // log clicked command + $aseco->console('player {1} clicked command "/tmxrecs "', $player->login); + // records panel TMX field + $command = array(); + $command['author'] = $player; + $command['params'] = ''; + if (function_exists('chat_tmxrecs')) chat_tmxrecs($aseco, $command); + return; + + // /list Env fields + case 11: + // close main window because /list can take a while + mainwindow_off($aseco, $login); + // log clicked command + $aseco->console('player {1} clicked command "/list env:Stadium"', $player->login); + // /list Env field Stadium + $command = array(); + $command['author'] = $player; + $command['params'] = 'env:Stadium'; + chat_list($aseco, $command); + return; + case 12: + // close main window because /list can take a while + mainwindow_off($aseco, $login); + // log clicked command + $aseco->console('player {1} clicked command "/list env:Alpine"', $player->login); + // /list Env field Alpine + $command = array(); + $command['author'] = $player; + $command['params'] = 'env:Alpine'; + chat_list($aseco, $command); + return; + case 13: + // close main window because /list can take a while + mainwindow_off($aseco, $login); + // log clicked command + $aseco->console('player {1} clicked command "/list env:Bay"', $player->login); + // /list Env field Bay + $command = array(); + $command['author'] = $player; + $command['params'] = 'env:Bay'; + chat_list($aseco, $command); + return; + case 14: + // close main window because /list can take a while + mainwindow_off($aseco, $login); + // log clicked command + $aseco->console('player {1} clicked command "/list env:Coast"', $player->login); + // /list Env field Coast + $command = array(); + $command['author'] = $player; + $command['params'] = 'env:Coast'; + chat_list($aseco, $command); + return; + case 15: + // close main window because /list can take a while + mainwindow_off($aseco, $login); + // log clicked command + $aseco->console('player {1} clicked command "/list env:Island"', $player->login); + // /list Env field Island + $command = array(); + $command['author'] = $player; + $command['params'] = 'env:Island'; + chat_list($aseco, $command); + return; + case 16: + // close main window because /list can take a while + mainwindow_off($aseco, $login); + // log clicked command + $aseco->console('player {1} clicked command "/list env:Rally"', $player->login); + // /list Env field Rally + $command = array(); + $command['author'] = $player; + $command['params'] = 'env:Rally'; + chat_list($aseco, $command); + return; + case 17: + // close main window because /list can take a while + mainwindow_off($aseco, $login); + // log clicked command + $aseco->console('player {1} clicked command "/list env:Speed"', $player->login); + // /list Env field Speed + $command = array(); + $command['author'] = $player; + $command['params'] = 'env:Speed'; + chat_list($aseco, $command); + return; + + // Vote panel buttons/keys + case 18: + // log clicked command + $aseco->console('player {1} clicked command "/y "', $player->login); + // /y on chat-based vote + $command = array(); + $command['author'] = $player; + chat_y($aseco, $command); + return; + case 19: + // log clicked command + $aseco->console('player {1} clicked command "/n " (ignored)', $player->login); + // /n on chat-based vote (ignored) + return; + + case 20: + // log clicked command + $aseco->console('player {1} clicked command "/admin clearjukebox"', $player->login); + // close main window + mainwindow_off($aseco, $login); + // /jukebox display Clear Jukebox button + $command = array(); + $command['author'] = $player; + $command['params'] = 'clearjukebox'; + chat_admin($aseco, $command); + return; + + // Admin panel buttons + case 21: + // log clicked command + $aseco->console('player {1} clicked command "/admin restartmap"', $player->login); + // admin panel ClipRewind button + $command = array(); + $command['author'] = $player; + $command['params'] = 'restartmap'; + chat_admin($aseco, $command); + return; + case 22: + // log clicked command + $aseco->console('player {1} clicked command "/admin endround"', $player->login); + // admin panel ClipPause button + $command = array(); + $command['author'] = $player; + $command['params'] = 'endround'; + chat_admin($aseco, $command); + return; + case 23: + // log clicked command + $aseco->console('player {1} clicked command "/admin nextmap"', $player->login); + // admin panel ClipPlay button + $command = array(); + $command['author'] = $player; + $command['params'] = 'nextmap'; + chat_admin($aseco, $command); + return; + case 24: + // log clicked command + $aseco->console('player {1} clicked command "/admin replaymap"', $player->login); + // admin panel Refresh button + $command = array(); + $command['author'] = $player; + $command['params'] = 'replaymap'; + chat_admin($aseco, $command); + return; + case 25: + // log clicked command + $aseco->console('player {1} clicked command "/admin pass"', $player->login); + // admin panel ArrowGreen button + $command = array(); + $command['author'] = $player; + $command['params'] = 'pass'; + chat_admin($aseco, $command); + return; + case 26: + // log clicked command + $aseco->console('player {1} clicked command "/admin cancel"', $player->login); + // admin panel ArrowRed button + $command = array(); + $command['author'] = $player; + $command['params'] = 'cancel'; + chat_admin($aseco, $command); + return; + case 27: + // log clicked command + $aseco->console('player {1} clicked command "/admin players live"', $player->login); + // admin panel Buddies button + $command = array(); + $command['author'] = $player; + $command['params'] = 'players live'; + chat_admin($aseco, $command); + return; + + // Payment dialog buttons + case 28: + // log clicked command + $aseco->console('player {1} confirmed command "/admin pay"', $player->login); + admin_pay($aseco, $player->login, true); + return; + case 29: + // log clicked command + $aseco->console('player {1} cancelled command "/admin pay"', $player->login); + admin_pay($aseco, $player->login, false); + return; + + // Donate panel buttons + case 30: + // log clicked command + $aseco->console('player {1} clicked command "/donate ' . $donation_values[0] . '"', $player->login); + // donate panel field 1 + $command = array(); + $command['author'] = $player; + $command['params'] = $donation_values[0]; + chat_donate($aseco, $command); + return; + case 31: + // log clicked command + $aseco->console('player {1} clicked command "/donate ' . $donation_values[1] . '"', $player->login); + // donate panel field 2 + $command = array(); + $command['author'] = $player; + $command['params'] = $donation_values[1]; + chat_donate($aseco, $command); + return; + case 32: + // log clicked command + $aseco->console('player {1} clicked command "/donate ' . $donation_values[2] . '"', $player->login); + // donate panel field 3 + $command = array(); + $command['author'] = $player; + $command['params'] = $donation_values[2]; + chat_donate($aseco, $command); + return; + case 33: + // log clicked command + $aseco->console('player {1} clicked command "/donate ' . $donation_values[3] . '"', $player->login); + // donate panel field 4 + $command = array(); + $command['author'] = $player; + $command['params'] = $donation_values[3]; + chat_donate($aseco, $command); + return; + case 34: + // log clicked command + $aseco->console('player {1} clicked command "/donate ' . $donation_values[4] . '"', $player->login); + // donate panel field 5 + $command = array(); + $command['author'] = $player; + $command['params'] = $donation_values[4]; + chat_donate($aseco, $command); + return; + case 35: + // log clicked command + $aseco->console('player {1} clicked command "/donate ' . $donation_values[5] . '"', $player->login); + // donate panel field 6 + $command = array(); + $command['author'] = $player; + $command['params'] = $donation_values[5]; + chat_donate($aseco, $command); + return; + case 36: + // log clicked command + $aseco->console('player {1} clicked command "/donate ' . $donation_values[6] . '"', $player->login); + // donate panel field 7 + $command = array(); + $command['author'] = $player; + $command['params'] = $donation_values[6]; + chat_donate($aseco, $command); + return; + } + + // Handle multi-page ManiaLink windows in all styles + // update page pointer + $tot = count($player->msgs) - 1; + switch ($answer[2]) { + case -4: $player->msgs[0][0] = 1; break; + case -3: $player->msgs[0][0] -= 5; break; + case -2: $player->msgs[0][0] -= 1; break; + case 1: break; // stay on current page + case 2: $player->msgs[0][0] += 1; break; + case 3: $player->msgs[0][0] += 5; break; + case 4: $player->msgs[0][0] = $tot; break; + } + + // stay within boundaries + if ($player->msgs[0][0] < 1) + $player->msgs[0][0] = 1; + elseif ($player->msgs[0][0] > $tot) + $player->msgs[0][0] = $tot; + + // get control variables + $ptr = $player->msgs[0][0]; + $header = $player->msgs[0][1]; + $widths = $player->msgs[0][2]; + $icon = $player->msgs[0][3]; + $style = $player->style; + + // check for old TMN-style window + if (empty($style)) { + + $tsp = 'B'; // 'F' = solid, '0' = invisible + $txt = '333' . $tsp; // dark grey + $bgd = 'FFF' . $tsp; // white + $spc = 'DDD' . $tsp; // light grey + + // build manialink header + $xml = '' . + '' . LF . + '' . LF; + + // add header + $xml .= ' $o' . htmlspecialchars(validateUTF8String($header)) . '' . + '$n(' . $ptr . '/' . $tot . ')' . LF; + + // add spacer + $xml .= '' . LF; + $xml .= '$' . LF; + + // add lines with optional columns + foreach ($player->msgs[$ptr] as $line) { + $xml .= ''; + if (!empty($line)) { + if (count($line) > 1) { + for ($i = 0; $i < count($widths)-1; $i++) { + if (is_array($line[$i])) { + $xml .= ' $o' . htmlspecialchars(validateUTF8String($line[$i][0])) . ''; + } else { + $xml .= ' $o' . htmlspecialchars(validateUTF8String($line[$i])) . ''; + } + } + } else { + $xml .= ' $o' . htmlspecialchars(validateUTF8String($line[0])) . ''; + } + } else { // spacer + $xml .= '$'; + } + $xml .= '' . LF; + } + + // add spacer + $xml .= '$' . LF; + + // add button(s) + $add5 = ($tot > 5); + $butw = ($widths[0] - ($add5 ? 0.22 : 0)) / 3; + $xml .= ''; + // check for preceding page(s), then Prev(5) button(s) + if ($ptr > 1) { + if ($add5) + $xml .= '$oPrev5'; + $xml .= '$oPrev'; + } else { + if ($add5) + $xml .= '$'; + $xml .= '$'; + } + // always a Close button + $xml .= '$oClose'; + // check for succeeding page(s), then Next(5) button(s) + if ($ptr < $tot) { + $xml .= '$oNext'; + if ($add5) + $xml .= '$oNext5'; + } else { + $xml .= '$'; + if ($add5) + $xml .= '$'; + } + $xml .= ''; + + } else { // TMF-style window + $hsize = $style['HEADER'][0]['TEXTSIZE'][0]; + $bsize = $style['BODY'][0]['TEXTSIZE'][0]; + $lines = count($player->msgs[$ptr]); + // fill up multipage windows + if ($tot > 1) + $lines = max($lines, count($player->msgs[1])); + + // build manialink header & window + $xml = '' . + '' . LF; + + // add header + $xml .= '' . LF; + if (is_array($icon)) { + $isize = $hsize; + if (isset($icon[2])) + $isize += $icon[2]; + $xml .= '' . LF; + $xml .= ''; + $xml = str_replace('{#black}', $style['WINDOW'][0]['BLACKCOLOR'][0], $xml); + } + + //$aseco->console_text($xml); + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($player->login, $aseco->formatColors($xml), 0, false)); +} // event_manialink + + +/** + * Displays a payment dialog + * + * $login : player login to send dialog to + * $server: server name for payment + * $label : payment label string + */ +function display_payment($aseco, $login, $server, $label) { + + // build manialink + $xml = '' . + '' . + ''; + + //$aseco->console_text($xml); + // disable dialog once clicked + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($login, $xml, 0, true)); +} // display_payment + +/** + * Closes main window + * + * $login: player login to close window for + */ +function mainwindow_off($aseco, $login) { + + // close main window + $xml = ''; + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($login, $xml, 0, false)); +} // mainwindow_off + +// called @ onEndRace +function allwindows_off($aseco, $data) { + + if ($aseco->server->getGame() == 'TMF') { + // disable all pop-up windows and records & donate panels + $xml = ''; + $aseco->client->query('SendDisplayManialinkPage', $xml, 0, false); + } +} // allwindows_off + + +/** + * Displays a CheckPoints panel + * + * $login: player login(s) to send panel to + * $cp : CP number + * $diff : color+sign+diff + */ +function display_cpspanel($aseco, $login, $cp, $diff) { + + // build manialink + $xml = '' . + '' . + ''; + + //$aseco->console_text($xml); + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($login, $xml, 0, false)); +} // display_cpspanel + +/** + * Disables a CheckPoints panel + * + * $login: player login(s) to disable panel for + */ +function cpspanel_off($aseco, $login) { + + $xml = ''; + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($login, $xml, 0, false)); +} // cpspanel_off + +/** + * Disables all CheckPoints panels + */ +function allcpspanels_off($aseco) { + + $xml = ''; + $aseco->client->query('SendDisplayManialinkPage', $xml, 0, false); +} // allcpspanels_off + + +/** + * Displays an Admin panel + * + * $player: player to send panel to + */ +function display_admpanel($aseco, $player) { + + // build manialink + $xml = $player->panels['admin']; + + //$aseco->console_text($xml); + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($player->login, $xml, 0, false)); +} // display_admpanel + +/** + * Disables an Admin panel + * + * $login: player login to disable panel for + */ +function admpanel_off($aseco, $login) { + + $xml = ''; + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($login, $xml, 0, false)); +} // admpanel_off + + +/** + * Displays a Donate panel + * + * $player : player to send panel to + * $coppers: donation values + */ +function display_donpanel($aseco, $player, $coppers) { + + // build manialink + $xml = $player->panels['donate']; + for ($i = 1; $i <= 7; $i++) + $xml = str_replace('%COP' . $i . '%', $coppers[$i-1], $xml); + + //$aseco->console_text($xml); + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($player->login, $xml, 0, false)); +} // display_donpanel + +/** + * Disables a Donate panel + * + * $login: player login to disable panel for + */ +function donpanel_off($aseco, $login) { + + $xml = ''; + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($login, $xml, 0, false)); +} // donpanel_off + + +/** + * Displays a Records panel + * + * $player: player to send panel to + * $pb : personal best + */ +function display_recpanel($aseco, $player, $pb) { + global $ml_records; + + // build manialink + $xml = str_replace(array('%PB%', '%TMX%', '%LCL%', '%DED%'), + array($pb, $ml_records['tmx'], $ml_records['local'], $ml_records['dedi']), + $player->panels['records']); + + //$aseco->console_text($xml); + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($player->login, $xml, 0, false)); +} // display_recpanel + +/** + * Disables a Records panel + * + * $login: player login to disable panel for + */ +function recpanel_off($aseco, $login) { + + $xml = ''; + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($login, $xml, 0, false)); +} // recpanel_off + +function setRecordsPanel($field, $value) { + global $ml_records; + + $ml_records[$field] = $value; +} // setRecordsPanel + + +/** + * Displays a Vote panel + * + * $player : player to send panel to + * $yesstr : string for the Yes button + * $nostr : string for the No button + * $timeout: timeout for temporary panel (used only by /votepanel list) + */ +function display_votepanel($aseco, $player, $yesstr, $nostr, $timeout) { + + // build manialink + $xml = str_replace(array('%YES%', '%NO%'), + array($yesstr, $nostr), $player->panels['vote']); + + //$aseco->console_text($xml); + // disable panel once clicked + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($player->login, $xml, $timeout, true)); +} // display_votepanel + +/** + * Disables a Vote panel + * + * $login: player login to disable panel for + */ +function votepanel_off($aseco, $login) { + + $xml = ''; + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($login, $xml, 0, false)); +} // votepanel_off + +/** + * Disables all Vote panels + */ +function allvotepanels_off($aseco) { + + $xml = ''; + $aseco->client->addCall('SendDisplayManialinkPage', array($xml, 0, false)); +} // allvotepanels_off + + +/** + * Displays the Message window + * + * $msgs : lines to be displayed + * $timeout: timeout for window in msec + */ +function display_msgwindow($aseco, $msgs, $timeout) { + + $cnt = count($msgs); + $xml = '' . LF . + '' . LF; + $pos = -1; + foreach ($msgs as $msg) { + $xml .= ''; + + //$aseco->console_text($xml); + $aseco->client->addCall('SendDisplayManialinkPage', array($xml, $timeout, false)); +} // display_msgwindow + +/** + * Displays the /msglog button + * + * $login: player login to display button for + */ +function display_msglogbutton($aseco, $login) { + + $xml = '' . LF . + '' . LF . + ''; + + //$aseco->console_text($xml); + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($login, $xml, 0, false)); +} // display_msglogbutton + + +/** + * Displays a Scoreboard Stats panel + * + * $player : player to send panel to + * $rank : server rank + * $avg : record average + * $recs : records total + * $wins : wins total + * $play : session play time + * $dons : donations total + */ +function display_statspanel($aseco, $player, $rank, $avg, $recs, $wins, $play, $dons) { + + // build manialink + $xml = str_replace(array('%RANK%', '%AVG%', '%RECS%', '%WINS%', '%PLAY%', '%DONS%'), + array($rank, $avg, $recs, $wins, $play, $dons), $aseco->statspanel); + + //$aseco->console_text($xml); + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($player->login, $xml, 0, false)); +} // display_votepanel + +/** + * Disables all Scoreboard Stats panels + * + * called @ onNewChallenge + */ +function statspanels_off($aseco, $data) { + + $xml = ''; + $aseco->client->addCall('SendDisplayManialinkPage', array($xml, 0, false)); +} // statspanels_off + + +// called @ onNewChallenge +// Disables Automatic Scorepanel at start of track if $auto_scorepanel = off +function scorepanel_off($aseco, $data) { + global $auto_scorepanel; + + if ($aseco->server->getGame() == 'TMF' && !$auto_scorepanel) { + setCustomUIField('scoretable', false); + // dummy ManiaLink to preserve custom_ui + $xml = '' . + getCustomUIBlock() . ''; + $aseco->client->addCall('SendDisplayManialinkPage', array($xml, 0, false)); + } +} // scorepanel_off + +// called @ onEndRace +// Enables Automatic Scorepanel at end of track +function scorepanel_on($aseco, $data) { + + if ($aseco->server->getGame() == 'TMF') { + setCustomUIField('scoretable', true); + // dummy ManiaLink to preserve custom_ui + $xml = '' . + getCustomUIBlock() . ''; + $aseco->client->addCall('SendDisplayManialinkPage', array($xml, 0, false)); + } +} // scorepanel_on + +// called @ onBeginRound +// Disables Rounds Finishpanel at start of round if $rounds_finishpanel = off +function roundspanel_off($aseco) { + global $auto_scorepanel, $rounds_finishpanel; + + // check for Rounds/Team/Cup modes + if ($aseco->server->gameinfo->mode == Gameinfo::RNDS || + $aseco->server->gameinfo->mode == Gameinfo::TEAM || + $aseco->server->gameinfo->mode == Gameinfo::CUP) { + // check whether to disable panel + if ($aseco->server->getGame() == 'TMF' && !$rounds_finishpanel) { + setCustomUIField('round_scores', false); + // dummy ManiaLink to preserve custom_ui + $xml = '' . + getCustomUIBlock() . ''; + $aseco->client->addCall('SendDisplayManialinkPage', array($xml, 0, false)); + } + } +} // roundspanel_off + +// called @ onPlayerFinish +// Enables Rounds Finishpanel at player finish +function roundspanel_on($aseco, $finish_item) { + global $auto_scorepanel, $rounds_finishpanel; + + // check for Rounds/Team/Cup modes + if ($aseco->server->gameinfo->mode == Gameinfo::RNDS || + $aseco->server->gameinfo->mode == Gameinfo::TEAM || + $aseco->server->gameinfo->mode == Gameinfo::CUP) { + // check whether panel was disabled + if ($aseco->server->getGame() == 'TMF' && !$rounds_finishpanel) { + setCustomUIField('round_scores', true); + // dummy ManiaLink to preserve custom_ui + $xml = '' . + getCustomUIBlock() . ''; + $aseco->client->addCall('SendDisplayManialinkPageToLogin', array($finish_item->player->login, $xml, 0, false)); + } + } +} // roundspanel_on + +function setCustomUIField($field, $value) { + global $ml_custom_ui; + + $ml_custom_ui[$field] = $value; +} // setCustomUIField + +function getCustomUIBlock() { + global $ml_custom_ui; + + return '' . + '' . + '' . + '' . + '' . + '' . + '' . + '' . + '' . + ''; +} // getCustomUIBlock +?> diff --git a/docker-xaseco/xaseco/includes/ogg_comments.inc.php b/docker-xaseco/xaseco/includes/ogg_comments.inc.php new file mode 100644 index 0000000..ff43535 --- /dev/null +++ b/docker-xaseco/xaseco/includes/ogg_comments.inc.php @@ -0,0 +1,154 @@ + + * Derived from Ogg.class.php v1.3e by Nicolas Ricquemaque + * For more info see: http://opensource.grisambre.net/ogg/ + * Ogg format: http://en.wikipedia.org/wiki/Ogg + * Comments : http://www.xiph.org/vorbis/doc/v-comment.html + * + * v1.1: Improved get_1block URL parsing; added User-Agent to the GET request + * v1.0: Initial release + */ + +define('BLOCKSIZE', 512); + +class Ogg_Comments { + + public $comments = array(); + + /** + * Fetches all comments from a .ogg local file or URL + * + * @param String $path + * The .ogg file or URL + * @param Boolean $utf8 + * If true, return fields in UTF-8, otherwise in ASCII/ISO-8859-1 + * @return Ogg_Comments + * If $comments is empty, no .ogg file + */ + public function Ogg_Comments($path, $utf8 = false) { + + // check for local file or URL + if (strpos($path, '://') === false) { + if (!$fp = @fopen($path, 'rb')) + return false; + $file = fread($fp, BLOCKSIZE); + fclose($fp); + if ($file === false) + return false; + } else { + $file = $this->get_1block($path); + if ($file === false || $file == -1) + return false; + } + + // read OGG pages + for ($pos = 0; ($pos = strpos($file, 'OggS', $pos)) !== false; $pos++) { + // check stream version + if (ord($file[$pos+4]) != 0) + continue; + + // compute offset of packet after header + $offset = $pos + 27 + ord($file[$pos+26]); + // check for second (== comments) packet + if ($this->read_intle($file, $pos+18) == 1) { + // check for vorbis comments + if (ord($file[$offset]) != 0x03 || + substr($file, $offset+1, 6) != 'vorbis') + continue; + + // read vendor string + $offset += 7; + $vndlen = $this->read_intle($file, $offset); + $this->comments['VENDOR'] = $this->decode_field(substr($file, $offset+4, $vndlen), $utf8); + // read comments count + $offset += 4 + $vndlen; + $cmtcnt = $this->read_intle($file, $offset); + + // read/parse all comment fields + $offset += 4; + for ($i = 0; $i < $cmtcnt; $i++) { + $cmtlen = $this->read_intle($file, $offset); + $comment = substr($file, $offset+4, $cmtlen); + $offset += 4 + $cmtlen; + // store field name=value pair + $comment = explode('=', $comment, 2); + // check for repeated field & append + if (isset($this->comments[strtoupper($comment[0])])) + $this->comments[strtoupper($comment[0])] .= ', ' . $this->decode_field($comment[1], $utf8); + else + $this->comments[strtoupper($comment[0])] = $this->decode_field($comment[1], $utf8); + } + } + // check whether done + if (isset($this->comments['VENDOR'])) + break; + } + } // Ogg_Comments + + // Read 32-bits Little Endian integer + private function read_intle(&$buf, $pos) { + + return (ord($buf[$pos+0]) + (ord($buf[$pos+1]) << 8) + + (ord($buf[$pos+2]) << 16) + (ord($buf[$pos+3]) << 24)); + } // read_intle + + // Decode comment field into specified charset + private function decode_field($str, $utf8) { + + if ($utf8) + // return UTF8 or encode it in UTF8 + return ((utf8_encode(utf8_decode($str)) == $str) ? + $str : utf8_encode($str)); + else + // decode it to ASCII or return ASCII + return ((utf8_encode(utf8_decode($str)) == $str) ? + utf8_decode($str) : $str); + } // decode_field + + // Simple HTTP Get 1 Block function with timeout + // ok: return string || error: return false || timeout: return -1 + private function get_1block($url) { + + $url = parse_url($url); + $port = isset($url['port']) ? $url['port'] : 80; + $query = isset($url['query']) ? '?' . $url['query'] : ''; + + $fp = @fsockopen($url['host'], $port, $errno, $errstr, 4); + if (!$fp) + return false; + + $uri = ''; + foreach (explode('/', $url['path']) as $subpath) + $uri .= rawurlencode($subpath) . '/'; + $uri = substr($uri, 0, strlen($uri)-1); // strip trailing '/' + + fwrite($fp, 'GET ' . $uri . $query . " HTTP/1.0\r\n" . + 'Host: ' . $url['host'] . "\r\n" . + 'User-Agent: Ogg_Comments (' . PHP_OS . ")\r\n\r\n"); + stream_set_timeout($fp, 2); + $res = ''; + $info['timed_out'] = false; + for ($i = 0; $i < 2; $i++) + if (feof($fp) || $info['timed_out']) { + break; + } else { + $res .= fread($fp, BLOCKSIZE); + $info = stream_get_meta_data($fp); + } + fclose($fp); + + if ($info['timed_out']) { + return -1; + } else { + if (substr($res, 9, 3) != '200') + return false; + $page = explode("\r\n\r\n", $res, 2); + return trim($page[1]); + } + } // get_1block +} // class Ogg_Comments +?> diff --git a/docker-xaseco/xaseco/includes/rasp.funcs.php b/docker-xaseco/xaseco/includes/rasp.funcs.php new file mode 100644 index 0000000..9e974a8 --- /dev/null +++ b/docker-xaseco/xaseco/includes/rasp.funcs.php @@ -0,0 +1,1949 @@ +debug) + $aseco->console_text('challenges cache cleared'); + } +} // clearChallengesCache + +// called @ onTracklistChanged & !TMF +function clearChallengesCache2($aseco, $event) { + global $challengeListCache; + + // clear cache on add/remove/read/juke/unjuke events if not TMF + if ($aseco->server->getGame() != 'TMF' && + $event[0] != 'rename' && $event[0] != 'write') { + $challengeListCache = array(); + if ($aseco->debug) + $aseco->console_text('challenges cache cleared upon: ' . $event[0]); + } +} // clearChallengesCache2 + +// called @ onNewChallenge2 +function initChallengesCache($aseco, $challenge) { + global $challengeListCache, $reset_cache_start; + + if ($reset_cache_start) { + $challengeListCache = array(); + if ($aseco->debug) + $aseco->console_text('challenges cache reset'); + } + getChallengesCache($aseco); + if ($aseco->debug) + $aseco->console_text('challenges cache inited: ' . count($challengeListCache)); +} // initChallengesCache + +function getChallengesCache($aseco) { + global $challengeListCache; + + if (empty($challengeListCache)) { + if ($aseco->debug) + $aseco->console_text('challenges cache loading...'); + // get new list of all tracks + $aseco->client->resetError(); + $newlist = array(); + $done = false; + $size = 300; + $i = 0; + while (!$done) { + $aseco->client->query('GetChallengeList', $size, $i); + $tracks = $aseco->client->getResponse(); + if (!empty($tracks)) { + if ($aseco->client->isError()) { + // warning if no tracks found + if (empty($newlist)) + trigger_error('[' . $aseco->client->getErrorCode() . '] GetChallengeList - ' . $aseco->client->getErrorMessage() . ' - No tracks found!', E_USER_WARNING); + $done = true; + break; + } + foreach ($tracks as $trow) { + // obtain various author fields too + $trackinfo = getChallengeData($aseco->server->trackdir . $trow['FileName'], false); + if ($trackinfo['name'] != 'file not found') { + if ($aseco->server->getGame() != 'TMF') + $trow['Author'] = $trackinfo['author']; + $trow['AuthorTime'] = $trackinfo['authortime']; + $trow['AuthorScore'] = $trackinfo['authorscore']; + } + $trow['Name'] = stripNewlines($trow['Name']); + $newlist[$trow['UId']] = $trow; + } + if (count($tracks) < $size) { + // got less than 300 tracks, might as well leave + $done = true; + } else { + $i += $size; + } + } else { + $done = true; + } + } + + $challengeListCache = $newlist; + if ($aseco->debug) + $aseco->console_text('challenges cache loaded: ' . count($challengeListCache)); + } + + return $challengeListCache; +} // getChallengesCache + + +// calls function get_recs() from chat.records2.php +function getAllChallenges($player, $wildcard, $env) { + global $aseco, $jb_buffer, $maxrecs; + + $player->tracklist = array(); + + // get list of ranked records + $reclist = get_recs($player->id); + // get new/cached list of tracks + $newlist = getChallengesCache($aseco); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Tracks On This Server:' . LF . 'Id Rec Name' . LF; + $msg = ''; + $tid = 1; + $lines = 0; + $player->msgs = array(); + $player->msgs[0] = 1; + + foreach ($newlist as $row) { + // check for wildcard, track name or author name + if ($wildcard == '*') { + $pos = 0; + } else { + $pos = stripos(stripColors($row['Name']), $wildcard); + if ($pos === false) { + $pos = stripos($row['Author'], $wildcard); + } + } + // check for any match + if ($pos !== false) { + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else + $trackname = '{#black}' . $trackname; + + // get corresponding record + $pos = isset($reclist[$row['UId']]) ? $reclist[$row['UId']] : 0; + $pos = ($pos >= 1 && $pos <= $maxrecs) ? str_pad($pos, 2, '0', STR_PAD_LEFT) : ' -- '; + + $msg .= '$z' . str_pad($tid, 3, '0', STR_PAD_LEFT) . '. ' . $pos . '. ' + . $trackname . LF; + $tid++; + if (++$lines > 9) { + $player->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + } + // add if last batch exists + if ($msg != '') + $player->msgs[] = $aseco->formatColors($head . $msg); + + } elseif ($aseco->server->getGame() == 'TMF') { + $envids = array('Stadium' => 11, 'Alpine' => 12, 'Bay' => 13, 'Coast' => 14, 'Island' => 15, 'Rally' => 16, 'Speed' => 17); + $head = 'Tracks On This Server:'; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Rec', 'Name', 'Author', 'Env'); + else + $msg[] = array('Id', 'Rec', 'Name', 'Author'); + $tid = 1; + $lines = 0; + $player->msgs = array(); + // reserve extra width for $w tags + $extra = ($aseco->settings['lists_colortracks'] ? 0.2 : 0); + if ($aseco->server->packmask != 'Stadium') + $player->msgs[0] = array(1, $head, array(1.39+$extra, 0.12, 0.1, 0.6+$extra, 0.4, 0.17), array('Icons128x128_1', 'NewTrack', 0.02)); + else + $player->msgs[0] = array(1, $head, array(1.22+$extra, 0.12, 0.1, 0.6+$extra, 0.4), array('Icons128x128_1', 'NewTrack', 0.02)); + + foreach ($newlist as $row) { + // check for wildcard, track name or author name + if ($wildcard == '*') { + $pos = 0; + } else { + $pos = stripos(stripColors($row['Name']), $wildcard); + if ($pos === false) { + $pos = stripos($row['Author'], $wildcard); + } + } + // check for environment + if ($env == '*') { + $pose = 0; + } else { + $pose = stripos($row['Environnement'], $env); + } + // check for any match + if ($pos !== false && $pose !== false) { + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['author'] = $row['Author']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else { + $trackname = '{#black}' . $trackname; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackname = array($trackname, $tid+100); // action id + } + // format author name + $trackauthor = $row['Author']; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackauthor = array($trackauthor, -100-$tid); // action id + // format env name + $trackenv = $row['Environnement']; + // add clickable button + if ($aseco->settings['clickable_lists']) + $trackenv = array($trackenv, $envids[$row['Environnement']]); // action id + + // get corresponding record + $pos = isset($reclist[$row['UId']]) ? $reclist[$row['UId']] : 0; + $pos = ($pos >= 1 && $pos <= $maxrecs) ? str_pad($pos, 2, '0', STR_PAD_LEFT) : '-- '; + + if ($aseco->server->packmask != 'Stadium') + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $pos . '.', $trackname, $trackauthor, $trackenv); + else + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $pos . '.', $trackname, $trackauthor); + $tid++; + if (++$lines > 14) { + $player->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Rec', 'Name', 'Author', 'Env'); + else + $msg[] = array('Id', 'Rec', 'Name', 'Author'); + } + } + } + // add if last batch exists + if (count($msg) > 1) + $player->msgs[] = $msg; + } +} // getAllChallenges + +function getChallengesByKarma($player, $karmaval) { + global $aseco, $jb_buffer; + + $player->tracklist = array(); + + // get list of karma values for all matching tracks + $order = ($karmaval <= 0 ? 'ASC' : 'DESC'); + if ($karmaval == 0) { + $sql = '(SELECT uid, SUM(score) AS karma FROM challenges, rs_karma + WHERE challenges.id=rs_karma.challengeid + GROUP BY uid HAVING karma = 0) + UNION + (SELECT uid, 0 FROM challenges WHERE id NOT IN + (SELECT DISTINCT challengeid FROM rs_karma)) + ORDER BY karma ' . $order; + } else { + $sql = 'SELECT uid, SUM(score) AS karma FROM challenges, rs_karma + WHERE challenges.id=rs_karma.challengeid + GROUP BY uid + HAVING karma ' . ($karmaval < 0 ? "<= $karmaval" : ">= $karmaval") . ' + ORDER BY karma ' . $order; + } + $result = mysql_query($sql); + if (mysql_num_rows($result) == 0) { + mysql_free_result($result); + return; + } + + // get new/cached list of tracks + $newlist = getChallengesCache($aseco); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Tracks by Karma (' . $order . '):' . LF . 'Id Karma Name' . LF; + $msg = ''; + $tid = 1; + $lines = 0; + $player->msgs = array(); + $player->msgs[0] = 1; + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else + $trackname = '{#black}' . $trackname; + + $msg .= '$z' . str_pad($tid, 3, '0', STR_PAD_LEFT) . '. ' + . str_pad($dbrow[1], 4, ' ', STR_PAD_LEFT) . ' ' + . $trackname . LF; + $tid++; + if (++$lines > 9) { + $player->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + } + // add if last batch exists + if ($msg != '') + $player->msgs[] = $aseco->formatColors($head . $msg); + + } elseif ($aseco->server->getGame() == 'TMF') { + $envids = array('Stadium' => 11, 'Alpine' => 12, 'Bay' => 13, 'Coast' => 14, 'Island' => 15, 'Rally' => 16, 'Speed' => 17); + $head = 'Tracks by Karma (' . $order . '):'; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Karma', 'Name', 'Author', 'Env'); + else + $msg[] = array('Id', 'Karma', 'Name', 'Author'); + $tid = 1; + $lines = 0; + $player->msgs = array(); + // reserve extra width for $w tags + $extra = ($aseco->settings['lists_colortracks'] ? 0.2 : 0); + if ($aseco->server->packmask != 'Stadium') + $player->msgs[0] = array(1, $head, array(1.44+$extra, 0.12, 0.15, 0.6+$extra, 0.4, 0.17), array('Icons128x128_1', 'NewTrack', 0.02)); + else + $player->msgs[0] = array(1, $head, array(1.27+$extra, 0.12, 0.15, 0.6+$extra, 0.4), array('Icons128x128_1', 'NewTrack', 0.02)); + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['author'] = $row['Author']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else { + $trackname = '{#black}' . $trackname; + } + // format author name + $trackauthor = $row['Author']; + // format karma + $trackkarma = str_pad($dbrow[1], 4, ' ', STR_PAD_LEFT); + // format env name + $trackenv = $row['Environnement']; + // add clickable button + if ($aseco->settings['clickable_lists']) + $trackenv = array($trackenv, $envids[$row['Environnement']]); + + // add clickable buttons + if ($aseco->settings['clickable_lists'] && $tid <= 1900) { + $trackname = array($trackname, $tid+100); // action ids + $trackauthor = array($trackauthor, -100-$tid); + $trackkarma = array($trackkarma, -6000-$tid); + } + + if ($aseco->server->packmask != 'Stadium') + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackkarma, $trackname, $trackauthor, $trackenv); + else + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackkarma, $trackname, $trackauthor); + $tid++; + if (++$lines > 14) { + $player->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Karma', 'Name', 'Author', 'Env'); + else + $msg[] = array('Id', 'Karma', 'Name', 'Author'); + } + } + } + // add if last batch exists + if (count($msg) > 1) + $player->msgs[] = $msg; + } + + mysql_free_result($result); +} // getChallengesByKarma + +function getChallengesNoFinish($player) { + global $aseco, $jb_buffer; + + $player->tracklist = array(); + + // get list of finished tracks + $sql = 'SELECT DISTINCT challengeID FROM rs_times + WHERE playerID=' . $player->id . ' ORDER BY challengeID'; + $result = mysql_query($sql); + $finished = array(); + if (mysql_num_rows($result) > 0) { + while ($dbrow = mysql_fetch_array($result)) + $finished[] = $dbrow[0]; + } + mysql_free_result($result); + + // get list of unfinished tracks + // simpler but less efficient query: + // $sql = 'SELECT uid FROM challenges WHERE id NOT IN + // (SELECT DISTINCT challengeID FROM rs_times, players + // WHERE rs_times.playerID=players.id AND players.login=' . quotedString($player->login) . ')'; + $sql = 'SELECT uid FROM challenges'; + if (!empty($finished)) + $sql .= ' WHERE id NOT IN (' . implode(',', $finished) . ')'; + $result = mysql_query($sql); + if (mysql_num_rows($result) == 0) { + mysql_free_result($result); + return; + } + + // get new/cached list of tracks + $newlist = getChallengesCache($aseco); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Tracks You Haven\'t Finished:' . LF . 'Id Name' . LF; + $msg = ''; + $tid = 1; + $lines = 0; + $player->msgs = array(); + $player->msgs[0] = 1; + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else + $trackname = '{#black}' . $trackname; + + $msg .= '$z' . str_pad($tid, 3, '0', STR_PAD_LEFT) . '. ' + . $trackname . LF; + $tid++; + if (++$lines > 9) { + $player->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + } + // add if last batch exists + if ($msg != '') + $player->msgs[] = $aseco->formatColors($head . $msg); + + } elseif ($aseco->server->getGame() == 'TMF') { + $envids = array('Stadium' => 11, 'Alpine' => 12, 'Bay' => 13, 'Coast' => 14, 'Island' => 15, 'Rally' => 16, 'Speed' => 17); + $head = 'Tracks You Haven\'t Finished:'; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Author', 'Env'); + else + $msg[] = array('Id', 'Name', 'Author'); + $tid = 1; + $lines = 0; + $player->msgs = array(); + // reserve extra width for $w tags + $extra = ($aseco->settings['lists_colortracks'] ? 0.2 : 0); + if ($aseco->server->packmask != 'Stadium') + $player->msgs[0] = array(1, $head, array(1.29+$extra, 0.12, 0.6+$extra, 0.4, 0.17), array('Icons128x128_1', 'NewTrack', 0.02)); + else + $player->msgs[0] = array(1, $head, array(1.12+$extra, 0.12, 0.6+$extra, 0.4), array('Icons128x128_1', 'NewTrack', 0.02)); + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['author'] = $row['Author']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else { + $trackname = '{#black}' . $trackname; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackname = array($trackname, $tid+100); // action id + } + // format author name + $trackauthor = $row['Author']; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackauthor = array($trackauthor, -100-$tid); // action id + // format env name + $trackenv = $row['Environnement']; + // add clickable button + if ($aseco->settings['clickable_lists']) + $trackenv = array($trackenv, $envids[$row['Environnement']]); // action id + + if ($aseco->server->packmask != 'Stadium') + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor, $trackenv); + else + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor); + $tid++; + if (++$lines > 14) { + $player->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Author', 'Env'); + else + $msg[] = array('Id', 'Name', 'Author'); + } + } + } + // add if last batch exists + if (count($msg) > 1) + $player->msgs[] = $msg; + } + + mysql_free_result($result); +} // getChallengesNoFinish + +function getChallengesNoRank($player) { + global $aseco, $jb_buffer, $maxrecs; + + $player->tracklist = array(); + + // get list of finished tracks + $sql = 'SELECT DISTINCT challengeID FROM rs_times + WHERE playerID=' . $player->id . ' ORDER BY challengeID'; + $result = mysql_query($sql); + $finished = array(); + if (mysql_num_rows($result) > 0) { + while ($dbrow = mysql_fetch_array($result)) + $finished[] = $dbrow[0]; + } + mysql_free_result($result); + + // get list of finished tracks + // simpler but less efficient query: + // $sql = 'SELECT id,uid FROM challenges WHERE id IN + // (SELECT DISTINCT challengeID FROM rs_times, players + // WHERE rs_times.playerID=players.id AND players.login=' . quotedString($player->login) . ')'; + $sql = 'SELECT id,uid FROM challenges WHERE id '; + if (!empty($finished)) + $sql .= 'IN (' . implode(',', $finished) . ')'; + else + $sql .= '= 0'; // empty list + $result = mysql_query($sql); + if (mysql_num_rows($result) == 0) { + mysql_free_result($result); + return; + } + + $order = ($aseco->server->gameinfo->mode == Gameinfo::STNT ? 'DESC' : 'ASC'); + $unranked = array(); + $i = 0; + // check if player not in top $maxrecs on each track + while ($dbrow = mysql_fetch_array($result)) { + // more efficient but unsupported query: :( + // $sql2 = 'SELECT id FROM players WHERE (id=' . $player->id . ') AND (id NOT IN + // (SELECT playerid FROM records WHERE challengeid=' . $dbrow[0] . ' ORDER by score, date LIMIT ' . $maxrecs . '))'; + $sql2 = 'SELECT playerid FROM records + WHERE challengeid=' . $dbrow[0] . ' + ORDER by score ' . $order . ', date ASC LIMIT ' . $maxrecs; + $result2 = mysql_query($sql2); + $found = false; + if (mysql_num_rows($result2) > 0) { + while ($plrow = mysql_fetch_array($result2)) { + if ($player->id == $plrow[0]) { + $found = true; + break; + } + } + } + if (!$found) { + $unranked[$i++] = $dbrow[1]; + } + mysql_free_result($result2); + } + if (empty($unranked)) { + mysql_free_result($result); + return; + } + + // get new/cached list of tracks + $newlist = getChallengesCache($aseco); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Tracks You Have No Rank On:' . LF . 'Id Name' . LF; + $msg = ''; + $tid = 1; + $lines = 0; + $player->msgs = array(); + $player->msgs[0] = 1; + + for ($i = 0; $i < count($unranked); $i++) { + // does the uid exist in the current server track list? + if (array_key_exists($unranked[$i], $newlist)) { + $row = $newlist[$unranked[$i]]; + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else + $trackname = '{#black}' . $trackname; + + $msg .= '$z' . str_pad($tid, 3, '0', STR_PAD_LEFT) . '. ' + . $trackname . LF; + $tid++; + if (++$lines > 9) { + $player->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + } + // add if last batch exists + if ($msg != '') + $player->msgs[] = $aseco->formatColors($head . $msg); + + } elseif ($aseco->server->getGame() == 'TMF') { + $envids = array('Stadium' => 11, 'Alpine' => 12, 'Bay' => 13, 'Coast' => 14, 'Island' => 15, 'Rally' => 16, 'Speed' => 17); + $head = 'Tracks You Have No Rank On:'; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Author', 'Env'); + else + $msg[] = array('Id', 'Name', 'Author'); + $tid = 1; + $lines = 0; + $player->msgs = array(); + // reserve extra width for $w tags + $extra = ($aseco->settings['lists_colortracks'] ? 0.2 : 0); + if ($aseco->server->packmask != 'Stadium') + $player->msgs[0] = array(1, $head, array(1.29+$extra, 0.12, 0.6+$extra, 0.4, 0.17), array('Icons128x128_1', 'NewTrack', 0.02)); + else + $player->msgs[0] = array(1, $head, array(1.12+$extra, 0.12, 0.6+$extra, 0.4), array('Icons128x128_1', 'NewTrack', 0.02)); + + for ($i = 0; $i < count($unranked); $i++) { + // does the uid exist in the current server track list? + if (array_key_exists($unranked[$i], $newlist)) { + $row = $newlist[$unranked[$i]]; + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['author'] = $row['Author']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else { + $trackname = '{#black}' . $trackname; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackname = array($trackname, $tid+100); // action id + } + // format author name + $trackauthor = $row['Author']; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackauthor = array($trackauthor, -100-$tid); // action id + // format env name + $trackenv = $row['Environnement']; + // add clickable button + if ($aseco->settings['clickable_lists']) + $trackenv = array($trackenv, $envids[$row['Environnement']]); // action id + + if ($aseco->server->packmask != 'Stadium') + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor, $trackenv); + else + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor); + $tid++; + if (++$lines > 9) { + $player->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Author', 'Env'); + else + $msg[] = array('Id', 'Name', 'Author'); + } + } + } + // add if last batch exists + if (count($msg)) + $player->msgs[] = $msg; + } + + mysql_free_result($result); +} // getChallengesNoRank + +function getChallengesNoGold($player) { + global $aseco, $jb_buffer; + + $player->tracklist = array(); + + // check for Stunts mode + if ($aseco->server->gameinfo->mode != Gameinfo::STNT) { + + // get list of finished tracks with their best (minimum) times + $sql = 'SELECT DISTINCT c.uid,t1.score FROM rs_times t1, challenges c + WHERE (playerID=' . $player->id . ' AND t1.challengeID=c.id AND + score=(SELECT MIN(t2.score) FROM rs_times t2 + WHERE playerID=' . $player->id . ' AND t1.challengeID=t2.challengeID))'; + $result = mysql_query($sql); + if (mysql_num_rows($result) == 0) { + mysql_free_result($result); + return; + } + + // get new/cached list of tracks + $newlist = getChallengesCache($aseco); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Tracks You Didn\'t Beat Gold Time On:' . LF . 'Id Name $n(+Time)$m' . LF; + $msg = ''; + $tid = 1; + $lines = 0; + $player->msgs = array(); + $player->msgs[0] = 1; + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // does best time beat track's Gold time? + if ($dbrow[1] > $row['GoldTime']) { + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else + $trackname = '{#black}' . $trackname; + + // compute difference to Gold time + $diff = $dbrow[1] - $row['GoldTime']; + $sec = floor($diff/1000); + $hun = ($diff - ($sec * 1000)) / 10; + + $msg .= str_pad($tid, 3, '0', STR_PAD_LEFT) . '. ' . $trackname + . ' $z$n(+' . sprintf("%d.%02d", $sec, $hun) . ')$m' . LF; + $tid++; + if (++$lines > 9) { + $player->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + } + } + // add if last batch exists + if ($msg != '') + $player->msgs[] = $aseco->formatColors($head . $msg); + + } elseif ($aseco->server->getGame() == 'TMF') { + $envids = array('Stadium' => 11, 'Alpine' => 12, 'Bay' => 13, 'Coast' => 14, 'Island' => 15, 'Rally' => 16, 'Speed' => 17); + $head = 'Tracks You Didn\'t Beat Gold Time On:'; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Author', 'Env', 'Time'); + else + $msg[] = array('Id', 'Name', 'Author', 'Time'); + $tid = 1; + $lines = 0; + $player->msgs = array(); + // reserve extra width for $w tags + $extra = ($aseco->settings['lists_colortracks'] ? 0.2 : 0); + if ($aseco->server->packmask != 'Stadium') + $player->msgs[0] = array(1, $head, array(1.42+$extra, 0.12, 0.6+$extra, 0.4, 0.15, 0.15), array('Icons128x128_1', 'NewTrack', 0.02)); + else + $player->msgs[0] = array(1, $head, array(1.27+$extra, 0.12, 0.6+$extra, 0.4, 0.15), array('Icons128x128_1', 'NewTrack', 0.02)); + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // does best time beat track's Gold time? + if ($dbrow[1] > $row['GoldTime']) { + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['author'] = $row['Author']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else { + $trackname = '{#black}' . $trackname; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackname = array($trackname, $tid+100); // action id + } + // format author name + $trackauthor = $row['Author']; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackauthor = array($trackauthor, -100-$tid); // action id + // format env name + $trackenv = $row['Environnement']; + // add clickable button + if ($aseco->settings['clickable_lists']) + $trackenv = array($trackenv, $envids[$row['Environnement']]); // action id + + // compute difference to Gold time + $diff = $dbrow[1] - $row['GoldTime']; + $sec = floor($diff/1000); + $hun = ($diff - ($sec * 1000)) / 10; + + if ($aseco->server->packmask != 'Stadium') + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor, $trackenv, + '+' . sprintf("%d.%02d", $sec, $hun)); + else + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor, + '+' . sprintf("%d.%02d", $sec, $hun)); + $tid++; + if (++$lines > 14) { + $player->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Author', 'Env', 'Time'); + else + $msg[] = array('Id', 'Name', 'Author', 'Time'); + } + } + } + } + // add if last batch exists + if (count($msg) > 1) + $player->msgs[] = $msg; + } + + } else { // Stunts mode + + // get list of finished tracks with their best (maximum) scores + $sql = 'SELECT DISTINCT c.uid,t1.score FROM rs_times t1, challenges c + WHERE (playerID=' . $player->id . ' AND t1.challengeID=c.id AND + score=(SELECT MAX(t2.score) FROM rs_times t2 + WHERE playerID=' . $player->id . ' AND t1.challengeID=t2.challengeID))'; + $result = mysql_query($sql); + if (mysql_num_rows($result) == 0) { + mysql_free_result($result); + return; + } + + // get new/cached list of tracks + $newlist = getChallengesCache($aseco); + + // only in TMUF anyway + { + $head = 'Tracks You Didn\'t Beat Gold Score On:'; + $msg = array(); + $msg[] = array('Id', 'Name', 'Author', 'Env', 'Score'); + $tid = 1; + $lines = 0; + $player->msgs = array(); + // reserve extra width for $w tags + $extra = ($aseco->settings['lists_colortracks'] ? 0.2 : 0); + $player->msgs[0] = array(1, $head, array(1.42+$extra, 0.12, 0.6+$extra, 0.4, 0.15, 0.15), array('Icons128x128_1', 'NewTrack', 0.02)); + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // does best score beat track's Gold score? + if ($dbrow[1] < $row['GoldTime']) { + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['author'] = $row['Author']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else { + $trackname = '{#black}' . $trackname; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackname = array($trackname, $tid+100); // action id + } + // format author name + $trackauthor = $row['Author']; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackauthor = array($trackauthor, -100-$tid); // action id + // format env name + $trackenv = $row['Environnement']; + // add clickable button + if ($aseco->settings['clickable_lists']) + $trackenv = array($trackenv, $envids[$row['Environnement']]); // action id + + // compute difference to Gold score + $diff = $row['GoldTime'] - $dbrow[1]; + + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor, $trackenv, + '-' . $diff); + $tid++; + if (++$lines > 14) { + $player->msgs[] = $msg; + $lines = 0; + $msg = array(); + $msg[] = array('Id', 'Name', 'Author', 'Env', 'Score'); + } + } + } + } + // add if last batch exists + if (count($msg) > 1) + $player->msgs[] = $msg; + } + } + + mysql_free_result($result); +} // getChallengesNoGold + +function getChallengesNoAuthor($player) { + global $aseco, $jb_buffer; + + $player->tracklist = array(); + + // check for Stunts mode + if ($aseco->server->gameinfo->mode != Gameinfo::STNT) { + + // get list of finished tracks with their best (minimum) times + $sql = 'SELECT DISTINCT c.uid,t1.score FROM rs_times t1, challenges c + WHERE (playerID=' . $player->id . ' AND t1.challengeID=c.id AND + score=(SELECT MIN(t2.score) FROM rs_times t2 + WHERE playerID=' . $player->id . ' AND t1.challengeID=t2.challengeID))'; + $result = mysql_query($sql); + if (mysql_num_rows($result) == 0) { + mysql_free_result($result); + return; + } + + // get new/cached list of tracks + $newlist = getChallengesCache($aseco); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Tracks You Didn\'t Beat Author Time On:' . LF . 'Id Name $n(+Time)$m' . LF; + $msg = ''; + $tid = 1; + $lines = 0; + $player->msgs = array(); + $player->msgs[0] = 1; + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // does best time beat track's Author time? + if ($dbrow[1] > $row['AuthorTime']) { + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else + $trackname = '{#black}' . $trackname; + + // compute difference to Author time + $diff = $dbrow[1] - $row['AuthorTime']; + $sec = floor($diff/1000); + $hun = ($diff - ($sec * 1000)) / 10; + + $msg .= str_pad($tid, 3, '0', STR_PAD_LEFT) . '. ' . $trackname + . ' $z$n(+' . sprintf("%d.%02d", $sec, $hun) . ')$m' . LF; + $tid++; + if (++$lines > 9) { + $player->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + } + } + // add if last batch exists + if ($msg != '') + $player->msgs[] = $aseco->formatColors($head . $msg); + + } elseif ($aseco->server->getGame() == 'TMF') { + $envids = array('Stadium' => 11, 'Alpine' => 12, 'Bay' => 13, 'Coast' => 14, 'Island' => 15, 'Rally' => 16, 'Speed' => 17); + $head = 'Tracks You Didn\'t Beat Author Time On:'; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Author', 'Env', 'Time'); + else + $msg[] = array('Id', 'Name', 'Author', 'Time'); + $tid = 1; + $lines = 0; + $player->msgs = array(); + // reserve extra width for $w tags + $extra = ($aseco->settings['lists_colortracks'] ? 0.2 : 0); + if ($aseco->server->packmask != 'Stadium') + $player->msgs[0] = array(1, $head, array(1.42+$extra, 0.12, 0.6+$extra, 0.4, 0.15, 0.15), array('Icons128x128_1', 'NewTrack', 0.02)); + else + $player->msgs[0] = array(1, $head, array(1.27+$extra, 0.12, 0.6+$extra, 0.4, 0.15), array('Icons128x128_1', 'NewTrack', 0.02)); + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // does best time beat track's Author time? + if ($dbrow[1] > $row['AuthorTime']) { + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['author'] = $row['Author']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else { + $trackname = '{#black}' . $trackname; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackname = array($trackname, $tid+100); // action id + } + // format author name + $trackauthor = $row['Author']; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackauthor = array($trackauthor, -100-$tid); // action id + // format env name + $trackenv = $row['Environnement']; + // add clickable button + if ($aseco->settings['clickable_lists']) + $trackenv = array($trackenv, $envids[$row['Environnement']]); // action id + + // compute difference to Author time + $diff = $dbrow[1] - $row['AuthorTime']; + $sec = floor($diff/1000); + $hun = ($diff - ($sec * 1000)) / 10; + + if ($aseco->server->packmask != 'Stadium') + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor, $trackenv, + '+' . sprintf("%d.%02d", $sec, $hun)); + else + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor, + '+' . sprintf("%d.%02d", $sec, $hun)); + $tid++; + if (++$lines > 14) { + $player->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Author', 'Env', 'Time'); + else + $msg[] = array('Id', 'Name', 'Author', 'Time'); + } + } + } + } + // add if last batch exists + if (count($msg) > 1) + $player->msgs[] = $msg; + } + + } else { // Stunts mode + + // get list of finished tracks with their best (maximum) scores + $sql = 'SELECT DISTINCT c.uid,t1.score FROM rs_times t1, challenges c + WHERE (playerID=' . $player->id . ' AND t1.challengeID=c.id AND + score=(SELECT MAX(t2.score) FROM rs_times t2 + WHERE playerID=' . $player->id . ' AND t1.challengeID=t2.challengeID))'; + $result = mysql_query($sql); + if (mysql_num_rows($result) == 0) { + mysql_free_result($result); + return; + } + + // get new/cached list of tracks + $newlist = getChallengesCache($aseco); + + // only in TMUF anyway + { + $head = 'Tracks You Didn\'t Beat Author Score On:'; + $msg = array(); + $msg[] = array('Id', 'Name', 'Author', 'Env', 'Score'); + $tid = 1; + $lines = 0; + $player->msgs = array(); + // reserve extra width for $w tags + $extra = ($aseco->settings['lists_colortracks'] ? 0.2 : 0); + $player->msgs[0] = array(1, $head, array(1.42+$extra, 0.12, 0.6+$extra, 0.4, 0.15, 0.15), array('Icons128x128_1', 'NewTrack', 0.02)); + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // does best score beat track's Author score? + if ($dbrow[1] < $row['AuthorScore']) { + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['author'] = $row['Author']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else { + $trackname = '{#black}' . $trackname; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackname = array($trackname, $tid+100); // action id + } + // format author name + $trackauthor = $row['Author']; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackauthor = array($trackauthor, -100-$tid); // action id + // format env name + $trackenv = $row['Environnement']; + // add clickable button + if ($aseco->settings['clickable_lists']) + $trackenv = array($trackenv, $envids[$row['Environnement']]); // action id + + // compute difference to Author score + $diff = $row['AuthorScore'] - $dbrow[1]; + + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor, $trackenv, + '-' . $diff); + $tid++; + if (++$lines > 14) { + $player->msgs[] = $msg; + $lines = 0; + $msg = array(); + $msg[] = array('Id', 'Name', 'Author', 'Env', 'Score'); + } + } + } + } + // add if last batch exists + if (count($msg) > 1) + $player->msgs[] = $msg; + } + } + + mysql_free_result($result); +} // getChallengesNoAuthor + +// calls function get_recs() from chat.records2.php +function getChallengesNoRecent($player) { + global $aseco, $jb_buffer, $maxrecs; + + $player->tracklist = array(); + + // get list of finished tracks with their most recent (maximum) dates + $sql = 'SELECT DISTINCT c.uid,t1.date FROM rs_times t1, challenges c + WHERE (playerID=' . $player->id . ' AND t1.challengeID=c.id AND + date=(SELECT MAX(t2.date) FROM rs_times t2 + WHERE playerID=' . $player->id . ' AND t1.challengeID=t2.challengeID)) + ORDER BY t1.date'; + $result = mysql_query($sql); + if (mysql_num_rows($result) == 0) { + mysql_free_result($result); + return; + } + + // get list of ranked records + $reclist = get_recs($player->id); + // get new/cached list of tracks + $newlist = getChallengesCache($aseco); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Tracks You Didn\'t Play Recently:' . LF . 'Id Rec Name $n(Date)$m' . LF; + $msg = ''; + $tid = 1; + $lines = 0; + $player->msgs = array(); + $player->msgs[0] = 1; + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else + $trackname = '{#black}' . $trackname; + + // get corresponding record + $pos = isset($reclist[$dbrow[0]]) ? $reclist[$dbrow[0]] : 0; + $pos = ($pos >= 1 && $pos <= $maxrecs) ? str_pad($pos, 2, '0', STR_PAD_LEFT) : ' -- '; + + $msg .= str_pad($tid, 3, '0', STR_PAD_LEFT) . '. ' . $pos . '. ' + . $trackname . ' $z$n(' . date('Y/m/d', $dbrow[1]) . ')$m' . LF; + $tid++; + if (++$lines > 9) { + $player->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + } + // add if last batch exists + if ($msg != '') + $player->msgs[] = $aseco->formatColors($head . $msg); + + } elseif ($aseco->server->getGame() == 'TMF') { + $envids = array('Stadium' => 11, 'Alpine' => 12, 'Bay' => 13, 'Coast' => 14, 'Island' => 15, 'Rally' => 16, 'Speed' => 17); + $head = 'Tracks You Didn\'t Play Recently:'; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Rec', 'Name', 'Author', 'Env', 'Date'); + else + $msg[] = array('Id', 'Rec', 'Name', 'Author', 'Date'); + $tid = 1; + $lines = 0; + $player->msgs = array(); + // reserve extra width for $w tags + $extra = ($aseco->settings['lists_colortracks'] ? 0.2 : 0); + if ($aseco->server->packmask != 'Stadium') + $player->msgs[0] = array(1, $head, array(1.58+$extra, 0.12, 0.1, 0.6+$extra, 0.4, 0.15, 0.21), array('Icons128x128_1', 'NewTrack', 0.02)); + else + $player->msgs[0] = array(1, $head, array(1.43+$extra, 0.12, 0.1, 0.6+$extra, 0.4, 0.21), array('Icons128x128_1', 'NewTrack', 0.02)); + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['author'] = $row['Author']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else { + $trackname = '{#black}' . $trackname; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackname = array($trackname, $tid+100); // action id + } + // format author name + $trackauthor = $row['Author']; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackauthor = array($trackauthor, -100-$tid); // action id + // format env name + $trackenv = $row['Environnement']; + // add clickable button + if ($aseco->settings['clickable_lists']) + $trackenv = array($trackenv, $envids[$row['Environnement']]); // action id + + // get corresponding record + $pos = isset($reclist[$dbrow[0]]) ? $reclist[$dbrow[0]] : 0; + $pos = ($pos >= 1 && $pos <= $maxrecs) ? str_pad($pos, 2, '0', STR_PAD_LEFT) : '-- '; + + if ($aseco->server->packmask != 'Stadium') + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $pos . '.', $trackname, $trackauthor, $trackenv, + date('Y/m/d', $dbrow[1])); + else + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $pos . '.', $trackname, $trackauthor, + date('Y/m/d', $dbrow[1])); + $tid++; + if (++$lines > 14) { + $player->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Rec', 'Name', 'Author', 'Env', 'Date'); + else + $msg[] = array('Id', 'Rec', 'Name', 'Author', 'Date'); + } + } + } + // add if last batch exists + if (count($msg) > 1) + $player->msgs[] = $msg; + } + + mysql_free_result($result); +} // getChallengesNoRecent + +function getChallengesByLength($player, $order) { + global $aseco, $jb_buffer; + + $player->tracklist = array(); + + // if Stunts mode, bail out immediately + if ($aseco->server->gameinfo->mode == Gameinfo::STNT) return; + + // get new/cached list of tracks + $newlist = getChallengesCache($aseco); + + // build list of author times + $times = array(); + foreach ($newlist as $uid => $row) + $times[$uid] = $row['AuthorTime']; + + // sort for shortest or longest author times + $order ? asort($times) : arsort($times); + + if ($aseco->server->getGame() == 'TMN') { + $head = ($order ? 'Shortest' : 'Longest') . ' Tracks On This Server:' . LF . 'Id Name $n(Author Time)$m' . LF; + $msg = ''; + $tid = 1; + $lines = 0; + $player->msgs = array(); + $player->msgs[0] = 1; + + foreach ($times as $uid => $time) { + $row = $newlist[$uid]; + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else + $trackname = '{#black}' . $trackname; + + $msg .= str_pad($tid, 3, '0', STR_PAD_LEFT) . '. ' . $trackname + . ' $z$n(' . formatTime($time) . ')$m' . LF; + $tid++; + if (++$lines > 9) { + $player->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + // add if last batch exists + if ($msg != '') + $player->msgs[] = $aseco->formatColors($head . $msg); + + } elseif ($aseco->server->getGame() == 'TMF') { + $envids = array('Stadium' => 11, 'Alpine' => 12, 'Bay' => 13, 'Coast' => 14, 'Island' => 15, 'Rally' => 16, 'Speed' => 17); + $head = ($order ? 'Shortest' : 'Longest') . ' Tracks On This Server:'; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Author', 'Env', 'AuthTime'); + else + $msg[] = array('Id', 'Name', 'Author', 'AuthTime'); + $tid = 1; + $lines = 0; + $player->msgs = array(); + // reserve extra width for $w tags + $extra = ($aseco->settings['lists_colortracks'] ? 0.2 : 0); + if ($aseco->server->packmask != 'Stadium') + $player->msgs[0] = array(1, $head, array(1.44+$extra, 0.12, 0.6+$extra, 0.4, 0.15, 0.17), array('Icons128x128_1', 'NewTrack', 0.02)); + else + $player->msgs[0] = array(1, $head, array(1.29+$extra, 0.12, 0.6+$extra, 0.4, 0.17), array('Icons128x128_1', 'NewTrack', 0.02)); + + foreach ($times as $uid => $time) { + $row = $newlist[$uid]; + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['author'] = $row['Author']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else { + $trackname = '{#black}' . $trackname; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackname = array($trackname, $tid+100); // action id + } + // format author name + $trackauthor = $row['Author']; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackauthor = array($trackauthor, -100-$tid); // action id + // format env name + $trackenv = $row['Environnement']; + // add clickable button + if ($aseco->settings['clickable_lists']) + $trackenv = array($trackenv, $envids[$row['Environnement']]); // action id + + if ($aseco->server->packmask != 'Stadium') + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor, $trackenv, formatTime($time)); + else + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor, formatTime($time)); + $tid++; + if (++$lines > 14) { + $player->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Author', 'Env', 'AuthTime'); + else + $msg[] = array('Id', 'Name', 'Author', 'AuthTime'); + } + } + // add if last batch exists + if (count($msg) > 1) + $player->msgs[] = $msg; + } +} // getChallengesByLength + +function getChallengesByAdd($player, $order, $count) { + global $aseco, $jb_buffer; + + $player->tracklist = array(); + + // get list of tracks in reverse order of addition + $sql = 'SELECT uid FROM challenges + ORDER BY id ' . ($order ? 'DESC' : 'ASC'); + $result = mysql_query($sql); + if (mysql_num_rows($result) == 0) { + mysql_free_result($result); + return; + } + + // get new/cached list of tracks + $newlist = getChallengesCache($aseco); + + $tcnt = 0; + if ($aseco->server->getGame() == 'TMN') { + $head = ($order ? 'Newest' : 'Oldest') . ' Tracks On This Server:' . LF . 'Id Name' . LF; + $msg = ''; + $tid = 1; + $lines = 0; + $player->msgs = array(); + $player->msgs[0] = 1; + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else + $trackname = '{#black}' . $trackname; + + $msg .= '$z' . str_pad($tid, 3, '0', STR_PAD_LEFT) . '. ' + . $trackname . LF; + $tid++; + if (++$lines > 9) { + $player->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + // check if we have enough tracks already + if (++$tcnt == $count) break; + } + } + // add if last batch exists + if ($msg != '') + $player->msgs[] = $aseco->formatColors($head . $msg); + + } elseif ($aseco->server->getGame() == 'TMF') { + $envids = array('Stadium' => 11, 'Alpine' => 12, 'Bay' => 13, 'Coast' => 14, 'Island' => 15, 'Rally' => 16, 'Speed' => 17); + $head = ($order ? 'Newest' : 'Oldest') . ' Tracks On This Server:'; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Author', 'Env'); + else + $msg[] = array('Id', 'Name', 'Author'); + $tid = 1; + $lines = 0; + $player->msgs = array(); + // reserve extra width for $w tags + $extra = ($aseco->settings['lists_colortracks'] ? 0.2 : 0); + if ($aseco->server->packmask != 'Stadium') + $player->msgs[0] = array(1, $head, array(1.29+$extra, 0.12, 0.6+$extra, 0.4, 0.17), array('Icons128x128_1', 'NewTrack', 0.02)); + else + $player->msgs[0] = array(1, $head, array(1.12+$extra, 0.12, 0.6+$extra, 0.4), array('Icons128x128_1', 'NewTrack', 0.02)); + + while ($dbrow = mysql_fetch_array($result)) { + // does the uid exist in the current server track list? + if (array_key_exists($dbrow[0], $newlist)) { + $row = $newlist[$dbrow[0]]; + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['author'] = $row['Author']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else { + $trackname = '{#black}' . $trackname; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackname = array($trackname, $tid+100); // action id + } + // format author name + $trackauthor = $row['Author']; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackauthor = array($trackauthor, -100-$tid); // action id + // format env name + $trackenv = $row['Environnement']; + // add clickable button + if ($aseco->settings['clickable_lists']) + $trackenv = array($trackenv, $envids[$row['Environnement']]); // action id + + if ($aseco->server->packmask != 'Stadium') + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor, $trackenv); + else + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $trackname, $trackauthor); + $tid++; + if (++$lines > 14) { + $player->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Author', 'Env'); + else + $msg[] = array('Id', 'Name', 'Author'); + } + // check if we have enough tracks already + if (++$tcnt == $count) break; + } + } + // add if last batch exists + if (count($msg) > 1) + $player->msgs[] = $msg; + } + + mysql_free_result($result); +} // getChallengesByAdd + +function getChallengesNoVote($player) { + global $aseco, $jb_buffer, $maxrecs; + + $player->tracklist = array(); + + // get list of ranked records + $reclist = get_recs($player->id); + + // get new/cached list of tracks + $newlist = getChallengesCache($aseco); + + // get list of voted tracks and remove those + $sql = 'SELECT uid FROM challenges c, rs_karma k + WHERE c.id=k.challengeID AND k.playerID=' . $player->id; + $result = mysql_query($sql); + if (mysql_num_rows($result) > 0) { + while ($dbrow = mysql_fetch_array($result)) + unset($newlist[$dbrow[0]]); + } + mysql_free_result($result); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Tracks You Didn\'t Vote For:' . LF . 'Id Rec Name' . LF; + $msg = ''; + $tid = 1; + $lines = 0; + $player->msgs = array(); + $player->msgs[0] = 1; + + foreach ($newlist as $row) { + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else + $trackname = '{#black}' . $trackname; + + // get corresponding record + $pos = isset($reclist[$row['UId']]) ? $reclist[$row['UId']] : 0; + $pos = ($pos >= 1 && $pos <= $maxrecs) ? str_pad($pos, 2, '0', STR_PAD_LEFT) : ' -- '; + + $msg .= '$z' . str_pad($tid, 3, '0', STR_PAD_LEFT) . '. ' . $pos . '. ' + . $trackname . LF; + $tid++; + if (++$lines > 9) { + $player->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + // add if last batch exists + if ($msg != '') + $player->msgs[] = $aseco->formatColors($head . $msg); + + } elseif ($aseco->server->getGame() == 'TMF') { + $envids = array('Stadium' => 11, 'Alpine' => 12, 'Bay' => 13, 'Coast' => 14, 'Island' => 15, 'Rally' => 16, 'Speed' => 17); + $head = 'Tracks You Didn\'t Vote For:'; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Rec', 'Name', 'Author', 'Env'); + else + $msg[] = array('Id', 'Rec', 'Name', 'Author'); + $tid = 1; + $lines = 0; + $player->msgs = array(); + // reserve extra width for $w tags + $extra = ($aseco->settings['lists_colortracks'] ? 0.2 : 0); + if ($aseco->server->packmask != 'Stadium') + $player->msgs[0] = array(1, $head, array(1.39+$extra, 0.12, 0.1, 0.6+$extra, 0.4, 0.17), array('Icons128x128_1', 'NewTrack', 0.02)); + else + $player->msgs[0] = array(1, $head, array(1.22+$extra, 0.12, 0.1, 0.6+$extra, 0.4), array('Icons128x128_1', 'NewTrack', 0.02)); + + foreach ($newlist as $row) { + // store track in player object for jukeboxing + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['author'] = $row['Author']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $player->tracklist[] = $trkarr; + + // format track name + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + // grey out if in history + if (in_array($row['UId'], $jb_buffer)) + $trackname = '{#grey}' . stripColors($trackname); + else { + $trackname = '{#black}' . $trackname; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackname = array($trackname, $tid+100); // action id + } + // format author name + $trackauthor = $row['Author']; + // add clickable button + if ($aseco->settings['clickable_lists'] && $tid <= 1900) + $trackauthor = array($trackauthor, -100-$tid); // action id + // format env name + $trackenv = $row['Environnement']; + // add clickable button + if ($aseco->settings['clickable_lists']) + $trackenv = array($trackenv, $envids[$row['Environnement']]); // action id + + // get corresponding record + $pos = isset($reclist[$row['UId']]) ? $reclist[$row['UId']] : 0; + $pos = ($pos >= 1 && $pos <= $maxrecs) ? str_pad($pos, 2, '0', STR_PAD_LEFT) : '-- '; + + if ($aseco->server->packmask != 'Stadium') + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $pos . '.', $trackname, $trackauthor, $trackenv); + else + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + $pos . '.', $trackname, $trackauthor); + $tid++; + if (++$lines > 14) { + $player->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Rec', 'Name', 'Author', 'Env'); + else + $msg[] = array('Id', 'Rec', 'Name', 'Author'); + } + } + // add if last batch exists + if (count($msg) > 1) + $player->msgs[] = $msg; + } +} // getChallengesNoVote + + +// called @ onPlayerServerMessageAnswer +// handles all pop-up window responses +// [0]=PlayerUid, [1]=Login, [2]=Answer +function event_multi_message($aseco, $answer) { + + $login = $answer[1]; + $player = $aseco->server->players->getPlayer($login); + $cnt = count($player->msgs); + + // check for 'Next' response + if ($answer[2] == 2 && $cnt > 0) { + $player->msgs[0]++; // primed at 1 in the $player->msgs functions + if (($player->msgs[0] + 1) < $cnt) { // multiple pages to display + $btn1 = 'Close'; + $btn2 = 'Next'; + $msg = $player->msgs[$player->msgs[0]]; + } elseif (($player->msgs[0] + 1) == $cnt) { // last page to display + $btn1 = 'OK'; + $btn2 = ''; + $msg = $player->msgs[$player->msgs[0]]; + } else { // all done + return; + } + // display the next page + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $msg, $btn1, $btn2, 0); + // 'Close' response + } else { + } +} // event_multi_message + +function getChallengeData($filename, $rtnvotes) { + global $aseco, $tmxvoteratio; + + $ret = array(); + if (!file_exists($filename)) { + $ret['name'] = 'file not found'; + $ret['votes'] = 500; + return $ret; + } + // check whether votes are needed + if ($rtnvotes) { + $ret['votes'] = required_votes($tmxvoteratio); // from plugin.rasp_votes.php + if ($aseco->debug) { + $ret['votes'] = 1; + } + } else { + $ret['votes'] = 1; + } + + $gbx = new GBXChallMapFetcher(); + try + { + $gbx->processFile($filename); + + $ret['uid'] = $gbx->uid; + $ret['name'] = stripNewlines($gbx->name); + $ret['author'] = $gbx->author; + $ret['environment'] = $gbx->envir; + $ret['authortime'] = $gbx->authorTime; + $ret['authorscore'] = $gbx->authorScore; + $ret['coppers'] = $gbx->cost; + } + catch (Exception $e) + { + $ret['votes'] = 500; + $ret['name'] = $e->getMessage(); + } + return $ret; +} // getChallengeData +?> diff --git a/docker-xaseco/xaseco/includes/rasp.settings.php b/docker-xaseco/xaseco/includes/rasp.settings.php new file mode 100644 index 0000000..6019620 --- /dev/null +++ b/docker-xaseco/xaseco/includes/rasp.settings.php @@ -0,0 +1,158 @@ + is 2 or 6 AND player has ranked record, OR +// player uses the records panel, then PB message is not shown at track start +$always_show_pb = true; + +//Set to true ONLY if you use the karma feature. +//If you set this to true when you are not, it will produce errors +$feature_karma = false; +//Set to true if you allow ++ & -- votes as well as /++ & /-- +$allow_public_karma = false; +//Set to true if you want to show the karma message at the start of each track +$karma_show_start = false; +//Set to true if you want to show vote counts & percentages +$karma_show_details = false; +//Set to true if you want to show players their actual votes +$karma_show_votes = false; +//Set to the number of times a player should have finished a track before +//being allowed to karma vote for it +//Note: this is the total number of finishes since the first time a player +//tried a track, not the number in the current session +$karma_require_finish = 0; +//Remind player to vote karma if [s]he hasn't yet +$remind_karma = 0; // 2 = every finish; 1 = at end of race; 0 = none + +//Set to true if you want jukebox functionality +$feature_jukebox = true; +//Set to true if you want jukebox to be extended to include the TMX /add feature +$feature_tmxadd = false; +//Set to true if you want jukebox to skip tracks requested by players that left +$jukebox_skipleft = true; +//Set to true if you want jukebox to _not_ skip tracks requested by admins +//(any tier) that left (and $jukebox_skipleft is true) +$jukebox_adminnoskip = false; +//Set to true if you want /add to permanently add tracks to the server +$jukebox_permadd = false; +//Set to true if you want /admin add to automatically jukebox the downloaded track (just like a passed /add vote) +$jukebox_adminadd = true; +//Set to true if you want jukebox messages diverted to TMF message window +$jukebox_in_window = false; + +//Set to true to reset the challenges list cache at the start of each map +$reset_cache_start = true; + +//Set to contact (email, ICQ, etc) to show in /server command, leave empty to skip entry +$admin_contact = 'YOUR@EMAIL.COM'; + +//Set to filename to enable autosaving matchsettings upon every track switch +$autosave_matchsettings = ''; // e.g. 'autosave.txt' + +//Set to true if you want start-up to prune records/rs_times for players and +// challenges deleted from database, and for tracks deleted from the server +//Only enable this if you know what you're doing! +$prune_records_times = false; + +//Set to true if you want to disable normal CallVotes & enable chat-based votes +$feature_votes = false; + +//Set to true to perform XASECO version check at start-up & MasterAdmin connect +$uptodate_check = true; + +//Set to true to perform global blacklist merge at MasterAdmin connect +$globalbl_merge = false; + +//Set to true to process only United accounts in global blacklist merge +$globalbl_united = false; + +//Set to global blacklist in XML format, same as in dedicated_cfg.txt (TMF) +// e.g. http://www.gamers.org/tmf/dedimania_blacklist.txt (TMF-only) +//On TMN in dedicated.cfg isn't loaded at start-up, so no need to define that +$globalbl_url = ''; + +//################################################################## +//#-------------------- Performance Variables ---------------------# +//# These variables are used in the main plugin. # +//# They specify how much data should be used for calculations # +//# # +//# If your server slows down considerably when calculating # +//# ranks it is recommended that you lower/increase these values # +//################################################################## + +//Sets the maximum number of records stored per track +// Lower = Faster +$maxrecs = 50; + +//Sets the minimum amount of records required for a player to be ranked +// Higher = Faster +$minrank = 3; + +//Sets the number of times used to calculate a player's average +// Lower = Faster +$maxavg = 10; + +//################################################################## +//#-------------------- Jukebox Variables -------------------------# +//# These variables are used by the jukebox. # +//################################################################## + +//Specifies how large the track history buffer is. +//If a track that is in the buffer gets requested, it won't be jukeboxed. +$buffersize = 20; + +//Specifies the required vote ratio for a TMX /add request to be successful. +$tmxvoteratio = 0.66; + +//The location of the tracks folders for saving TMX tracks, relative +//to the dedicated server's GameData/Tracks/ directory: +//$tmxdir for tracks downloaded via /admin add, and user tracks approved +// via /admin addthis. +//$tmxtmpdir for tracks downloaded via /add user votes. +//There must be full write permissions on these folders. +//In linux the command will be: chmod 777. +//Regardless of OS, use the / character for pathing. +$tmxdir = 'Challenges/TMX'; +$tmxtmpdir = 'Challenges/TMXtmp'; + +//################################################################## +//#------------------------ IRC Variables -------------------------# +//# These variables are used by the IRC plugin. # +//################################################################## + +$CONFIG = array(); +$CONFIG['server'] = 'localhost'; // server (i.e. irc.gamesnet.net) +$CONFIG['nick'] = 'botname'; // nick (i.e. demonbot) +$CONFIG['port'] = 6667; // port (standard: 6667) +$CONFIG['channel'] = '#channel'; // channel (i.e. #php) +$CONFIG['name'] = 'botlogin'; // bot name (i.e. demonbot) +$show_connect = false; //If set to true, the IRC connection messages will be displayed in the console. + +//----------------------------------------- +//Do not modify anything below this line... +//----------------------------------------- +$linesbuffer = array(); +$ircmsgs = array(); +$outbuffer = array(); +$con = array(); +$jukebox = array(); +$jb_buffer = array(); +$tmxadd = array(); +$tmxplaying = false; +$tmxplayed = false; +?> diff --git a/docker-xaseco/xaseco/includes/tmndatafetcher.inc.php b/docker-xaseco/xaseco/includes/tmndatafetcher.inc.php new file mode 100644 index 0000000..691e98d --- /dev/null +++ b/docker-xaseco/xaseco/includes/tmndatafetcher.inc.php @@ -0,0 +1,290 @@ + + * + * v1.8: Improved error reporting via $error; fixed file check in extendedInfo + * processing; added magic __set_state function to support var_export() + * v1.7: Improve handling of empty API responses + * v1.6: Fixed $teamrank type into int; added User-Agent to the GET request + * v1.5: Tweaked initial get_file return value check + * v1.4: Fixed get_file return value checks; fixed $nationrank check + * v1.3: Optimized get_file URL parsing + * v1.2: Added get_file function to handle master server timeouts + * v1.1: General code cleanup; added more comments; added $lastmatch, + * $totalplayers, $nationplayers, $nationpos, $nationpoints, + * $totalnations, $servernick, $serverdesc, $servernation; + * renamed $actualserver to $serverlogin + * v1.0: Initial release + */ +class TMNDataFetcher { + + public $version, $extended, $error, + $login, $nickname, $worldrank, $totalplayers, + $points, $lastmatch, $wins, $losses, $draws, + $stars, $stardays, $teamname, $teamrank, $totalteams, + $nation, $nationrank, $nationplayers, + $nationpos, $nationpoints, $totalnations, + $online, $serverlogin, $servernick, $serverdesc, $servernation; + + /** + * Fetches a hell of a lot of data about a TMN login + * + * @param String $login + * The TMN login to search for + * @param Boolean $extendedInfo + * If true, the script also searches for the server that the + * player is on at the moment (also determines online-state) + * @return TMNDataFetcher + * If $nickname is empty, login was not found + */ + function TMNDataFetcher($login, $extendedInfo) { + + $this->version = '0.1.7.9'; + $this->error = ''; + $this->extended = $extendedInfo; + $this->login = strtolower($login); + $this->getData(); + } // TMNDataFetcher + + public static function __set_state($import) { + + $tmn = new TMNDataFetcher('', true); + + $tmn->version = $import['version']; + $tmn->extended = $import['extended']; + $tmn->error = ''; + $tmn->login = $import['login']; + $tmn->nickname = $import['nickname']; + $tmn->worldrank = $import['worldrank']; + $tmn->totalplayers = $import['totalplayers']; + $tmn->points = $import['points']; + $tmn->lastmatch = $import['lastmatch']; + $tmn->wins = $import['wins']; + $tmn->losses = $import['losses']; + $tmn->draws = $import['draws']; + $tmn->stars = $import['stars']; + $tmn->stardays = $import['stardays']; + $tmn->teamname = $import['teamname']; + $tmn->teamrank = $import['teamrank']; + $tmn->totalteams = $import['totalteams']; + $tmn->nation = $import['nation']; + $tmn->nationrank = $import['nationrank']; + $tmn->nationplayers = $import['nationplayers']; + $tmn->nationpos = $import['nationpos']; + $tmn->nationpoints = $import['nationpoints']; + $tmn->totalnations = $import['totalnations']; + $tmn->online = $import['online']; + $tmn->serverlogin = $import['serverlogin']; + $tmn->servernick = $import['servernick']; + $tmn->serverdesc = $import['serverdesc']; + $tmn->servernation = $import['servernation']; + + return $tmn; + } // __set_state + + private function getData() { + + $url = 'http://game.trackmanianations.com/online_game/getplayerinfos.php?ver=' . $this->version . '&lang=en&login=' . $this->login; + $line = $this->get_file($url); + if ($line === false) { + $this->error = 'Connection or response error on ' . $url; + return; + } else if ($line === -1) { + $this->error = 'Timed out while reading data from ' . $url; + return; + } else if ($line == '' || strpos($line, '
    ') !== false) { + $this->error = 'No data returned from ' . $url; + return; + } + + $array = explode(';', $line); + if (!isset($array[6])) { + $this->error = 'Cannot parse data returned data from ' . $url; + return; + } + // 0 = $array[0]; + // login = $array[1]; + $this->nickname = urldecode($array[2]); + $this->nation = $array[3]; + // empty = $array[4]; + $this->stars = $array[5]; + $this->stardays = $array[6]; + + $url = 'http://ladder.trackmanianations.com/ladder/getstats.php?ver=' . $this->version . '&laddertype=g&login=' . $this->login; + $line = $this->get_file($url); + if ($line === false) { + $this->error = 'Connection or response error on ' . $url; + return; + } else if ($line === -1) { + $this->error = 'Timed out while reading data from ' . $url; + return; + } else if ($line == '' || strpos($line, '
    ') !== false) { + $this->error = 'No data returned from ' . $url; + return; + } + + $array = explode(';', $line); + if (!isset($array[10])) { + $this->error = 'Cannot parse data returned data from ' . $url; + return; + } + // 0 = $array[0]; + $this->totalplayers = $array[1]; + $this->wins = $array[2]; + $this->losses = $array[3]; + $this->draws = $array[4]; + $this->worldrank = $array[5]; + $this->points = $array[6]; + $this->lastmatch = $array[7]; + $this->teamname = urldecode($array[8]); + $this->teamrank = (int)$array[9]; + $this->totalteams = $array[10]; + + $url = 'http://ladder.trackmanianations.com/ladder/getstats.php?ver=' . $this->version . '&laddertype=g&login=' . $this->login . '&country=' . $this->nation; + $line = $this->get_file($url); + if ($line === false) { + $this->error = 'Connection or response error on ' . $url; + return; + } else if ($line === -1) { + $this->error = 'Timed out while reading data from ' . $url; + return; + } else if ($line == '' || strpos($line, '
    ') !== false) { + $this->error = 'No data returned from ' . $url; + return; + } + + $array = explode(';', $line); + // 0 = $array[1]; + if (isset($array[5])) + $this->nationrank = $array[5]; + else + $this->nationrank = ''; + // the remaining fields are the same as the world stats above + + $url = 'http://ladder.trackmanianations.com/ladder/getrankings.php?ver=' . $this->version . '&laddertype=g&start=0&limit=0&country=' . $this->nation; + $line = $this->get_file($url); + if ($line === false) { + $this->error = 'Connection or response error on ' . $url; + return; + } else if ($line === -1) { + $this->error = 'Timed out while reading data from ' . $url; + return; + } else if ($line == '' || strpos($line, '
    ') !== false) { + $this->error = 'No data returned from ' . $url; + return; + } + + $array = explode(';', $line); + if (!isset($array[1])) { + $this->error = 'Cannot parse data returned data from ' . $url; + return; + } + // 0 = $array[1]; + $this->nationplayers = $array[1]; + // 1;login;nickname;nation;points = $array[2-6]; + // 2;login;nickname;nation;points = $array[7-11]; etc.etc. + + $url = 'http://ladder.trackmanianations.com/ladder/getcountriesrankings.php?ver=' . $this->version . '&laddertype=g&lang=en&start=0&limit=100'; + $line = $this->get_file($url); + if ($line === false) { + $this->error = 'Connection or response error on ' . $url; + return; + } else if ($line === -1) { + $this->error = 'Timed out while reading data from ' . $url; + return; + } else if ($line == '' || strpos($line, '
    ') !== false) { + $this->error = 'No data returned from ' . $url; + return; + } + + $array = explode(';', $line); + if (!isset($array[1])) { + $this->error = 'Cannot parse data returned data from ' . $url; + return; + } + // 0 = $array[1]; + $this->totalnations = $array[1]; + // 1;nation;points = $array[2-4]; + // 2;nation;points = $array[5-7]; etc.etc. + $i = 2; + while (isset($array[$i]) && $array[$i] != '') { + if ($array[$i+1] == $this->nation) { + $this->nationpos = $array[$i]; + $this->nationpoints = $array[$i+2]; + break; + } + $i += 3; + } + + $this->online = false; + // check online status too? + if ($this->extended) { + $page = $this->get_file('http://game.trackmanianations.com/online_game/www_serverslist.php'); + if ($page === false || $page == -1 || $page == '') + // no error message if main info was already fetched + return; + + $lines = explode('', $page); + foreach ($lines as $line) { + if (stripos($line, '' . $this->login . '') !== false) { + $this->online = true; + $this->serverlogin = substr($line, 0, strpos($line, '')); + break; + } + } + + if ($this->online) { + $page = $this->get_file('http://game.trackmanianations.com/online_game/browse_top.php?ver=0.1.7.9&lang=en&key=XXXX-XXXX-XXXX-XXXX-XXX&nb=100&page=1&flatall=1'); + if ($page === false || $page == -1 || $page == '') + // no error message if main info was already fetched + return; + + $server = $this->serverlogin; // can't use object member inside pattern + if (preg_match("/^${server};([^;]+);([^;]+);([A-Z]+);/m", $page, $fields)) { + $this->serverdesc = $fields[1]; + $this->servernick = ($fields[2] != 'x' ? urldecode($fields[2]) : ''); + $this->servernation = $fields[3]; + } + } + } + } // getData + + // Simple HTTP Get function with timeout + // ok: return string || error: return false || timeout: return -1 + private function get_file($url) { + + $url = parse_url($url); + $port = isset($url['port']) ? $url['port'] : 80; + $query = isset($url['query']) ? "?" . $url['query'] : ""; + + $fp = @fsockopen($url['host'], $port, $errno, $errstr, 4); + if (!$fp) + return false; + + fwrite($fp, 'GET ' . $url['path'] . $query . " HTTP/1.0\r\n" . + 'Host: ' . $url['host'] . "\r\n" . + 'User-Agent: TMNDataFetcher (' . PHP_OS . ")\r\n\r\n"); + stream_set_timeout($fp, 2); + $res = ''; + $info['timed_out'] = false; + while (!feof($fp) && !$info['timed_out']) { + $res .= fread($fp, 512); + $info = stream_get_meta_data($fp); + } + fclose($fp); + + if ($info['timed_out']) { + return -1; + } else { + if (substr($res, 9, 3) != '200') + return false; + $page = explode("\r\n\r\n", $res, 2); + return trim($page[1]); + } + } // get_file +} // class TMNDataFetcher +?> diff --git a/docker-xaseco/xaseco/includes/tmxinfofetcher.inc.php b/docker-xaseco/xaseco/includes/tmxinfofetcher.inc.php new file mode 100644 index 0000000..da09eef --- /dev/null +++ b/docker-xaseco/xaseco/includes/tmxinfofetcher.inc.php @@ -0,0 +1,297 @@ + + * Inspired by TMNDataFetcher & "Stats for TMN Aseco+RASP" + * + * v1.19: Allowed 24-char UIDs too + * v1.18: Added 'replayurl' to the entries in $recordslist + * v1.17: Allowed 25-char UIDs too + * v1.16: Improved error reporting via $error; parsed more comment formatting + * v1.15: Added User-Agent to the GET request + * v1.14: Fixed PHP Strict level warning + * v1.13: Fixed handling of empty API responses + * v1.12: Renamed $worldrec into $custimg to correct its meaning; fixed + * $replayid check + * v1.11: Added another check for valid $replayid + * v1.10: Added magic __set_state function to support var_export() + * v1.9: Fixed fetch records run-time warnings + * v1.8: Optimized get_file URL parsing + * v1.7: Added $worldrec (boolean); changed $visible to boolean; renamed + * $download to $dloadurl, $imgurl to $imageurl & $imgurlsmall to + * $thumburl; minor tweaks + * v1.6: Added check for valid $replayid & $replayurl + * v1.5: Added TMNF compatibility + * v1.4: Added get_file function to handle TMX site timeouts + * v1.3: Added $awards, $comments, $replayid, $replayurl; renamed $comment to + * $acomment (to better distinguish author comment from # of comments) + * v1.2: Allowed 26-char UIDs too; added $pageurl + * v1.1: Allowed TMX IDs too + * v1.0: Initial release + */ +class TMXInfoFetcher { + + public $section, $prefix, $uid, $id, $records, $error, + $name, $userid, $author, $uploaded, $updated, + $visible, $type, $envir, $mood, $style, $routes, + $length, $diffic, $lbrating, $awards, $comments, $custimg, + $game, $acomment, $pageurl, $replayid, $replayurl, + $imageurl, $thumburl, $dloadurl, $recordlist; + + /** + * Fetches a hell of a lot of data about a TMX track + * + * @param String $game + * TMX section for 'TMO', 'TMS', 'TMN', 'TMU', 'TMNF' + * @param String $id + * The challenge UID to search for (if a 24-27 char alphanum string), + * otherwise the TMX ID to search for (if a number) + * @param Boolean $records + * If true, the script also returns the world records (max. 10) + * @return TMXInfoFetcher + * If $error is not an empty string, it's an error message + */ + public function TMXInfoFetcher($game, $id, $records) { + + $this->section = $game; + switch ($game) { + case 'TMO': + $this->prefix = 'original'; + break; + case 'TMS': + $this->prefix = 'sunrise'; + break; + case 'TMN': + $this->prefix = 'nations'; + break; + case 'TMU': + $this->prefix = 'united'; + break; + case 'TMNF': + $this->prefix = 'tmnforever'; + break; + default: + $this->prefix = ''; + return; + } + + $this->error = ''; + $this->records = $records; + // check for UID string + if (preg_match('/^\w{24,27}$/', $id)) { + $this->uid = $id; + $this->getData(true); + // check for TMX ID + } elseif (is_numeric($id) && $id > 0) { + $this->id = floor($id); + $this->getData(false); + } + } // TMXInfoFetcher + + public static function __set_state($import) { + + $tmx = new TMXInfoFetcher('', 0, false); + + $tmx->section = $import['section']; + $tmx->prefix = $import['prefix']; + $tmx->uid = $import['uid']; + $tmx->id = $import['id']; + $tmx->records = $import['records']; + $tmx->error = ''; + $tmx->name = $import['name']; + $tmx->userid = $import['userid']; + $tmx->author = $import['author']; + $tmx->uploaded = $import['uploaded']; + $tmx->updated = $import['updated']; + $tmx->visible = $import['visible']; + $tmx->type = $import['type']; + $tmx->envir = $import['envir']; + $tmx->mood = $import['mood']; + $tmx->style = $import['style']; + $tmx->routes = $import['routes']; + $tmx->length = $import['length']; + $tmx->diffic = $import['diffic']; + $tmx->lbrating = $import['lbrating']; + $tmx->awards = $import['awards']; + $tmx->comments = $import['comments']; + $tmx->custimg = $import['custimg']; + $tmx->game = $import['game']; + $tmx->acomment = $import['acomment']; + $tmx->pageurl = $import['pageurl']; + $tmx->replayid = $import['replayid']; + $tmx->replayurl = $import['replayurl']; + $tmx->imageurl = $import['imageurl']; + $tmx->thumburl = $import['thumburl']; + $tmx->dloadurl = $import['dloadurl']; + $tmx->recordlist = null; + + return $tmx; + } // __set_state + + private function getData($isuid) { + + // get main track info + $url = 'http://' . $this->prefix . '.tm-exchange.com/apiget.aspx?action=apitrackinfo&' . ($isuid ? 'u' : '') . 'id=' . ($isuid ? $this->uid : $this->id); + $file = $this->get_file($url); + if ($file === false) { + $this->error = 'Connection or response error on ' . $url; + return; + } else if ($file === -1) { + $this->error = 'Timed out while reading data from ' . $url; + return; + } else if ($file == '') { + $this->error = 'No data returned from ' . $url; + return; + } + + // check for API error message + if (strpos($file, chr(27)) !== false) { + $this->error = 'Cannot decode main track info'; + return; + } + + // separate columns on Tabs + $fields = explode(chr(9), $file); + + if ($isuid) + $this->id = $fields[0]; + + $this->name = $fields[1]; + $this->userid = $fields[2]; + $this->author = $fields[3]; + $this->uploaded = $fields[4]; + $this->updated = $fields[5]; + $this->visible = (strtolower($fields[6]) == 'true'); + $this->type = $fields[7]; + $this->envir = $fields[8]; + $this->mood = $fields[9]; + $this->style = $fields[10]; + $this->routes = $fields[11]; + $this->length = $fields[12]; + $this->diffic = $fields[13]; + $this->lbrating = ($fields[14] > 0 ? $fields[14] : 'Classic!'); + $this->game = $fields[15]; + + $search = array(chr(31), '[b]', '[/b]', '[i]', '[/i]', '[u]', '[/u]', '[url]', '[/url]'); + $replace = array('
    ', '', '', '', '', '', '', '', ''); + $this->acomment = str_ireplace($search, $replace, $fields[16]); + $this->acomment = preg_replace('/\[url=".*"\]/', '', $this->acomment); + + $this->pageurl = 'http://' . $this->prefix . '.tm-exchange.com/main.aspx?action=trackshow&id=' . $this->id; + $this->imageurl = 'http://' . $this->prefix . '.tm-exchange.com/get.aspx?action=trackscreen&id=' . $this->id; + $this->thumburl = 'http://' . $this->prefix . '.tm-exchange.com/get.aspx?action=trackscreensmall&id=' . $this->id; + $this->dloadurl = 'http://' . $this->prefix . '.tm-exchange.com/get.aspx?action=trackgbx&id=' . $this->id; + + $this->awards = 0; + $this->comments = 0; + $this->custimg = false; + $this->replayid = 0; + $this->replayurl = ''; + + // get misc. track info + $url = 'http://' . $this->prefix . '.tm-exchange.com/apiget.aspx?action=apisearch&trackid=' . $this->id; + $file = $this->get_file($url); + if ($file === false || $file === -1 || $file == '') + // no error message if main info was already fetched + return; + + // check for API error message + if (strpos($file, chr(27)) !== false) + return; + + // separate columns on Tabs + $fields = explode(chr(9), $file); + + // id = $fields[0]; + // name = $fields[1]; + // userid = $fields[2]; + // author = $fields[3]; + // type = $fields[4]; + // envir = $fields[5]; + // mood = $fields[6]; + // style = $fields[7]; + // routes = $fields[8]; + // length = $fields[9]; + // diffic = $fields[10]; + // lbrating = ($fields[11] > 0 ? $fields[11] : 'Classic!'); + $this->awards = $fields[12]; + $this->comments = $fields[13]; + $this->custimg = (strtolower($fields[14]) == 'true'); + // game = $fields[15]; + $this->replayid = $fields[16]; + // unknown = $fields[17-21]; + // uploaded = $fields[22]; + // updated = $fields[23]; + + if ($this->replayid > 0) { + $this->replayurl = 'http://' . $this->prefix . '.tm-exchange.com/get.aspx?action=recordgbx&id=' . $this->replayid; + } + + // fetch records too? + $this->recordlist = array(); + if ($this->records) { + $url = 'http://' . $this->prefix . '.tm-exchange.com/apiget.aspx?action=apitrackrecords&id=' . $this->id; + $file = $this->get_file($url); + if ($file === false || $file === -1 || $file == '') + // no error message if main info was already fetched + return; + + $file = explode("\r\n", $file); + $i = 0; + while ($i < 10 && isset($file[$i]) && $file[$i] != '') { + // separate columns on Tabs + $fields = explode(chr(9), $file[$i]); + $this->recordlist[$i++] = array( + 'replayid' => $fields[0], + 'userid' => $fields[1], + 'name' => $fields[2], + 'time' => $fields[3], + 'replayat' => $fields[4], + 'trackat' => $fields[5], + 'approved' => $fields[6], + 'score' => $fields[7], + 'expires' => $fields[8], + 'lockspan' => $fields[9], + 'replayurl'=> 'http://' . $this->prefix . '.tm-exchange.com/get.aspx?action=recordgbx&id=' . $fields[0], + ); + } + } + } // getData + + // Simple HTTP Get function with timeout + // ok: return string || error: return false || timeout: return -1 + private function get_file($url) { + + $url = parse_url($url); + $port = isset($url['port']) ? $url['port'] : 80; + $query = isset($url['query']) ? "?" . $url['query'] : ""; + + $fp = @fsockopen($url['host'], $port, $errno, $errstr, 4); + if (!$fp) + return false; + + fwrite($fp, 'GET ' . $url['path'] . $query . " HTTP/1.0\r\n" . + 'Host: ' . $url['host'] . "\r\n" . + 'User-Agent: TMXInfoFetcher (' . PHP_OS . ")\r\n\r\n"); + stream_set_timeout($fp, 2); + $res = ''; + $info['timed_out'] = false; + while (!feof($fp) && !$info['timed_out']) { + $res .= fread($fp, 512); + $info = stream_get_meta_data($fp); + } + fclose($fp); + + if ($info['timed_out']) { + return -1; + } else { + if (substr($res, 9, 3) != '200') + return false; + $page = explode("\r\n\r\n", $res, 2); + return trim($page[1]); + } + } // get_file +} // class TMXInfoFetcher +?> diff --git a/docker-xaseco/xaseco/includes/tmxinfosearcher.inc.php b/docker-xaseco/xaseco/includes/tmxinfosearcher.inc.php new file mode 100644 index 0000000..b699014 --- /dev/null +++ b/docker-xaseco/xaseco/includes/tmxinfosearcher.inc.php @@ -0,0 +1,288 @@ + + * Based on TMXInfoFetcher & http://united.tm-exchange.com/main.aspx?action=threadshow&id=619302 + * + * v1.7: Added Countable interface to searcher class + * v1.6: Fixed an error checking bug + * v1.5: Improved error reporting via $error + * v1.4: Added User-Agent to the GET request + * v1.3: Renamed $worldrec into $custimg to correct its meaning; fixed + * $replayid check + * v1.2: Fixed TMXInfo processing false $track + * v1.1: Optimized get_file URL parsing + * v1.0: Initial release + */ +class TMXInfoSearcher implements Iterator,Countable { + + public $error; + protected $tracks = array(); + private $section; + private $prefix; + + /** + * Searches TMX for tracks matching name, author and/or environment; + * or search TMX for the 10 most recent tracks + * + * @param String $game + * TMX section for 'TMO', 'TMS', 'TMN', 'TMU', 'TMNF' + * @param String $name + * The track name to search for (partial, case-insensitive match) + * @param String $author + * The track author to search for (partial, case-insensitive match) + * @param String $env + * The environment to search for (exact case-insensitive match + * from: Desert, Snow, Rally, Bay, Coast, Island, Stadium); + * ignored when searching TMN or TMNF + * @param Boolean $recent + * If true, ignore search parameters and just return 10 newest tracks + * (max. one per author) + * @return TMXInfoSearcher + * If ->valid() is false, no matching track was found; + * otherwise, an iterator of TMXInfo objects for a 'foreach' loop. + * Returns at most 500 tracks ($maxpage * 20) for TMNF/TMU(F), + * and at most 20 tracks for the other TMX sections. + */ + public function __construct($game, $name, $author, $env, $recent) { + + $this->section = $game; + switch ($game) { + case 'TMO': + $this->prefix = 'original'; + break; + case 'TMS': + $this->prefix = 'sunrise'; + break; + case 'TMN': + $this->prefix = 'nations'; + $env = ''; // ignore possible environment + break; + case 'TMU': + $this->prefix = 'united'; + break; + case 'TMNF': + $this->prefix = 'tmnforever'; + $env = ''; // ignore possible environment + break; + default: + $this->prefix = ''; + $this->error = 'Unknown TMX section: ' . $game; + return; + } + + $this->error = ''; + if ($recent) { + $this->tracks = $this->getRecent(); + } else { + $this->tracks = $this->getList($name, $author, $env); + } + } // __construct + + // define standard Iterator functions + public function rewind() { + reset($this->tracks); + } + public function current() { + return new TMXInfo($this->section, $this->prefix, current($this->tracks)); + } + public function next() { + return new TMXInfo($this->section, $this->prefix, next($this->tracks)); + } + public function key() { + return key($this->tracks); + } + public function valid() { + return (current($this->tracks) !== false); + } + // define standard Countable function + public function count() { + return count($this->tracks); + } + + private function getRecent() { + + // get 10 most recent tracks + $url = 'http://' . $this->prefix . '.tm-exchange.com/apiget.aspx?action=apirecent'; + $file = $this->get_file($url); + if ($file === false) { + $this->error = 'Connection or response error on ' . $url; + return array(); + } else if ($file === -1) { + $this->error = 'Timed out while reading data from ' . $url; + return array(); + } else if ($file == '') { + $this->error = 'No data returned from ' . $url; + return array(); + } + + // check for API error message + if (strpos($file, chr(27)) !== false) { + $this->error = 'Cannot decode recent track info from ' . $url; + return array(); + } + + // return list of tracks as array of strings + return explode("\r\n", $file); + } // getRecent + + private function getList($name, $author, $env) { + + // older TMX sections don't support multi-page results :( + $maxpage = 1; + if ($this->prefix == 'tmnforever' || $this->prefix == 'united') + $maxpage = 25; // max. 500 tracks + + // compile search URL + $url = 'http://' . $this->prefix . '.tm-exchange.com/apiget.aspx?action=apisearch'; + if ($name != '') + $url .= '&track=' . $name; + if ($author != '') + $url .= '&author=' . $author; + if ($env != '') + $url .= '&env=' . $env; + $url .= '&page='; + + $tracks = ''; + $page = 0; + $done = false; + + // get results 20 tracks at a time + while ($page < $maxpage && !$done) { + $file = $this->get_file($url . $page); + if ($file === false) { + $this->error = 'Connection or response error on ' . $url; + return array(); + } else if ($file === -1) { + $this->error = 'Timed out while reading data from ' . $url; + return array(); + } else if ($file == '') { + if ($tracks == '') { + $this->error = 'No data returned from ' . $url; + return array(); + } else { + break; + } + } + + // check for API error message + if (strpos($file, chr(27)) !== false) { + $this->error = 'Cannot decode searched track info from ' . $url; + return array(); + } + + // check for results + if ($file != '') { + // no line break before first page + $tracks .= ($page++ > 0 ? "\r\n" : '') . $file; + } else { + $done = true; + } + } + + // return list of tracks as array of strings + if ($tracks != '') + return explode("\r\n", $tracks); + else + return array(); + } // getList + + // Simple HTTP Get function with timeout + // ok: return string || error: return false || timeout: return -1 + private function get_file($url) { + + $url = parse_url($url); + $port = isset($url['port']) ? $url['port'] : 80; + $query = isset($url['query']) ? "?" . $url['query'] : ""; + + $fp = @fsockopen($url['host'], $port, $errno, $errstr, 4); + if (!$fp) + return false; + + fwrite($fp, 'GET ' . $url['path'] . $query . " HTTP/1.0\r\n" . + 'Host: ' . $url['host'] . "\r\n" . + 'User-Agent: TMXInfoSearcher (' . PHP_OS . ")\r\n\r\n"); + stream_set_timeout($fp, 2); + $res = ''; + $info['timed_out'] = false; + while (!feof($fp) && !$info['timed_out']) { + $res .= fread($fp, 512); + $info = stream_get_meta_data($fp); + } + fclose($fp); + + if ($info['timed_out']) { + return -1; + } else { + if (substr($res, 9, 3) != '200') + return false; + $page = explode("\r\n\r\n", $res, 2); + return trim($page[1]); + } + } // get_file +} // class TMXInfoSearcher + + +class TMXInfo { + + public $section, $prefix, $id, $name, $userid, $author, + $type, $envir, $mood, $style, $routes, $length, $diffic, + $lbrating, $awards, $comments, $custimg, $game, $uploaded, $updated, + $pageurl, $replayid, $replayurl, $imageurl, $thumburl, $dloadurl; + + /** + * Returns track object with a hell of a lot of data from TMX track string + * + * @param String $section + * TMX section + * @param String $prefix + * TMX URL prefix + * @param String $track + * The TMX track string from TMXInfoSearcher + * @return TMXInfo + */ + public function TMXInfo($section, $prefix, $track) { + + $this->section = $section; + $this->prefix = $prefix; + if ($track) { + // separate columns on Tabs + $fields = explode(chr(9), $track); + + $this->id = $fields[0]; + $this->name = $fields[1]; + $this->userid = $fields[2]; + $this->author = $fields[3]; + $this->type = $fields[4]; + $this->envir = $fields[5]; + $this->mood = $fields[6]; + $this->style = $fields[7]; + $this->routes = $fields[8]; + $this->length = $fields[9]; + $this->diffic = $fields[10]; + $this->lbrating = ($fields[11] > 0 ? $fields[11] : 'Classic!'); + $this->awards = $fields[12]; + $this->comments = $fields[13]; + $this->custimg = (strtolower($fields[14]) == 'true'); + $this->game = $fields[15]; + $this->replayid = $fields[16]; + // unknown = $fields[17-21]; + $this->uploaded = $fields[22]; + $this->updated = $fields[23]; + + $this->pageurl = 'http://' . $prefix . '.tm-exchange.com/main.aspx?action=trackshow&id=' . $this->id; + $this->imageurl = 'http://' . $prefix . '.tm-exchange.com/get.aspx?action=trackscreen&id=' . $this->id; + $this->thumburl = 'http://' . $prefix . '.tm-exchange.com/get.aspx?action=trackscreensmall&id=' . $this->id; + $this->dloadurl = 'http://' . $prefix . '.tm-exchange.com/get.aspx?action=trackgbx&id=' . $this->id; + + if ($this->replayid > 0) { + $this->replayurl = 'http://' . $prefix . '.tm-exchange.com/get.aspx?action=recordgbx&id=' . $this->replayid; + } else { + $this->replayurl = ''; + } + } + } // TMXInfo +} // class TMXInfo +?> diff --git a/docker-xaseco/xaseco/includes/types.inc.php b/docker-xaseco/xaseco/includes/types.inc.php new file mode 100644 index 0000000..277803a --- /dev/null +++ b/docker-xaseco/xaseco/includes/types.inc.php @@ -0,0 +1,511 @@ +record_list = array(); + $this->max = $limit; + } + + function setLimit($limit) { + $this->max = $limit; + } + + function getRecord($rank) { + if (isset($this->record_list[$rank])) + return $this->record_list[$rank]; + else + return false; + } + + function setRecord($rank, $record) { + if (isset($this->record_list[$rank])) { + return $this->record_list[$rank] = $record; + } else { + return false; + } + } + + function moveRecord($from, $to) { + moveArrayElement($this->record_list, $from, $to); + } + + function addRecord($record, $rank = -1) { + + // if no rank was set for this record, then put it to the end of the list + if ($rank == -1) { + $rank = count($this->record_list); + } + + // do not insert a record behind the border of the list + if ($rank >= $this->max) return; + + // do not insert a record with no score + if ($record->score <= 0) return; + + // if the given object is a record + if (get_class($record) == 'Record') { + + // if records are getting too much, drop the last from the list + if (count($this->record_list) >= $this->max) { + array_pop($this->record_list); + } + + // insert the record at the specified position + return insertArrayElement($this->record_list, $record, $rank); + } + } + + function delRecord($rank = -1) { + + // do not remove a record outside the current list + if ($rank < 0 || $rank >= count($this->record_list)) return; + + // remove the record from the specified position + return removeArrayElement($this->record_list, $rank); + } + + function count() { + return count($this->record_list); + } + + function clear() { + $this->record_list = array(); + } +} // class RecordList + + +/** + * Structure of a Player. + * Can be instantiated with an RPC 'GetPlayerInfo' or + * 'GetDetailedPlayerInfo' response. + */ +class Player { + var $id; + var $pid; + var $login; + var $nickname; + var $teamname; + var $ip; + var $client; + var $ipport; + var $zone; + var $nation; + var $prevstatus; + var $isspectator; + var $isofficial; + var $rights; + var $language; + var $avatar; + var $teamid; + var $unlocked; + var $ladderrank; + var $ladderscore; + var $created; + var $wins; + var $newwins; + var $timeplayed; + var $tracklist; + var $playerlist; + var $msgs; + var $pmbuf; + var $mutelist; + var $mutebuf; + var $style; + var $panels; + var $speclogin; + var $dedirank; + + function getWins() { + return $this->wins + $this->newwins; + } + + function getTimePlayed() { + return $this->timeplayed + $this->getTimeOnline(); + } + + function getTimeOnline() { + return $this->created > 0 ? time() - $this->created : 0; + } + + // instantiates the player with an RPC response + function Player($rpc_infos = null) { + $this->id = 0; + if ($rpc_infos) { + $this->pid = $rpc_infos['PlayerId']; + $this->login = $rpc_infos['Login']; + $this->nickname = $rpc_infos['NickName']; + $this->ipport = $rpc_infos['IPAddress']; + $this->ip = preg_replace('/:\d+/', '', $rpc_infos['IPAddress']); // strip port + $this->prevstatus = false; + $this->isspectator = $rpc_infos['IsSpectator']; + $this->isofficial = $rpc_infos['IsInOfficialMode']; + $this->teamname = $rpc_infos['LadderStats']['TeamName']; + if (isset($rpc_infos['Nation'])) { // TMN (TMS/TMO?) + $this->zone = $rpc_infos['Nation']; + $this->nation = $rpc_infos['Nation']; + $this->ladderrank = $rpc_infos['LadderStats']['Ranking']; + $this->ladderscore = $rpc_infos['LadderStats']['Score']; + $this->client = ''; + $this->rights = false; + $this->language = ''; + $this->avatar = ''; + $this->teamid = 0; + } else { // TMF + $this->zone = substr($rpc_infos['Path'], 6); // strip 'World|' + $this->nation = explode('|', $rpc_infos['Path']); + if (isset($this->nation[1])) + $this->nation = $this->nation[1]; + else + $this->nation = ''; + $this->ladderrank = $rpc_infos['LadderStats']['PlayerRankings'][0]['Ranking']; + $this->ladderscore = round($rpc_infos['LadderStats']['PlayerRankings'][0]['Score'], 2); + $this->client = $rpc_infos['ClientVersion']; + $this->rights = ($rpc_infos['OnlineRights'] == 3); // United = true + $this->language = $rpc_infos['Language']; + $this->avatar = $rpc_infos['Avatar']['FileName']; + $this->teamid = $rpc_infos['TeamId']; + } + $this->created = time(); + } else { + // set defaults + $this->pid = 0; + $this->login = ''; + $this->nickname = ''; + $this->ipport = ''; + $this->ip = ''; + $this->prevstatus = false; + $this->isspectator = false; + $this->isofficial = false; + $this->teamname = ''; + $this->zone = ''; + $this->nation = ''; + $this->ladderrank = 0; + $this->ladderscore = 0; + $this->rights = false; + $this->created = 0; + } + $this->wins = 0; + $this->newwins = 0; + $this->timeplayed = 0; + $this->unlocked = false; + $this->pmbuf = array(); + $this->mutelist = array(); + $this->mutebuf = array(); + $this->style = array(); + $this->panels = array(); + $this->speclogin = ''; + $this->dedirank = 0; + } +} // class Player + +/** + * Manages players on the server. + * Add player and remove them. + */ +class PlayerList { + var $player_list; + + // instantiates the empty player list + function PlayerList() { + $this->player_list = array(); + } + + function nextPlayer() { + if (is_array($this->player_list)) { + $player_item = current($this->player_list); + next($this->player_list); + return $player_item; + } else { + $this->resetPlayers(); + return false; + } + } + + function resetPlayers() { + if (is_array($this->player_list)) { + reset($this->player_list); + } + } + + function addPlayer($player) { + if (get_class($player) == 'Player' && $player->login != '') { + $this->player_list[$player->login] = $player; + return true; + } else { + return false; + } + } + + function removePlayer($login) { + if (isset($this->player_list[$login])) { + $player = $this->player_list[$login]; + unset($this->player_list[$login]); + } else { + $player = false; + } + return $player; + } + + function getPlayer($login) { + if (isset($this->player_list[$login])) + return $this->player_list[$login]; + else + return false; + } +} // class PlayerList + + +/** + * Can store challenge information. + * You can instantiate with an RPC 'GetChallengeInfo' response. + */ +class Challenge { + var $id; + var $name; + var $uid; + var $filename; + var $author; + var $environment; + var $mood; + var $bronzetime; + var $silvertime; + var $goldtime; + var $authortime; + var $copperprice; + var $laprace; + var $forcedlaps; + var $nblaps; + var $nbchecks; + var $score; + var $starttime; + var $gbx; + var $tmx; + + // instantiates the challenge with an RPC response + function Challenge($rpc_infos = null) { + $this->id = 0; + if ($rpc_infos) { + $this->name = stripNewlines($rpc_infos['Name']); + $this->uid = $rpc_infos['UId']; + $this->filename = $rpc_infos['FileName']; + $this->author = $rpc_infos['Author']; + $this->environment = $rpc_infos['Environnement']; + $this->mood = $rpc_infos['Mood']; + $this->bronzetime = $rpc_infos['BronzeTime']; + $this->silvertime = $rpc_infos['SilverTime']; + $this->goldtime = $rpc_infos['GoldTime']; + $this->authortime = $rpc_infos['AuthorTime']; + $this->copperprice = $rpc_infos['CopperPrice']; + $this->laprace = $rpc_infos['LapRace']; + $this->forcedlaps = 0; + if (isset($rpc_infos['NbLaps'])) + $this->nblaps = $rpc_infos['NbLaps']; + else + $this->nblaps = 0; + if (isset($rpc_infos['NbCheckpoints'])) + $this->nbchecks = $rpc_infos['NbCheckpoints']; + else + $this->nbchecks = 0; + } else { + // set defaults + $this->name = 'undefined'; + } + } +} // class Challenge + + +/** + * Contains information about an RPC call. + */ +class RPCCall { + var $index; + var $id; + var $callback; + var $call; + + // instantiates the RPC call with the parameters + function RPCCall($id, $index, $callback, $call) { + $this->id = $id; + $this->index = $index; + $this->callback = $callback; + $this->call = $call; + } +} // class RPCCall + + +/** + * Contains information about a chat command. + */ +class ChatCommand { + var $name; + var $help; + var $isadmin; + + // instantiates the chat command with the parameters + function ChatCommand($name, $help, $isadmin) { + $this->name = $name; + $this->help = $help; + $this->isadmin = $isadmin; + } +} // class ChatCommand + + +/** + * Stores basic information of the server XASECO is running on. + */ +class Server { + var $id; + var $name; + var $game; + var $serverlogin; + var $nickname; + var $zone; + var $rights; + var $ip; + var $port; + var $timeout; + var $version; + var $build; + var $packmask; + var $laddermin; + var $laddermax; + var $login; + var $pass; + var $maxplay; + var $maxspec; + var $challenge; + var $records; + var $players; + var $mutelist; + var $gamestate; + var $gameinfo; + var $gamedir; + var $trackdir; + var $votetime; + var $voterate; + var $uptime; + var $starttime; + var $isrelay; + var $relaymaster; + var $relayslist; + + // game states + const RACE = 'race'; + const SCORE = 'score'; + + function getGame() { + switch ($this->game) { + case 'TmForever': + return 'TMF'; + case 'TmNationsESWC': + return 'TMN'; + case 'TmSunrise': + return 'TMS'; + case 'TmOriginal': + return 'TMO'; + default: // TMU was never supported + return 'Unknown'; + } + } + + // instantiates the server with default parameters + function Server($ip, $port, $login, $pass) { + $this->ip = $ip; + $this->port = $port; + $this->login = $login; + $this->pass = $pass; + $this->starttime = time(); + } +} // class Server + +/** + * Contains information to the current game which is played. + */ +class Gameinfo { + var $mode; + var $numchall; + var $rndslimit; + var $timelimit; + var $teamlimit; + var $lapslimit; + var $cuplimit; + var $forcedlaps; + + const RNDS = 0; + const TA = 1; + const TEAM = 2; + const LAPS = 3; + const STNT = 4; + const CUP = 5; + + // returns current game mode as string + function getMode() { + switch ($this->mode) { + case self::RNDS: + return 'Rounds'; + case self::TA: + return 'TimeAttack'; + case self::TEAM: + return 'Team'; + case self::LAPS: + return 'Laps'; + case self::STNT: + return 'Stunts'; + case self::CUP: + return 'Cup'; + default: + return 'Undefined'; + } + } + + // instantiates the game info with an RPC response + function Gameinfo($rpc_infos = null) { + if ($rpc_infos) { + $this->mode = $rpc_infos['GameMode']; + $this->numchall = $rpc_infos['NbChallenge']; + if (isset($rpc_infos['RoundsUseNewRules']) && $rpc_infos['RoundsUseNewRules']) + $this->rndslimit = $rpc_infos['RoundsPointsLimitNewRules']; + else + $this->rndslimit = $rpc_infos['RoundsPointsLimit']; + $this->timelimit = $rpc_infos['TimeAttackLimit']; + if (isset($rpc_infos['TeamUseNewRules']) && $rpc_infos['TeamUseNewRules']) + $this->teamlimit = $rpc_infos['TeamPointsLimitNewRules']; + else + $this->teamlimit = $rpc_infos['TeamPointsLimit']; + $this->lapslimit = $rpc_infos['LapsTimeLimit']; + if (isset($rpc_infos['CupPointsLimit'])) + $this->cuplimit = $rpc_infos['CupPointsLimit']; + if (isset($rpc_infos['RoundsForcedLaps'])) + $this->forcedlaps = $rpc_infos['RoundsForcedLaps']; + else + $this->forcedlaps = 0; + } else { + $this->mode = -1; + } + } +} // class Gameinfo +?> diff --git a/docker-xaseco/xaseco/includes/urlsafebase64.php b/docker-xaseco/xaseco/includes/urlsafebase64.php new file mode 100644 index 0000000..958cd7b --- /dev/null +++ b/docker-xaseco/xaseco/includes/urlsafebase64.php @@ -0,0 +1,26 @@ + diff --git a/docker-xaseco/xaseco/includes/votes.config.php b/docker-xaseco/xaseco/includes/votes.config.php new file mode 100644 index 0000000..f5f5484 --- /dev/null +++ b/docker-xaseco/xaseco/includes/votes.config.php @@ -0,0 +1,151 @@ + 1, // endround + 1 => 2, // ladder + 2 => 3, // replay + 3 => 2, // skip + 4 => 3, // kick + 5 => 3, // add + 6 => 3, // ignore + ); + // set to true to show a vote reminder at each of those rounds + $r_show_reminder = true; + + // maximum number of seconds before a vote expires + $ta_expire_limit = array( // seconds + 0 => 0, // endround, N/A + 1 => 90, // ladder + 2 => 120, // replay + 3 => 90, // skip + 4 => 120, // kick + 5 => 120, // add + 6 => 120, // ignore + ); + // set to true to show a vote reminder at an (approx.) interval + $ta_show_reminder = true; + // interval length at which to (approx.) repeat reminder + $ta_show_interval = 30; // seconds + + // check for active voting system + if ($feature_votes) { + // disable CallVotes + $aseco->client->query('SetCallVoteRatio', 1.0); + + // really disable all CallVotes on TMF + if ($aseco->server->getGame() == 'TMF') { + $ratios = array(array('Command' => '*', 'Ratio' => -1.0)); + $aseco->client->query('SetCallVoteRatios', $ratios); + } + + // if 2, the voting explanation is sent to all players when one + // new player joins; use this during an introduction period + // if 1, the voting explanation is only sent to the new player + // upon joining + // if 0, no explanations are sent at all + $global_explain = 2; + + // define the vote ratios for all types + $vote_ratios = array( + 0 => 0.4, // endround + 1 => 0.5, // ladder + 2 => 0.6, // replay + 3 => 0.6, // skip + 4 => 0.7, // kick + 5 => 1.0, // add - ignored, defined by $tmxvoteratio + 6 => 0.6, // ignore + ); + + // divert vote messages to TMF message window? + $vote_in_window = false; + + // disable voting commands while an admin (any tier) is online? + $disable_upon_admin = false; + + // disable voting commands during scoreboard at end of track? + $disable_while_sb = true; + + // allow kicks & allow user to kick-vote any admin? + $allow_kickvotes = true; + $allow_admin_kick = false; + // allow ignores & allow user to ignore-vote any admin? + $allow_ignorevotes = true; + $allow_admin_ignore = false; + + // maximum number of these votes per track; set to 0 to disable a + // vote type, or to some really high number for unlimited votes + $max_laddervotes = 2; + $max_replayvotes = 2; + $max_skipvotes = 2; + + // limit the number of times a track can be /replay-ed; 0 = unlimited + $replays_limit = 0; + + // if true, does restart via quick ChallengeRestart + // this is what most users are accustomed to, but it stops + // a track's music (if in use) + // if false, does restart via jukebox prepend & NextChallenge + // this takes longer and may confuse users into thinking + // the restart is actually loading the next track, but + // it insures music resumes playing + $ladder_fast_restart = true; + + // enable Rounds points limits? use this to restrict the use of the + // track-related votes if the _first_ player already has reached a + // specific percentage of the server's Rounds points limit + $r_points_limits = true; + + // percentage of Rounds points limit _after_ which /ladder is disabled + $r_ladder_max = 0.4; + // percentage of Rounds points limit _before_ which /replay is disabled + $r_replay_min = 0.5; + // percentage of Rounds points limit _after_ which /skip is disabled + $r_skip_max = 0.5; + + // enable Time Attack time limits? use this to restrict the use of the + // track-related votes if the current track is already _running_ for a + // specific percentage of the server's TA time limit + // this requires function time_playing() from plugin.track.php + $ta_time_limits = true; + + // percentage of TA time limit _after_ which /ladder is disabled + $ta_ladder_max = 0.4; + // percentage of TA time limit _before_ which /replay is disabled + $ta_replay_min = 0.5; + // percentage of TA time limit _after_ which /skip is disabled + $ta_skip_max = 0.5; + + // no restrictions in other modes + } +?> diff --git a/docker-xaseco/xaseco/includes/web_access.inc.php b/docker-xaseco/xaseco/includes/web_access.inc.php new file mode 100644 index 0000000..174b7e3 --- /dev/null +++ b/docker-xaseco/xaseco/includes/web_access.inc.php @@ -0,0 +1,1341 @@ +request($url, array('func_name',xxx), $datas, $is_xmlrpc, $keepalive_min_timeout); +// $url: the web script URL. +// $datas: string to send in http body (xml, xml_rpc or POST data) +// $is_xmlrpc: true if it's an xml or xml-rpc request, false if it's a +// standard html GET or POST +// $keepalive_min_timeout: minimal value of server keepalive timeout to +// send a keepalive request, +// else make a request with close connection. +// func_name is the callback function name, which will be called this way: +// func_name(array('Code'=>code,'Reason'=>reason,'Headers'=>headers,'Message'=>message), xxx) +// where: +// xxx is the same as given previously in callback description. +// code is the returned http code +// (http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6) +// reason is the returned http reason +// (http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6) +// headers are the http headers of the reply +// message is the returned text body +// +// IMPORTANT: to have this work, the main part of your program must include a +// $_webaccess->select() call periodically, which work exactly like stream_select(). +// This is because the send and receive are asynchronous and will be completed +// later in the select() when datas are ready to be received and sent. + + +// This class can be used to make a synchronous query too. For that use null +// for the callback, so make the request this way: +// $response = $_webaccess->request($url, null, $datas, $is_xmlrpc, $keepalive_min_timeout); +// where $response is an array('Code'=>code,'Reason'=>reason,'Headers'=>headers,'Message'=>message) +// like the one passed to the callback function of the asynchronous request. +// If you use only synchronous queries then there is no need to call select() +// as the function will return when the reply will be fully returned. +// If the connection itself fail, the array response will include a 'Error' string. + + +// Other functions: +// list($host, $port, $path) = getHostPortPath($url); +// gzdecode() workaround + + +global $_web_access_compress_xmlrpc_request, $_web_access_compress_reply, + $_web_access_keepalive, $_web_access_keepalive_timeout, + $_web_access_keepalive_max, $_web_access_retry_timeout, + $_web_access_retry_timeout_max, $_web_access_post_xmlrpc; + + +// Will compress xmlrpc request ('never','accept','force','force-gzip','force-deflate') +// If set to 'accept' the first request will be made without, and the eventual +// 'Accept-Encoding' in reply will permit to decide if request compression can +// be used (and if gzip or deflate) +$_web_access_compress_xmlrpc_request = 'accept'; + +// Will ask server for compressed reply (false, true) +// If true then will add a 'Accept-Encoding' header to tell the server to +// compress the reply if it supports it. +$_web_access_compress_reply = true; + + +// Keep alive connection ? else close it after the reply. +// Unless false, first request will be with keepalive, to get server timeout +// and max values after timeout will be compared with the request +// $keepalive_min_timeout value to decide if keepalive have to be used or not. +// Note that Apache2 timeout is short (about 15s). +// The classes will open, re-open or use existing connection as needed. +$_web_access_keepalive = true; +// timeout(s) without request before close, for keepalive +$_web_access_keepalive_timeout = 600; +// max requests before close, for keepalive +$_web_access_keepalive_max = 2000; + + +// For asynchronous call, in case of error, timeout before retrying. +// It will be x2 for each error (on request or auto retry) until max, +// then stop automatic retry, and next request calls will return false. +// When stopped, a retry() or synchronous request will force a retry. +$_web_access_retry_timeout = 20; +$_web_access_retry_timeout_max = 5*60; + + +// Use text/html with xmlrpc=, instead of of pure text/xml request (false, true) +// Standard xml-rpc use pure text/xml request, where the xml is simply the body +// of the http request (and it's how the xml-rpc reply will be made). As a +// facility Dedimania also supports to get the xml in a html GET or POST, +// where xmlrpc= will contain a urlsafe base64 of the xml. Default to false, +// so use pure text/xml. +$_web_access_post_xmlrpc = false; + +// Note that in each request the text/xml or xmlrpc= will be used only if +// $is_xmlrpc is true. If false then the request will be a standard +// application/x-www-form-urlencoded html GET or POST request; in that case +// you have to build the URL (GET) and/or body data (POST) yourself. +// If $is_xmlrpc is a string, then it's used as the Content-type: value. + + +require_once('includes/urlsafebase64.php'); + +class Webaccess { + + var $_WebaccessList; + + function Webaccess() { + $this->_WebaccessList = array(); + } + + + function request($url, $callback, $datas, $is_xmlrpc = false, $keepalive_min_timeout = 300, $opentimeout = 3, $waittimeout = 5, $agent = 'XMLaccess') { + global $aseco, $_web_access_keepalive, $_web_access_keepalive_timeout, $_web_access_keepalive_max; + + list($host, $port, $path) = getHostPortPath($url); + + if ($host === false) + $aseco->console('Webaccess request(): Bad URL: ' . $url); + + else { + $server = $host . ':' . $port; + // create object if needed + if (!isset($this->_WebaccessList[$server]) || $this->_WebaccessList[$server] === null) { + $this->_WebaccessList[$server] = new WebaccessUrl($this, $host, $port, + $_web_access_keepalive, + $_web_access_keepalive_timeout, + $_web_access_keepalive_max, + $agent); + } + + // increase the default timeout for sync/wait request + if ($callback == null && $waittimeout == 5) + $waittimeout = 12; + + // call request + if ($this->_WebaccessList[$server] !== null) { + $query = array('Path' => $path, + 'Callback' => $callback, + 'QueryData' => $datas, + 'IsXmlrpc' => $is_xmlrpc, + 'KeepaliveMinTimeout' => $keepalive_min_timeout, + 'OpenTimeout' => $opentimeout, + 'WaitTimeout' => $waittimeout + ); + + return $this->_WebaccessList[$server]->request($query); + } + } + return false; + } // request + + + function retry($url) { + global $aseco; + + list($host, $port, $path) = getHostPortPath($url); + if ($host === false) { + $aseco->console('Webaccess retry(): Bad URL: ' . $url); + } else { + $server = $host . ':' . $port; + if (isset($this->_WebaccessList[$server])) + $this->_WebaccessList[$server]->retry(); + } + } // retry + + + function select(&$read, &$write, &$except, $tv_sec, $tv_usec = 0) { + + $timeout = (int)($tv_sec*1000000 + $tv_usec); + if ($read == null) + $read = array(); + if ($write == null) + $write = array(); + if ($except == null) + $except = array(); + + $read = $this->_getWebaccessReadSockets($read); + $write = $this->_getWebaccessWriteSockets($write); + //$except = $this->_getWebaccessReadSockets($except); + + //print_r($this->_WebaccessList); + //print_r($read); + //print_r($write); + //print_r($except); + + // if no socket to select then return + if (count($read) + count($write) + count($except) == 0) { + // sleep the asked timeout... + if ($timeout > 1000) + usleep($timeout); + return 0; + } + + $utime = (int)(microtime(true)*1000000); + $nb = @stream_select($read, $write, $except, $tv_sec, $tv_usec); + if ($nb === false) { + // in case stream_select "forgot" to wait, sleep the remaining asked timeout... + $dtime = (int)(microtime(true)*1000000) - $utime; + $timeout -= $dtime; + if ($timeout > 1000) + usleep($timeout); + return false; + } + + $this->_manageWebaccessSockets($read, $write, $except); + // workaround for stream_select bug with amd64, replace $nb with sum of arrays + return count($read) + count($write) + count($except); + } // select + + + private function _manageWebaccessSockets(&$receive, &$send, &$except) { + + // send pending data on all webaccess sockets + if (is_array($send) && count($send) > 0) { + foreach ($send as $key => $socket) { + $i = $this->_findWebaccessSocket($socket); + if ($i !== false) { + if (isset($this->_WebaccessList[$i]->_spool[0]['State']) && + $this->_WebaccessList[$i]->_spool[0]['State'] == 'OPEN') + $this->_WebaccessList[$i]->_open(); + else + $this->_WebaccessList[$i]->_send(); + unset($send[$key]); + } + } + } + + // read data from all needed webaccess sockets + if (is_array($receive) && count($receive) > 0) { + foreach ($receive as $key => $socket) { + $i = $this->_findWebaccessSocket($socket); + if ($i !== false) { + $this->_WebaccessList[$i]->_receive(); + unset($receive[$key]); + } + } + } + } // _manageWebaccessSockets + + + private function _findWebaccessSocket($socket) { + + foreach ($this->_WebaccessList as $key => $wau) { + if ($wau->_socket == $socket) + return $key; + } + return false; + } // _findWebaccessSocket + + + private function _getWebaccessReadSockets($socks) { + + foreach ($this->_WebaccessList as $key => $wau) { + if ($wau->_state == 'OPENED' && $wau->_socket) + $socks[] = $wau->_socket; + } + return $socks; + } // _getWebaccessReadSockets + + + private function _getWebaccessWriteSockets($socks) { + + foreach ($this->_WebaccessList as $key => $wau) { + if (isset($wau->_spool[0]['State']) && + ($wau->_spool[0]['State'] == 'OPEN' || + $wau->_spool[0]['State'] == 'BAD' || + $wau->_spool[0]['State'] == 'SEND')) { + + if (($wau->_state == 'CLOSED' || $wau->_state == 'BAD') && !$wau->_socket) + $wau->_open(); + + if ($wau->_state == 'OPENED' && $wau->_socket) + $socks[] = $wau->_socket; + } + } + return $socks; + } // _getWebaccessWriteSockets + + + function getAllSpools() { + + $num = 0; + $bad = 0; + foreach ($this->_WebaccessList as $key => $wau) { + if ($wau->_state == 'OPENED' || $wau->_state == 'CLOSED') + $num += count($wau->_spool); + elseif ($wau->_state == 'BAD') + $bad += count($wau->_spool); + } + return array($num, $bad); + } // getAllSpools +} // class Webaccess + + +// useful data to handle received headers +$_wa_header_separator = array('cookie' => ';', 'set-cookie' => ';'); +$_wa_header_multi = array('set-cookie' => true); + + +class WebaccessUrl { + //----------------------------- + // Fields + //----------------------------- + + var $wa; + var $_host; + var $_port; + var $_compress_request; + var $_socket; + var $_state; + var $_keepalive; + var $_keepalive_timeout; + var $_keepalive_max; + var $_serv_keepalive_timeout; + var $_serv_keepalive_max; + var $_spool; + var $_wait; + var $_response; + var $_query_num; + var $_request_time; + var $_cookies; + var $_webaccess_str; + var $_bad_time; + var $_bad_timeout; + var $_read_time; + var $_agent; + + // $_state values: + // 'OPENED' : socket is opened + // 'CLOSED' : socket is closed (asked, completed, or closed by server) + // 'BAD' : socket is closed, bad/error or beginning state + + // $query['State'] values: (note: $query is added in $_spool, so $this->_spool[0] is the first $query to handle) + // 'BAD' : bad/error or beginning state + // 'OPEN' : should prepare request data then send them + // 'SEND' : request data are prepared, send them + // 'RECEIVE': request data are sent, receive reply data + // 'DONE' : request completed + + //----------------------------- + // Methods + //----------------------------- + + function WebaccessUrl(&$wa, $host, $port, $keepalive = true, $keepalive_timeout = 600, $keepalive_max = 300, $agent = 'XMLaccess') { + global $_web_access_compress_xmlrpc_request, $_web_access_retry_timeout; + + $this->wa = &$wa; + $this->_host = $host; + $this->_port = $port; + $this->_webaccess_str = 'Webaccess (' . $this->_host . ':' . $this->_port . '): '; + $this->_agent = $agent; + + // request compression setting + if ($_web_access_compress_xmlrpc_request == 'accept') + $this->_compress_request = 'accept'; + elseif ($_web_access_compress_xmlrpc_request == 'force') { + if (function_exists('gzencode')) + $this->_compress_request = 'gzip'; + elseif (function_exists('gzdeflate')) + $this->_compress_request = 'deflate'; + else + $this->_compress_request = false; + } + elseif ($_web_access_compress_xmlrpc_request == 'force-gzip' && function_exists('gzencode')) + $this->_compress_request = 'gzip'; + elseif ($_web_access_compress_xmlrpc_request == 'force-deflate' && function_exists('gzdeflate')) + $this->_compress_request = 'deflate'; + else + $this->_compress_request = false; + + $this->_socket = null; + $this->_state = 'CLOSED'; + $this->_keepalive = $keepalive; + $this->_keepalive_timeout = $keepalive_timeout; + $this->_keepalive_max = $keepalive_max; + $this->_serv_keepalive_timeout = $keepalive_timeout; + $this->_serv_keepalive_max = $keepalive_max; + $this->_spool = array(); + $this->_wait = false; + $this->_response = ''; + $this->_query_num = 0; + $this->_query_time = time(); + $this->_cookies = array(); + $this->_bad_time = time(); + $this->_bad_timeout = 0; + $this->_read_time = 0; + } // WebaccessUrl + + + // put connection in BAD state + function _bad($errstr, $isbad = true) { + global $aseco, $_web_access_retry_timeout; + + $aseco->console($this->_webaccess_str . $errstr); + $this->infos(); + + if ($this->_socket) + @fclose($this->_socket); + $this->_socket = null; + + if ($isbad) { + if (isset($this->_spool[0]['State'])) + $this->_spool[0]['State'] = 'BAD'; + $this->_state = 'BAD'; + + $this->_bad_time = time(); + if ($this->_bad_timeout < $_web_access_retry_timeout) + $this->_bad_timeout = $_web_access_retry_timeout; + else + $this->_bad_timeout *= 2; + + } else { + if (isset($this->_spool[0]['State'])) + $this->_spool[0]['State'] = 'OPEN'; + $this->_state = 'CLOSED'; + } + $this->_callCallback($this->_webaccess_str . $errstr); + } // _bad + + + function retry() { + global $_web_access_retry_timeout; + + if ($this->_state == 'BAD') { + $this->_bad_time = time(); + $this->_bad_timeout = 0; + } + } // retry + + + //$query = array('Path' => $path, + // 'Callback' => $callback, + // 'QueryData' => $datas, + // 'IsXmlrpc' => $is_xmlrpc, + // 'KeepaliveMinTimeout' => $keepalive_min_timeout, + // 'OpenTimeout' => $opentimeout, + // 'WaitTimeout' => $waittimeout ); + // will add: 'State', 'HDatas', 'Datas', 'DatasSize', 'DatasSent', + // 'Response', 'ResponseSize', 'Headers', 'Close', 'Times' + function request(&$query) { + global $aseco, $_web_access_compress_reply, $_web_access_post_xmlrpc, $_web_access_retry_timeout, $_web_access_retry_timeout_max; + + $query['State'] = 'BAD'; + $query['HDatas'] = ''; + $query['Datas'] = ''; + $query['DatasSize'] = 0; + $query['DatasSent'] = 0; + $query['Response'] = ''; + $query['ResponseSize'] = 0; + $query['Headers'] = array(); + $query['Close'] = false; + $query['Times'] = array('open' => array(-1.0,-1.0), 'send' => array(-1.0,-1.0), 'receive' => array(-1.0,-1.0,0)); + + // if asynch, in error, and maximal timeout, then forget the request and return false + if (($query['Callback'] != null) && ($this->_state == 'BAD')) { + if ($this->_bad_timeout > $_web_access_retry_timeout_max) { + $aseco->console($this->_webaccess_str . 'Request refused for consecutive errors (' . $this->_bad_timeout . ' / ' . $_web_access_retry_timeout_max . ')'); + return false; + + } else { + // if not max then accept the request and try a request (minimum $_web_access_retry_timeout/2 after previous try) + $time = time(); + $timeout = ($this->_bad_timeout / 2) - ($time - $this->_bad_time); + if ($timeout < 0) + $timeout = 0; + $this->_bad_time = $time - $this->_bad_timeout + $timeout; + } + } + + // build data to send + if (($query['Callback'] == null) || (is_array($query['Callback']) && + isset($query['Callback'][0]) && + is_callable($query['Callback'][0]))) { + + if (is_string($query['QueryData']) && strlen($query['QueryData']) > 0) { + $msg = "POST " . $query['Path'] . " HTTP/1.1\r\n"; + $msg .= "Host: " . $this->_host . "\r\n"; + $msg .= "User-Agent: " . $this->_agent . "\r\n"; + $msg .= "Cache-Control: no-cache\r\n"; + + if ($_web_access_compress_reply) { + // ask compression of response if gzdecode() and/or gzinflate() is available + if (function_exists('gzdecode') && function_exists('gzinflate')) + $msg .= "Accept-Encoding: deflate, gzip\r\n"; + elseif (function_exists('gzdecode')) + $msg .= "Accept-Encoding: gzip\r\n"; + elseif (function_exists('gzinflate')) + $msg .= "Accept-Encoding: deflate\r\n"; + } + + //echo "\nData:\n\n" . $query['QueryData'] . "\n"; + + if ($query['IsXmlrpc'] === true) { + if ($_web_access_post_xmlrpc) { + $msg .= "Content-type: application/x-www-form-urlencoded; charset=UTF-8\r\n"; + + //echo "\n=========================== Data =================================\n\n" . $datas . "\n"; + //$d2 = urlsafe_base64_encode($datas); + //$d3 = urlsafe_base64_decode($d2); + //echo "\n--------------------------- Data ---------------------------------\n\n" . $d3 . "\n"; + + $query['QueryData'] = 'xmlrpc=' . urlsafe_base64_encode($query['QueryData']); + } + else { + $msg .= "Content-type: text/xml; charset=UTF-8\r\n"; + } + + if ($this->_compress_request == 'gzip' && function_exists('gzencode')) { + $msg .= "Content-Encoding: gzip\r\n"; + $query['QueryData'] = gzencode($query['QueryData']); + } elseif ($this->_compress_request == 'deflate' && function_exists('gzdeflate')) { + $msg .= "Content-Encoding: deflate\r\n"; + $query['QueryData'] = gzdeflate($query['QueryData']); + } + + } + elseif (is_string($query['IsXmlrpc'])) { + $msg .= "Content-type: " . $query['IsXmlrpc'] . "\r\n"; + $msg .= "Accept: */*\r\n"; + } else { + $msg .= "Content-type: application/x-www-form-urlencoded; charset=UTF-8\r\n"; + } + $msg .= "Content-length: " . strlen($query['QueryData']) . "\r\n"; + $query['HDatas'] = $msg; + $query['State'] = 'OPEN'; + $query['Retries'] = 0; + + //print_r($msg); + + // add the query in spool + $this->_spool[] = &$query; + + if ($query['Callback'] == null) { + $this->_wait = true; + $this->_open($query['OpenTimeout'], $query['WaitTimeout']); // wait more in not callback mode + $this->_spool = array(); + $this->_wait = false; + return $query['Response']; + } else + $this->_open(); + + } else { + $aseco->console($this->_webaccess_str . 'Bad data'); + return false; + } + + } else { + $aseco->console($this->_webaccess_str . 'Bad callback function: ' . $query['Callback']); + return false; + } + return true; + } // request + + + // open the socket (close it before if needed) + private function _open_socket($opentimeout = 0.0) { + global $aseco; + + // if socket not opened, then open it (2 tries) + if (!$this->_socket || $this->_state != 'OPENED') { + $time = microtime(true); + $this->_spool[0]['Times']['open'][0] = $time; + + $errno = ''; + $errstr = ''; + $this->_socket = @fsockopen($this->_host, $this->_port, $errno, $errstr, 1.8); // first try + if (!$this->_socket) { + + if ($opentimeout >= 1.0) + $this->_socket = @fsockopen($this->_host, $this->_port, $errno, $errstr, $opentimeout); + if (!$this->_socket) { + $this->_bad('Error(' . $errno . ') ' . $errstr . ', connection failed!'); + return; + } + } + $this->_state = 'OPENED'; + //$aseco->console($this->_webaccess_str . 'connection opened!'); + + // new socket connection: reset all pending request original values + for ($i = 0; $i < count($this->_spool); $i++) { + $this->_spool[$i]['State'] = 'OPEN'; + $this->_spool[$i]['DatasSent'] = 0; + $this->_spool[$i]['Response'] = ''; + $this->_spool[$i]['Headers'] = array(); + } + $this->_response = ''; + $this->_query_num = 0; + $this->_query_time = time(); + $time = microtime(true); + $this->_spool[0]['Times']['open'][1] = $time - $this->_spool[0]['Times']['open'][0]; + } + } // _open_socket + + + // open the connection (if not already opened) and send + function _open($opentimeout = 0.0, $waittimeout = 5.0) { + global $aseco, $_web_access_retry_timeout_max; + + if (!isset($this->_spool[0]['State'])) + return false; + $time = time(); + + // if asynch, in error, then return false until timeout or if >max) + if (!$this->_wait && $this->_state == 'BAD' && + (($this->_bad_timeout > $_web_access_retry_timeout_max) || + (($time - $this->_bad_time) < $this->_bad_timeout))) { + //$aseco->console($this->_webaccess_str . 'wait to retry (' . ($time - $this->_bad_time) . ' / ' . $this->_bad_timeout . ')'); + return false; + } + + // if the socket is probably in timeout, close it + if ($this->_socket && $this->_state == 'OPENED' && + ($this->_serv_keepalive_timeout <= ($time - $this->_query_time))) { + //$aseco->console($this->_webaccess_str . 'timeout, close it!'); + $this->_state = 'CLOSED'; + @fclose($this->_socket); + $this->_socket = null; + } + + // if socket is not opened, open it + if (!$this->_socket || $this->_state != 'OPENED') + $this->_open_socket($opentimeout); + + // if socket is open, send data if possible + if ($this->_socket) { + $this->_read_time = microtime(true); + + // if wait (synchronous query) then go on all pending write/read until the last + if ($this->_wait) { + @stream_set_timeout($this->_socket, 0, 10000); // timeout 10 ms + + while (isset($this->_spool[0]['State']) && + ($this->_spool[0]['State'] == 'OPEN' || + $this->_spool[0]['State'] == 'SEND' || + $this->_spool[0]['State'] == 'RECEIVE')) { + //echo 'State=' . $this->_spool[0]['State'] . " (" . count($this->_spool) . ")\n"; + if (!$this->_socket || $this->_state != 'OPENED') + $this->_open_socket($opentimeout); + + if ($this->_spool[0]['State'] == 'OPEN') { + $time = microtime(true); + $this->_spool[0]['Times']['send'][0] = $time; + $this->_send($waittimeout); + } + elseif ($this->_spool[0]['State'] == 'SEND') + $this->_send($waittimeout); + elseif ($this->_spool[0]['State'] == 'RECEIVE') + $this->_receive($waittimeout*4); + + // if timeout then error + if (($difftime = microtime(true) - $this->_read_time) > $waittimeout) { + $this->_bad('Request timeout, in _open (' . round($difftime) . ' > ' . $waittimeout . 's) state=' . $this->_spool[0]['State']); + return; + } + } + if ($this->_socket) + @stream_set_timeout($this->_socket, 0, 2000); // timeout 2 ms + } + + // else just do a send on the current + elseif (isset($this->_spool[0]['State']) && $this->_spool[0]['State'] == 'OPEN') { + @stream_set_timeout($this->_socket, 0, 2000); // timeout 2 ms + $this->_send($waittimeout); + } + } + } // _open + + + function _send($waittimeout = 20) { + + if (!isset($this->_spool[0]['State'])) + return; + + // if OPEN then become SEND + if ($this->_spool[0]['State'] == 'OPEN') { + + $this->_spool[0]['State'] = 'SEND'; + $time = microtime(true); + $this->_spool[0]['Times']['send'][0] = $time; + $this->_spool[0]['Response'] = ''; + $this->_spool[0]['Headers'] = array(); + + // finish to prepare header and data to send + $msg = $this->_spool[0]['HDatas']; + if (!$this->_keepalive || ($this->_spool[0]['KeepaliveMinTimeout'] < 0) || + ($this->_serv_keepalive_timeout < $this->_spool[0]['KeepaliveMinTimeout']) || + ($this->_serv_keepalive_max <= ($this->_query_num + 2)) || + ($this->_serv_keepalive_timeout <= (time() - $this->_query_time + 2))) { + $msg .= "Connection: close\r\n"; + $this->_spool[0]['Close'] = true; + } + else { + $msg .= 'Keep-Alive: timeout=' . $this->_keepalive_timeout . ', max=' . $this->_keepalive_max + . "\r\nConnection: Keep-Alive\r\n"; + } + + // add cookie header + if (count($this->_cookies) > 0) { + $cookie_msg = ''; + $sep = ''; + foreach ($this->_cookies as $name => $cookie) { + if (!isset($cookie['path']) || + strncmp($this->_spool[0]['Path'], $cookie['path'], strlen($cookie['path'])) == 0) { + $cookie_msg .= $sep . $name . '=' . $cookie['Value']; + $sep = '; '; + } + } + if ($cookie_msg != '') + $msg .= "Cookie: $cookie_msg\r\n"; + } + + $msg .= "\r\n"; + $msg .= $this->_spool[0]['QueryData']; + $this->_spool[0]['Datas'] = $msg; + $this->_spool[0]['DatasSize'] = strlen($msg); + $this->_spool[0]['DatasSent'] = 0; + + //print_r($msg); + } + + // if not SEND then stop + if ($this->_spool[0]['State'] != 'SEND') + return; + + do { + $sent = @stream_socket_sendto($this->_socket, + substr($this->_spool[0]['Datas'], $this->_spool[0]['DatasSent'], + ($this->_spool[0]['DatasSize'] - $this->_spool[0]['DatasSent']))); + if ($sent == false) { + + $time = microtime(true); + $this->_spool[0]['Times']['send'][1] = $time - $this->_spool[0]['Times']['send'][0]; + //var_dump($this->_spool[0]['Datas']); + $this->_bad('Error(' . $errno . ') ' . $errstr . ', could not send data! (' + . $sent . ' / ' . ($this->_spool[0]['DatasSize'] - $this->_spool[0]['DatasSent']) . ', ' + . $this->_spool[0]['DatasSent'] . ' / ' . $this->_spool[0]['DatasSize'] . ')'); + if ($this->_wait) + return; + break; + + } else { + $this->_spool[0]['DatasSent'] += $sent; + if ($this->_spool[0]['DatasSent'] >= $this->_spool[0]['DatasSize']) { + // All is sent, prepare to receive the reply + $this->_query_num++; + $this->_query_time = time(); + + $time = microtime(true); + $this->_spool[0]['Times']['send'][1] = $time - $this->_spool[0]['Times']['send'][0]; + + //@stream_set_blocking($this->_socket, 0); + $this->_spool[0]['State'] = 'RECEIVE'; + $this->_spool[0]['Times']['receive'][0] = $time; + } + + // if timeout then error + elseif (($difftime = microtime(true) - $this->_read_time) > $waittimeout) { + $this->_bad('Request timeout, in _send (' . round($difftime) . ' > ' . $waittimeout . 's)'); + } + } + + // if not async-callback then continue until all is sent + } while ($this->_wait && isset($this->_spool[0]['State']) && ($this->_spool[0]['State'] == 'SEND')); + } // _send + + + function _receive($waittimeout = 40) { + global $aseco, $_Webaccess_last_response; + + if (!$this->_socket || $this->_state != 'OPENED') + return; + + $state = false; + $time0 = microtime(true); + $timeout = ($this->_wait) ? $waittimeout : 0; + do { + $r = array($this->_socket); + $w = null; + $e = null; + $nb = @stream_select($r, $w, $e, $timeout); + if ($nb === 0) + $nb = count($r); + + while (!@feof($this->_socket) && $nb !== false && $nb > 0) { + $timeout = 0; + + if (count($r) > 0) { + $res = @stream_socket_recvfrom($this->_socket, 8192); + + if ($res == '') { // should not happen habitually, but... + break; + } elseif ($res !== false) { + $this->_response .= $res; + } + else { + if (isset($this->_spool[0])) { + $time = microtime(true); + $this->_spool[0]['Times']['receive'][1] = $time - $this->_spool[0]['Times']['receive'][0]; + } + $this->_bad('Error(' . $errno . ') ' . $errstr . ', could not read all data!'); + return; + } + } + + // if timeout then error + if (($difftime = microtime(true) - $this->_read_time) > $waittimeout) { + $this->_bad('Request timeout, in _receive (' . round($difftime) . ' > ' . $waittimeout . 's)'); + break; + } + + $r = array($this->_socket); + $w = null; + $e = null; + $nb = @stream_select($r, $w, $e, $timeout); + if ($nb === 0) + $nb = count($r); + } + + if (isset($this->_spool[0]['Times']['receive'][2])) { + $time = microtime(true); + $this->_spool[0]['Times']['receive'][2] += ($time - $time0); + } + + // get headers and full message + $state = $this->_handleHeaders(); + //echo "receive9\n"; + //var_dump($state); + + } while ($this->_wait && $state === false && $this->_socket && !@feof($this->_socket)); + + if (!isset($this->_spool[0]['State']) || $this->_spool[0]['State'] != 'RECEIVE') { + // in case of (probably keepalive) connection closed by server + if ($this->_socket && @feof($this->_socket)){ + //$aseco->console($this->_webaccess_str . 'Socket closed by server (' . $this->_host . ')'); + $this->_state = 'CLOSED'; + @fclose($this->_socket); + $this->_socket = null; + } + return; + } + + // terminated but incomplete! more than probably closed by server... + if ($state === false && $this->_socket && @feof($this->_socket)) { + $this->_state = 'CLOSED'; + if (isset($this->_spool[0])) { + $time = microtime(true); + $this->_spool[0]['State'] = 'OPEN'; + $this->_spool[0]['Times']['receive'][1] = $time - $this->_spool[0]['Times']['receive'][0]; + } + if (strlen($this->_response) > 0) // if not 0 sized then show error message + $this->_bad('Error: closed with incomplete read: re-open socket and re-send! (' . strlen($this->_response) . ')'); + else + $this->_bad('Closed by server when reading: re-open socket and re-send! (' . strlen($this->_response) . ')', false); + + $this->_spool[0]['Retries']++; + if ($this->_spool[0]['Retries'] > 2) { + // 3 tries failed, remove entry from spool + $aseco->console($this->_webaccess_str . "failed {$this->_spool[0]['Retries']} times: skip current request"); + array_shift($this->_spool); + } + + return; + } + + // reply is complete :) + if ($state === true) { + $this->_bad_timeout = 0; // reset error timeout + + $this->_spool[0]['Times']['receive'][1] = $time - $this->_spool[0]['Times']['receive'][0]; + $this->_spool[0]['State'] = 'DONE'; + + // store http/xml response in global $_Webaccess_last_response for debugging use + $_Webaccess_last_response = $this->_spool[0]['Response']; + //debugPrint('Webaccess->_receive - Response', $_Webaccess_last_response); + + // call callback func + $this->_callCallback(); + $this->_query_time = time(); + + if (!$this->_keepalive || $this->_spool[0]['Close']) { + //if ($this->_spool[0]['Close']) + // $aseco->console($this->_webaccess_str . 'close connection (asked in headers)'); + $this->_state = 'CLOSED'; + @fclose($this->_socket); + $this->_socket = null; + } + + $this->infos(); + + // request completed, remove it from spool! + array_shift($this->_spool); + } + } // _receive + + private function _callCallback($error = null) { + + // store optional error message + if ($error !== null) + $this->_spool[0]['Response']['Error'] = $error; + + // call callback func + if (isset($this->_spool[0]['Callback'])) { + $callbackinfo = $this->_spool[0]['Callback']; + if (isset($callbackinfo[0]) && is_callable($callbackinfo[0])) { + $callback_func = $callbackinfo[0]; + $callbackinfo[0] = $this->_spool[0]['Response']; + call_user_func_array($callback_func, $callbackinfo); + } + } + } + + private function _handleHeaders() { + global $aseco, $_wa_header_separator, $_wa_header_multi; + + if (!isset($this->_spool[0]['State'])) + return false; + + if (strlen($this->_response) < 8) // not enough data, continue read + return false; + if (strncmp($this->_response, 'HTTP/', 5) != 0) { // not HTTP! + $this->_bad("Error, not HTTP response ! **********\n" . substr($this->_response, 0, 300) . "\n***************\n"); + return null; + } + + // separate headers and data + $datas = explode("\r\n\r\n", $this->_response, 2); + if (count($datas) < 2) { + $datas = explode("\n\n", $this->_response, 2); + if (count($datas) < 2) { + $datas = explode("\r\r", $this->_response, 2); + if (count($datas) < 2) + return false; // not complete headers, continue read + } + } + + // get headers if not done on previous read + if (!isset($this->_spool[0]['Headers']['Command'][0])) { + // separate headers + //echo "Get Headers! (" . strlen($datas[0]) . ")\n"; + + $headers = array(); + $heads = explode("\n", str_replace("\r", "\n", str_replace("\r\n", "\n", $datas[0]))); + if (count($heads) < 2) { + $this->_bad("Error, uncomplete headers! **********\n" . $datas[0] . "\n***************\n"); + return null; + } + + $headers['Command'] = explode(' ', $heads[0], 3); + + for ($i = 1; $i < count($heads); $i++) { + $header = explode(':', $heads[$i], 2); + if (count($header) > 1) { + $headername = strtolower(trim($header[0])); + if (isset($_wa_header_separator[$headername])) + $sep = $_wa_header_separator[$headername]; + else + $sep = ','; + if (isset($_wa_header_multi[$headername]) && $_wa_header_multi[$headername]) { + if (!isset($headers[$headername])) + $headers[$headername] = array(); + $headers[$headername][] = explode($sep, trim($header[1])); + } else + $headers[$headername] = explode($sep, trim($header[1])); + } + } + + if (isset($headers['content-length'][0])) + $headers['content-length'][0] += 0; // convert to int + + $this->_spool[0]['Headers'] = $headers; + + // add header specific info in case of Dedimania reply + if (isset($headers['server'][0])) + $this->_webaccess_str = 'Webaccess (' . $this->_host . ':' . $this->_port .'/'. $headers['server'][0] . '): '; + } + else { + $headers = &$this->_spool[0]['Headers']; + //echo "Previous Headers! (" . strlen($datas[0]) . ")\n"; + } + + // get real message + $datasize = strlen($datas[1]); + if (isset($headers['content-length'][0]) && $headers['content-length'][0] >= 0) { + //echo 'mess_size0=' . strlen($datas[1]) . "\n"; + + if ($headers['content-length'][0] > $datasize) // incomplete message + return false; + + elseif ($headers['content-length'][0] < $datasize) { + $message = substr($datas[1], 0, $headers['content-length'][0]); + // remaining buffer for next reply + $this->_response = substr($datas[1], $headers['content-length'][0]); + } + else { + $message = $datas[1]; + $this->_response = ''; + } + $this->_spool[0]['ResponseSize'] = strlen($datas[0]) + 4 + $headers['content-length'][0]; + } + + // get real message when reply is chunked + elseif (isset($headers['transfer-encoding'][0]) && $headers['transfer-encoding'][0] == 'chunked') { + + // get chunk size and make message with chunks data + $size = -1; + $chunkpos = 0; + if (($datapos = strpos($datas[1], "\r\n", $chunkpos)) !== false) { + $message = ''; + $chunk = explode(';', substr($datas[1], $chunkpos, $datapos - $chunkpos)); + $size = hexdec($chunk[0]); + //debugPrint("Webaccess->Response - chunk - $chunkpos, $datapos, $size (" . strlen($datas[1]) . ")", $chunk); + while ($size > 0) { + if ($datapos + 2 + $size > $datasize) // incomplete message + return false; + $message .= substr($datas[1], $datapos + 2, $size); + $chunkpos = $datapos + 2 + $size + 2; + if (($datapos = strpos($datas[1], "\r\n", $chunkpos)) !== false) { + $chunk = explode(';', substr($datas[1], $chunkpos, $datapos - $chunkpos)); + $size = hexdec($chunk[0]); + } else + $size = -1; + //debugPrint("Webaccess->Response - chunk - $chunkpos, $datapos, $size (" . strlen($datas[1]) . ")", $chunk); + } + + } + if ($size < 0) // error bad size or incomplete message + return false; + + if (strpos($datas[1], "\r\n\r\n", $chunkpos) === false) // incomplete message: end is missing + return false; + + // store complete message size + $msize = strlen($message); + $headers['transfer-encoding'][1] = 'total_size=' . $msize; // add message size after 'chunked' for information + $this->_spool[0]['ResponseSize'] = strlen($datas[0]) + 4 + $msize; + + // after the message itself... + $message_end = explode("\r\n\r\n", substr($datas[1], $chunkpos), 2); + + // add end headers if any + $heads = explode("\n", str_replace("\r", "\n", str_replace("\r\n", "\n", $message_end[0]))); + for ($i = 1; $i < count($heads); $i++) { + $header = explode(':', $heads[$i], 2); + if (count($header) > 1) { + $headername = strtolower(trim($header[0])); + if (isset($_wa_header_separator[$headername])) + $sep = $_wa_header_separator[$headername]; + else + $sep = ','; + if (isset($_wa_header_multi[$headername]) && $_wa_header_multi[$headername]) { + if (!isset($headers[$headername])) + $headers[$headername] = array(); + $headers[$headername][] = explode($sep, trim($header[1])); + } else + $headers[$headername] = explode($sep, trim($header[1])); + } + } + $this->_spool[0]['Headers'] = $headers; + + // remaining buffer for next reply + if (isset($message_end[1]) && strlen($message_end[1]) > 0) { + $this->_response = $message_end[1]; + } + else + $this->_response = ''; + } + // no content-length and not chunked! + else { + $this->_bad("Error, bad http, no content-length and not chunked! **********\n" . $datas[0] . "\n***************\n"); + return null; + } + + //echo 'mess_size1=' . strlen($message) . "\n"; + + // if Content-Encoding: gzip or Content-Encoding: deflate + if (isset($headers['content-encoding'][0])) { + if ($headers['content-encoding'][0] == 'gzip') + $message = @gzdecode($message); + elseif ($headers['content-encoding'][0] == 'deflate') + $message = @gzinflate($message); + } + + // if Accept-Encoding: gzip or deflate + if ($this->_compress_request == 'accept' && isset($headers['accept-encoding'][0])) { + foreach ($headers['accept-encoding'] as $comp) { + $comp = trim($comp); + if ($comp == 'gzip' && function_exists('gzencode')) { + $this->_compress_request = 'gzip'; + break; + } + elseif ($comp == 'deflate' && function_exists('gzdeflate')) { + $this->_compress_request = 'deflate'; + break; + } + } + if ($this->_compress_request == 'accept') + $this->_compress_request = false; + + $aseco->console($this->_webaccess_str . 'send: ' . ($this->_compress_request === false ? 'no compression' : $this->_compress_request) + . ', receive: ' . (isset($headers['content-encoding'][0]) ? $headers['content-encoding'][0] : 'no compression')); + } + + // get cookies values + if (isset($headers['set-cookie'])) { + foreach ($headers['set-cookie'] as $cookie) { + $cook = explode('=', $cookie[0], 2); + if (count($cook) > 1) { + // set main cookie value + $cookname = trim($cook[0]); + if (!isset($this->_cookies[$cookname])) + $this->_cookies[$cookname] = array(); + $this->_cookies[$cookname]['Value'] = trim($cook[1]); + + // set cookie options + for ($i = 1; $i < count($cookie); $i++) { + $cook = explode('=', $cookie[$i], 2); + $cookarg = strtolower(trim($cook[0])); + if (isset($cook[1])) + $this->_cookies[$cookname][$cookarg] = trim($cook[1]); + } + } + } + //debugPrint('SET-COOKIES: ', $headers['set-cookie']); + //debugPrint('STORED COOKIES: ', $this->_cookies); + } + + // if the server reply ask to close, then close + if (!isset($headers['connection'][0]) || $headers['connection'][0] == 'close') { + //if (!$this->_spool[0]['Close']) + // $aseco->console($this->_webaccess_str . 'server ask to close connection'); + $this->_spool[0]['Close'] = true; + } + + // verify server keepalive value and use them if lower + if (isset($headers['keep-alive'])) { + $kasize = count($headers['keep-alive']); + for ($i = 0; $i < $kasize; $i++) { + $keep = explode('=', $headers['keep-alive'][$i], 2); + if (count($keep) > 1) + $headers['keep-alive'][trim(strtolower($keep[0]))] = intval(trim($keep[1])); + } + if (isset($headers['keep-alive']['timeout'])) + $this->_serv_keepalive_timeout = $headers['keep-alive']['timeout']; + if (isset($headers['keep-alive']['max'])) + $this->_serv_keepalive_max = $headers['keep-alive']['max']; + //$aseco->console($this->_webaccess_str . 'max=' . $this->_serv_keepalive_max . ', timeout=' . $this->_serv_keepalive_timeout . "\n"); + } + + // store complete reply message for the request + $this->_spool[0]['Response'] = array('Code' => intval($headers['Command'][1]), + 'Reason' => $headers['Command'][2], + 'Headers' => $headers, + 'Message' => $message + ); + //echo 'mess_size2=' . strlen($message) . "\n"; + return true; + } // _handleHeaders + + + function infos() { + global $aseco; + + $size = (isset($this->_spool[0]['Response']['Message'])) ? strlen($this->_spool[0]['Response']['Message']) : 0; + $msg = $this->_webaccess_str + . sprintf('[%s,%s]: %0.3f / %0.3f / %0.3f (%0.3f) / %d [%d,%d,%d]', + $this->_state, $this->_spool[0]['State'], + $this->_spool[0]['Times']['open'][1], + $this->_spool[0]['Times']['send'][1], + $this->_spool[0]['Times']['receive'][1], + $this->_spool[0]['Times']['receive'][2], + $this->_query_num, $this->_spool[0]['DatasSize'], + $size, $this->_spool[0]['ResponseSize']); + //$aseco->console($msg); + } // infos +} // class WebaccessUrl + + +// use: list($host, $port, $path) = getHostPortPath($url); +function getHostPortPath($url) { + + $http_pos = strpos($url, 'http://'); + if ($http_pos !== false) { + $script = explode('/', substr($url, $http_pos + 7), 2); + if (isset($script[1])) + $path = '/' . $script[1]; + else + $path = '/'; + $serv = explode(':', $script[0], 2); + $host = $serv[0]; + if (isset($serv[1])) + $port = (int)$serv[1]; + else + $port = 80; + if (strlen($host) > 2) + return array($host, $port, $path); + } + return array(false, false, false); +} // getHostPortPath + + +// gzdecode() workaround +if (!function_exists('gzdecode') && function_exists('gzinflate')) { + + function gzdecode($data) { + + $len = strlen($data); + if ($len < 18 || strcmp(substr($data, 0, 2), "\x1f\x8b")) { + return null; // Not GZIP format (See RFC 1952) + } + $method = ord(substr($data, 2, 1)); // Compression method + $flags = ord(substr($data, 3, 1)); // Flags + if ($flags & 31 != $flags) { + // Reserved bits are set -- NOT ALLOWED by RFC 1952 + return null; + } + // NOTE: $mtime may be negative (PHP integer limitations) + $mtime = unpack('V', substr($data, 4, 4)); + $mtime = $mtime[1]; + $xfl = substr($data, 8, 1); + $os = substr($data, 8, 1); + $headerlen = 10; + $extralen = 0; + $extra = ''; + if ($flags & 4) { + // 2-byte length prefixed EXTRA data in header + if ($len - $headerlen - 2 < 8) { + return false; // Invalid format + } + $extralen = unpack('v', substr($data, 8, 2)); + $extralen = $extralen[1]; + if ($len - $headerlen - 2 - $extralen < 8) { + return false; // Invalid format + } + $extra = substr($data, 10, $extralen); + $headerlen += $extralen + 2; + } + + $filenamelen = 0; + $filename = ''; + if ($flags & 8) { + // C-style string file NAME data in header + if ($len - $headerlen - 1 < 8) { + return false; // Invalid format + } + $filenamelen = strpos(substr($data, 8 + $extralen), chr(0)); + if ($filenamelen === false || $len - $headerlen - $filenamelen - 1 < 8) { + return false; // Invalid format + } + $filename = substr($data, $headerlen, $filenamelen); + $headerlen += $filenamelen + 1; + } + + $commentlen = 0; + $comment = ''; + if ($flags & 16) { + // C-style string COMMENT data in header + if ($len - $headerlen - 1 < 8) { + return false; // Invalid format + } + $commentlen = strpos(substr($data, 8 + $extralen + $filenamelen), chr(0)); + if ($commentlen === false || $len - $headerlen - $commentlen - 1 < 8) { + return false; // Invalid header format + } + $comment = substr($data, $headerlen, $commentlen); + $headerlen += $commentlen + 1; + } + + $headercrc = ''; + if ($flags & 1) { + // 2-bytes (lowest order) of CRC32 on header present + if ($len - $headerlen - 2 < 8) { + return false; // Invalid format + } + $calccrc = crc32(substr($data, 0, $headerlen)) & 0xffff; + $headercrc = unpack('v', substr($data, $headerlen, 2)); + $headercrc = $headercrc[1]; + if ($headercrc != $calccrc) { + return false; // Bad header CRC + } + $headerlen += 2; + } + + // GZIP FOOTER - These be negative due to PHP's limitations + $datacrc = unpack('V', substr($data, -8, 4)); + $datacrc = $datacrc[1]; + $isize = unpack('V', substr($data, -4)); + $isize = $isize[1]; + + // Perform the decompression: + $bodylen = $len - $headerlen - 8; + if ($bodylen < 1) { + // This should never happen - IMPLEMENTATION BUG! + return null; + } + $body = substr($data, $headerlen, $bodylen); + $data = ''; + if ($bodylen > 0) { + switch ($method) { + case 8: + // Currently the only supported compression method: + $data = gzinflate($body); + break; + default: + // Unknown compression method + return false; + } + } else { + // I'm not sure if zero-byte body content is allowed. + // Allow it for now... Do nothing... + } + + // Verify decompressed size and CRC32: + // NOTE: This may fail with large data sizes depending on how + // PHP's integer limitations affect strlen() since $isize + // may be negative for large sizes + if ($isize != strlen($data) || crc32($data) != $datacrc) { + // Bad format! Length or CRC doesn't match! + return false; + } + return $data; + } // gzdecode +} +?> diff --git a/docker-xaseco/xaseco/includes/xmlparser.inc.php b/docker-xaseco/xaseco/includes/xmlparser.inc.php new file mode 100644 index 0000000..e53d80c --- /dev/null +++ b/docker-xaseco/xaseco/includes/xmlparser.inc.php @@ -0,0 +1,121 @@ +stack = array(); + $this->struct = array(); + $this->utf8enc = $utf8enc; + + // create the parser + $this->parser = xml_parser_create(); + xml_set_object($this->parser, $this); + xml_set_element_handler($this->parser, 'openTag', 'closeTag'); + xml_set_character_data_handler($this->parser, 'tagData'); + + // load the xml file + if ($isfile) + $this->data = @file_get_contents($source); + else + $this->data = $source; + + // escape '&' characters + $this->data = str_replace('&', '', $this->data); + + // parse xml file + $parsed = xml_parse($this->parser, $this->data); + + // display errors + if (!$parsed) { + $code = xml_get_error_code($this->parser); + $err = xml_error_string($code); + $line = xml_get_current_line_number($this->parser); + trigger_error("[XML Error $code] $err on line $line", E_USER_WARNING); + return false; + } + return $this->struct; + } + + private function openTag($parser, $name, $attributes) { + $this->stack[] = $name; + $this->struct[$name] = ''; + } + + private function tagData($parser, $data) { + if (trim($data)) { + $index = $this->stack[count($this->stack)-1]; + // use raw, don't decode '+' into space + if ($this->utf8enc) + $this->struct[$index] .= rawurldecode($data); + else + $this->struct[$index] .= utf8_decode(rawurldecode($data)); + } + } + + private function closeTag($parser, $name) { + if (count($this->stack) > 1) { + $from = array_pop($this->stack); + $to = $this->stack[count($this->stack)-1]; + $this->struct[$to][$from][] = $this->struct[$from]; + unset($this->struct[$from]); + } + } + + /** + * Parses an array into an XML structure. + */ + function parseArray($array) { + $xmlstring = ''; + $xmlstring .= $this->parseArrayElements($array); + return $xmlstring; + } + + private function parseArrayElements($array, $opt_tag = '') { + + // read each element of the array + for ($i = 0; $i < count($array); $i++) { + + // check if array is associative + if (is_numeric(key($array))) { + $xml .= '<'.$opt_tag.'>'; + if (is_array(current($array))) { + $xml .= $this->parseArrayElements(current($array), key($array)); + } else { + // use raw, don't encode space into '+' + $xml .= rawurlencode(utf8_encode(current($array))); + } + $xml .= ''; + } else { + if (is_array(current($array))) { + $xml .= $this->parseArrayElements(current($array), key($array)); + } else { + $xml .= '<'.key($array).'>'; + // use raw, don't encode space into '+' + $xml .= rawurlencode(utf8_encode(current($array))); + $xml .= ''; + } + } + next($array); + } + return $xml; + } +} +?> diff --git a/docker-xaseco/xaseco/includes/xmlrpc_db.inc.php b/docker-xaseco/xaseco/includes/xmlrpc_db.inc.php new file mode 100644 index 0000000..e19a930 --- /dev/null +++ b/docker-xaseco/xaseco/includes/xmlrpc_db.inc.php @@ -0,0 +1,385 @@ +_debug = 0; // max debug level = 3 + $this->_webaccess = $webaccess; + $this->_url = $url; + $this->_server = array('Game' => $game, + 'Login' => $login, + 'Password' => $password, + 'Tool' => $tool, + 'Version' => $version, + 'Nation' => $nation, + 'Packmask' => $packmask, + 'PlayersGame' => true + ); + $this->_auth_cb = array('xmlrpc_auth_cb'); + + $this->_bad = false; + $this->_bad_time = -1; + // in case webaccess URL connection was previously in error, ask to retry + $this->_webaccess->retry($this->_url); + + // prepare to add requests + $this->_initRequest(); + } // XmlrpcDB + + // change the packmask value + function setPackmask($packmask = '') { + + $this->_server['Packmask'] = $packmask; + } // setPackmask + + // is the connection in recurrent error? + function isBad() { + + return $this->_bad; + } // isBad + + // get time since the error state was set + function badTime() { + + return (time() - $this->_bad_time); + } // badTime + + // stop the bad state: will try again at next RequestWait(), + // sendRequestsWait() or sendRequests() + function retry() { + + $this->_bad = false; + $this->_bad_time = -1; + // set webaccess object to retry on that URL too + $this->_webaccess->retry($this->_url); + } // retry + + // clear all requests, and get them if asked + function clearRequests($get_requests = false) { + + if ($get_requests) { + $return = array($_requests, $_callbacks); + $this->_initRequest(); + return $return; + } + $this->_initRequest(); + } // clearRequests + + // add a request + function addRequest($callback, $method) { + + $args = func_get_args(); + $callback = array_shift($args); + $method = array_shift($args); + return $this->addRequestArray($callback, $method, $args); + } // addRequest + + // add a request + function addRequestArray($callback, $method, $args) { + + $this->_callbacks[] = $callback; + $this->_requests[] = array('methodName' => $method, 'params' => $args); + return count($this->_requests) - 1; + } // addRequestArray + + // send added requests, callbacks will be called when response come + function sendRequests() { + global $aseco; + + if (count($this->_callbacks) > 1) { + $this->addRequest(null, 'dedimania.WarningsAndTTR'); + $webdatas = $this->_makeXMLdatas(); + $response = $this->_webaccess->request($this->_url, + array( array($this, '_callCB'), $this->_callbacks, $this->_requests), + $webdatas, true); + $this->_initRequest(); + if ($response === false) { + if (!$this->_bad) { + $this->_bad = true; + $this->_bad_time = time(); + } + if ($this->_debug > 2) + $aseco->console_text('XmlrpcDB->sendRequests - this' . CRLF . print_r($this, true)); + return false; + } + } + return true; + } // sendRequests + + // send added requests, wait response, then call callbacks + function sendRequestsWait() { + global $aseco; + + if (count($this->_callbacks) > 1) { + $this->addRequest(null, 'dedimania.WarningsAndTTR'); + $webdatas = $this->_makeXMLdatas(); + $response = $this->_webaccess->request($this->_url, + null, $webdatas, true); + if ($response === false) { + if (!$this->_bad) { + $this->_bad = true; + $this->_bad_time = time(); + } + if ($this->_debug > 0) + $aseco->console_text('XmlrpcDB->sendRequestsWait - this' . CRLF . print_r($this, true)); + $this->_initRequest(); + return false; + } else { + $this->_callCB($response, $this->_callbacks, $this->_requests); + $this->_initRequest(); + } + } + return true; + } // sendRequestsWait + + // send a request, wait response, and return the response + function RequestWait($method) { + + $args = func_get_args(); + $method = array_shift($args); + return $this->RequestWaitArray($method, $args); + } // RequestWait + + // send a request, wait response, and return the response + function RequestWaitArray($method, $args) { + global $aseco; + + if ($this->sendRequestsWait() === false) { + if (!$this->_bad) { + $this->_bad = true; + $this->_bad_time = time(); + } + return false; + } + + $reqnum = $this->addRequestArray(null, $method, $args); + + $this->addRequest(null, 'dedimania.WarningsAndTTR'); + $webdatas = $this->_makeXMLdatas(); + $response = $this->_webaccess->request($this->_url, null, $webdatas, true); + if (isset($response['Message']) && is_string($response['Message'])) { + if ($this->_debug > 1) + $aseco->console_text('XmlrpcDB->RequestWaitArray() - response[Message]' . CRLF . print_r($response['Message'], true)); + + $xmlrpc_message = new IXR_Message($response['Message']); + if ($xmlrpc_message->parse() && $xmlrpc_message->messageType != 'fault') { + if ($this->_debug > 1) { + $aseco->console_text('XmlrpcDB->RequestWaitArray() - message' . CRLF . print_r($xmlrpc_message->message, true)); + $aseco->console_text('XmlrpcDB->RequestWaitArray() - params' . CRLF . print_r($xmlrpc_message->params, true)); + } + + //$datas = array('methodName' => $xmlrpc_message->methodName, 'params' => $xmlrpc_message->params); + $datas = $this->_makeResponseDatas($xmlrpc_message->methodName, $xmlrpc_message->params, $this->_requests); + } else { + if ($this->_debug > 0) + $aseco->console_text('XmlrpcDB->RequestWaitArray() - message fault' . CRLF . print_r($xmlrpc_message->message, true)); + $datas = array(); + } + } else { + $datas = array(); + } + + if ($this->_debug > 0) + $aseco->console_text('XmlrpcDB->RequestWaitArray() - datas' . CRLF . print_r($datas, true)); + if (isset($datas['params']) && isset($datas['params'][$reqnum])) { + $response['Data'] = $datas['params'][$reqnum]; + $param_end = end($datas['params']); + if (isset($param_end['globalTTR']) && !isset($response['Data']['globalTTR'])) + $response['Data']['globalTTR'] = $param_end['globalTTR']; + } else { + $response['Data'] = $datas; + } + if ($this->_debug > 0) + $aseco->console_text('XmlrpcDB->RequestWaitArray() - response[Data]' . CRLF . print_r($response['Data'], true)); + + $this->_initRequest(); + return $response; + } // RequestWaitArray + + // init the request and callback array + function _initRequest() { + + $this->_callbacks = array(); + $this->_requests = array(); + $this->addRequest($this->_auth_cb, 'dedimania.Authenticate', $this->_server); + } // _initRequest + + // make the xmlrpc string, encode it in base64, and pass it as value + // to xmlrpc URL post parameter + function _makeXMLdatas() { + global $aseco; + + $xmlrpc_request = new IXR_RequestStd('system.multicall', $this->_requests); + if ($this->_debug > 1) + $aseco->console_text('XmlrpcDB->_makeXMLdatas() - getXml()' . CRLF . print_r($xmlrpc_request->getXml(), true)); + return $xmlrpc_request->getXml(); + } // _makeXMLdatas + + function _callCB($response, $callbacks, $requests) { + global $aseco; + + $globalTTR = 0; + if (isset($response['Message']) && is_string($response['Message'])) { + $xmlrpc_message = new IXR_Message($response['Message']); + if ($xmlrpc_message->parse() && $xmlrpc_message->messageType != 'fault') { + if ($this->_debug > 1) + $aseco->console_text('XmlrpcDB->_callCB() - message' . CRLF . print_r($xmlrpc_message->message, true)); + + //$datas = array('methodName' => $xmlrpc_message->methodName, 'params' => $xmlrpc_message->params); + $datas = $this->_makeResponseDatas($xmlrpc_message->methodName, $xmlrpc_message->params, $requests); + + if (isset($datas['params']) && is_array($datas['params'])) { + $param_end = end($datas['params']); + if (isset($param_end['globalTTR'])) + $globalTTR = $param_end['globalTTR']; + } else { + if ($this->_debug > 0) + $aseco->console_text('XmlrpcDB->_callCB() - message fault' . CRLF . print_r($xmlrpc_message->message, true)); + $datas = array(); + } + } else { + if ($this->_debug > 0) + $aseco->console_text('XmlrpcDB->_callCB() - message fault' . CRLF . print_r($xmlrpc_message->message, true)); + $datas = array(); + } + } else { + if (!isset($response['Error']) || + (strpos($response['Error'], 'connection failed') === false && strpos($response['Error'], 'Request timeout') === false)) { + $infos = array('Url'=>$this->_url, 'Requests'=>$requests, 'Callbacks'=>$callbacks, 'Response'=>$response); + $serinfos = serialize($infos); + $aseco->console_text('XmlrpcDB->_callCB() - no response message:' . CRLF . $serinfos); + } + $datas = array(); + } + + for ($i = 0; $i < count($callbacks); $i++) { + if ($callbacks[$i] != null) { + $callback = $callbacks[$i][0]; + if (isset($datas['params']) && isset($datas['params'][$i])) { + $response['Data'] = $datas['params'][$i]; + if (!isset($response['Data']['globalTTR'])) + $response['Data']['globalTTR'] = $globalTTR; + } else { + $response['Data'] = $datas; + } + $callbacks[$i][0] = $response; + call_user_func_array($callback, $callbacks[$i]); + } + } + } // _callCB + + // build the datas array from XAseco or Dedimania server + // remove the first array level into params if needed + // add methodResponse name if needed + // rename sub responses params array from [0] to ['params'] if needed + function _makeResponseDatas($methodname, $params, $requests) { + global $aseco; + + if (is_array($params) && count($params) == 1 && is_array($params[0])) + $params = $params[0]; + if ($this->_debug > 2) + $aseco->console_text('XmlrpcDB->_makeResponseDatas() - requests' . CRLF . print_r($requests, true)); + if ($this->_debug > 1) + $aseco->console_text('XmlrpcDB->_makeResponseDatas() - params' . CRLF . print_r($params, true)); + + if (is_array($params) && is_array($params[0]) && !isset($params[0]['methodResponse'])) { + $params2 = array(); + foreach ($params as $key => $param) { + $errors = null; + if (isset($param['faultCode'])) { + $errors[] = array('Code' => $param['faultCode'], 'Message' => $param['faultString']); + } + + if (isset($requests[$key]['methodName'])) + $methodresponse = $requests[$key]['methodName']; + else + $methodresponse = 'Unknown'; + + $ttr = 0.000001; + + if (isset($param[0])) + $param = $param[0]; + else + $param = array(); + + $params2[$key] = array('methodResponse' => $methodresponse, + 'params' => $param, + 'errors' => $errors, + 'TTR' => $ttr, + 'globalTTR' => $ttr + ); + + if ($methodresponse == 'dedimania.WarningsAndTTR') { + if ($this->_debug > 1) { + $aseco->console_text('XmlrpcDB->_makeResponseDatas() - param' . CRLF . print_r($param, true)); + $aseco->console_text('XmlrpcDB->_makeResponseDatas() - params2' . CRLF . print_r($params2, true)); + } + $globalTTR = $param['globalTTR']; + foreach ($param['methods'] as $key3 => $param3) { + $key2 = 0; + while ($key2 < count($params2) && $params2[$key2]['methodResponse'] != $param3['methodName']) { + $params2[$key2]['globalTTR'] = $globalTTR; + $key2++; + } + if ($this->_debug > 1) + $aseco->console_text("XmlrpcDB->_makeResponseDatas() - key2=$key2 - key3=$key3 - param3" . CRLF . print_r($param3, true)); + if ($key2 < count($params2)) { + $params2[$key2]['errors'] = $param3['errors']; + $params2[$key2]['TTR'] = $param3['TTR']; + $params2[$key2]['globalTTR'] = $globalTTR; + } + } + } + } + if ($this->_debug > 1) + $aseco->console_text('XmlrpcDB->_makeResponseDatas() - params2' . CRLF . print_r($params2, true)); + return array('methodName' => $methodname, 'params' => $params2); + + } else { + return array('methodName' => $methodname, 'params' => $params); + } + } // _makeResponseDatas +} // class XmlrpcDB + + +// Dedimania.Authenticate callback used to catch errors +function xmlrpc_auth_cb($response) { + global $aseco; + + if (isset($response['Data']['errors']) && $response['Data']['errors'] != '') { + if (is_array($response['Data']['errors'])) + $aseco->console('xmlrpc_auth_cb() - response[Data][errors]' . CRLF . print_r($response['Data']['errors'], true)); + else + $aseco->console('xmlrpc_auth_cb() - response[Data][errors]' . CRLF . print_r($response, true)); + } +} // xmlrpc_auth_cb +?> diff --git a/docker-xaseco/xaseco/localdatabase.xml b/docker-xaseco/xaseco/localdatabase.xml new file mode 100644 index 0000000..5aa1f21 --- /dev/null +++ b/docker-xaseco/xaseco/localdatabase.xml @@ -0,0 +1,22 @@ + + + + --$DB_HOST-- + --$DB_LOGIN-- + --$DB_LOGIN_PASSWORD-- + --$DB_NAME-- + + true + + + + 50 + + + + {#server}>> {#highlite}{1}{#record} secured his/her {#rank}{2}{#record}. Local Record! {3}: {#highlite}{4}{#record} $n({#rank}{5}{#highlite}{6}{#record}) + {#server}>> {#highlite}{1}{#record} equaled his/her {#rank}{2}{#record}. Local Record! {3}: {#highlite}{4} + {#server}>> {#highlite}{1}{#record} gained the {#rank}{2}{#record}. Local Record! {3}: {#highlite}{4}{#record} $n({#rank}{5}{#highlite}{6}{#record}) + {#server}>> {#highlite}{1}{#record} claimed the {#rank}{2}{#record}. Local Record! {3}: {#highlite}{4} + + diff --git a/docker-xaseco/xaseco/localdb/aseco.sql b/docker-xaseco/xaseco/localdb/aseco.sql new file mode 100644 index 0000000..3b05520 --- /dev/null +++ b/docker-xaseco/xaseco/localdb/aseco.sql @@ -0,0 +1,56 @@ +-- Database: `aseco` +-- +-- -------------------------------------------------------- + +-- +-- Tablestructure for Table `challenges` +-- + +CREATE TABLE IF NOT EXISTS `challenges` ( + `Id` mediumint(9) NOT NULL auto_increment, + `Uid` varchar(27) NOT NULL default '', + `Name` varchar(100) NOT NULL default '', + `Author` varchar(30) NOT NULL default '', + `Environment` varchar(10) NOT NULL default '', + PRIMARY KEY (`Id`), + UNIQUE KEY `Uid` (`Uid`) +) ENGINE=MyISAM; + +-- -------------------------------------------------------- + +-- +-- Tablestructure for Table `players` +-- + +CREATE TABLE IF NOT EXISTS `players` ( + `Id` mediumint(9) NOT NULL auto_increment, + `Login` varchar(50) NOT NULL default '', + `Game` varchar(3) NOT NULL default '', + `NickName` varchar(100) NOT NULL default '', + `Nation` varchar(3) NOT NULL default '', + `UpdatedAt` datetime NOT NULL default '1970-01-01 00:00:00', + `Wins` mediumint(9) NOT NULL default 0, + `TimePlayed` int(10) unsigned NOT NULL default 0, + `TeamName` char(60) NOT NULL default '', + PRIMARY KEY (`Id`), + UNIQUE KEY `Login` (`Login`), + KEY `Game` (`Game`) +) ENGINE=MyISAM; + +-- -------------------------------------------------------- + +-- +-- Tablestructure for Table `records` +-- + +CREATE TABLE IF NOT EXISTS `records` ( + `Id` int(11) NOT NULL auto_increment, + `ChallengeId` mediumint(9) NOT NULL default 0, + `PlayerId` mediumint(9) NOT NULL default 0, + `Score` int(11) NOT NULL default 0, + `Date` datetime NOT NULL default '1970-01-01 00:00:00', + `Checkpoints` text NOT NULL, + PRIMARY KEY (`Id`), + UNIQUE KEY `PlayerId` (`PlayerId`,`ChallengeId`), + KEY `ChallengeId` (`ChallengeId`) +) ENGINE=MyISAM; diff --git a/docker-xaseco/xaseco/localdb/extra.sql b/docker-xaseco/xaseco/localdb/extra.sql new file mode 100644 index 0000000..2629128 --- /dev/null +++ b/docker-xaseco/xaseco/localdb/extra.sql @@ -0,0 +1,18 @@ +-- Database: `aseco` +-- +-- -------------------------------------------------------- + +-- +-- Tablestructure for Table `players_extra` +-- + +CREATE TABLE IF NOT EXISTS `players_extra` ( + `playerID` mediumint(9) NOT NULL default 0, + `cps` smallint(3) NOT NULL default -1, + `dedicps` smallint(3) NOT NULL default -1, + `donations` mediumint(9) NOT NULL default 0, + `style` varchar(20) NOT NULL default '', + `panels` varchar(255) NOT NULL default '', + PRIMARY KEY (`playerID`), + KEY `donations` (`donations`) +) ENGINE=MyISAM; diff --git a/docker-xaseco/xaseco/localdb/rasp.sql b/docker-xaseco/xaseco/localdb/rasp.sql new file mode 100644 index 0000000..898b5d8 --- /dev/null +++ b/docker-xaseco/xaseco/localdb/rasp.sql @@ -0,0 +1,47 @@ +-- Database: `aseco` +-- +-- -------------------------------------------------------- + +-- +-- Tablestructure for Table `rs_karma` +-- + +CREATE TABLE IF NOT EXISTS `rs_karma` ( + `Id` int(11) NOT NULL auto_increment, + `ChallengeId` mediumint(9) NOT NULL default 0, + `PlayerId` mediumint(9) NOT NULL default 0, + `Score` tinyint(4) NOT NULL default 0, + PRIMARY KEY (`Id`), + UNIQUE KEY `PlayerId` (`PlayerId`,`ChallengeId`), + KEY `ChallengeId` (`ChallengeId`) +) ENGINE=MyISAM; + +-- -------------------------------------------------------- + +-- +-- Tablestructure for Table `rs_rank` +-- + +CREATE TABLE IF NOT EXISTS `rs_rank` ( + `playerID` mediumint(9) NOT NULL default 0, + `avg` float NOT NULL default 0, + KEY `playerID` (`playerID`) +) ENGINE=MyISAM; + +-- -------------------------------------------------------- + +-- +-- Tablestructure for Table `rs_times` +-- + +CREATE TABLE IF NOT EXISTS `rs_times` ( + `ID` int(11) NOT NULL auto_increment, + `challengeID` mediumint(9) NOT NULL default 0, + `playerID` mediumint(9) NOT NULL default 0, + `score` int(11) NOT NULL default 0, + `date` int(10) unsigned NOT NULL default 0, + `checkpoints` text NOT NULL, + PRIMARY KEY (`ID`), + KEY `playerID` (`playerID`,`challengeID`), + KEY `challengeID` (`challengeID`) +) ENGINE=MyISAM; diff --git a/docker-xaseco/xaseco/logfile.txt b/docker-xaseco/xaseco/logfile.txt new file mode 100644 index 0000000..850315d --- /dev/null +++ b/docker-xaseco/xaseco/logfile.txt @@ -0,0 +1,4 @@ +[XAseco] PHP Version is 5.6.40-7+0~20190503101815.14+stretch~1.gbp1a44f9 on Linux +[XAseco] Load settings [config.xml] +[XASECO Warning] [XML Error 80] Comment must not contain '--' (double-hyphen) on line 10 +[XASECO Fatal Error] Could not read/parse config file config.xml ! on line 459 in file /home/admin/docker/build/xaseco/xaseco/aseco.php diff --git a/docker-xaseco/xaseco/matchsave.xml b/docker-xaseco/xaseco/matchsave.xml new file mode 100644 index 0000000..d9ad128 --- /dev/null +++ b/docker-xaseco/xaseco/matchsave.xml @@ -0,0 +1,100 @@ + + + + + + + + + XXX + + + xxx-max + siteguru + + + + + + + + Challengers + + + + + + + $i$0AFPoints This Round + $i$AF0Points This Match + {#emotic} + {#message} + {#server} + $000 + $f00 + $0af + $i$n + $fff + $n$888 + {#highlite} + {#highlite} + $0af + {#server} + $af0 + {#server} + + + + True + + + {#server}To join a team type "{#welcome}/team {#message}yourteamname{#server}" + + {#server}See all team options by typing "{#welcome}/team {#message}help{#server}" + {#server}Join a team and chat with your teammates via "{#welcome}/tc {#message}Hello Teammates{#server}" + {#server}See the current match standings by typing "{#welcome}/standings{#server}" + + + {#server}Change your teamname with "{#welcome}/team {#message}yourteamname{#server}" + + {#server}See all team options by typing "{#welcome}/team {#message}help{#server}" + {#server}See the current match standings by typing "{#welcome}/standings{#server}" + {#server}Chat with your teammates by typing "{#welcome}/tc {#message}Hello Teammates{#server}" + + + + + true + True + true + {#highlite}[Team] + False + True + False + 12 + 5000 + True + True + + + html.tpl + race_results.html + race_results_last.html + match_results.html + + + + + + /home/tmf/TMF/GameData/Tracks/MatchSettings + + + 16 + Y-m-d + H:i + 10, 8, 6, 4, 2, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0 + + + True + + diff --git a/docker-xaseco/xaseco/musicserver.xml b/docker-xaseco/xaseco/musicserver.xml new file mode 100644 index 0000000..22e578d --- /dev/null +++ b/docker-xaseco/xaseco/musicserver.xml @@ -0,0 +1,55 @@ + + + + False + + True + + False + + True + + False + + False + + + True + + False + + musictagscache.xml + + + + + + YOUR_SERVER_URL + + + + + + YOUR_FIRST_SONG.ogg + YOUR_SECOND_SONG.mux + YOUR_THIRD_SONG.ogg + + + + + {#server}> {#music}The current song is: {#highlite}{1} + {#server}>> {#music}{1}$z$s {#highlite}{2}$z$s{#music} loaded the next song: {#highlite}{3} + {#server}>> {#music}{1}$z$s {#highlite}{2}$z$s{#music} reloaded music config and cleared jukebox! + {#server}>> {#music}{1}$z$s {#highlite}{2}$z$s{#music} sorted song list and cleared jukebox! + {#server}>> {#music}{1}$z$s {#highlite}{2}$z$s{#music} shuffled song list and cleared jukebox! + {#server}>> {#highlite}{1}{#music} jukeboxed song: {#highlite}{2} + {#server}> {#music}You already have a song in the jukebox! Wait till it's been played before adding another. + {#server}> {#music}This song has already been added to the jukebox, pick another one. + {#server}> {#music}Song_ID not found - Type {#highlite}/music list{#music} to see all songs. + {#server}>> {#music}Player {#highlite}{1}{#music} dropped his/her song {#highlite}{2}{#music} from jukebox! + {#server}> {#music}You don't have a song in the jukebox, use {#highlite}/music Song_ID{#music} to add one... + {#server}> {#music}No songs in the jukebox, use {#highlite}/music Song_ID{#music} to add one... + {#server}> {#highlite}/music #{#music} is not currently enabled on this server. + {#server}>> {#music}{1}$z$s {#highlite}{2}$z$s{#music} disabled server music! + + diff --git a/docker-xaseco/xaseco/panels/00README.txt b/docker-xaseco/xaseco/panels/00README.txt new file mode 100644 index 0000000..391aa9e --- /dev/null +++ b/docker-xaseco/xaseco/panels/00README.txt @@ -0,0 +1,34 @@ +Panels +====== + +This directory holds Admin, Donate, Records and Vote panel templates, +managed by plugin.panels.php. +Templates define the complete ManiaLink panel with position, size and +fonts, so you have full control to develop custom panels. + +To create a new template, copy an existing one to a new filename and +edit that, as the existing set will be overwritten in future releases. + +Use ManiaLink http://smurf1.free.fr/mle/index.xml +and webpage http://smurf1.free.fr/mle/list.php +to select styles and fonts. Note that not every (sub)style and font +fits everywhere due to size variations. + +New Admin templates must stick to manialink id="3" and preserve +action="21" through action="27" for the buttons. + +New Donate templates must stick to manialink id="6" and preserve +action="30" through action="36" for the buttons. + +New Record templates must stick to manialink id="4" and preserve the +mapping of text="%PB%" to action="7", "%LCL%" to "8", "%DED%" to "9" +and "%TMX%" to "10". + +New Vote templates must stick to manialink id="5" and preserve +action="18" for the Yes button and action="19" for the No button. + +If you create a nice template for any panel that's sufficiently distinct +from the standard ones, send it to me and I might include it in the +next XAseco release. :) + +Xymph diff --git a/docker-xaseco/xaseco/panels/AdminAboveCPList.xml b/docker-xaseco/xaseco/panels/AdminAboveCPList.xml new file mode 100644 index 0000000..5013657 --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminAboveCPList.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminAboveCPListWide.xml b/docker-xaseco/xaseco/panels/AdminAboveCPListWide.xml new file mode 100644 index 0000000..ec41084 --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminAboveCPListWide.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminAboveChat.xml b/docker-xaseco/xaseco/panels/AdminAboveChat.xml new file mode 100644 index 0000000..f2fea7c --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminAboveChat.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminAboveChatWide.xml b/docker-xaseco/xaseco/panels/AdminAboveChatWide.xml new file mode 100644 index 0000000..120b2b2 --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminAboveChatWide.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminAboveSpeed.xml b/docker-xaseco/xaseco/panels/AdminAboveSpeed.xml new file mode 100644 index 0000000..303be95 --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminAboveSpeed.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminAboveSpeed2.xml b/docker-xaseco/xaseco/panels/AdminAboveSpeed2.xml new file mode 100644 index 0000000..2582b30 --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminAboveSpeed2.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminBelowChat.xml b/docker-xaseco/xaseco/panels/AdminBelowChat.xml new file mode 100644 index 0000000..5a203a1 --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminBelowChat.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminBottomCenter.xml b/docker-xaseco/xaseco/panels/AdminBottomCenter.xml new file mode 100644 index 0000000..7925896 --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminBottomCenter.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminBottomCenterWide.xml b/docker-xaseco/xaseco/panels/AdminBottomCenterWide.xml new file mode 100644 index 0000000..a3981b0 --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminBottomCenterWide.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminCallVote.xml b/docker-xaseco/xaseco/panels/AdminCallVote.xml new file mode 100644 index 0000000..d7a9cd3 --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminCallVote.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminCallVoteBlur.xml b/docker-xaseco/xaseco/panels/AdminCallVoteBlur.xml new file mode 100644 index 0000000..002111d --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminCallVoteBlur.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminLeftEdge.xml b/docker-xaseco/xaseco/panels/AdminLeftEdge.xml new file mode 100644 index 0000000..1149266 --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminLeftEdge.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminRightEdge.xml b/docker-xaseco/xaseco/panels/AdminRightEdge.xml new file mode 100644 index 0000000..0d24fcd --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminRightEdge.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminTopCenter.xml b/docker-xaseco/xaseco/panels/AdminTopCenter.xml new file mode 100644 index 0000000..8f8bbb8 --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminTopCenter.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/AdminTopCenterWide.xml b/docker-xaseco/xaseco/panels/AdminTopCenterWide.xml new file mode 100644 index 0000000..48c7e27 --- /dev/null +++ b/docker-xaseco/xaseco/panels/AdminTopCenterWide.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/docker-xaseco/xaseco/panels/DonateBelowCPList.xml b/docker-xaseco/xaseco/panels/DonateBelowCPList.xml new file mode 100644 index 0000000..9b84e2c --- /dev/null +++ b/docker-xaseco/xaseco/panels/DonateBelowCPList.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/DonateBelowCPListRM.xml b/docker-xaseco/xaseco/panels/DonateBelowCPListRM.xml new file mode 100644 index 0000000..aa3207a --- /dev/null +++ b/docker-xaseco/xaseco/panels/DonateBelowCPListRM.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/DonateLeftEdge.xml b/docker-xaseco/xaseco/panels/DonateLeftEdge.xml new file mode 100644 index 0000000..1931f0a --- /dev/null +++ b/docker-xaseco/xaseco/panels/DonateLeftEdge.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/DonateLeftEdge2.xml b/docker-xaseco/xaseco/panels/DonateLeftEdge2.xml new file mode 100644 index 0000000..0edc2a3 --- /dev/null +++ b/docker-xaseco/xaseco/panels/DonateLeftEdge2.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/DonateLeftSmall.xml b/docker-xaseco/xaseco/panels/DonateLeftSmall.xml new file mode 100644 index 0000000..1b2ddc2 --- /dev/null +++ b/docker-xaseco/xaseco/panels/DonateLeftSmall.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/DonateRightEdge.xml b/docker-xaseco/xaseco/panels/DonateRightEdge.xml new file mode 100644 index 0000000..9242cee --- /dev/null +++ b/docker-xaseco/xaseco/panels/DonateRightEdge.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/DonateRightEdge2.xml b/docker-xaseco/xaseco/panels/DonateRightEdge2.xml new file mode 100644 index 0000000..02e45bd --- /dev/null +++ b/docker-xaseco/xaseco/panels/DonateRightEdge2.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/DonateRightSmall.xml b/docker-xaseco/xaseco/panels/DonateRightSmall.xml new file mode 100644 index 0000000..1fffe61 --- /dev/null +++ b/docker-xaseco/xaseco/panels/DonateRightSmall.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/DonateTopLeft.xml b/docker-xaseco/xaseco/panels/DonateTopLeft.xml new file mode 100644 index 0000000..fb88626 --- /dev/null +++ b/docker-xaseco/xaseco/panels/DonateTopLeft.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsLeftBlackBold.xml b/docker-xaseco/xaseco/panels/RecordsLeftBlackBold.xml new file mode 100644 index 0000000..fd66692 --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsLeftBlackBold.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsLeftBlue.xml b/docker-xaseco/xaseco/panels/RecordsLeftBlue.xml new file mode 100644 index 0000000..cde43be --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsLeftBlue.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsLeftBlueBold.xml b/docker-xaseco/xaseco/panels/RecordsLeftBlueBold.xml new file mode 100644 index 0000000..09c5862 --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsLeftBlueBold.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsLeftBlueLight.xml b/docker-xaseco/xaseco/panels/RecordsLeftBlueLight.xml new file mode 100644 index 0000000..a5dbd1c --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsLeftBlueLight.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsLeftGray.xml b/docker-xaseco/xaseco/panels/RecordsLeftGray.xml new file mode 100644 index 0000000..28007db --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsLeftGray.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsLeftGrayBold.xml b/docker-xaseco/xaseco/panels/RecordsLeftGrayBold.xml new file mode 100644 index 0000000..9837f4f --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsLeftGrayBold.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsLeftItalic.xml b/docker-xaseco/xaseco/panels/RecordsLeftItalic.xml new file mode 100644 index 0000000..945659e --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsLeftItalic.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsLeftOrange.xml b/docker-xaseco/xaseco/panels/RecordsLeftOrange.xml new file mode 100644 index 0000000..7963877 --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsLeftOrange.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsLeftSize1.xml b/docker-xaseco/xaseco/panels/RecordsLeftSize1.xml new file mode 100644 index 0000000..8035615 --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsLeftSize1.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsLeftSize1NoDedi.xml b/docker-xaseco/xaseco/panels/RecordsLeftSize1NoDedi.xml new file mode 100644 index 0000000..ef73496 --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsLeftSize1NoDedi.xml @@ -0,0 +1,11 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsLeftSize2.xml b/docker-xaseco/xaseco/panels/RecordsLeftSize2.xml new file mode 100644 index 0000000..e7ab8fb --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsLeftSize2.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsLeftSize2NoDedi.xml b/docker-xaseco/xaseco/panels/RecordsLeftSize2NoDedi.xml new file mode 100644 index 0000000..548720a --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsLeftSize2NoDedi.xml @@ -0,0 +1,11 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsLeftWhite.xml b/docker-xaseco/xaseco/panels/RecordsLeftWhite.xml new file mode 100644 index 0000000..f418f60 --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsLeftWhite.xml @@ -0,0 +1,13 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/RecordsRightBottom.xml b/docker-xaseco/xaseco/panels/RecordsRightBottom.xml new file mode 100644 index 0000000..f0c9688 --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsRightBottom.xml @@ -0,0 +1,12 @@ + + + diff --git a/docker-xaseco/xaseco/panels/RecordsRightBottomNoDedi.xml b/docker-xaseco/xaseco/panels/RecordsRightBottomNoDedi.xml new file mode 100644 index 0000000..4e17240 --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsRightBottomNoDedi.xml @@ -0,0 +1,10 @@ + + + diff --git a/docker-xaseco/xaseco/panels/RecordsRightBottomRM.xml b/docker-xaseco/xaseco/panels/RecordsRightBottomRM.xml new file mode 100644 index 0000000..e12c740 --- /dev/null +++ b/docker-xaseco/xaseco/panels/RecordsRightBottomRM.xml @@ -0,0 +1,12 @@ + + + diff --git a/docker-xaseco/xaseco/panels/StatsNations.xml b/docker-xaseco/xaseco/panels/StatsNations.xml new file mode 100644 index 0000000..edddd8d --- /dev/null +++ b/docker-xaseco/xaseco/panels/StatsNations.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/docker-xaseco/xaseco/panels/StatsUnited.xml b/docker-xaseco/xaseco/panels/StatsUnited.xml new file mode 100644 index 0000000..2090e0d --- /dev/null +++ b/docker-xaseco/xaseco/panels/StatsUnited.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/docker-xaseco/xaseco/panels/VoteBelowChat.xml b/docker-xaseco/xaseco/panels/VoteBelowChat.xml new file mode 100644 index 0000000..347590a --- /dev/null +++ b/docker-xaseco/xaseco/panels/VoteBelowChat.xml @@ -0,0 +1,7 @@ + + + diff --git a/docker-xaseco/xaseco/panels/VoteBottomCenter.xml b/docker-xaseco/xaseco/panels/VoteBottomCenter.xml new file mode 100644 index 0000000..fecc056 --- /dev/null +++ b/docker-xaseco/xaseco/panels/VoteBottomCenter.xml @@ -0,0 +1,7 @@ + + + diff --git a/docker-xaseco/xaseco/panels/VoteBottomCenterTransp.xml b/docker-xaseco/xaseco/panels/VoteBottomCenterTransp.xml new file mode 100644 index 0000000..98c15aa --- /dev/null +++ b/docker-xaseco/xaseco/panels/VoteBottomCenterTransp.xml @@ -0,0 +1,8 @@ + + + + diff --git a/docker-xaseco/xaseco/panels/VoteCallVote.xml b/docker-xaseco/xaseco/panels/VoteCallVote.xml new file mode 100644 index 0000000..b6b51ca --- /dev/null +++ b/docker-xaseco/xaseco/panels/VoteCallVote.xml @@ -0,0 +1,7 @@ + + + diff --git a/docker-xaseco/xaseco/panels/VoteTopCenter.xml b/docker-xaseco/xaseco/panels/VoteTopCenter.xml new file mode 100644 index 0000000..58868aa --- /dev/null +++ b/docker-xaseco/xaseco/panels/VoteTopCenter.xml @@ -0,0 +1,7 @@ + + + diff --git a/docker-xaseco/xaseco/plugins.xml b/docker-xaseco/xaseco/plugins.xml new file mode 100644 index 0000000..baad696 --- /dev/null +++ b/docker-xaseco/xaseco/plugins.xml @@ -0,0 +1,44 @@ + + + plugin.localdatabase.php + plugin.rounds.php + chat.admin.php + chat.help.php + chat.records.php + chat.records2.php + chat.recrels.php + chat.dedimania.php + chat.players.php + chat.players2.php + chat.wins.php + chat.laston.php + chat.lastwin.php + chat.stats.php + chat.server.php + chat.songmod.php + chat.me.php + + plugin.tmxinfo.php + plugin.track.php + plugin.checkpoints.php + plugin.dedimania.php + plugin.rasp.php + plugin.rasp_jukebox.php + plugin.rasp_chat.php + plugin.rasp_karma.php + plugin.rasp_nextmap.php + plugin.rasp_nextrank.php + plugin.rasp_votes.php + plugin.chatlog.php + + plugin.style.php + plugin.panels.php + + + plugin.uptodate.php + + + + jfreu.plugin.php + mistral.idlekick.php + diff --git a/docker-xaseco/xaseco/plugins/chat.admin.php b/docker-xaseco/xaseco/plugins/chat.admin.php new file mode 100644 index 0000000..c7bc75b --- /dev/null +++ b/docker-xaseco/xaseco/plugins/chat.admin.php @@ -0,0 +1,5772 @@ +... {sec})', true); +Aseco::addChatCommand('addthis', 'Adds current /add-ed track permanently', true); +Aseco::addChatCommand('addlocal', 'Adds a local track ()', true); +Aseco::addChatCommand('warn', 'Sends a kick/ban warning to a player', true); +Aseco::addChatCommand('kick', 'Kicks a player from server', true); +Aseco::addChatCommand('kickghost', 'Kicks a ghost player from server', true); +Aseco::addChatCommand('ban', 'Bans a player from server', true); +Aseco::addChatCommand('unban', 'UnBans a player from server', true); +Aseco::addChatCommand('banip', 'Bans an IP address from server', true); +Aseco::addChatCommand('unbanip', 'UnBans an IP address from server', true); +Aseco::addChatCommand('black', 'Blacklists a player from server', true); +Aseco::addChatCommand('unblack', 'UnBlacklists a player from server', true); +Aseco::addChatCommand('addguest', 'Adds a guest player to server', true); +Aseco::addChatCommand('removeguest', 'Removes a guest player from server', true); +Aseco::addChatCommand('pass', 'Passes a chat-based or TMX /add vote', true); +Aseco::addChatCommand('cancel/can', 'Cancels any running vote', true); +Aseco::addChatCommand('endround/er', 'Forces end of current round', true); +Aseco::addChatCommand('players', 'Displays list of known players {string}', true); +Aseco::addChatCommand('showbanlist/listbans', 'Displays current ban list', true); +Aseco::addChatCommand('showiplist/listips', 'Displays current banned IPs list', true); +Aseco::addChatCommand('showblacklist/listblacks', 'Displays current black list', true); +Aseco::addChatCommand('showguestlist/listguests', 'Displays current guest list', true); +Aseco::addChatCommand('writeiplist', 'Saves current banned IPs list (def: bannedips.xml)', true); +Aseco::addChatCommand('readiplist', 'Loads current banned IPs list (def: bannedips.xml)', true); +Aseco::addChatCommand('writeblacklist', 'Saves current black list (def: blacklist.txt)', true); +Aseco::addChatCommand('readblacklist', 'Loads current black list (def: blacklist.txt)', true); +Aseco::addChatCommand('writeguestlist', 'Saves current guest list (def: guestlist.txt)', true); +Aseco::addChatCommand('readguestlist', 'Loads current guest list (def: guestlist.txt)', true); +Aseco::addChatCommand('cleanbanlist', 'Cleans current ban list', true); +Aseco::addChatCommand('cleaniplist', 'Cleans current banned IPs list', true); +Aseco::addChatCommand('cleanblacklist', 'Cleans current black list', true); +Aseco::addChatCommand('cleanguestlist', 'Cleans current guest list', true); +Aseco::addChatCommand('mergegbl', 'Merges a global black list {URL}', true); +Aseco::addChatCommand('access', 'Handles player access control (see: /admin access help)', true); +Aseco::addChatCommand('writetracklist', 'Saves current track list (def: tracklist.txt)', true); +Aseco::addChatCommand('readtracklist', 'Loads current track list (def: tracklist.txt)', true); +Aseco::addChatCommand('shuffle/shufflemaps', 'Randomizes current track list', true); +Aseco::addChatCommand('listdupes', 'Displays list of duplicate tracks', true); +Aseco::addChatCommand('remove', 'Removes a track from rotation', true); +Aseco::addChatCommand('erase', 'Removes a track from rotation & deletes track file', true); +Aseco::addChatCommand('removethis', 'Removes this track from rotation', true); +Aseco::addChatCommand('erasethis', 'Removes this track from rotation & deletes track file', true); +Aseco::addChatCommand('mute/ignore', 'Adds a player to global mute/ignore list', true); +Aseco::addChatCommand('unmute/unignore', 'Removes a player from global mute/ignore list', true); +Aseco::addChatCommand('mutelist/listmutes', 'Displays global mute/ignore list', true); +Aseco::addChatCommand('ignorelist/listignores', 'Displays global mute/ignore list', true); +Aseco::addChatCommand('cleanmutes/cleanignores', 'Cleans global mute/ignore list', true); +Aseco::addChatCommand('addadmin', 'Adds a new admin', true); +Aseco::addChatCommand('removeadmin', 'Removes an admin', true); +Aseco::addChatCommand('addop', 'Adds a new operator', true); +Aseco::addChatCommand('removeop', 'Removes an operator', true); +Aseco::addChatCommand('listmasters', 'Displays current masteradmin list', true); +Aseco::addChatCommand('listadmins', 'Displays current admin list', true); +Aseco::addChatCommand('listops', 'Displays current operator list', true); +Aseco::addChatCommand('adminability', 'Shows/changes admin ability {ON/OFF}', true); +Aseco::addChatCommand('opability', 'Shows/changes operator ability {ON/OFF}', true); +Aseco::addChatCommand('listabilities', 'Displays current abilities list', true); +Aseco::addChatCommand('writeabilities', 'Saves current abilities list (def: adminops.xml)', true); +Aseco::addChatCommand('readabilities', 'Loads current abilities list (def: adminops.xml)', true); +Aseco::addChatCommand('wall/mta', 'Displays popup message to all players', true); +Aseco::addChatCommand('delrec', 'Deletes specific record on current track', true); +Aseco::addChatCommand('prunerecs', 'Deletes records for specified track', true); +Aseco::addChatCommand('rpoints', 'Sets custom Rounds points (see: /admin rpoints help)', true); +Aseco::addChatCommand('match', '{begin/end} to start/stop match tracking', true); +Aseco::addChatCommand('acdl', 'Sets AllowChallengeDownload {ON/OFF}', true); +Aseco::addChatCommand('autotime', 'Sets Auto TimeLimit {ON/OFF}', true); +Aseco::addChatCommand('disablerespawn', 'Disables respawn at CPs {ON/OFF}', true); +Aseco::addChatCommand('forceshowopp', 'Forces to show opponents {##/ALL/OFF}', true); +Aseco::addChatCommand('scorepanel', 'Shows automatic scorepanel {ON/OFF}', true); +Aseco::addChatCommand('roundsfinish', 'Shows rounds panel upon first finish {ON/OFF}', true); +Aseco::addChatCommand('forceteam', 'Forces player into {Blue} or {Red} team', true); +Aseco::addChatCommand('forcespec', 'Forces player into free spectator', true); +Aseco::addChatCommand('specfree', 'Forces spectator into free mode', true); +Aseco::addChatCommand('panel', 'Selects admin panel (see: /admin panel help)', true); +Aseco::addChatCommand('style', 'Selects default window style', true); +Aseco::addChatCommand('admpanel', 'Selects default admin panel', true); +Aseco::addChatCommand('donpanel', 'Selects default donate panel', true); +Aseco::addChatCommand('recpanel', 'Selects default records panel', true); +Aseco::addChatCommand('votepanel', 'Selects default vote panel', true); +Aseco::addChatCommand('coppers', 'Shows server\'s coppers amount', true); +Aseco::addChatCommand('pay', 'Pays server coppers to login', true); +Aseco::addChatCommand('relays', 'Displays relays list or shows relay master', true); +Aseco::addChatCommand('server', 'Displays server\'s detailed settings', true); +Aseco::addChatCommand('pm', 'Sends private message to all available admins', true); +Aseco::addChatCommand('pmlog', 'Displays log of recent private admin messages', true); +Aseco::addChatCommand('call', 'Executes direct server call (see: /admin call help)', true); +Aseco::addChatCommand('unlock', 'Unlocks admin commands & features', true); +Aseco::addChatCommand('debug', 'Toggles debugging output', true); +Aseco::addChatCommand('shutdown', 'Shuts down XASECO', true); +Aseco::addChatCommand('shutdownall', 'Shuts down Server & XASECO', true); +//Aseco::addChatCommand('uptodate', 'Checks current version of XASECO', true); // already defined in plugin.uptodate.php + +global $pmbuf; // pm history buffer +global $pmlen; // length of pm history +global $lnlen; // max length of pm line + +$pmbuf = array(); +$pmlen = 30; +$lnlen = 40; + +global $method_results, $auto_scorepanel, $rounds_finishpanel; +$auto_scorepanel = true; +$rounds_finishpanel = true; + +function chat_admin($aseco, $command) { + global $jukebox; // from plugin.rasp_jukebox.php + + $admin = $command['author']; + $login = $admin->login; + + // split params into arrays & insure optional parameters exist + $arglist = explode(' ', $command['params'], 2); + if (!isset($arglist[1])) $arglist[1] = ''; + $command['params'] = explode(' ', preg_replace('/ +/', ' ', $command['params'])); + if (!isset($command['params'][1])) $command['params'][1] = ''; + + // check if chat command was allowed for a masteradmin/admin/operator + if ($aseco->isMasterAdmin($admin)) { + $logtitle = 'MasterAdmin'; + $chattitle = $aseco->titles['MASTERADMIN'][0]; + } else { + if ($aseco->isAdmin($admin) && $aseco->allowAdminAbility($command['params'][0])) { + $logtitle = 'Admin'; + $chattitle = $aseco->titles['ADMIN'][0]; + } else { + if ($aseco->isOperator($admin) && $aseco->allowOpAbility($command['params'][0])) { + $logtitle = 'Operator'; + $chattitle = $aseco->titles['OPERATOR'][0]; + } else { + // write warning in console + $aseco->console($login . ' tried to use admin chat command (no permission!): ' . $arglist[0] . ' ' . $arglist[1]); + // show chat message + $aseco->client->query('ChatSendToLogin', $aseco->formatColors('{#error}You don\'t have the required admin rights to do that!'), $login); + return false; + } + } + } + + // check for unlocked password (or unlock command) + if ($aseco->settings['lock_password'] != '' && !$admin->unlocked && + $command['params'][0] != 'unlock') { + // write warning in console + $aseco->console($login . ' tried to use admin chat command (not unlocked!): ' . $arglist[0] . ' ' . $arglist[1]); + // show chat message + $aseco->client->query('ChatSendToLogin', $aseco->formatColors('{#error}You don\'t have the required admin rights to do that!'), $login); + return false; + } + + /** + * Show admin help. + */ + if ($command['params'][0] == 'help') { + // build list of currently active commands + $active_commands = array(); + foreach ($aseco->chat_commands as $cc) { + // strip off optional abbreviation + $name = preg_replace('/\/.*/', '', $cc->name); + + // check if admin command is within this admin's tier + if ($cc->isadmin && $aseco->allowAbility($admin, $name)) { + $active_command = new ChatCommand($cc->name, $cc->help, true); + $active_commands[] = $active_command; + } + } + + // show active admin commands on command line + showHelp($admin, $active_commands, $logtitle, true, false); + + /** + * Display admin help. + */ + } elseif ($command['params'][0] == 'helpall') { + + // build list of currently active commands + $active_commands = array(); + foreach ($aseco->chat_commands as $cc) { + // strip off optional abbreviation + $name = preg_replace('/\/.*/', '', $cc->name); + + // check if admin command is within this admin's tier + if ($cc->isadmin && $aseco->allowAbility($admin, $name)) { + $active_command = new ChatCommand($cc->name, $cc->help, true); + $active_commands[] = $active_command; + } + } + + // display active admin commands in popup with descriptions + showHelp($admin, $active_commands, $logtitle, true, true, 0.42); + + /** + * Sets a new server name (on the fly). + */ + } elseif ($command['params'][0] == 'setservername' && $command['params'][1] != '') { + + // set a new servername + $aseco->client->query('SetServerName', $arglist[1]); + + // log console message + $aseco->console('{1} [{2}] set new server name [{3}]', $logtitle, $login, $arglist[1]); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} sets servername to {#highlite}{3}', + $chattitle, $admin->nickname, $arglist[1]); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + /** + * Sets a new server comment (on the fly). + */ + } elseif ($command['params'][0] == 'setcomment' && $command['params'][1] != '') { + + // set a new server comment + $aseco->client->query('SetServerComment', $arglist[1]); + + // log console message + $aseco->console('{1} [{2}] set new server comment [{3}]', $logtitle, $login, $arglist[1]); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} sets server comment to {#highlite}{3}', + $chattitle, $admin->nickname, $arglist[1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Sets a new player password (on the fly). + */ + } elseif ($command['params'][0] == 'setpwd') { + + // set a new player password + $aseco->client->query('SetServerPassword', $arglist[1]); + + if ($arglist[1] != '') { + // log console message + $aseco->console('{1} [{2}] set new player password [{3}]', $logtitle, $login, $arglist[1]); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} sets player password to {#highlite}{3}', + $chattitle, $admin->nickname, $arglist[1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + // log console message + $aseco->console('{1} [{2}] disabled player password', $logtitle, $login); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} disables player password', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Sets a new spectator password (on the fly). + */ + } elseif ($command['params'][0] == 'setspecpwd') { + + // set a new spectator password + $aseco->client->query('SetServerPasswordForSpectator', $arglist[1]); + + if ($arglist[1] != '') { + // log console message + $aseco->console('{1} [{2}] set new spectator password [{3}]', $logtitle, $login, $arglist[1]); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} sets spectator password to {#highlite}{3}', + $chattitle, $admin->nickname, $arglist[1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + // log console message + $aseco->console('{1} [{2}] disabled spectator password', $logtitle, $login); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} disables spectator password', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Sets a new referee password (on the fly). + */ + } elseif ($command['params'][0] == 'setrefpwd') { + + if ($aseco->server->getGame() == 'TMF') { + // set a new referee password + $aseco->client->query('SetRefereePassword', $arglist[1]); + + if ($arglist[1] != '') { + // log console message + $aseco->console('{1} [{2}] set new referee password [{3}]', $logtitle, $login, $arglist[1]); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} sets referee password to {#highlite}{3}', + $chattitle, $admin->nickname, $arglist[1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + // log console message + $aseco->console('{1} [{2}] disabled referee password', $logtitle, $login); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} disables referee password', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Sets a new player maximum that is able to connect to the server. + */ + } elseif ($command['params'][0] == 'setmaxplayers' && is_numeric($command['params'][1]) && $command['params'][1] > 0) { + + // tell server to set new player max + $aseco->client->query('SetMaxPlayers', (int) $command['params'][1]); + + // log console message + $aseco->console('{1} [{2}] set new player maximum [{3}]', $logtitle, $login, $command['params'][1]); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} sets new player maximum to {#highlite}{3}{#admin} !', + $chattitle, $admin->nickname, $command['params'][1]); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + /** + * Sets a new spectator maximum that is able to connect to the server. + */ + } elseif ($command['params'][0] == 'setmaxspecs' && is_numeric($command['params'][1]) && $command['params'][1] >= 0) { + + // tell server to set new spectator max + $aseco->client->query('SetMaxSpectators', (int) $command['params'][1]); + + // log console message + $aseco->console('{1} [{2}] set new spectator maximum [{3}]', $logtitle, $login, $command['params'][1]); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} sets new spectator maximum to {#highlite}{3}{#admin} !', + $chattitle, $admin->nickname, $command['params'][1]); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + /** + * Sets new game mode that will be active upon the next track: + * ta,rounds,team,laps,stunts + */ + } elseif ($command['params'][0] == 'setgamemode' && $command['params'][1] != '') { + + // check mode parameter + switch (strtolower($command['params'][1])) { + case 'ta': + $mode = Gameinfo::TA; + break; + case 'round': // permit shortcut + case 'rounds': + $mode = Gameinfo::RNDS; + break; + case 'team': + $mode = Gameinfo::TEAM; + break; + case 'laps': + $mode = Gameinfo::LAPS; + break; + case 'stunts': + $mode = Gameinfo::STNT; + break; + case 'cup': + if ($aseco->server->getGame() == 'TMF') + $mode = Gameinfo::CUP; + else + $mode = -1; + break; + default: + $mode = -1; + } + + if ($mode >= 0) { + if ($aseco->changingmode || $mode != $aseco->server->gameinfo->mode) { + // tell server to set new game mode + $aseco->client->query('SetGameMode', $mode); + $aseco->changingmode = true; + + // log console message + $aseco->console('{1} [{2}] set new game mode [{3}]', $logtitle, $login, strtoupper($command['params'][1])); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} sets next game mode to {#highlite}{3}{#admin} !', + $chattitle, $admin->nickname, strtoupper($command['params'][1])); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $aseco->changingmode = false; + $message = '{#server}> Same game mode {#highlite}' . strtoupper($command['params'][1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = '{#server}> {#error}Invalid game mode {#highlite}$i ' . strtoupper($command['params'][1]) . '$z$s {#error}!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Sets new referee mode (0 = top3, 1 = all). + */ + } elseif ($command['params'][0] == 'setrefmode') { + + if ($aseco->server->getGame() == 'TMF') { + if (($mode = $command['params'][1]) != '') { + if (is_numeric($mode) && ($mode == 0 || $mode == 1)) { + // tell server to set new referee mode + $aseco->client->query('SetRefereeMode', (int) $mode); + + // log console message + $aseco->console('{1} [{2}] set new referee mode [{3}]', $logtitle, $login, strtoupper($mode)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} sets referee mode to {#highlite}{3}{#admin} !', + $chattitle, $admin->nickname, $mode); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $message = '{#server}> {#error}Invalid referee mode {#highlite}$i ' . strtoupper($mode) . '$z$s {#error}!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + // tell server to get current referee mode + $aseco->client->query('GetRefereeMode'); + $mode = $aseco->client->getResponse(); + + // show chat message + $message = formatText('{#server}> {#admin}Referee mode is set to {#highlite}{1}', + ($mode == 1 ? 'All' : 'Top-3')); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Forces the server to load next track. + */ + } elseif ($command['params'][0] == 'nextmap' || + $command['params'][0] == 'next' || + $command['params'][0] == 'skipmap' || + $command['params'][0] == 'skip') { + + // load the next map + // don't clear scores if in TMF Cup mode + if ($aseco->server->gameinfo->mode == Gameinfo::CUP) + $aseco->client->query('NextChallenge', true); + else + $aseco->client->query('NextChallenge'); + + // log console message + $aseco->console('{1} [{2}] skips challenge!', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} skips challenge!', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + /** + * Forces the server to load previous track. + */ + } elseif ($command['params'][0] == 'previous' || + $command['params'][0] == 'prev') { + + // get current track + $aseco->client->query('GetCurrentChallengeIndex'); + $current = $aseco->client->getResponse(); + + // check if not the first track + if ($current > 0) { + // find previous track + $aseco->client->query('GetChallengeList', 1, --$current); + $track = $aseco->client->getResponse(); + $prev = array(); + $prev['name'] = $track[0]['Name']; + $prev['environment'] = $track[0]['Environnement']; + $prev['filename'] = $track[0]['FileName']; + $prev['uid'] = $track[0]['UId']; + } else { + // dummy player to easily obtain entire track list + $list = new Player(); + getAllChallenges($list, '*', '*'); + // find last track + $prev = end($list->tracklist); + unset($list); + } + + // prepend previous challenge to start of jukebox + $uid = $prev['uid']; + $jukebox = array_reverse($jukebox, true); + $jukebox[$uid]['FileName'] = $prev['filename']; + $jukebox[$uid]['Name'] = $prev['name']; + $jukebox[$uid]['Env'] = $prev['environment']; + $jukebox[$uid]['Login'] = $admin->login; + $jukebox[$uid]['Nick'] = $admin->nickname; + $jukebox[$uid]['source'] = 'Previous'; + $jukebox[$uid]['tmx'] = false; + $jukebox[$uid]['uid'] = $uid; + $jukebox = array_reverse($jukebox, true); + + if ($aseco->debug) { + $aseco->console_text('/admin prev jukebox:' . CRLF . + print_r($jukebox, true)); + } + + // load the previous track + // don't clear scores if in TMF Cup mode + if ($aseco->server->gameinfo->mode == Gameinfo::CUP) + $aseco->client->query('NextChallenge', true); + else + $aseco->client->query('NextChallenge'); + + // log console message + $aseco->console('{1} [{2}] revisits previous challenge!', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} revisits previous challenge!', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + // throw 'jukebox changed' event + $aseco->releaseEvent('onJukeboxChanged', array('previous', $jukebox[$uid])); + + /** + * Loads the next track in the same environment. + */ + } elseif ($command['params'][0] == 'nextenv') { + + // check for TMF United + if ($aseco->server->getGame() == 'TMF' && + $aseco->server->packmask != 'Stadium') { + // dummy player to easily obtain environment track list + $list = new Player(); + getAllChallenges($list, '*', $aseco->server->challenge->environment); + + // search for current track + $next = null; + $found = false; + foreach ($list->tracklist as $track) { + if ($found) { + $next = $track; + break; + } + if ($track['uid'] == $aseco->server->challenge->uid) + $found = true; + } + // check for last track and loop back to first + if ($next === null) + $next = $list->tracklist[0]; + unset($list); + + // prepend next env challenge to start of jukebox + $uid = $next['uid']; + $jukebox = array_reverse($jukebox, true); + $jukebox[$uid]['FileName'] = $next['filename']; + $jukebox[$uid]['Name'] = $next['name']; + $jukebox[$uid]['Env'] = $next['environment']; + $jukebox[$uid]['Login'] = $admin->login; + $jukebox[$uid]['Nick'] = $admin->nickname; + $jukebox[$uid]['source'] = 'Previous'; + $jukebox[$uid]['tmx'] = false; + $jukebox[$uid]['uid'] = $uid; + $jukebox = array_reverse($jukebox, true); + + if ($aseco->debug) { + $aseco->console_text('/admin nextenv jukebox:' . CRLF . + print_r($jukebox, true)); + } + + // load the next environment track + // don't clear scores if in TMF Cup mode + if ($aseco->server->gameinfo->mode == Gameinfo::CUP) + $aseco->client->query('NextChallenge', true); + else + $aseco->client->query('NextChallenge'); + + // log console message + $aseco->console('{1} [{2}] skips to next {3} challenge!', $logtitle, $login, $aseco->server->challenge->environment); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} skips to next {#highlite}{3}{#admin} challenge!', + $chattitle, $admin->nickname, $aseco->server->challenge->environment); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + // throw 'jukebox changed' event + $aseco->releaseEvent('onJukeboxChanged', array('nextenv', $jukebox[$uid])); + + } else { // TMN(F) + $message = '{#server}> {#error}Command only available on TMU Forever!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Restarts the currently running map. + */ + } elseif ($command['params'][0] == 'restartmap' || + $command['params'][0] == 'res') { + global $atl_restart; // from plugin.autotime.php + + // restart the track + if (isset($atl_restart)) $atl_restart = true; + // don't clear scores if in TMF Cup mode + if ($aseco->server->gameinfo->mode == Gameinfo::CUP) + $aseco->client->query('ChallengeRestart', true); + else + $aseco->client->query('ChallengeRestart'); + + // log console message + $aseco->console('{1} [{2}] restarts challenge!', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} restarts challenge!', + $chattitle, $admin->nickname); + + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + /** + * Replays the current map (queues it at start of jukebox). + */ + } elseif ($command['params'][0] == 'replaymap' || + $command['params'][0] == 'replay') { + global $chatvote; // from plugin.rasp_votes.php + + // cancel possibly ongoing replay/restart vote + $aseco->client->query('CancelVote'); + if (!empty($chatvote) && $chatvote['type'] == 2) { + $chatvote = array(); + // disable all vote panels + if ($aseco->server->getGame() == 'TMF') + allvotepanels_off($aseco); + } + + // check if track already in jukebox + if (!empty($jukebox) && array_key_exists($aseco->server->challenge->uid, $jukebox)) { + $message = '{#server}> {#error}Track is already getting replayed!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + return; + } + + // prepend current challenge to start of jukebox + $uid = $aseco->server->challenge->uid; + $jukebox = array_reverse($jukebox, true); + $jukebox[$uid]['FileName'] = $aseco->server->challenge->filename; + $jukebox[$uid]['Name'] = $aseco->server->challenge->name; + $jukebox[$uid]['Env'] = $aseco->server->challenge->environment; + $jukebox[$uid]['Login'] = $admin->login; + $jukebox[$uid]['Nick'] = $admin->nickname; + $jukebox[$uid]['source'] = 'AdminReplay'; + $jukebox[$uid]['tmx'] = false; + $jukebox[$uid]['uid'] = $uid; + $jukebox = array_reverse($jukebox, true); + + if ($aseco->debug) { + $aseco->console_text('/admin replay jukebox:' . CRLF . + print_r($jukebox, true)); + } + + // log console message + $aseco->console('{1} [{2}] requeues challenge!', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} queues challenge for replay!', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + // throw 'jukebox changed' event + $aseco->releaseEvent('onJukeboxChanged', array('replay', $jukebox[$uid])); + + /** + * Drops a track from the jukebox (for use with rasp jukebox plugin). + */ + } elseif ($command['params'][0] == 'dropjukebox' || + $command['params'][0] == 'djb') { + + // verify parameter + if (is_numeric($command['params'][1]) && + $command['params'][1] >= 1 && $command['params'][1] <= count($jukebox)) { + $i = 1; + foreach ($jukebox as $item) { + if ($i++ == $command['params'][1]) { + $name = stripColors($item['Name']); + $uid = $item['uid']; + break; + } + } + $drop = $jukebox[$uid]; + unset($jukebox[$uid]); + + // log console message + $aseco->console('{1} [{2}] drops track {3} from jukebox!', $logtitle, $login, stripColors($name, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} drops track {#highlite}{3}{#admin} from jukebox!', + $chattitle, $admin->nickname, $name); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + // throw 'jukebox changed' event + $aseco->releaseEvent('onJukeboxChanged', array('drop', $drop)); + } else { + $message = '{#server}> {#error}Jukebox entry not found! Type {#highlite}$i /jukebox list{#error} or {#highlite}$i /jukebox display{#error} for its contents.'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Clears the jukebox (for use with rasp jukebox plugin). + */ + } elseif ($command['params'][0] == 'clearjukebox' || + $command['params'][0] == 'cjb') { + + // clear jukebox + $jukebox = array(); + + // log console message + $aseco->console('{1} [{2}] clears jukebox!', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} clears jukebox!', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + // throw 'jukebox changed' event + $aseco->releaseEvent('onJukeboxChanged', array('clear', null)); + + /** + * Clears (part of) track history. + */ + } elseif ($command['params'][0] == 'clearhist') { + global $buffersize, $jb_buffer; // from rasp.settings.php + + // check for optional portion (pos = newest, neg = oldest) + if ($command['params'][1] != '' && is_numeric($command['params'][1]) && $command['params'][1] != 0) { + $clear = intval($command['params'][1]); + + // log console message + $aseco->console('{1} [{2}] clears {3} track{4} from history!', $logtitle, $login, + ($clear > 0 ? 'newest ' : 'oldest ') . abs($clear), + abs($clear) == 1 ? '' : 's'); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} clears {3}{#admin} track{4} from history!', + $chattitle, $admin->nickname, + ($clear > 0 ? 'newest {#highlite}' : 'oldest {#highlite}') . abs($clear), + abs($clear) == 1 ? '' : 's'); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } elseif (strtolower($command['params'][1]) == 'all') { // entire history + $clear = $buffersize; + + // log console message + $aseco->console('{1} [{2}] clears entire track history!', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} clears entire track history!', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + // show chat message + $message = formatText('{#server}> {#admin}The track history contains {#highlite}{3}{#admin} track{4}', + $chattitle, $admin->nickname, count($jb_buffer), + (count($jb_buffer) == 1 ? '' : 's')); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + return; + } + + // clear track history (portion) + $i = 0; + if ($clear > 0) { + if ($clear > $buffersize) $clear = $buffersize; + while ($i++ < $clear) array_pop($jb_buffer); + } else { + if ($clear < -$buffersize) $clear = -$buffersize; + while ($i-- > $clear) array_shift($jb_buffer); + } + + /** + * Adds TMX tracks to the track rotation. + */ + } elseif ($command['params'][0] == 'add') { + global $rasp, $tmxdir, $jukebox_adminadd; // from plugin.rasp.php, rasp.settings.php + + $sections = array('TMO' => 'original', + 'TMS' => 'sunrise', + 'TMN' => 'nations', + 'TMU' => 'united', + 'TMNF' => 'tmnforever'); + + // check last parameter + $last = strtoupper(end($command['params'])); + // try to load the track(s) from TMX + $source = 'TMX'; + $section = $aseco->server->getGame(); + if ($section == 'TMF' && count($command['params']) > 2 && + substr($last, 0, 2) == 'TM') { + $section = $last; + array_pop($command['params']); + if (!array_key_exists($section, $sections)) { + $message = '{#server}> {#error}No such section on TMX!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + return; + } + } else { // TMN/TMS/TMO or no section + if ($section == 'TMF') { + if ($aseco->server->packmask == 'Stadium') + $section = 'TMNF'; + else + $section = 'TMU'; + } + } + $remotelink = 'http://' . $sections[$section] . '.tm-exchange.com/get.aspx?action=trackgbx&id='; + + if (count($command['params']) == 1) { + $message = '{#server}> {#error}You must include a TMX Track_ID!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + return; + } + + // try all specified tracks + for ($id = 1; $id < count($command['params']); $id++) { + // check for valid TMX ID + if (is_numeric($command['params'][$id]) && $command['params'][$id] >= 0) { + $trkid = ltrim($command['params'][$id], '0'); + $file = http_get_file($remotelink . $trkid); + if ($file === false || $file == -1) { + $message = '{#server}> {#error}Error downloading, or wrong TMX section, or TMX is down!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + // check for maximum online track size (256 KB) + if (strlen($file) >= 256 * 1024) { + $message = formatText($rasp->messages['TRACK_TOO_LARGE'][0], + round(strlen($file) / 1024)); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + continue; + } + $sepchar = substr($aseco->server->trackdir, -1, 1); + $partialdir = $tmxdir . $sepchar . $trkid . '.Challenge.gbx'; + $localfile = $aseco->server->trackdir . $partialdir; + if ($nocasepath = file_exists_nocase($localfile)) { + if (!unlink($nocasepath)) { + $message = '{#server}> {#error}Error erasing old file - unable to erase {#highlite}$i ' . $localfile; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + continue; + } + } + if (!$lfile = @fopen($localfile, 'wb')) { + $message = '{#server}> {#error}Error creating file - unable to create {#highlite}$i ' . $localfile; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + continue; + } + if (!fwrite($lfile, $file)) { + $message = '{#server}> {#error}Error saving file - unable to write data'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + fclose($lfile); + continue; + } + fclose($lfile); + $newtrk = getChallengeData($localfile, false); // 2nd parm is whether or not to get players & votes required + if ($newtrk['votes'] == 500 && $newtrk['name'] == 'Not a GBX file') { + $message = '{#server}> {#error}No such track on ' . $source; + if ($source == 'TMX' && $aseco->server->getGame() == 'TMF') + $message .= ' section ' . $section; + $message .= '!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + unlink($localfile); + continue; + } + // dummy player to easily obtain entire track list + $list = new Player(); + getAllChallenges($list, '*', '*'); + // check for track presence on server + foreach ($list->tracklist as $key) { + if ($key['uid'] == $newtrk['uid']) { + $message = $rasp->messages['ADD_PRESENT'][0]; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + unlink($localfile); + unset($list); + continue 2; // outer for loop + } + } + unset($list); + // rename ID filename to track's name + $md5new = md5_file($localfile); + $filename = trim(utf8_decode(stripColors($newtrk['name']))); + $filename = preg_replace('/[^A-Za-z0-9 \'#=+~_,.-]/', '_', $filename); + $filename = preg_replace('/ +/', ' ', preg_replace('/_+/', '_', $filename)); + $partialdir = $tmxdir . $sepchar . $filename . '_' . $trkid . '.Challenge.gbx'; + // insure unique filename by incrementing sequence number, + // if not a duplicate track + $i = 1; + $dupl = false; + while ($nocasepath = file_exists_nocase($aseco->server->trackdir . $partialdir)) { + $md5old = md5_file($nocasepath); + if ($md5old == $md5new) { + $dupl = true; + $partialdir = str_replace($aseco->server->trackdir, '', $nocasepath); + break; + } else { + $partialdir = $tmxdir . $sepchar . $filename . '_' . $trkid . '-' . $i++ . '.Challenge.gbx'; + } + } + if ($dupl) { + unlink($localfile); + } else { + rename($localfile, $aseco->server->trackdir . $partialdir); + } + + // check track vs. server settings + if ($aseco->server->getGame() == 'TMF') + $rtn = $aseco->client->query('CheckChallengeForCurrentServerParams', $partialdir); + else + $rtn = true; + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] CheckChallengeForCurrentServerParams - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $message = formatText($rasp->messages['JUKEBOX_IGNORED'][0], + stripColors($newtrk['name']), $aseco->client->getErrorMessage()); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + // permanently add the track to the server list + $rtn = $aseco->client->query('AddChallenge', $partialdir); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] AddChallenge - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + } else { + $aseco->client->resetError(); + $aseco->client->query('GetChallengeInfo', $partialdir); + $track = $aseco->client->getResponse(); + if ($aseco->client->isError()) { + trigger_error('[' . $aseco->client->getErrorCode() . '] GetChallengeInfo - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $message = formatText('{#server}> {#error}Error getting info on track {#highlite}$i {1} {#error}!', + $partialdir); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + $track['Name'] = stripNewlines($track['Name']); + // check whether to jukebox as well + // overrules /add-ed but not yet played track + if ($jukebox_adminadd) { + $uid = $track['UId']; + $jukebox[$uid]['FileName'] = $track['FileName']; + $jukebox[$uid]['Name'] = $track['Name']; + $jukebox[$uid]['Env'] = $track['Environnement']; + $jukebox[$uid]['Login'] = $login; + $jukebox[$uid]['Nick'] = $admin->nickname; + $jukebox[$uid]['source'] = $source; + $jukebox[$uid]['tmx'] = false; + $jukebox[$uid]['uid'] = $uid; + } + + // log console message + $aseco->console('{1} [{2}] adds track "{3}" from {4}!', $logtitle, $login, stripColors($track['Name'], false), $source); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s {#admin}adds {3}track: {#highlite}{4} {#admin}from {5}', + $chattitle, $admin->nickname, + ($jukebox_adminadd ? '& jukeboxes ' : ''), + stripColors($track['Name']), $source); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + // throw 'tracklist changed' event + $aseco->releaseEvent('onTracklistChanged', array('add', $partialdir)); + + // throw 'jukebox changed' event + if ($jukebox_adminadd) + $aseco->releaseEvent('onJukeboxChanged', array('add', $jukebox[$uid])); + } + } + } + } + } else { + $message = formatText('{#server}> {#highlite}{1}{#error} is not a valid TMX Track_ID!', + $command['params'][$id]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + + /** + * Adds current /add-ed track permanently to server's track list + * by preventing its removal that normally occurs afterwards + */ + } elseif ($command['params'][0] == 'addthis') { + global $tmxplayed, $tmxdir, $tmxtmpdir; // from plugin.rasp_jukebox.php, rasp.settings.php + + // check for TMX /add-ed track + if ($tmxplayed) { + // remove track with old path + $rtn = $aseco->client->query('RemoveChallenge', $tmxplayed); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] RemoveChallenge - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + return; + } else { + // move the track file + $tmxnew = str_replace($tmxtmpdir, $tmxdir, $tmxplayed); + if (!rename($aseco->server->trackdir . $tmxplayed, + $aseco->server->trackdir . $tmxnew)) { + trigger_error('Could not rename TMX track ' . $tmxplayed . ' to ' . $tmxnew, E_USER_WARNING); + return; + } else { + // add track with new path + $rtn = $aseco->client->query('AddChallenge', $tmxnew); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] AddChallenge - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + return; + } else { // store new path + $aseco->server->challenge->filename = $tmxnew; + + // throw 'tracklist changed' event + $aseco->releaseEvent('onTracklistChanged', array('rename', $tmxnew)); + } + } + } + + // disable track removal afterwards + $tmxplayed = false; + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s {#admin}permanently adds current track: {#highlite}{3}', + $chattitle, $admin->nickname, + stripColors($aseco->server->challenge->name)); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $message = formatText('{#server}> {#error}Current track {#highlite}$i {1} {#error}already permanently in track list!', + stripColors($aseco->server->challenge->name)); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Add a local track to the track rotation. + */ + } elseif ($command['params'][0] == 'addlocal') { + global $rasp, $jukebox_adminadd; // from plugin.rasp.php, rasp.settings.php + + // check for local track file + if ($arglist[1] != '') { + $sepchar = substr($aseco->server->trackdir, -1, 1); + $partialdir = 'Challenges' . $sepchar . 'Downloaded' . $sepchar . $arglist[1]; + if (!stristr($partialdir, '.Challenge.gbx')) { + $partialdir .= '.Challenge.gbx'; + } + $localfile = $aseco->server->trackdir . $partialdir; + if ($nocasepath = file_exists_nocase($localfile)) { + // check for maximum online track size (256 KB) + if (filesize($nocasepath) >= 256 * 1024) { + $message = formatText($rasp->messages['TRACK_TOO_LARGE'][0], + round(filesize($nocasepath) / 1024)); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + return; + } + $partialdir = str_replace($aseco->server->trackdir, '', $nocasepath); + + // check track vs. server settings + if ($aseco->server->getGame() == 'TMF') + $rtn = $aseco->client->query('CheckChallengeForCurrentServerParams', $partialdir); + else + $rtn = true; + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] CheckChallengeForCurrentServerParams - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $message = formatText($rasp->messages['JUKEBOX_IGNORED'][0], + stripColors(str_replace('Challenges' . $sepchar . 'Downloaded' . $sepchar, '', $partialdir)), $aseco->client->getErrorMessage()); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + // permanently add the track to the server list + $rtn = $aseco->client->query('AddChallenge', $partialdir); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] AddChallenge - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + } else { + $aseco->client->resetError(); + $aseco->client->query('GetChallengeInfo', $partialdir); + $track = $aseco->client->getResponse(); + if ($aseco->client->isError()) { + trigger_error('[' . $aseco->client->getErrorCode() . '] GetChallengeInfo - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $message = formatText('{#server}> {#error}Error getting info on track {#highlite}$i {1} {#error}!', + $partialdir); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + $track['Name'] = stripNewlines($track['Name']); + // check whether to jukebox as well + // overrules /add-ed but not yet played track + if ($jukebox_adminadd) { + $uid = $track['UId']; + $jukebox[$uid]['FileName'] = $track['FileName']; + $jukebox[$uid]['Name'] = $track['Name']; + $jukebox[$uid]['Env'] = $track['Environnement']; + $jukebox[$uid]['Login'] = $login; + $jukebox[$uid]['Nick'] = $admin->nickname; + $jukebox[$uid]['source'] = 'Local'; + $jukebox[$uid]['tmx'] = false; + $jukebox[$uid]['uid'] = $uid; + } + + // log console message + $aseco->console('{1} [{2}] adds local track {3} !', $logtitle, $login, stripColors($track['Name'], false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s {#admin}adds {3}track: {#highlite}{4}', + $chattitle, $admin->nickname, + ($jukebox_adminadd ? '& jukeboxes ' : ''), + stripColors($track['Name'])); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + // throw 'tracklist changed' event + $aseco->releaseEvent('onTracklistChanged', array('add', $partialdir)); + + // throw 'jukebox changed' event + if ($jukebox_adminadd) + $aseco->releaseEvent('onJukeboxChanged', array('add', $jukebox[$uid])); + } + } + } + } else { + $message = '{#server}> {#highlite}' . $partialdir . '{#error} not found!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = '{#server}> {#error}You must include a local track filename!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Warns a player with the specified login/PlayerID. + */ + } elseif ($command['params'][0] == 'warn' && $command['params'][1] != '') { + + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1])) { + // display warning message + $message = $aseco->getChatMessage('WARNING'); + if ($aseco->server->getGame() == 'TMF') { + $message = preg_split('/{br}/', $aseco->formatColors($message)); + foreach ($message as &$line) + $line = array($line); + + display_manialink($target->login, $aseco->formatColors('{#welcome}WARNING:'), array('Icons64x64_1', 'TV'), + $message, array(0.8), 'OK'); + } else { // TMN + $message = str_replace('{br}', LF, $aseco->formatColors($message)); + $aseco->client->query('SendDisplayServerMessageToLogin', $target->login, $message, 'OK', '', 0); + } + // log console message + $aseco->console('{1} [{2}] warned player {3}!', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} warned {#highlite}{3}$z$s{#admin} !', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } + + /** + * Kicks a player with the specified login/PlayerID. + */ + } elseif ($command['params'][0] == 'kick' && $command['params'][1] != '') { + + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1])) { + // log console message + $aseco->console('{1} [{2}] kicked player {3}!', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} kicked {#highlite}{3}$z$s{#admin} !', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + // kick the player + $aseco->client->query('Kick', $target->login); + } + + /** + * Kicks a ghost player with the specified login. + * This variant for ghost players that got disconnected doesn't + * check the login for validity and doesn't work with Player_IDs. + */ + } elseif ($command['params'][0] == 'kickghost' && $command['params'][1] != '') { + + // get player login without validation + $target = $command['params'][1]; + + // log console message + $aseco->console('{1} [{2}] kicked ghost player {3}!', $logtitle, $login, $target); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} kicked ghost {#highlite}{3}$z$s{#admin} !', + $chattitle, $admin->nickname, $target); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + // kick the ghost player + $aseco->client->query('Kick', $target); + + /** + * Ban a player with the specified login/PlayerID. + */ + } elseif ($command['params'][0] == 'ban' && $command['params'][1] != '') { + + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1])) { + // log console message + $aseco->console('{1} [{2}] bans player {3}!', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} bans {#highlite}{3}$z$s{#admin} !', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + // update banned IPs file + $aseco->bannedips[] = $target->ip; + $aseco->writeIPs(); + + // ban the player and also kick him + $aseco->client->query('Ban', $target->login); + } + + /** + * Un-bans player with the specified login/PlayerID. + */ + } elseif ($command['params'][0] == 'unban' && $command['params'][1] != '') { + + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1], true)) { + $bans = get_banlist($aseco); + // unban the player + $rtn = $aseco->client->query('UnBan', $target->login); + if (!$rtn) { + $message = formatText('{#server}> {#highlite}{1}{#error} is not a banned player!', + $command['params'][1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + if (($i = array_search($bans[$target->login][2], $aseco->bannedips)) !== false) { + // update banned IPs file + $aseco->bannedips[$i] = ''; + $aseco->writeIPs(); + } + + // log console message + $aseco->console('{1} [{2}] unbans player {3}', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} un-bans {#highlite}{3}', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + + /** + * Ban a player with the specified IP address. + */ + } elseif ($command['params'][0] == 'banip' && $command['params'][1] != '') { + + // check for valid IP not already banned + $ipaddr = $command['params'][1]; + if (preg_match('/^\d+\.\d+\.\d+\.\d+$/', $ipaddr)) { + if (empty($aseco->bannedips) || !in_array($ipaddr, $aseco->bannedips)) { + // log console message + $aseco->console('{1} [{2}] banned IP {3}!', $logtitle, $login, $ipaddr); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} bans IP {#highlite}{3}$z$s{#admin} !', + $chattitle, $admin->nickname, $ipaddr); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + // update banned IPs file + $aseco->bannedips[] = $ipaddr; + $aseco->writeIPs(); + } else { + $message = formatText('{#server}> {#highlite}{1}{#error} is already banned!', + $ipaddr); + } + } else { + $message = formatText('{#server}> {#highlite}{1}{#error} is not a valid IP address!', + $ipaddr); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Un-bans player with the specified IP address. + */ + } elseif ($command['params'][0] == 'unbanip' && $command['params'][1] != '') { + + // check for banned IP + if (($i = array_search($command['params'][1], $aseco->bannedips)) === false) { + $message = formatText('{#server}> {#highlite}{1}{#error} is not a banned IP address!', + $command['params'][1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + // update banned IPs file + $aseco->bannedips[$i] = ''; + $aseco->writeIPs(); + + // log console message + $aseco->console('{1} [{2}] unbans IP {3}', $logtitle, $login, $command['params'][1]); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} un-bans IP {#highlite}{3}', + $chattitle, $admin->nickname, $command['params'][1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Blacklists a player with the specified login/PlayerID. + */ + } elseif ($command['params'][0] == 'black' && $command['params'][1] != '') { + + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1], true)) { + // log console message + $aseco->console('{1} [{2}] blacklists player {3}!', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} blacklists {#highlite}{3}$z$s{#admin} !', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + // blacklist the player and then kick him + $aseco->client->query('BlackList', $target->login); + $aseco->client->query('Kick', $target->login); + + // update blacklist file + $filename = $aseco->settings['blacklist_file']; + $rtn = $aseco->client->query('SaveBlackList', $filename); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] SaveBlackList - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + } + } + + /** + * Un-blacklists player with the specified login/PlayerID. + */ + } elseif ($command['params'][0] == 'unblack' && $command['params'][1] != '') { + + $target = false; + $param = $command['params'][1]; + + // get new list of all blacklisted players + $blacks = get_blacklist($aseco); + // check as login + if (array_key_exists($param, $blacks)) { + $target = new Player(); + // check as player ID + } elseif (is_numeric($param) && $param > 0) { + if (empty($admin->playerlist)) { + $message = '{#server}> {#error}Use {#highlite}$i/players {#error}first (optionally {#highlite}$i/players {#error})'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + return false; + } + $pid = ltrim($param, '0'); + $pid--; + // find player by given # + if (array_key_exists($pid, $admin->playerlist)) { + $param = $admin->playerlist[$pid]['login']; + $target = new Player(); + } else { + $message = '{#server}> {#error}Player_ID not found! Type {#highlite}$i/players {#error}to see all players.'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + return false; + } + } + + // check for valid param + if ($target !== false) { + $target->login = $param; + $target->nickname = $aseco->getPlayerNick($param); + if ($target->nickname == '') + $target->nickname = $param; + + // unblacklist the player + $rtn = $aseco->client->query('UnBlackList', $target->login); + if (!$rtn) { + $message = formatText('{#server}> {#highlite}{1}{#error} is not a blacklisted player!', + $command['params'][1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + // log console message + $aseco->console('{1} [{2}] unblacklists player {3}', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} un-blacklists {#highlite}{3}', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + // update blacklist file + $filename = $aseco->settings['blacklist_file']; + $rtn = $aseco->client->query('SaveBlackList', $filename); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] SaveBlackList - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + } + } + } else { + $message = '{#server}> {#highlite}' . $param . ' {#error}is not a valid player! Use {#highlite}$i/players {#error}to find the correct login or Player_ID.'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Adds a guest player with the specified login/PlayerID. + */ + } elseif ($command['params'][0] == 'addguest' && $command['params'][1] != '') { + + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1], true)) { + // add the guest player + $aseco->client->query('AddGuest', $target->login); + + // log console message + $aseco->console('{1} [{2}] adds guest player {3}', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} adds guest {#highlite}{3}', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + // update guestlist file + $filename = $aseco->settings['guestlist_file']; + $rtn = $aseco->client->query('SaveGuestList', $filename); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] SaveGuestList - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + } + } + + /** + * Removes a guest player with the specified login/PlayerID. + */ + } elseif ($command['params'][0] == 'removeguest' && $command['params'][1] != '') { + + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1], true)) { + // remove the guest player + $rtn = $aseco->client->query('RemoveGuest', $target->login); + if (!$rtn) { + $message = formatText('{#server}> {#highlite}{1}{#error} is not a guest player!', + $command['params'][1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + // log console message + $aseco->console('{1} [{2}] removes guest player {3}', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} removes guest {#highlite}{3}', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + // update guestlist file + $filename = $aseco->settings['guestlist_file']; + $rtn = $aseco->client->query('SaveGuestList', $filename); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] SaveGuestList - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + } + } + } + + /** + * Passes a chat-based or TMX /add vote. + */ + } elseif ($command['params'][0] == 'pass') { + global $tmxadd, $chatvote, $plrvotes; // from plugin.rasp_jukebox.php, plugin.rasp_votes.php + + // pass any TMX and chat vote + if (!empty($tmxadd)) { + // force required votes down to the last one + $tmxadd['votes'] = 1; + } + elseif (!empty($chatvote)) { + $chatvote['votes'] = 1; + } + else { // no vote in progress + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}There is no vote right now!'), $login); + return; + } + + // log console message + $aseco->console('{1} [{2}] passes vote!', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} passes vote!', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + // bypass double vote check + $plrvotes = array(); + // enter the last vote + chat_y($aseco, $command); + + /** + * Cancels any vote. + */ + } elseif ($command['params'][0] == 'cancel' || + $command['params'][0] == 'can') { + global $tmxadd, $chatvote; // from plugin.rasp_jukebox.php, plugin.rasp_votes.php + + // cancel any CallVote, TMX and chat vote + $aseco->client->query('CancelVote'); + $tmxadd = array(); + $chatvote = array(); + // disable all vote panels + if ($aseco->server->getGame() == 'TMF') + allvotepanels_off($aseco); + + // log console message + $aseco->console('{1} [{2}] cancels vote!', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} cancels vote!', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + /** + * Forces end of current round. + */ + } elseif ($command['params'][0] == 'endround' || + $command['params'][0] == 'er') { + global $chatvote; // from plugin.rasp_votes.php + + // cancel possibly ongoing endround vote + if (!empty($chatvote) && $chatvote['type'] == 0) { + $chatvote = array(); + // disable all vote panels + if ($aseco->server->getGame() == 'TMF') + allvotepanels_off($aseco); + } + + // end this round + $aseco->client->query('ForceEndRound'); + + // log console message + $aseco->console('{1} [{2}] forces round end!', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} forces round end!', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + /** + * Displays the live or known players (on/offline) list. + * TMF player management inspired by Mistral. + */ + } elseif ($command['params'][0] == 'players') { + + $admin->playerlist = array(); + $admin->msgs = array(); + + // remember players parameter for possible refresh + $admin->panels['plyparam'] = $command['params'][1]; + $onlineonly = (strtolower($command['params'][1]) == 'live'); + // get current ignore/ban/black/guest lists + if ($aseco->server->getGame() == 'TMF') { + $ignores = get_ignorelist($aseco); + $bans = get_banlist($aseco); + $blacks = get_blacklist($aseco); + $guests = get_guestlist($aseco); + } + + // create new list of online players + $aseco->client->resetError(); + $onlinelist = array(); + // get current players on the server (hardlimited to 300) + if ($aseco->server->getGame() == 'TMF') + $aseco->client->query('GetPlayerList', 300, 0, 1); + else + $aseco->client->query('GetPlayerList', 300, 0); + $players = $aseco->client->getResponse(); + if ($aseco->client->isError()) { + trigger_error('[' . $aseco->client->getErrorCode() . '] GetPlayerList - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + } else { + foreach ($players as $pl) + if ($aseco->server->getGame() == 'TMF') { + // on relay, check for player from master server + if (!$aseco->server->isrelay || floor($pl['Flags'] / 10000) % 10 == 0) + $onlinelist[$pl['Login']] = array('login' => $pl['Login'], + 'nick' => $pl['NickName'], + 'spec' => $pl['SpectatorStatus']); + } else { + $onlinelist[$pl['Login']] = array('login' => $pl['Login'], + 'nick' => $pl['NickName'], + 'spec' => $pl['IsSpectator']); + } + } + + // use online list? + if ($onlineonly) { + $playerlist = $onlinelist; + } else { + // search for known players + $query = 'SELECT login,nickname FROM players + WHERE login LIKE ' . quotedString('%' . $arglist[1] . '%') . + ' OR nickname LIKE ' . quotedString('%' . $arglist[1] . '%') . + ' LIMIT 5000'; // prevent possible memory overrun + $result = mysql_query($query); + + $playerlist = array(); + if (mysql_num_rows($result) > 0) { + while ($row = mysql_fetch_row($result)) { + // skip any LAN logins + if (!isLANLogin($row[0])) + $playerlist[$row[0]] = array('login' => $row[0], + 'nick' => $row[1], + 'spec' => false); + } + } + mysql_free_result($result); + } + + if (!empty($playerlist)) { + if ($aseco->server->getGame() == 'TMN') { + $head = ($onlineonly ? 'Online' : 'Known') . ' Players On This Server:' . LF . + 'Id {#nick}Nick $g/{#login} Login' . LF; + $msg = ''; + $pid = 1; + $lines = 0; + $admin->msgs[0] = 1; + foreach ($playerlist as $pl) { + $plarr = array(); + $plarr['login'] = $pl['login']; + $admin->playerlist[] = $plarr; + + $msg .= '$g' . str_pad($pid, 2, '0', STR_PAD_LEFT) . '. {#black}' + . str_ireplace('$w', '', $pl['nick']) . '$z / ' + . ($aseco->isAnyAdminL($pl['login']) ? '{#logina}' : '{#login}' ) + . $pl['login'] . LF; + $pid++; + if (++$lines > 9) { + $admin->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + // add if last batch exists + if ($msg != '') + $admin->msgs[] = $aseco->formatColors($head . $msg); + + // display popup message + if (count($admin->msgs) == 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'OK', '', 0); + } elseif (count($admin->msgs) > 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No player(s) found!'), $login); + } + + } elseif ($aseco->server->getGame() == 'TMF') { + $head = ($onlineonly ? 'Online' : 'Known') . ' Players On This Server:'; + $msg = array(); + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login', 'Warn', 'Ignore', 'Kick', 'Ban', 'Black', 'Guest', 'Spec'); + $pid = 1; + $lines = 0; + $admin->msgs[0] = array(1, $head, array(1.49, 0.15, 0.5, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12, 0.12), array('Icons128x128_1', 'Buddies')); + + foreach ($playerlist as $lg => $pl) { + $plarr = array(); + $plarr['login'] = $lg; + $admin->playerlist[] = $plarr; + + // format nickname & login + $ply = '{#black}' . str_ireplace('$w', '', $pl['nick']) . '$z / ' + . ($aseco->isAnyAdminL($pl['login']) ? '{#logina}' : '{#login}' ) + . $pl['login']; + // define colored column strings + $wrn = '$ff3Warn'; + $ign = '$f93Ignore'; + $uig = '$d93UnIgn'; + $kck = '$c3fKick'; + $ban = '$f30Ban'; + $ubn = '$c30UnBan'; + $blk = '$f03Black'; + $ubk = '$c03UnBlack'; + $gst = '$3c3Add'; + $ugt = '$393Remove'; + $frc = '$09fForce'; + $off = '$09cOffln'; + $spc = '$09cSpec'; + + // always add clickable buttons + if ($pid <= 200) { + $ply = array($ply, $pid+2000); + if (array_key_exists($lg, $onlinelist)) { + // determine online operations + $wrn = array($wrn, $pid+2200); + if (array_key_exists($lg, $ignores)) + $ign = array($uig, $pid+2600); + else + $ign = array($ign, $pid+2400); + $kck = array($kck, $pid+2800); + if (array_key_exists($lg, $bans)) + $ban = array($ubn, $pid+3200); + else + $ban = array($ban, $pid+3000); + if (array_key_exists($lg, $blacks)) + $blk = array($ubk, $pid+3600); + else + $blk = array($blk, $pid+3400); + if (array_key_exists($lg, $guests)) + $gst = array($ugt, $pid+4000); + else + $gst = array($gst, $pid+3800); + if (!$onlinelist[$lg]['spec']) + $spc = array($frc, $pid+4200); + } else { + // determine offline operations + if (array_key_exists($lg, $ignores)) + $ign = array($uig, $pid+2600); + if (array_key_exists($lg, $bans)) + $ban = array($ubn, $pid+3200); + if (array_key_exists($lg, $blacks)) + $blk = array($ubk, $pid+3600); + else + $blk = array($blk, $pid+3400); + if (array_key_exists($lg, $guests)) + $gst = array($ugt, $pid+4000); + else + $gst = array($gst, $pid+3800); + $spc = $off; + } + } else { + // no more buttons + if (array_key_exists($lg, $ignores)) + $ign = $uig; + if (array_key_exists($lg, $bans)) + $ban = $ubn; + if (array_key_exists($lg, $blacks)) + $blk = $ubk; + if (array_key_exists($lg, $guests)) + $gst = $ugt; + if (array_key_exists($lg, $onlinelist)) { + if (!$onlinelist[$lg]['spec']) + $spc = $frc; + } else { + $spc = $off; + } + } + + $msg[] = array(str_pad($pid, 2, '0', STR_PAD_LEFT) . '.', $ply, + $wrn, $ign, $kck, $ban, $blk, $gst, $spc); + $pid++; + if (++$lines > 14) { + $admin->msgs[] = $msg; + $lines = 0; + $msg = array(); + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login', 'Warn', 'Ignore', 'Kick', 'Ban', 'Black', 'Guest', 'Spec'); + } + } + // add if last batch exists + if (count($msg) > 1) + $admin->msgs[] = $msg; + + // display ManiaLink message + if (count($admin->msgs) > 1) { + display_manialink_multi($admin); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No player(s) found!'), $login); + } + } + } else { + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No player(s) found!'), $login); + } + + /** + * Displays the ban list. + */ + } elseif ($command['params'][0] == 'showbanlist' || + $command['params'][0] == 'listbans') { + + $admin->playerlist = array(); + $admin->msgs = array(); + + // get new list of all banned players + $newlist = get_banlist($aseco); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Currently Banned Players:' . LF . 'Id {#nick}Nick $g/{#login} Login' . LF; + $msg = ''; + $pid = 1; + $lines = 0; + $admin->msgs[0] = 1; + foreach ($newlist as $player) { + $plarr = array(); + $plarr['login'] = $player[0]; + $admin->playerlist[] = $plarr; + + $msg .= '$g' . str_pad($pid, 2, '0', STR_PAD_LEFT) . '. {#black}' + . str_ireplace('$w', '', $player[1]) . '$z / {#login}' . $player[0] . LF; + $pid++; + if (++$lines > 9) { + $admin->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + // add if last batch exists + if ($msg != '') + $admin->msgs[] = $aseco->formatColors($head . $msg); + + // display popup message + if (count($admin->msgs) == 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'OK', '', 0); + } elseif (count($admin->msgs) > 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No banned player(s) found!'), $login); + } + + } elseif ($aseco->server->getGame() == 'TMF') { + $head = 'Currently Banned Players:'; + $msg = array(); + if ($aseco->settings['clickable_lists']) + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login$g (click to UnBan)'); + else + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + $pid = 1; + $lines = 0; + $admin->msgs[0] = array(1, $head, array(0.9, 0.1, 0.8), array('Icons64x64_1', 'NotBuddy')); + foreach ($newlist as $player) { + $plarr = array(); + $plarr['login'] = $player[0]; + $admin->playerlist[] = $plarr; + + // format nickname & login + $ply = '{#black}' . str_ireplace('$w', '', $player[1]) + . '$z / {#login}' . $player[0]; + // add clickable button + if ($aseco->settings['clickable_lists'] && $pid <= 200) + $ply = array($ply, $pid+4600); // action id + + $msg[] = array(str_pad($pid, 2, '0', STR_PAD_LEFT) . '.', $ply); + $pid++; + if (++$lines > 14) { + $admin->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->settings['clickable_lists']) + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login$g (click to UnBan)'); + else + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + } + } + // add if last batch exists + if (count($msg) > 1) + $admin->msgs[] = $msg; + + // display ManiaLink message + if (count($admin->msgs) > 1) { + display_manialink_multi($admin); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No banned player(s) found!'), $login); + } + } + + /** + * Displays the banned IPs list. + */ + } elseif ($command['params'][0] == 'showiplist' || + $command['params'][0] == 'listips') { + + $admin->playerlist = array(); + $admin->msgs = array(); + + // get new list of all banned IPs + $newlist = $aseco->bannedips; + if (empty($newlist)) { + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No banned IP(s) found!'), $login); + return; + } + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Currently Banned IPs:' . LF . 'Id {#login}IP' . LF; + $msg = ''; + $pid = 1; + $lines = 0; + $admin->msgs[0] = 1; + foreach ($newlist as $ip) { + if ($ip != '') { + $plarr = array(); + $plarr['ip'] = $ip; + $admin->playerlist[] = $plarr; + + $msg .= '$g' . str_pad($pid, 2, '0', STR_PAD_LEFT) . '. {#login}' . $ip . LF; + $pid++; + if (++$lines > 9) { + $admin->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + } + // add if last batch exists + if ($msg != '') + $admin->msgs[] = $aseco->formatColors($head . $msg); + + // display popup message + if (count($admin->msgs) == 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'OK', '', 0); + } elseif (count($admin->msgs) > 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No banned IP(s) found!'), $login); + } + + } elseif ($aseco->server->getGame() == 'TMF') { + $head = 'Currently Banned IPs:'; + $msg = array(); + if ($aseco->settings['clickable_lists']) + $msg[] = array('Id', '{#nick}IP$g (click to UnBan)'); + else + $msg[] = array('Id', '{#nick}IP'); + $pid = 1; + $lines = 0; + $admin->msgs[0] = array(1, $head, array(0.6, 0.1, 0.5), array('Icons64x64_1', 'NotBuddy')); + foreach ($newlist as $ip) { + if ($ip != '') { + $plarr = array(); + $plarr['ip'] = $ip; + $admin->playerlist[] = $plarr; + + // format IP + $ply = '{#black}' . $ip; + // add clickable button + if ($aseco->settings['clickable_lists'] && $pid <= 200) + $ply = array($ply, -7900-$pid); // action id + + $msg[] = array(str_pad($pid, 2, '0', STR_PAD_LEFT) . '.', $ply); + $pid++; + if (++$lines > 14) { + $admin->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->settings['clickable_lists']) + $msg[] = array('Id', '{#login}IP$g (click to UnBan)'); + else + $msg[] = array('Id', '{#login}IP'); + } + } + } + // add if last batch exists + if (count($msg) > 1) + $admin->msgs[] = $msg; + + // display ManiaLink message + if (count($admin->msgs) > 1) { + display_manialink_multi($admin); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No banned IP(s) found!'), $login); + } + } + + /** + * Displays the black list. + */ + } elseif ($command['params'][0] == 'showblacklist' || + $command['params'][0] == 'listblacks') { + + $admin->playerlist = array(); + $admin->msgs = array(); + + // get new list of all blacklisted players + $newlist = get_blacklist($aseco); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Currently Blacklisted Players:' . LF . 'Id {#nick}Nick $g/{#login} Login' . LF; + $msg = ''; + $pid = 1; + $lines = 0; + $admin->msgs[0] = 1; + foreach ($newlist as $player) { + $plarr = array(); + $plarr['login'] = $player[0]; + $admin->playerlist[] = $plarr; + + $msg .= '$g' . str_pad($pid, 2, '0', STR_PAD_LEFT) . '. {#black}' + . str_ireplace('$w', '', $player[1]) . '$z / {#login}' . $player[0] . LF; + $pid++; + if (++$lines > 9) { + $admin->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + // add if last batch exists + if ($msg != '') + $admin->msgs[] = $aseco->formatColors($head . $msg); + + // display popup message + if (count($admin->msgs) == 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'OK', '', 0); + } elseif (count($admin->msgs) > 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No blacklisted player(s) found!'), $login); + } + + } elseif ($aseco->server->getGame() == 'TMF') { + $head = 'Currently Blacklisted Players:'; + $msg = array(); + if ($aseco->settings['clickable_lists']) + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login$g (click to UnBlack)'); + else + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + $pid = 1; + $lines = 0; + $admin->msgs[0] = array(1, $head, array(0.9, 0.1, 0.8), array('Icons64x64_1', 'NotBuddy')); + foreach ($newlist as $player) { + $plarr = array(); + $plarr['login'] = $player[0]; + $admin->playerlist[] = $plarr; + + // format nickname & login + $ply = '{#black}' . str_ireplace('$w', '', $player[1]) + . '$z / {#login}' . $player[0]; + // add clickable button + if ($aseco->settings['clickable_lists'] && $pid <= 200) + $ply = array($ply, $pid+4800); // action id + + $msg[] = array(str_pad($pid, 2, '0', STR_PAD_LEFT) . '.', $ply); + $pid++; + if (++$lines > 14) { + $admin->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->settings['clickable_lists']) + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login$g (click to UnBlack)'); + else + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + } + } + // add if last batch exists + if (count($msg) > 1) + $admin->msgs[] = $msg; + + // display ManiaLink message + if (count($admin->msgs) > 1) { + display_manialink_multi($admin); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No blacklisted player(s) found!'), $login); + } + } + + /** + * Displays the guest list. + */ + } elseif ($command['params'][0] == 'showguestlist' || + $command['params'][0] == 'listguests') { + + $admin->playerlist = array(); + $admin->msgs = array(); + + // get new list of all guest players + $newlist = get_guestlist($aseco); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Current Guest Players:' . LF . 'Id {#nick}Nick $g/{#login} Login' . LF; + $msg = ''; + $pid = 1; + $lines = 0; + $admin->msgs[0] = 1; + foreach ($newlist as $player) { + $plarr = array(); + $plarr['login'] = $player[0]; + $admin->playerlist[] = $plarr; + + $msg .= '$g' . str_pad($pid, 2, '0', STR_PAD_LEFT) . '. {#black}' + . str_ireplace('$w', '', $player[1]) . '$z / {#login}' . $player[0] . LF; + $pid++; + if (++$lines > 9) { + $admin->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + // add if last batch exists + if ($msg != '') + $admin->msgs[] = $aseco->formatColors($head . $msg); + + // display popup message + if (count($admin->msgs) == 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'OK', '', 0); + } elseif (count($admin->msgs) > 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No guest player(s) found!'), $login); + } + + } elseif ($aseco->server->getGame() == 'TMF') { + $head = 'Current Guest Players:'; + $msg = array(); + if ($aseco->settings['clickable_lists']) + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login$g (click to Remove)'); + else + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + $pid = 1; + $lines = 0; + $admin->msgs[0] = array(1, $head, array(0.9, 0.1, 0.8), array('Icons128x128_1', 'Invite')); + foreach ($newlist as $player) { + $plarr = array(); + $plarr['login'] = $player[0]; + $admin->playerlist[] = $plarr; + + // format nickname & login + $ply = '{#black}' . str_ireplace('$w', '', $player[1]) + . '$z / {#login}' . $player[0]; + // add clickable button + if ($aseco->settings['clickable_lists'] && $pid <= 200) + $ply = array($ply, $pid+5000); // action id + + $msg[] = array(str_pad($pid, 2, '0', STR_PAD_LEFT) . '.', $ply); + $pid++; + if (++$lines > 14) { + $admin->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->settings['clickable_lists']) + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login$g (click to Remove)'); + else + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + } + } + // add if last batch exists + if (count($msg) > 1) + $admin->msgs[] = $msg; + + // display ManiaLink message + if (count($admin->msgs) > 1) { + display_manialink_multi($admin); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No guest player(s) found!'), $login); + } + } + + /** + * Saves the banned IPs list to bannedips.xml (default). + */ + } elseif ($command['params'][0] == 'writeiplist') { + + // write banned IPs file + $filename = $aseco->settings['bannedips_file']; + if (!$aseco->writeIPs()) { + $message = '{#server}> {#error}Error writing {#highlite}$i ' . $filename . ' {#error}!'; + } else { + // log console message + $aseco->console('{1} [{2}] wrote ' . $filename . '!', $logtitle, $login); + + $message = '{#server}> {#highlite}' . $filename . ' {#admin}written'; + } + // show chat message + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Loads the banned IPs list from bannedips.xml (default). + */ + } elseif ($command['params'][0] == 'readiplist') { + + // read banned IPs file + $filename = $aseco->settings['bannedips_file']; + if (!$aseco->readIPs()) { + $message = '{#server}> {#highlite}' . $filename . ' {#error}not found, or error reading!'; + } else { + // log console message + $aseco->console('{1} [{2}] read ' . $filename . '!', $logtitle, $login); + + $message = '{#server}> {#highlite}' . $filename . ' {#admin}read'; + } + // show chat message + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Saves the black list to blacklist.txt (default). + */ + } elseif ($command['params'][0] == 'writeblacklist') { + + $filename = $aseco->settings['blacklist_file']; + $rtn = $aseco->client->query('SaveBlackList', $filename); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] SaveBlackList - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $message = '{#server}> {#error}Error writing {#highlite}$i ' . $filename . ' {#error}!'; + } else { + // log console message + $aseco->console('{1} [{2}] wrote ' . $filename . '!', $logtitle, $login); + + $message = '{#server}> {#highlite}' . $filename . ' {#admin}written'; + } + // show chat message + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Loads the black list from blacklist.txt (default). + */ + } elseif ($command['params'][0] == 'readblacklist') { + + $filename = $aseco->settings['blacklist_file']; + $rtn = $aseco->client->query('LoadBlackList', $filename); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] LoadBlackList - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $message = '{#server}> {#highlite}' . $filename . ' {#error}not found, or error reading!'; + } else { + // log console message + $aseco->console('{1} [{2}] read ' . $filename . '!', $logtitle, $login); + + $message = '{#server}> {#highlite}' . $filename . ' {#admin}read'; + } + // show chat message + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Saves the guest list to guestlist.txt (default). + */ + } elseif ($command['params'][0] == 'writeguestlist') { + + $filename = $aseco->settings['guestlist_file']; + $rtn = $aseco->client->query('SaveGuestList', $filename); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] SaveGuestList - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $message = '{#server}> {#error}Error writing {#highlite}$i ' . $filename . ' {#error}!'; + } else { + // log console message + $aseco->console('{1} [{2}] wrote ' . $filename . '!', $logtitle, $login); + + $message = '{#server}> {#highlite}' . $filename . ' {#admin}written'; + } + // show chat message + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Loads the guest list from guestlist.txt (default). + */ + } elseif ($command['params'][0] == 'readguestlist') { + + $filename = $aseco->settings['guestlist_file']; + $rtn = $aseco->client->query('LoadGuestList', $filename); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] LoadGuestList - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $message = '{#server}> {#highlite}' . $filename . ' {#error}not found, or error loading!'; + } else { + // log console message + $aseco->console('{1} [{2}] read ' . $filename . '!', $logtitle, $login); + + $message = '{#server}> {#highlite}' . $filename . ' {#admin}read'; + } + // show chat message + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Cleans the ban list. + */ + } elseif ($command['params'][0] == 'cleanbanlist') { + + // clean server ban list + $aseco->client->query('CleanBanList'); + + // log console message + $aseco->console('{1} [{2}] cleaned ban list!', $logtitle, $login); + + // show chat message + $message = '{#server}> {#admin}Cleaned ban list!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Cleans the banned IPs list. + */ + } elseif ($command['params'][0] == 'cleaniplist') { + + // clean banned IPs file + $aseco->bannedips = array(); + $aseco->writeIPs(); + + // log console message + $aseco->console('{1} [{2}] cleaned banned IPs list!', $logtitle, $login); + + // show chat message + $message = '{#server}> {#admin}Cleaned banned IPs list!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Cleans the black list. + */ + } elseif ($command['params'][0] == 'cleanblacklist') { + + // clean server black list + $aseco->client->query('CleanBlackList'); + + // log console message + $aseco->console('{1} [{2}] cleaned black list!', $logtitle, $login); + + // show chat message + $message = '{#server}> {#admin}Cleaned black list!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Cleans the guest list. + */ + } elseif ($command['params'][0] == 'cleanguestlist') { + + // clean server guest list + $aseco->client->query('CleanGuestList'); + + // log console message + $aseco->console('{1} [{2}] cleaned guest list!', $logtitle, $login); + + // show chat message + $message = '{#server}> {#admin}Cleaned guest list!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Merges a global black list. + */ + } elseif ($command['params'][0] == 'mergegbl') { + global $globalbl_url; // from rasp.settings.php + + if (function_exists('admin_mergegbl')) { + if (isset($command['params'][1]) && $command['params'][1] != '') { + if (preg_match('/^https?:\/\/[-\w:.]+\//i', $command['params'][1])) { + admin_mergegbl($aseco, $logtitle, $login, true, $command['params'][1]); // from plugin.uptodate.php + } else { + $message = '{#server}> {#highlite}' . $command['params'][1] . ' {#error}is an invalid HTTP URL!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + admin_mergegbl($aseco, $logtitle, $login, true, $globalbl_url); + } + } else { + // show chat message + $message = '{#server}> {#admin}Merge Global BL unavailable - include plugins.uptodate.php in plugins.xml'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Shows/reloads player access control. + */ + } elseif ($command['params'][0] == 'access') { + + if (function_exists('admin_access')) { + $command['params'] = $command['params'][1]; + admin_access($aseco, $command); // from plugin.access.php + } else { + // show chat message + $message = '{#server}> {#admin}Access control unavailable - include plugins.access.php in plugins.xml'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Saves the track list to tracklist.txt (default). + */ + } elseif ($command['params'][0] == 'writetracklist') { + + $filename = $aseco->settings['default_tracklist']; + // check for optional alternate filename + if ($command['params'][1] != '') { + $filename = $command['params'][1]; + if (!stristr($filename, '.txt')) { + $filename .= '.txt'; + } + } + $rtn = $aseco->client->query('SaveMatchSettings', 'MatchSettings/' . $filename); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] SaveMatchSettings - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $message = '{#server}> {#error}Error writing {#highlite}$i ' . $filename . ' {#error}!'; + } else { + // should a random filter be added? + if ($aseco->settings['writetracklist_random']) { + $tracksfile = $aseco->server->trackdir . 'MatchSettings/' . $filename; + // read the match settings file + if (!$list = @file_get_contents($tracksfile)) { + trigger_error('Could not read match settings file ' . $tracksfile . ' !', E_USER_WARNING); + } else { + // insert random filter after section + $list = preg_replace('/<\/gameinfos>/', '$0' . CRLF . CRLF . + "\t" . CRLF . + "\t\t1" . CRLF . + "\t", $list); + + // write out the match settings file + if (!@file_put_contents($tracksfile, $list)) { + trigger_error('Could not write match settings file ' . $tracksfile . ' !', E_USER_WARNING); + } + } + } + + // log console message + $aseco->console('{1} [{2}] wrote track list: {3} !', $logtitle, $login, $filename); + + $message = '{#server}> {#highlite}' . $filename . '{#admin} written'; + + // throw 'tracklist changed' event + $aseco->releaseEvent('onTracklistChanged', array('write', null)); + } + // show chat message + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Loads the track list from tracklist.txt (default). + */ + } elseif ($command['params'][0] == 'readtracklist') { + + $filename = $aseco->settings['default_tracklist']; + // check for optional alternate filename + if ($command['params'][1] != '') { + $filename = $command['params'][1]; + if (!stristr($filename, '.txt')) { + $filename .= '.txt'; + } + } + if (file_exists($aseco->server->trackdir . 'MatchSettings/' . $filename)) { + $rtn = $aseco->client->query('LoadMatchSettings', 'MatchSettings/' . $filename); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] LoadMatchSettings - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $message = '{#server}> {#error}Error reading {#highlite}$i ' . $filename . ' {#error}!'; + } else { + // get track count + $cnt = $aseco->client->getResponse(); + + // log console message + $aseco->console('{1} [{2}] read track list: {3} ({4} tracks)!', $logtitle, $login, $filename, $cnt); + + $message = '{#server}> {#highlite}' . $filename . '{#admin} read with {#highlite}' . $cnt . '{#admin} track' . ($cnt == 1 ? '' : 's'); + + // throw 'tracklist changed' event + $aseco->releaseEvent('onTracklistChanged', array('read', null)); + } + } else { + $message = '{#server}> {#error}Cannot find {#highlite}$i ' . $filename . ' {#error}!'; + } + // show chat message + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Randomizes current track list. + */ + } elseif ($command['params'][0] == 'shuffle' || + $command['params'][0] == 'shufflemaps') { + global $autosave_matchsettings; // from rasp.settings.php + + if ($aseco->settings['writetracklist_random']) { + if ($autosave_matchsettings) { + if (file_exists($aseco->server->trackdir . 'MatchSettings/' . $autosave_matchsettings)) { + $rtn = $aseco->client->query('LoadMatchSettings', 'MatchSettings/' . $autosave_matchsettings); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] LoadMatchSettings - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $message = '{#server}> {#error}Error reading {#highlite}$i ' . $autosave_matchsettings . ' {#error}!'; + } else { + // get track count + $cnt = $aseco->client->getResponse(); + + // log console message + $aseco->console('{1} [{2}] shuffled track list: {3} ({4} tracks)!', $logtitle, $login, $autosave_matchsettings, $cnt); + + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} shuffled track list with {#highlite}{3}{#admin} track{4}!', + $chattitle, $admin->nickname, $cnt, ($cnt == 1 ? '' : 's')); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + return; + } + } else { + $message = '{#server}> {#error}Cannot find autosave matchsettings file {#highlite}$i ' . $autosave_matchsettings . ' {#error}!'; + } + } else { + $message = '{#server}> {#error}No autosave matchsettings file defined in {#highlite}$i rasp.settings.php {#error}!'; + } + } else { + $message = '{#server}> {#error}No tracklist randomization defined in {#highlite}$i config.xml {#error}!'; + } + // show chat message + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Displays list of duplicate tracks. + */ + } elseif ($command['params'][0] == 'listdupes') { + + $admin->tracklist = array(); + $admin->msgs = array(); + + // get new list of all tracks + $aseco->client->resetError(); + $dupelist = array(); + $newlist = array(); + $done = false; + $size = 300; + $i = 0; + while (!$done) { + $aseco->client->query('GetChallengeList', $size, $i); + $tracks = $aseco->client->getResponse(); + if (!empty($tracks)) { + if ($aseco->client->isError()) { + // warning if no tracks found + if (empty($newlist)) + trigger_error('[' . $aseco->client->getErrorCode() . '] GetChallengeList - ' . $aseco->client->getErrorMessage() . ' - No tracks found!', E_USER_WARNING); + $done = true; + break; + } + foreach ($tracks as $trow) { + $trow['Name'] = stripNewlines($trow['Name']); + // store duplicate track + if (isset($newlist[$trow['UId']])) { + $dupelist[] = $trow; + } else { + $newlist[$trow['UId']] = $trow; + } + } + if (count($tracks) < $size) { + // got less than 300 tracks, might as well leave + $done = true; + } else { + $i += $size; + } + } else { + $done = true; + } + } + + // check for duplicate tracks + if (!empty($dupelist)) { + if ($aseco->server->getGame() == 'TMN') { + $head = 'Duplicate Tracks On This Server:' . LF . 'Id Name' . LF; + $msg = ''; + $tid = 1; + $lines = 0; + $admin->msgs[0] = 1; + foreach ($dupelist as $row) { + $trackname = $row['Name']; + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + + // store track in player object for remove/erase + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $admin->tracklist[] = $trkarr; + + $msg .= '$g' . str_pad($tid, 3, '0', STR_PAD_LEFT) . '. {#black}' . $trackname . LF; + $tid++; + if (++$lines > 9) { + $admin->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + // add if last batch exists + if ($msg != '') + $admin->msgs[] = $aseco->formatColors($head . $msg); + + // display popup message + if (count($admin->msgs) == 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'OK', '', 0); + } else { // > 2 + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + } + + } elseif ($aseco->server->getGame() == 'TMF') { + $head = 'Duplicate Tracks On This Server:'; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Env'); + else + $msg[] = array('Id', 'Name'); + $tid = 1; + $lines = 0; + // reserve extra width for $w tags + $extra = ($aseco->settings['lists_colortracks'] ? 0.2 : 0); + if ($aseco->server->packmask != 'Stadium') + $admin->msgs[0] = array(1, $head, array(0.90+$extra, 0.15, 0.6+$extra, 0.15), array('Icons128x128_1', 'Challenge')); + else + $admin->msgs[0] = array(1, $head, array(0.75+$extra, 0.15, 0.6+$extra), array('Icons128x128_1', 'Challenge')); + foreach ($dupelist as $row) { + $trackname = stripColors($row['Name']); + if (!$aseco->settings['lists_colortracks']) + $trackname = stripColors($trackname); + + // store track in player object for remove/erase + $trkarr = array(); + $trkarr['name'] = $row['Name']; + $trkarr['environment'] = $row['Environnement']; + $trkarr['filename'] = $row['FileName']; + $trkarr['uid'] = $row['UId']; + $admin->tracklist[] = $trkarr; + + if ($aseco->server->packmask != 'Stadium') + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + '{#black}' . $trackname, + $trkarr['environment']); + else + $msg[] = array(str_pad($tid, 3, '0', STR_PAD_LEFT) . '.', + '{#black}' . $trackname); + $tid++; + if (++$lines > 14) { + $admin->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->server->packmask != 'Stadium') + $msg[] = array('Id', 'Name', 'Env'); + else + $msg[] = array('Id', 'Name'); + } + } + // add if last batch exists + if (count($msg) > 1) + $admin->msgs[] = $msg; + + // display ManiaLink message + display_manialink_multi($admin); + } + + } else { + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No duplicate track(s) found!'), $login); + return; + } + + /** + * Remove a track from the active rotation, optionally erase track file too. + * Doesn't update match settings unfortunately - command 'writetracklist' will though. + */ + } elseif (($command['params'][0] == 'remove' && $command['params'][1] != '') || + ($command['params'][0] == 'erase' && $command['params'][1] != '')) { + global $rasp; // from plugin.rasp.php + + // verify parameter + $param = $command['params'][1]; + if (is_numeric($param) && $param >= 0) { + if (empty($admin->tracklist)) { + $message = $rasp->messages['LIST_HELP'][0]; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + return; + } + // find track by given # + $tid = ltrim($param, '0'); + $tid--; + if (array_key_exists($tid, $admin->tracklist)) { + $name = stripColors($admin->tracklist[$tid]['name']); + $filename = $aseco->server->trackdir . $admin->tracklist[$tid]['filename']; + $rtn = $aseco->client->query('RemoveChallenge', $filename); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] RemoveChallenge - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $message = formatText('{#server}> {#error}Error removing track {#highlite}$i {1} {#error}!', + $filename); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s {#admin}removes track: {#highlite}{3}', + $chattitle, $admin->nickname, $name); + if ($command['params'][0] == 'erase' && is_file($filename)) { + if (unlink($filename)) { + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s {#admin}erases track: {#highlite}{3}', + $chattitle, $admin->nickname, $name); + } else { + $message = '{#server}> {#error}Delete file {#highlite}$i ' . $filename . '{#error} failed'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s {#admin}erase track failed: {#highlite}{3}', + $chattitle, $admin->nickname, $name); + } + } + // show chat message + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + // log console message + $aseco->console('{1} [{2}] ' . $command['params'][0] . 'd track {3}', $logtitle, $login, stripColors($name, false)); + + // throw 'tracklist changed' event + $aseco->releaseEvent('onTracklistChanged', array('remove', $filename)); + } + } else { + $message = $rasp->messages['JUKEBOX_NOTFOUND'][0]; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $rasp->messages['JUKEBOX_HELP'][0]; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Remove current track from the active rotation, optionally erase track file too. + * Doesn't update match settings unfortunately - command 'writetracklist' will though. + */ + } elseif ($command['params'][0] == 'removethis' || + $command['params'][0] == 'erasethis') { + + // get current track info and remove it from rotation + $name = stripColors($aseco->server->challenge->name); + $filename = $aseco->server->trackdir . $aseco->server->challenge->filename; + $rtn = $aseco->client->query('RemoveChallenge', $filename); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] RemoveChallenge - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $message = formatText('{#server}> {#error}Error removing track {#highlite}$i {1} {#error}!', + $filename); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s {#admin}removes current track: {#highlite}{3}', + $chattitle, $admin->nickname, $name); + if ($command['params'][0] == 'erasethis' && is_file($filename)) { + if (unlink($filename)) { + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s {#admin}erases current track: {#highlite}{3}', + $chattitle, $admin->nickname, $name); + } else { + $message = '{#server}> {#error}Delete file {#highlite}$i ' . $filename . '{#error} failed'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s {#admin}erase track failed: {#highlite}{3}', + $chattitle, $admin->nickname, $name); + } + } + // show chat message + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + // log console message + $aseco->console('{1} [{2}] ' . $command['params'][0] . '-ed track {3}', $logtitle, $login, stripColors($name, false)); + + // throw 'tracklist changed' event + $aseco->releaseEvent('onTracklistChanged', array('remove', $filename)); + } + + /** + * Adds a player to global mute/ignore list + */ + } elseif (($command['params'][0] == 'mute' || $command['params'][0] == 'ignore') + && $command['params'][1] != '') { + global $muting_available; // from plugin.muting.php + + if ($aseco->server->getGame() != 'TMF') { + // check for muting plugin + if ($muting_available) { + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1])) { + // check if not yet in global mute/ignore list + if (!in_array($target->login, $aseco->server->mutelist)) { + // mute this player + $aseco->server->mutelist[] = $target->login; + + // log console message + $aseco->console('{1} [{2}] mutes player [{3} : {4}]!', $logtitle, $login, $target->login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} mutes {#highlite}{3}$z$s{#admin} !', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $message = '{#server}> {#error}Player {#highlite}$i ' . stripColors($target->nickname) . '{#error} is already in global mute/ignore list!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + } else { + $message = '{#server}> {#error}Player muting not available!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { // TMF + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1])) { + // ignore the player + $aseco->client->query('Ignore', $target->login); + + // check if in global mute/ignore list + if (!in_array($target->login, $aseco->server->mutelist)) { + // add player to list + $aseco->server->mutelist[] = $target->login; + } + + // log console message + $aseco->console('{1} [{2}] ignores player {3}!', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} ignores {#highlite}{3}$z$s{#admin} !', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } + } + + /** + * Removes a player from global mute/ignore list + */ + } elseif (($command['params'][0] == 'unmute' || $command['params'][0] == 'unignore') + && $command['params'][1] != '') { + global $muting_available; // from plugin.muting.php + + if ($aseco->server->getGame() != 'TMF') { + // check for muting plugin + if ($muting_available) { + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1], true)) { + // check if already in global mute/ignore list + if (($i = array_search($target->login, $aseco->server->mutelist)) !== false) { + // unmute this player + $aseco->server->mutelist[$i] = ''; + + // log console message + $aseco->console('{1} [{2}] unmutes player [{3} : {4}]!', $logtitle, $login, $target->login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} unmutes {#highlite}{3}$z$s{#admin} !', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $message = '{#server}> {#error}Player {#highlite}$i ' . stripColors($target->nickname) . '{#error} is not in global mute/ignore list!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + } else { + $message = '{#server}> {#error}Player muting not available!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { // TMF + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1], true)) { + // unignore the player + $rtn = $aseco->client->query('UnIgnore', $target->login); + if (!$rtn) { + $message = formatText('{#server}> {#highlite}{1}{#error} is not an ignored player!', + $command['params'][1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + // check if in global mute/ignore list + if (($i = array_search($target->login, $aseco->server->mutelist)) !== false) { + // remove player from list + $aseco->server->mutelist[$i] = ''; + } + + // log console message + $aseco->console('{1} [{2}] unignores player {3}', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} un-ignores {#highlite}{3}', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + } + + /** + * Displays the global mute/ignore list. + */ + } elseif ($command['params'][0] == 'mutelist' || + $command['params'][0] == 'listmutes' || + $command['params'][0] == 'ignorelist' || + $command['params'][0] == 'listignores') { + global $muting_available; // from plugin.muting.php + + $admin->playerlist = array(); + $admin->msgs = array(); + + if ($aseco->server->getGame() == 'TMN') { + // check for muting plugin + if ($muting_available) { + // check for muted players + if (empty($aseco->server->mutelist)) { + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No muted/ignored players found!'), $login); + return; + } + + $head = 'Globally Muted/Ignored Players:' . LF . 'Id {#nick}Nick $g/{#login} Login' . LF; + $msg = ''; + $pid = 1; + $lines = 0; + $admin->msgs[0] = 1; + foreach ($aseco->server->mutelist as $pl) { + if ($pl != '') { + $plarr = array(); + $plarr['login'] = $pl; + $admin->playerlist[] = $plarr; + + $msg .= '$g' . str_pad($pid, 2, '0', STR_PAD_LEFT) . '. {#black}' + . $aseco->getPlayerNick($pl) . '$z / {#login}' . $pl . LF; + $pid++; + if (++$lines > 9) { + $admin->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + } + // add if last batch exists + if ($msg != '') + $admin->msgs[] = $aseco->formatColors($head . $msg); + + // display popup message + if (count($admin->msgs) == 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'OK', '', 0); + } elseif (count($admin->msgs) > 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No muted/ignored players found!'), $login); + } + + } else { + $message = '{#server}> {#error}Player muting not available!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + } elseif ($aseco->server->getGame() == 'TMF') { + // get new list of all ignored players + $newlist = get_ignorelist($aseco); + + $head = 'Globally Muted/Ignored Players:'; + $msg = array(); + if ($aseco->settings['clickable_lists']) + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login$g (click to UnIgnore)'); + else + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + $pid = 1; + $lines = 0; + $admin->msgs[0] = array(1, $head, array(0.9, 0.1, 0.8), array('Icons128x128_1', 'Padlock', 0.01)); + foreach ($newlist as $player) { + $plarr = array(); + $plarr['login'] = $player[0]; + $admin->playerlist[] = $plarr; + + // format nickname & login + $ply = '{#black}' . str_ireplace('$w', '', $player[1]) + . '$z / {#login}' . $player[0]; + // add clickable button + if ($aseco->settings['clickable_lists'] && $pid <= 200) + $ply = array($ply, $pid+4400); // action id + + $msg[] = array(str_pad($pid, 2, '0', STR_PAD_LEFT) . '.', $ply); + $pid++; + if (++$lines > 14) { + $admin->msgs[] = $msg; + $lines = 0; + $msg = array(); + if ($aseco->settings['clickable_lists']) + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login$g (click to UnIgnore)'); + else + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + } + } + // add if last batch exists + if (count($msg) > 1) + $admin->msgs[] = $msg; + + // display ManiaLink message + if (count($admin->msgs) > 1) { + display_manialink_multi($admin); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No muted/ignored players found!'), $login); + } + } + + /** + * Cleans the global mute/ignore list. + */ + } elseif ($command['params'][0] == 'cleanmutes' || + $command['params'][0] == 'cleanignores') { + + // clean internal and server list + $aseco->server->mutelist = array(); + if ($aseco->server->getGame() == 'TMF') + $aseco->client->query('CleanIgnoreList'); + + // log console message + $aseco->console('{1} [{2}] cleaned global mute/ignore list!', $logtitle, $login); + + // show chat message + $message = '{#server}> {#admin}Cleaned global mute/ignore list!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Adds a new admin. + */ + } elseif ($command['params'][0] == 'addadmin' && $command['params'][1] != '') { + + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1])) { + // check if player not already admin + if (!$aseco->isAdminL($target->login)) { + // add the new admin + $aseco->admin_list['TMLOGIN'][] = $target->login; + $aseco->admin_list['IPADDRESS'][] = ($aseco->settings['auto_admin_addip'] ? $target->ip : ''); + $aseco->writeLists(); + + // log console message + $aseco->console('{1} [{2}] adds admin [{3} : {4}]!', $logtitle, $login, $target->login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} adds new {3}$z$s{#admin}: {#highlite}{4}$z$s{#admin} !', + $chattitle, $admin->nickname, + $aseco->titles['ADMIN'][0], $target->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $message = formatText('{#server}> {#error}Login {#highlite}$i {1}{#error} is already in Admin List!', $target->login); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + + /** + * Removes an admin. + */ + } elseif ($command['params'][0] == 'removeadmin' && $command['params'][1] != '') { + + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1], true)) { + // check if player is indeed admin + if ($aseco->isAdminL($target->login)) { + $i = array_search($target->login, $aseco->admin_list['TMLOGIN']); + $aseco->admin_list['TMLOGIN'][$i] = ''; + $aseco->admin_list['IPADDRESS'][$i] = ''; + $aseco->writeLists(); + + // log console message + $aseco->console('{1} [{2}] removes admin [{3} : {4}]!', $logtitle, $login, $target->login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} removes {3}$z$s{#admin}: {#highlite}{4}$z$s{#admin} !', + $chattitle, $admin->nickname, + $aseco->titles['ADMIN'][0], $target->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $message = formatText('{#server}> {#error}Login {#highlite}$i {1}{#error} is not in Admin List!', $target->login); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + + /** + * Adds a new operator. + */ + } elseif ($command['params'][0] == 'addop' && $command['params'][1] != '') { + + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1])) { + // check if player not already operator + if (!$aseco->isOperatorL($target->login)) { + // add the new operator + $aseco->operator_list['TMLOGIN'][] = $target->login; + $aseco->operator_list['IPADDRESS'][] = ($aseco->settings['auto_admin_addip'] ? $target->ip : ''); + $aseco->writeLists(); + + // log console message + $aseco->console('{1} [{2}] adds operator [{3} : {4}]!', $logtitle, $login, $target->login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} adds new {3}$z$s{#admin}: {#highlite}{4}$z$s{#admin} !', + $chattitle, $admin->nickname, + $aseco->titles['OPERATOR'][0], $target->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $message = formatText('{#server}> {#error}Login {#highlite}$i {1}{#error} is already in Operator List!', $target->login); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + + /** + * Removes an operator. + */ + } elseif ($command['params'][0] == 'removeop' && $command['params'][1] != '') { + + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1], true)) { + // check if player is indeed operator + if ($aseco->isOperatorL($target->login)) { + $i = array_search($target->login, $aseco->operator_list['TMLOGIN']); + $aseco->operator_list['TMLOGIN'][$i] = ''; + $aseco->operator_list['IPADDRESS'][$i] = ''; + $aseco->writeLists(); + + // log console message + $aseco->console('{1} [{2}] removes operator [{3} : {4}]!', $logtitle, $login, $target->login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} removes {3}$z$s{#admin}: {#highlite}{4}$z$s{#admin} !', + $chattitle, $admin->nickname, + $aseco->titles['OPERATOR'][0], $target->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $message = formatText('{#server}> {#error}Login {#highlite}$i {1}{#error} is not in Operator List!', $target->login); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + + /** + * Displays the masteradmins list. + */ + } elseif ($command['params'][0] == 'listmasters') { + + $admin->playerlist = array(); + $admin->msgs = array(); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Current MasterAdmins:' . LF . 'Id {#nick}Nick $g/{#login} Login' . LF; + $msg = ''; + $pid = 1; + $lines = 0; + $admin->msgs[0] = 1; + foreach ($aseco->masteradmin_list['TMLOGIN'] as $player) { + // skip any LAN logins + if ($player != '' && !isLANLogin($player)) { + $plarr = array(); + $plarr['login'] = $player; + $admin->playerlist[] = $plarr; + + $msg .= '$g' . str_pad($pid, 2, '0', STR_PAD_LEFT) . '. {#black}' + . $aseco->getPlayerNick($player) . '$z / {#login}' . $player . LF; + $pid++; + if (++$lines > 9) { + $admin->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + } + // add if last batch exists + if ($msg != '') + $admin->msgs[] = $aseco->formatColors($head . $msg); + + // display popup message + if (count($admin->msgs) == 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'OK', '', 0); + } elseif (count($admin->msgs) > 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No masteradmin(s) found!'), $login); + } + + } elseif ($aseco->server->getGame() == 'TMF') { + $head = 'Current MasterAdmins:'; + $msg = array(); + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + $pid = 1; + $lines = 0; + $admin->msgs[0] = array(1, $head, array(0.9, 0.1, 0.8), array('Icons128x128_1', 'Solo')); + foreach ($aseco->masteradmin_list['TMLOGIN'] as $player) { + // skip any LAN logins + if ($player != '' && !isLANLogin($player)) { + $plarr = array(); + $plarr['login'] = $player; + $admin->playerlist[] = $plarr; + + $msg[] = array(str_pad($pid, 2, '0', STR_PAD_LEFT) . '.', + '{#black}' . $aseco->getPlayerNick($player) + . '$z / {#login}' . $player); + $pid++; + if (++$lines > 14) { + $admin->msgs[] = $msg; + $lines = 0; + $msg = array(); + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + } + } + } + // add if last batch exists + if (count($msg) > 1) + $admin->msgs[] = $msg; + + // display ManiaLink message + if (count($admin->msgs) > 1) { + display_manialink_multi($admin); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No masteradmin(s) found!'), $login); + } + } + + /** + * Displays the admins list. + */ + } elseif ($command['params'][0] == 'listadmins') { + + if (empty($aseco->admin_list['TMLOGIN'])) { + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No admin(s) found!'), $login); + return; + } + + $admin->playerlist = array(); + $admin->msgs = array(); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Current Admins:' . LF . 'Id {#nick}Nick $g/{#login} Login' . LF; + $msg = ''; + $pid = 1; + $lines = 0; + $admin->msgs[0] = 1; + foreach ($aseco->admin_list['TMLOGIN'] as $player) { + if ($player != '') { + $plarr = array(); + $plarr['login'] = $player; + $admin->playerlist[] = $plarr; + + $msg .= '$g' . str_pad($pid, 2, '0', STR_PAD_LEFT) . '. {#black}' + . $aseco->getPlayerNick($player) . '$z / {#login}' . $player . LF; + $pid++; + if (++$lines > 9) { + $admin->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + } + // add if last batch exists + if ($msg != '') + $admin->msgs[] = $aseco->formatColors($head . $msg); + + // display popup message + if (count($admin->msgs) == 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'OK', '', 0); + } elseif (count($admin->msgs) > 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No admin(s) found!'), $login); + } + + } elseif ($aseco->server->getGame() == 'TMF') { + $head = 'Current Admins:'; + $msg = array(); + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + $pid = 1; + $lines = 0; + $admin->msgs[0] = array(1, $head, array(0.9, 0.1, 0.8), array('Icons128x128_1', 'Solo')); + foreach ($aseco->admin_list['TMLOGIN'] as $player) { + if ($player != '') { + $plarr = array(); + $plarr['login'] = $player; + $admin->playerlist[] = $plarr; + + $msg[] = array(str_pad($pid, 2, '0', STR_PAD_LEFT) . '.', + '{#black}' . $aseco->getPlayerNick($player) + . '$z / {#login}' . $player); + $pid++; + if (++$lines > 14) { + $admin->msgs[] = $msg; + $lines = 0; + $msg = array(); + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + } + } + } + // add if last batch exists + if (count($msg) > 1) + $admin->msgs[] = $msg; + + // display ManiaLink message + if (count($admin->msgs) > 1) { + display_manialink_multi($admin); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No admin(s) found!'), $login); + } + } + + /** + * Displays the operators list. + */ + } elseif ($command['params'][0] == 'listops') { + + if (empty($aseco->operator_list['TMLOGIN'])) { + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No operator(s) found!'), $login); + return; + } + + $admin->playerlist = array(); + $admin->msgs = array(); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Current Operators:' . LF . 'Id {#nick}Nick $g/{#login} Login' . LF; + $msg = ''; + $pid = 1; + $lines = 0; + $admin->msgs[0] = 1; + foreach ($aseco->operator_list['TMLOGIN'] as $player) { + if ($player != '') { + $plarr = array(); + $plarr['login'] = $player; + $admin->playerlist[] = $plarr; + + $msg .= '$g' . str_pad($pid, 2, '0', STR_PAD_LEFT) . '. {#black}' + . $aseco->getPlayerNick($player) . '$z / {#login}' . $player . LF; + $pid++; + if (++$lines > 9) { + $admin->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + } + // add if last batch exists + if ($msg != '') + $admin->msgs[] = $aseco->formatColors($head . $msg); + + // display popup message + if (count($admin->msgs) == 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'OK', '', 0); + } elseif (count($admin->msgs) > 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No operator(s) found!'), $login); + } + + } elseif ($aseco->server->getGame() == 'TMF') { + $head = 'Current Operators:'; + $msg = array(); + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + $pid = 1; + $lines = 0; + $admin->msgs[0] = array(1, $head, array(0.9, 0.1, 0.8), array('Icons128x128_1', 'Solo')); + foreach ($aseco->operator_list['TMLOGIN'] as $player) { + if ($player != '') { + $plarr = array(); + $plarr['login'] = $player; + $admin->playerlist[] = $plarr; + + $msg[] = array(str_pad($pid, 2, '0', STR_PAD_LEFT) . '.', + '{#black}' . $aseco->getPlayerNick($player) + . '$z / {#login}' . $player); + $pid++; + if (++$lines > 14) { + $admin->msgs[] = $msg; + $lines = 0; + $msg = array(); + $msg[] = array('Id', '{#nick}Nick $g/{#login} Login'); + } + } + } + // add if last batch exists + if (count($msg) > 1) + $admin->msgs[] = $msg; + + // display ManiaLink message + if (count($admin->msgs) > 1) { + display_manialink_multi($admin); + } else { // == 1 + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No operator(s) found!'), $login); + } + } + + /** + * Show/change an admin ability + */ + } elseif ($command['params'][0] == 'adminability') { + + // check for ability parameter + if ($command['params'][1] != '') { + // map to uppercase before checking list + $ability = strtoupper($command['params'][1]); + + // check for valid ability + if (isset($aseco->adm_abilities[$ability])) { + if (isset($command['params'][2]) && $command['params'][2] != '') { + // update ability + if (strtoupper($command['params'][2]) == 'ON') { + $aseco->adm_abilities[$ability][0] = true; + $aseco->writeLists(); + + // log console message + $aseco->console('{1} [{2}] set new Admin ability: {3} ON', $logtitle, $login, strtolower($ability)); + } + elseif (strtoupper($command['params'][2]) == 'OFF') { + $aseco->adm_abilities[$ability][0] = false; + $aseco->writeLists(); + + // log console message + $aseco->console('{1} [{2}] set new Admin ability: {3} OFF', $logtitle, $login, strtolower($ability)); + } // else ignore bogus parameter + } + // show current/new ability message + $message = formatText('{#server}> {#admin}{1}$z$s {#admin}ability {#highlite}{2}{#admin} is: {#highlite}{3}', + $aseco->titles['ADMIN'][0], strtolower($ability), + ($aseco->adm_abilities[$ability][0] ? 'ON' : 'OFF')); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + $message = formatText('{#server}> {#error}No ability {#highlite}$i {1}{#error} known!', + $command['params'][1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = '{#server}> {#error}No ability specified - see {#highlite}$i /admin helpall{#error} and {#highlite}$i /admin listabilities{#error}!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Show/change an operator ability + */ + } elseif ($command['params'][0] == 'opability') { + + // check for ability parameter + if ($command['params'][1] != '') { + // map to uppercase before checking list + $ability = strtoupper($command['params'][1]); + + // check for valid ability + if (isset($aseco->op_abilities[$ability])) { + if (isset($command['params'][2]) && $command['params'][2] != '') { + // update ability + if (strtoupper($command['params'][2]) == 'ON') { + $aseco->op_abilities[$ability][0] = true; + $aseco->writeLists(); + + // log console message + $aseco->console('{1} [{2}] set new Operator ability: {3} ON', $logtitle, $login, strtolower($ability)); + } + elseif (strtoupper($command['params'][2]) == 'OFF') { + $aseco->op_abilities[$ability][0] = false; + $aseco->writeLists(); + + // log console message + $aseco->console('{1} [{2}] set new Operator ability: {3} OFF', $logtitle, $login, strtolower($ability)); + } // else ignore bogus parameter + } + // show current/new ability message + $message = formatText('{#server}> {#admin}{1}$z$s {#admin}ability {#highlite}{2}{#admin} is: {#highlite}{3}', + $aseco->titles['OPERATOR'][0], strtolower($ability), + ($aseco->op_abilities[$ability][0] ? 'ON' : 'OFF')); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + $message = formatText('{#server}> {#error}No ability {#highlite}$i {1}{#error} known!', + $command['params'][1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = '{#server}> {#error}No ability specified - see {#highlite}$i /admin helpall{#error} and {#highlite}$i /admin listabilities{#error}!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Displays Admin and Operator abilities + */ + } elseif ($command['params'][0] == 'listabilities') { + + $master = false; + if ($aseco->isMasterAdminL($login)) { + if ($command['params'][1] == '') { + $master = true; + $abilities = $aseco->adm_abilities; + $title = 'MasterAdmin'; + } else { + if (stripos('admin', $command['params'][1]) === 0) { + $abilities = $aseco->adm_abilities; + $title = 'Admin'; + } + elseif (stripos('operator', $command['params'][1]) === 0) { + $abilities = $aseco->op_abilities; + $title = 'Operator'; + } + // all three above fall through to listing below + else { + $message = formatText('{#server}> {#highlite}{1}{#error} is not a valid administrator tier!', + $command['params'][1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + return; + } + } + } + elseif ($aseco->isAdminL($login)) { + $abilities = $aseco->adm_abilities; + $title = 'Admin'; + } + else { // isOperator + $abilities = $aseco->op_abilities; + $title = 'Operator'; + } + + if ($aseco->server->getGame() == 'TMN') { + // compile current ability listing + $help = 'Current ' . $title . ' abilities:' . LF . LF; + $chat = false; + foreach ($abilities as $ability => $value) { + switch (strtolower($ability)) { + case 'chat_pma': + if ($value[0] || $master) { + $help .= 'chat_pma : {#black}/pma$g sends a PM to player & admins' . LF; + $chat = true; + } + break; + case 'chat_bestworst': + if ($value[0] || $master) { + $help .= 'chat_bestworst : {#black}/best$g & {#black}/worst$g accept login/Player_ID' . LF; + $chat = true; + } + break; + case 'chat_statsip': + if ($value[0] || $master) { + $help .= 'chat_statsip : {#black}/stats$g includes IP address' . LF; + $chat = true; + } + break; + case 'chat_summary': + if ($value[0] || $master) { + $help .= 'chat_summary : {#black}/summary$g accepts login/Player_ID' . LF; + $chat = true; + } + break; + case 'chat_jb_multi': + if ($value[0] || $master) { + $help .= 'chat_jb_multi : {#black}/jukebox$g adds more than one track' . LF; + $chat = true; + } + break; + case 'chat_jb_recent': + if ($value[0] || $master) { + $help .= 'chat_jb_recent : {#black}/jukebox$g adds recently played track' . LF; + $chat = true; + } + break; + case 'chat_add_tref': + if ($value[0] || $master) { + $help .= 'chat_add_tref : {#black}/add trackref$g writes TMX trackref file' . LF; + $chat = true; + } + break; + case 'chat_match': + if ($value[0] || $master) { + $help .= 'chat_match : {#black}/match$g allows match control' . LF; + $chat = true; + } + break; + case 'chat_tc_listen': + if ($value[0] || $master) { + $help .= 'chat_tc_listen : {#black}/tc$g will copy team chat to admins' . LF; + $chat = true; + } + break; + case 'chat_jfreu': + if ($value[0] || $master) { + $help .= 'chat_jfreu : use all {#black}/jfreu$g commands' . LF; + $chat = true; + } + break; + case 'noidlekick_play': + if ($value[0] || $master) { + $help .= 'noidlekick_play : no idlekick when {#black}player$g' . LF; + $chat = true; + } + break; + case 'noidlekick_spec': + if ($value[0] || $master) { + $help .= 'noidlekick_spec: no idlekick when {#black}spectator$g' . LF; + $chat = true; + } + break; + } + } + + if ($chat) $help .= LF; + $help .= 'See {#black}/admin helpall$g for available /admin commands'; + + // display popup message + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $aseco->formatColors($help), 'OK', '', 0); + + } elseif ($aseco->server->getGame() == 'TMF') { + // compile current ability listing + $header = 'Current ' . $title . ' abilities:'; + $help = array(); + $chat = false; + foreach ($abilities as $ability => $value) { + switch (strtolower($ability)) { + case 'chat_pma': + if ($value[0] || $master) { + $help[] = array('chat_pma', '{#black}/pma$g sends a PM to player & admins'); + $chat = true; + } + break; + case 'chat_bestworst': + if ($value[0] || $master) { + $help[] = array('chat_bestworst', '{#black}/best$g & {#black}/worst$g accept login/Player_ID'); + $chat = true; + } + break; + case 'chat_statsip': + if ($value[0] || $master) { + $help[] = array('chat_statsip', '{#black}/stats$g includes IP address'); + $chat = true; + } + break; + case 'chat_summary': + if ($value[0] || $master) { + $help[] = array('chat_summary', '{#black}/summary$g accepts login/Player_ID'); + $chat = true; + } + break; + case 'chat_jb_multi': + if ($value[0] || $master) { + $help[] = array('chat_jb_multi', '{#black}/jukebox$g adds more than one track'); + $chat = true; + } + break; + case 'chat_jb_recent': + if ($value[0] || $master) { + $help[] = array('chat_jb_recent', '{#black}/jukebox$g adds recently played track'); + $chat = true; + } + break; + case 'chat_add_tref': + if ($value[0] || $master) { + $help[] = array('chat_add_tref', '{#black}/add trackref$g writes TMX trackref file'); + $chat = true; + } + break; + case 'chat_match': + if ($value[0] || $master) { + $help[] = array('chat_match', '{#black}/match$g allows match control'); + $chat = true; + } + break; + case 'chat_tc_listen': + if ($value[0] || $master) { + $help[] = array('chat_tc_listen', '{#black}/tc$g will copy team chat to admins'); + $chat = true; + } + break; + case 'chat_jfreu': + if ($value[0] || $master) { + $help[] = array('chat_jfreu', 'use all {#black}/jfreu$g commands'); + $chat = true; + } + break; + case 'chat_musicadmin': + if ($value[0] || $master) { + $help[] = array('chat_musicadmin', 'use {#black}/music$g admin commands'); + $chat = true; + } + break; + case 'noidlekick_play': + if ($value[0] || $master) { + $help[] = array('noidlekick_play', 'no idlekick when {#black}player$g'); + $chat = true; + } + break; + case 'noidlekick_spec': + if ($value[0] || $master) { + $help[] = array('noidlekick_spec', 'no idlekick when {#black}spectator$g'); + $chat = true; + } + break; + case 'server_coppers': + if ($value[0] || $master) { + $help[] = array('server_coppers', 'view coppers amount in {#black}/server$g'); + $chat = true; + } + break; + } + } + + if ($chat) $help[] = array(); + $help[] = array('See {#black}/admin helpall$g for available /admin commands'); + + // display ManiaLink message + display_manialink($login, $header, array('Icons128x128_1', 'ProfileAdvanced', 0.02), $help, array(1.0, 0.3, 0.7), 'OK'); + } + + /** + * Saves the admins/operators/abilities list to adminops.xml (default). + */ + } elseif ($command['params'][0] == 'writeabilities') { + + // write admins/operators file + $filename = $aseco->settings['adminops_file']; + if (!$aseco->writeLists()) { + $message = '{#server}> {#error}Error writing {#highlite}$i ' . $filename . ' {#error}!'; + } else { + // log console message + $aseco->console('{1} [{2}] wrote ' . $filename . '!', $logtitle, $login); + + $message = '{#server}> {#highlite}' . $filename . ' {#admin}written'; + } + // show chat message + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Loads the admins/operators/abilities list from adminops.xml (default). + */ + } elseif ($command['params'][0] == 'readabilities') { + + // read admins/operators file + $filename = $aseco->settings['adminops_file']; + if (!$aseco->readLists()) { + $message = '{#server}> {#highlite}' . $filename . ' {#error}not found, or error reading!'; + } else { + // log console message + $aseco->console('{1} [{2}] read ' . $filename . '!', $logtitle, $login); + + $message = '{#server}> {#highlite}' . $filename . ' {#admin}read'; + } + // show chat message + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Display message in pop-up to all players + */ + } elseif ($command['params'][0] == 'wall' || + $command['params'][0] == 'mta') { + + // check for non-empty message + if ($arglist[1] != '') { + if ($aseco->server->getGame() == 'TMN') { + $message = $aseco->formatColors('{#black}') . $chattitle . ' ' . $admin->nickname . '$z :' . LF; + // insure window doesn't become too wide + $message .= wordwrap($aseco->formatColors('{#welcome}') . $arglist[1], 30, LF); + // display popup message to all players + $aseco->client->query('SendDisplayServerMessage', $message, 'OK', '', 0); + } elseif ($aseco->server->getGame() == 'TMF') { + $header = '{#black}' . $chattitle . ' ' . $admin->nickname . '$z :'; + // insure window doesn't become too wide + $message = wordwrap('{#welcome}' . $arglist[1], 40, LF . '{#welcome}'); + $message = explode(LF, $aseco->formatColors($message)); + foreach ($message as &$line) + $line = array($line); + + // display ManiaLink message to all players + foreach ($aseco->server->players->player_list as $target) + display_manialink($target->login, $header, array('Icons64x64_1', 'Inbox'), $message, array(0.8), 'OK'); + } + + // log console message + $aseco->console('{1} [{2}] sent wall message: {3}', $logtitle, $login, $arglist[1]); + } else { + $message = '{#server}> {#error}No message!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Delete records/rs_times database entries for specific record & sync. + */ + } elseif ($command['params'][0] == 'delrec' && $command['params'][1] != '') { + global $rasp; // from plugin.rasp.php + + // verify parameter + $param = $command['params'][1]; + if (is_numeric($param) && $param > 0 && $param <= $aseco->server->records->count()) { + $param = ltrim($param, '0'); + $param--; + // get record info + $record = $aseco->server->records->getRecord($param); + $pid = $aseco->getPlayerId($record->player->login); + + // remove times before record + if (method_exists($rasp, 'deleteTime')) + $rasp->deleteTime($aseco->server->challenge->id, $pid); + // remove record and fill up if necessary + ldb_removeRecord($aseco, $aseco->server->challenge->id, $pid, $param); + $param++; + + // log console message + $aseco->console('{1} [{2}] removed record {3} by {4} !', $logtitle, $login, $param, $record->player->login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s {#admin}removes record {#highlite}{3}{#admin} by {#highlite}{4}', + $chattitle, $admin->nickname, $param, stripColors($record->player->nickname)); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $message = '{#server}> {#error}No such record {#highlite}$i ' . $param . ' {#error}!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Prune records/rs_times database entries for specific track. + */ + } elseif ($command['params'][0] == 'prunerecs' && $command['params'][1] != '') { + global $rasp; // from plugin.rasp.php + + // verify parameter + $param = $command['params'][1]; + if (is_numeric($param) && $param >= 0) { + if (empty($admin->tracklist)) { + $message = $rasp->messages['LIST_HELP'][0]; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + return; + } + // find track by given # + $jid = ltrim($param, '0'); + $jid--; + if (array_key_exists($jid, $admin->tracklist)) { + $uid = $admin->tracklist[$jid]['uid']; + $name = stripColors($admin->tracklist[$jid]['name']); + $track = $aseco->getChallengeId($uid); + + if ($track > 0) { + // delete the records and rs_times + $query = 'DELETE FROM records WHERE ChallengeID=' . $track; + mysql_query($query); + $query = 'DELETE FROM rs_times WHERE challengeID=' . $track; + mysql_query($query); + + // log console message + $aseco->console('{1} [{2}] pruned records/times for track {3} !', $logtitle, $login, stripColors($name, false)); + + // show chat message + $message = '{#server}> {#admin}Deleted all records & times for track: {#highlite}' . $name; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + $message = '{#server}> {#error}Can\'t find ChallengeId for track: {#highlite}$i ' . $name . ' / ' . $uid; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $rasp->messages['JUKEBOX_NOTFOUND'][0]; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $rasp->messages['JUKEBOX_HELP'][0]; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Sets custom rounds points. + */ + } elseif ($command['params'][0] == 'rpoints' && $command['params'][1] != '') { + + if ($aseco->server->getGame() == 'TMF') { + if (function_exists('admin_rpoints')) { + admin_rpoints($aseco, $admin, $logtitle, $chattitle, $arglist[1]); // from plugin.rpoints.php + } else { + // show chat message + $message = '{#server}> {#admin}Custom Rounds points unavailable - include plugins.rpoints.php in plugins.xml'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Start or stop match tracking. + */ + } elseif ($command['params'][0] == 'match') { + global $MatchSettings; // from plugin.matchsave.php + + if (function_exists('match_loadsettings')) { + if ($command['params'][1] == 'begin') { + match_loadsettings(); // from plugin.matchsave.php + $MatchSettings['enable'] = true; + + // log console message + $aseco->console('{1} [{2}] started match!', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} has started the match', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } + elseif ($command['params'][1] == 'end') { + $MatchSettings['enable'] = false; + + // log console message + $aseco->console('{1} [{2}] ended match!', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} has ended the match', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } + else { + // show chat message + $message = '{#server}> {#admin}Match is currently ' . ($MatchSettings['enable'] ? 'Running' : 'Stopped'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + // show chat message + $message = '{#server}> {#admin}Match tracking unavailable - include plugins.matchsave.php in plugins.xml'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Shows or sets AllowChallengeDownload status. + */ + } elseif ($command['params'][0] == 'acdl') { + + $param = strtolower($command['params'][1]); + if ($param == 'on' || $param == 'off') { + $enabled = ($param == 'on'); + $aseco->client->query('AllowChallengeDownload', $enabled); + + // log console message + $aseco->console('{1} [{2}] set AllowChallengeDownload {3} !', $logtitle, $login, ($enabled ? 'ON' : 'OFF')); + + // show chat message + $message = '{#server}> {#admin}AllowChallengeDownload set to ' . ($enabled ? 'Enabled' : 'Disabled'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + $aseco->client->query('IsChallengeDownloadAllowed'); + $enabled = $aseco->client->getResponse(); + + // show chat message + $message = '{#server}> {#admin}AllowChallengeDownload is currently ' . ($enabled ? 'Enabled' : 'Disabled'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Shows or sets Auto TimeLimit status. + */ + } elseif ($command['params'][0] == 'autotime') { + global $atl_active; // from plugin.autotime.php + + // check for autotime plugin + if (isset($atl_active)) { + $param = strtolower($command['params'][1]); + if ($param == 'on' || $param == 'off') { + $atl_active = ($param == 'on'); + + // log console message + $aseco->console('{1} [{2}] set Auto TimeLimit {3} !', $logtitle, $login, ($atl_active ? 'ON' : 'OFF')); + + // show chat message + $message = '{#server}> {#admin}Auto TimeLimit set to ' . ($atl_active ? 'Enabled' : 'Disabled'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + // show chat message + $message = '{#server}> {#admin}Auto TimeLimit is currently ' . ($atl_active ? 'Enabled' : 'Disabled'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + // show chat message + $message = '{#server}> {#admin}Auto TimeLimit unavailable - include plugins.autotime.php in plugins.xml'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Shows or sets DisableRespawn status (TMF). + */ + } elseif ($command['params'][0] == 'disablerespawn') { + + if ($aseco->server->getGame() == 'TMF') { + $param = strtolower($command['params'][1]); + if ($param == 'on' || $param == 'off') { + $enabled = ($param == 'on'); + $aseco->client->query('SetDisableRespawn', $enabled); + + // log console message + $aseco->console('{1} [{2}] set DisableRespawn {3} !', $logtitle, $login, ($enabled ? 'ON' : 'OFF')); + + // show chat message + $message = '{#server}>> {#admin}DisableRespawn set to ' . ($enabled ? 'Enabled' : 'Disabled') . ' on the next track'; + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $aseco->client->query('GetDisableRespawn'); + $enabled = $aseco->client->getResponse(); + + // show chat message + $message = '{#server}> {#admin}DisableRespawn is currently ' . ($enabled['CurrentValue'] ? 'Enabled' : 'Disabled'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Shows or sets ForceShowAllOpponents status (TMF). + */ + } elseif ($command['params'][0] == 'forceshowopp') { + + if ($aseco->server->getGame() == 'TMF') { + $param = strtolower($command['params'][1]); + if ($param == 'all' || $param == 'off') { + $enabled = ($param == 'all' ? 1 : 0); + $aseco->client->query('SetForceShowAllOpponents', $enabled); + + // log console message + $aseco->console('{1} [{2}] set ForceShowAllOpponents {3} !', $logtitle, $login, ($enabled ? 'ALL' : 'OFF')); + + // show chat message + $message = '{#server}>> {#admin}ForceShowAllOpponents set to {#highlite}' . ($enabled ? 'Enabled' : 'Disabled') . '{#admin} on the next track'; + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } elseif (is_numeric($param) && $param > 1) { + $enabled = intval($param); + $aseco->client->query('SetForceShowAllOpponents', $enabled); + + // log console message + $aseco->console('{1} [{2}] set ForceShowAllOpponents to {3} !', $logtitle, $login, $enabled); + + // show chat message + $message = '{#server}>> {#admin}ForceShowAllOpponents set to {#highlite}' . $enabled . '{#admin} on the next track'; + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $aseco->client->query('GetForceShowAllOpponents'); + $enabled = $aseco->client->getResponse(); + $enabled = $enabled['CurrentValue']; + + // show chat message + $message = '{#server}> {#admin}ForceShowAllOpponents is set to: {#highlite}' . ($enabled != 0 ? ($enabled > 1 ? $enabled : 'All') : 'Off'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Shows or sets Automatic ScorePanel status (TMF). + */ + } elseif ($command['params'][0] == 'scorepanel') { + global $auto_scorepanel; + + if ($aseco->server->getGame() == 'TMF') { + $param = strtolower($command['params'][1]); + if ($param == 'on' || $param == 'off') { + $auto_scorepanel = ($param == 'on'); + scorepanel_off($aseco, null); + + // log console message + $aseco->console('{1} [{2}] set Automatic ScorePanel {3} !', $logtitle, $login, ($auto_scorepanel ? 'ON' : 'OFF')); + + // show chat message + $message = '{#server}>> {#admin}Automatic ScorePanel set to ' . ($auto_scorepanel ? 'Enabled' : 'Disabled'); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + // show chat message + $message = '{#server}> {#admin}Automatic ScorePanel is currently ' . ($auto_scorepanel ? 'Enabled' : 'Disabled'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Shows or sets Rounds Finishpanel status (TMF). + */ + } elseif ($command['params'][0] == 'roundsfinish') { + global $rounds_finishpanel; + + if ($aseco->server->getGame() == 'TMF') { + $param = strtolower($command['params'][1]); + if ($param == 'on' || $param == 'off') { + $rounds_finishpanel = ($param == 'on'); + + // log console message + $aseco->console('{1} [{2}] set Rounds Finishpanel {3} !', $logtitle, $login, ($rounds_finishpanel ? 'ON' : 'OFF')); + + // show chat message + $message = '{#server}>> {#admin}Rounds Finishpanel set to ' . ($rounds_finishpanel ? 'Enabled' : 'Disabled'); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + // show chat message + $message = '{#server}> {#admin}Rounds Finishpanel is currently ' . ($rounds_finishpanel ? 'Enabled' : 'Disabled'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Forces a player into Blue or Red team (TMF). + */ + } elseif ($command['params'][0] == 'forceteam' && $command['params'][1] != '') { + + if ($aseco->server->getGame() == 'TMF') { + // check for Team mode + if ($aseco->server->gameinfo->mode == Gameinfo::TEAM) { + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1])) { + // get player's team + $aseco->client->query('GetPlayerInfo', $target->login); + $info = $aseco->client->getResponse(); + // check for new team + if (isset($command['params'][2]) && $command['params'][2] != '') { + $team = strtolower($command['params'][2]); + + if (strpos('blue', $team) === 0) { + if ($info['TeamId'] != 0) { + // set player to Blue team + $aseco->client->query('ForcePlayerTeam', $target->login, 0); + + // log console message + $aseco->console('{1} [{2}] forces {3} into Blue team!', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} forces {#highlite}{3}$z$s{#admin} into $00fBlue{#admin} team!', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $message = '{#server}> {#admin}Player {#highlite}' . + stripColors($target->nickname) . + '{#admin} is already in $00fBlue{#admin} team'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + } elseif (strpos('red', $team) === 0) { + if ($info['TeamId'] != 1) { + // set player to Red team + $aseco->client->query('ForcePlayerTeam', $target->login, 1); + + // log console message + $aseco->console('{1} [{2}] forces {3} into Red team!', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} forces {#highlite}{3}$z$s{#admin} into $f00Red{#admin} team!', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $message = '{#server}> {#admin}Player {#highlite}' . + stripColors($target->nickname) . + '{#admin} is already in $f00Red{#admin} team'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + } else { + $message = '{#server}> {#highlite}' . $team . '$z$s{#error} is not a valid team!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + // show current team + $message = '{#server}> {#admin}Player {#highlite}' . + stripColors($target->nickname) . '{#admin} is in ' . + ($info['TeamId'] == 0 ? '$00fBlue' : '$f00Red') . + '{#admin} team'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + } else { + $message = '{#server}> {#error}Command only available in {#highlite}$i Team {#error}mode!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Forces player into free camera spectator (TMF). + */ + } elseif ($command['params'][0] == 'forcespec' && $command['params'][1] != '') { + + if ($aseco->server->getGame() == 'TMF') { + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1])) { + if (!$aseco->isSpectator($target)) { + // force player into free spectator + $rtn = $aseco->client->query('ForceSpectator', $target->login, 1); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] ForceSpectator - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + } else { + // allow spectator to switch back to player + $rtn = $aseco->client->query('ForceSpectator', $target->login, 0); + // force free camera mode on spectator + $aseco->client->addCall('ForceSpectatorTarget', array($target->login, '', 2)); + // free up player slot + $aseco->client->addCall('SpectatorReleasePlayerSlot', array($target->login)); + // log console message + $aseco->console('{1} [{2}] forces player {3} into spectator!', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} forces player {#highlite}{3}$z$s{#admin} into spectator!', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } + } else { + $message = formatText('{#server}> {#highlite}{1} {#error}is already a spectator!', + stripColors($target->nickname)); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Forces a spectator into free camera mode (TMF). + */ + } elseif ($command['params'][0] == 'specfree' && $command['params'][1] != '') { + + if ($aseco->server->getGame() == 'TMF') { + // get player information + if ($target = $aseco->getPlayerParam($admin, $command['params'][1])) { + if ($aseco->isSpectator($target)) { + // force free camera mode on spectator + $rtn = $aseco->client->query('ForceSpectatorTarget', $target->login, '', 2); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] ForceSpectatorTarget - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + } else { + // log console message + $aseco->console('{1} [{2}] forces spectator free mode on {3}!', $logtitle, $login, stripColors($target->nickname, false)); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} forces spectator free mode on {#highlite}{3}$z$s{#admin} !', + $chattitle, $admin->nickname, str_ireplace('$w', '', $target->nickname)); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } + } else { + $message = formatText('{#server}> {#highlite}{1} {#error}is not a spectator!', + stripColors($target->nickname)); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Selects default window style (TMF). + */ + } elseif ($command['params'][0] == 'panel') { + + if ($aseco->server->getGame() == 'TMF') { + if (function_exists('admin_panel')) { + $command['params'] = $command['params'][1]; + admin_panel($aseco, $command); // from plugin.panels.php + } else { + // show chat message + $message = '{#server}> {#admin}Admin panel unavailable - include plugins.panels.php in plugins.xml'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Selects default window style (TMF). + */ + } elseif ($command['params'][0] == 'style' && $command['params'][1] != '') { + + if ($aseco->server->getGame() == 'TMF') { + if (strtolower($command['params'][1]) == 'off') { + $aseco->style = array(); + $aseco->settings['window_style'] = 'Off'; + + // log console message + $aseco->console('{1} [{2}] reset default window style', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} reset default window style', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + $style_file = 'styles/' . $command['params'][1] . '.xml'; + // load default style + if (($style = $aseco->xml_parser->parseXml($style_file)) && isset($style['STYLES'])) { + $aseco->style = $style['STYLES']; + + // log console message + $aseco->console('{1} [{2}] selects default window style [{3}]', $logtitle, $login, $command['params'][1]); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} selects default window style {#highlite}{3}', + $chattitle, $admin->nickname, $command['params'][1]); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + // Could not read/parse XML file + $message = '{#server}> {#error}No valid style file, use {#highlite}$i /style list {#error}!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Selects default admin panel (TMF). + */ + } elseif ($command['params'][0] == 'admpanel' && $command['params'][1] != '') { + + if ($aseco->server->getGame() == 'TMF') { + if (strtolower($command['params'][1]) == 'off') { + $aseco->panels['admin'] = ''; + $aseco->settings['admin_panel'] = 'Off'; + + // log console message + $aseco->console('{1} [{2}] reset default admin panel', $logtitle, $login); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} reset default admin panel', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + // added file prefix + $panel = $command['params'][1]; + if (strtolower(substr($command['params'][1], 0, 5)) != 'admin') + $panel = 'Admin' . $panel; + $panel_file = 'panels/' . $panel . '.xml'; + // load default panel + if ($panel = @file_get_contents($panel_file)) { + $aseco->panels['admin'] = $panel; + + // log console message + $aseco->console('{1} [{2}] selects default admin panel [{3}]', $logtitle, $login, $command['params'][1]); + + // show chat message + $message = formatText('{#server}> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} selects default admin panel {#highlite}{3}', + $chattitle, $admin->nickname, $command['params'][1]); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + // Could not read XML file + $message = '{#server}> {#error}No valid admin panel file, use {#highlite}$i /admin panel list {#error}!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Selects default donate panel (TMUF). + */ + } elseif ($command['params'][0] == 'donpanel' && $command['params'][1] != '') { + + if ($aseco->server->getGame() == 'TMF') { + // check for TMUF server + if ($aseco->server->rights) { + if (strtolower($command['params'][1]) == 'off') { + $aseco->panels['donate'] = ''; + $aseco->settings['donate_panel'] = 'Off'; + + // log console message + $aseco->console('{1} [{2}] reset default donate panel', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} reset default donate panel', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + // added file prefix + $panel = $command['params'][1]; + if (strtolower(substr($command['params'][1], 0, 6)) != 'donate') + $panel = 'Donate' . $panel; + $panel_file = 'panels/' . $panel . '.xml'; + // load default panel + if ($panel = @file_get_contents($panel_file)) { + $aseco->panels['donate'] = $panel; + + // log console message + $aseco->console('{1} [{2}] selects default donate panel [{3}]', $logtitle, $login, $command['params'][1]); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} selects default donate panel {#highlite}{3}', + $chattitle, $admin->nickname, $command['params'][1]); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + // Could not read XML file + $message = '{#server}> {#error}No valid donate panel file, use {#highlite}$i /donpanel list {#error}!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + } else { + $message = formatText($aseco->getChatMessage('UNITED_ONLY'), 'server'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Selects default records panel (TMF). + */ + } elseif ($command['params'][0] == 'recpanel' && $command['params'][1] != '') { + + if ($aseco->server->getGame() == 'TMF') { + if (strtolower($command['params'][1]) == 'off') { + $aseco->panels['records'] = ''; + $aseco->settings['records_panel'] = 'Off'; + + // log console message + $aseco->console('{1} [{2}] reset default records panel', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} reset default records panel', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + // added file prefix + $panel = $command['params'][1]; + if (strtolower(substr($command['params'][1], 0, 7)) != 'records') + $panel = 'Records' . $panel; + $panel_file = 'panels/' . $panel . '.xml'; + // load default panel + if ($panel = @file_get_contents($panel_file)) { + $aseco->panels['records'] = $panel; + + // log console message + $aseco->console('{1} [{2}] selects default records panel [{3}]', $logtitle, $login, $command['params'][1]); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} selects default records panel {#highlite}{3}', + $chattitle, $admin->nickname, $command['params'][1]); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + // Could not read XML file + $message = '{#server}> {#error}No valid records panel file, use {#highlite}$i /recpanel list {#error}!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Selects default vote panel (TMF). + */ + } elseif ($command['params'][0] == 'votepanel' && $command['params'][1] != '') { + + if ($aseco->server->getGame() == 'TMF') { + if (strtolower($command['params'][1]) == 'off') { + $aseco->panels['vote'] = ''; + $aseco->settings['vote_panel'] = 'Off'; + + // log console message + $aseco->console('{1} [{2}] reset default vote panel', $logtitle, $login); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} reset default vote panel', + $chattitle, $admin->nickname); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + // added file prefix + $panel = $command['params'][1]; + if (strtolower(substr($command['params'][1], 0, 4)) != 'vote') + $panel = 'Vote' . $panel; + $panel_file = 'panels/' . $panel . '.xml'; + // load default panel + if ($panel = @file_get_contents($panel_file)) { + $aseco->panels['vote'] = $panel; + + // log console message + $aseco->console('{1} [{2}] selects default vote panel [{3}]', $logtitle, $login, $command['params'][1]); + + // show chat message + $message = formatText('{#server}>> {#admin}{1}$z$s {#highlite}{2}$z$s{#admin} selects default vote panel {#highlite}{3}', + $chattitle, $admin->nickname, $command['params'][1]); + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + } else { + // Could not read XML file + $message = '{#server}> {#error}No valid vote panel file, use {#highlite}$i /votepanel list {#error}!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Shows server's coppers amount (TMUF). + */ + } elseif ($command['params'][0] == 'coppers') { + + if ($aseco->server->getGame() == 'TMF') { + // check for TMUF server + if ($aseco->server->rights) { + // get server coppers + $aseco->client->query('GetServerCoppers'); + $coppers = $aseco->client->getResponse(); + + // show chat message + $message = formatText($aseco->getChatMessage('COPPERS'), + $aseco->server->name, $coppers); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + $message = formatText($aseco->getChatMessage('UNITED_ONLY'), 'server'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Pays server coppers to login (TMUF). + */ + } elseif ($command['params'][0] == 'pay') { + + if ($aseco->server->getGame() == 'TMF') { + // check for TMUF server + if ($aseco->server->rights) { + if (function_exists('admin_payment')) { + if (!isset($command['params'][2])) $command['params'][2] = ''; + admin_payment($aseco, $login, $command['params'][1], + $command['params'][2]); // from plugin.donate.php + } else { + // show chat message + $message = '{#server}> {#admin}Server payment unavailable - include plugins.donate.php in plugins.xml'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = formatText($aseco->getChatMessage('UNITED_ONLY'), 'server'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Displays relays list or shows relay master (TMF). + */ + } elseif ($command['params'][0] == 'relays') { + + if ($aseco->server->getGame() == 'TMF') { + if ($aseco->server->isrelay) { + // show chat message + $message = formatText($aseco->getChatMessage('RELAYMASTER'), + $aseco->server->relaymaster['Login'], $aseco->server->relaymaster['NickName']); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + if (empty($aseco->server->relayslist)) { + // show chat message + $message = formatText($aseco->getChatMessage('NO_RELAYS')); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + $header = 'Relay servers:'; + $relays = array(); + $relays[] = array('{#login}Login', '{#nick}Nick'); + foreach ($aseco->server->relayslist as $relay) + $relays[] = array($relay['Login'], $relay['NickName']); + + // display ManiaLink message + display_manialink($login, $header, array('BgRaceScore2', 'Spectator'), $relays, array(1.0, 0.35, 0.65), 'OK'); + } + } + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Shows server's detailed settings (TMF). + */ + } elseif ($command['params'][0] == 'server') { + + if ($aseco->server->getGame() == 'TMN') { + $version = $aseco->client->addCall('GetVersion', array()); + $network = $aseco->client->addCall('GetNetworkStats', array()); + $options = $aseco->client->addCall('GetServerOptions', array(1)); + $gameinfo = $aseco->client->addCall('GetCurrentGameInfo', array(1)); + if (!$aseco->client->multiquery()) { + trigger_error('[' . $aseco->client->getErrorCode() . '] GetServer (multi) - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + return; + } else { + $response = $aseco->client->getResponse(); + $version = $response[$version][0]; + $network = $response[$network][0]; + $options = $response[$options][0]; + $gameinfo = $response[$gameinfo][0]; + } + + // compile settings overview + $admin->msgs = array(); + $admin->msgs[0] = 1; + $head = 'System info for: ' . $options['Name'] . '$z' . LF . LF; + + $stats = $head . '{#black}GetVersion:' . LF; + foreach ($version as $key => $val) { + $stats .= '$g' . str_pad($key, 30) . '{#black}' . $val . LF; + } + + $stats .= '{#black}GetNetworkStats:' . LF; + foreach ($network as $key => $val) { + if ($key != 'PlayerNetInfos') + $stats .= '$g' . str_pad($key, 30) . '{#black}' . $val . LF; + } + + $admin->msgs[] = $aseco->formatColors($stats); + + $stats = $head . '{#black}GetServerOptions:' . LF; + foreach ($options as $key => $val) { + // show only Current values, not Next ones + if ($key != 'Name' && $key != 'Comment' && substr($key, 0, 4) != 'Next') + if (is_bool($val)) + $stats .= '$g' . str_pad($key, 30) . '{#black}' . bool2text($val) . LF; + else + $stats .= '$g' . str_pad($key, 30) . '{#black}' . $val . LF; + } + + $admin->msgs[] = $aseco->formatColors($stats); + + $stats = $head . '{#black}GetCurrentGameInfo:' . LF; + foreach ($gameinfo as $key => $val) { + if (is_bool($val)) + $stats .= '$g' . str_pad($key, 30) . '{#black}' . bool2text($val) . LF; + else + if ($key == 'GameMode') + $stats .= '$g' . str_pad($key, 30) . '{#black}' . $val . '$g (' . $aseco->server->gameinfo->getMode() . ')' . LF; + else + $stats .= '$g' . str_pad($key, 30) . '{#black}' . $val . LF; + } + + $admin->msgs[] = $aseco->formatColors($stats); + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + + } elseif ($aseco->server->getGame() == 'TMF') { + // get all server settings in one go + $version = $aseco->client->addCall('GetVersion', array()); + $info = $aseco->client->addCall('GetSystemInfo', array()); + $coppers = $aseco->client->addCall('GetServerCoppers', array()); + $ladderlim = $aseco->client->addCall('GetLadderServerLimits', array()); + $options = $aseco->client->addCall('GetServerOptions', array(1)); + $gameinfo = $aseco->client->addCall('GetCurrentGameInfo', array(1)); + $network = $aseco->client->addCall('GetNetworkStats', array()); + $callvotes = $aseco->client->addCall('GetCallVoteRatios', array()); + if (!$aseco->client->multiquery()) { + trigger_error('[' . $aseco->client->getErrorCode() . '] GetServer (multi) - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + return; + } else { + $response = $aseco->client->getResponse(); + $version = $response[$version][0]; + $info = $response[$info][0]; + $coppers = $response[$coppers][0]; + $ladderlim = $response[$ladderlim][0]; + $options = $response[$options][0]; + $gameinfo = $response[$gameinfo][0]; + $network = $response[$network][0]; + $callvotes = $response[$callvotes][0]; + } + + // compile settings overview + $head = 'System info for: ' . $options['Name']; + $admin->msgs = array(); + $admin->msgs[0] = array(1, $head, array(1.1, 0.6, 0.5), array('Icons64x64_1', 'DisplaySettings', 0.01)); + $stats = array(); + + $stats[] = array('{#black}GetVersion:', ''); + foreach ($version as $key => $val) { + $stats[] = array($key, '{#black}' . $val); + } + + $stats[] = array(); + $stats[] = array('{#black}GetSystemInfo:', ''); + foreach ($info as $key => $val) { + $stats[] = array($key, '{#black}' . $val); + } + + $stats[] = array(); + $stats[] = array('Rights', '{#black}' . ($aseco->server->rights ? 'United $gCoppers: {#black}' . $coppers : 'Nations')); + $stats[] = array('Packmask', '{#black}' . $aseco->server->packmask); + if ($aseco->server->isrelay) + $stats[] = array('Relays', '{#black}' . $aseco->server->relaymaster['Login']); + else + $stats[] = array('Master to', '{#black}' . count($aseco->server->relayslist) . + ' $grelay' . (count($aseco->server->relayslist) == 1 ? '' : 's')); + + $stats[] = array(); + $stats[] = array('{#black}GetLadderServerLimits:', ''); + foreach ($ladderlim as $key => $val) { + $stats[] = array($key, '{#black}' . $val); + } + + $admin->msgs[] = $stats; + $stats = array(); + + $stats[] = array('{#black}GetServerOptions:', ''); + foreach ($options as $key => $val) { + // show only Current values, not Next ones + if ($key != 'Name' && $key != 'Comment' && substr($key, 0, 4) != 'Next') + if (is_bool($val)) + $stats[] = array($key, '{#black}' . bool2text($val)); + else + $stats[] = array($key, '{#black}' . $val); + } + + $admin->msgs[] = $stats; + $stats = array(); + + $lines = 0; + $stats[] = array('{#black}GetCurrentGameInfo:', ''); + foreach ($gameinfo as $key => $val) { + if (is_bool($val)) + $stats[] = array($key, '{#black}' . bool2text($val)); + else + if ($key == 'GameMode') + $stats[] = array($key, '{#black}' . $val . '$g (' . $aseco->server->gameinfo->getMode() . ')'); + else + $stats[] = array($key, '{#black}' . $val); + + if (++$lines > 18) { + $admin->msgs[] = $stats; + $stats = array(); + $stats[] = array('{#black}GetCurrentGameInfo:', ''); + $lines = 0; + } + } + + $stats[] = array(); + $stats[] = array('{#black}GetNetworkStats:', ''); + foreach ($network as $key => $val) { + if ($key != 'PlayerNetInfos') + $stats[] = array($key, '{#black}' . $val); + } + + $stats[] = array(); + $stats[] = array('{#black}GetCallVoteRatios:', ''); + $stats[] = array('Command', 'Ratio'); + foreach ($callvotes as $entry) { + $stats[] = array('{#black}' . $entry['Command'], '{#black}' . round($entry['Ratio'], 2)); + } + + $admin->msgs[] = $stats; + display_manialink_multi($admin); + } else { + $message = $aseco->getChatMessage('FOREVER_ONLY'); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Send private message to all available admins. + */ + } elseif ($command['params'][0] == 'pm') { + global $pmbuf, $pmlen, $muting_available; // from plugin.muting.php + + // check for non-empty message + if ($arglist[1] != '') { + // drop oldest pm line if buffer full + if (count($pmbuf) >= $pmlen) { + array_shift($pmbuf); + } + // append timestamp, admin nickname (but strip wide font) and pm line to history + $nick = str_ireplace('$w', '', $admin->nickname); + $pmbuf[] = array(date('H:i:s'), $nick, $arglist[1]); + + // find and pm other masteradmins/admins/operators + $nicks = ''; + $msg = '{#error}-pm-$g[' . $nick . '$z$s$i->{#logina}Admins$g]$i {#interact}' . $arglist[1]; + $msg = $aseco->formatColors($msg); + foreach ($aseco->server->players->player_list as $pl) { + // check for admin ability + if ($pl->login != $login && $aseco->allowAbility($pl, 'pm')) { + $nicks .= str_ireplace('$w', '', $pl->nickname) . '$z$s$i,'; + $aseco->client->addCall('ChatSendServerMessageToLogin', array($msg, $pl->login)); + + // check if player muting is enabled + if ($muting_available) { + // drop oldest message if receiver's mute buffer full + if (count($pl->mutebuf) >= 28) { // chat window length + array_shift($pl->mutebuf); + } + // append pm line to receiver's mute buffer + $pl->mutebuf[] = $msg; + } + } + } + + // CC message to self + if ($nicks) { + $nicks = substr($nicks, 0, strlen($nicks)-1); // strip trailing ',' + $msg = '{#error}-pm-$g[' . $nick . '$z$s$i->' . $nicks . ']$i {#interact}' . $arglist[1]; + } else { + $msg = '{#server}> {#error}No other admins currectly available!'; + } + $msg = $aseco->formatColors($msg); + $aseco->client->addCall('ChatSendServerMessageToLogin', array($msg, $login)); + if (!$aseco->client->multiquery()) { + trigger_error('[' . $aseco->client->getErrorCode() . '] ChatSend PM (multi) - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + } + + // check if player muting is enabled + if ($muting_available) { + // drop oldest message if sender's mute buffer full + if (count($admin->mutebuf) >= 28) { // chat window length + array_shift($admin->mutebuf); + } + // append pm line to sender's mute buffer + $admin->mutebuf[] = $msg; + } + } else { + $msg = '{#server}> {#error}No message!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($msg), $login); + } + + /** + * Displays log of recent private admin messages. + */ + } elseif ($command['params'][0] == 'pmlog') { + global $pmbuf, $lnlen; + + if (!empty($pmbuf)) { + if ($aseco->server->getGame() == 'TMN') { + $head = 'Recent PM history:' . LF; + $msg = ''; + $lines = 0; + $admin->msgs = array(); + $admin->msgs[0] = 1; + foreach ($pmbuf as $item) { + // break up long lines into chunks with continuation strings + $multi = explode(LF, wordwrap(stripColors($item[2]), $lnlen, LF . '...')); + foreach ($multi as $line) { + $line = substr($line, 0, $lnlen+3); // chop off excessively long words + $msg .= '$z' . ($aseco->settings['chatpmlog_times'] ? '$n<{#server}' . $item[0] . '$z$n>$m ' : '') . + '[{#black}' . $item[1] . '$z] ' . $line . LF; + if (++$lines > 9) { + $admin->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + } + // add if last batch exists + if ($msg != '') + $admin->msgs[] = $aseco->formatColors($head . $msg); + + // display popup message + if (count($admin->msgs) == 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'OK', '', 0); + } else { // > 2 + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + } + + } elseif ($aseco->server->getGame() == 'TMF') { + $head = 'Recent Admin PM history:'; + $msg = array(); + $lines = 0; + $admin->msgs = array(); + $admin->msgs[0] = array(1, $head, array(1.2), array('Icons64x64_1', 'Outbox')); + foreach ($pmbuf as $item) { + // break up long lines into chunks with continuation strings + $multi = explode(LF, wordwrap(stripColors($item[2]), $lnlen+30, LF . '...')); + foreach ($multi as $line) { + $line = substr($line, 0, $lnlen+33); // chop off excessively long words + $msg[] = array('$z' . ($aseco->settings['chatpmlog_times'] ? '<{#server}' . $item[0] . '$z> ' : '') . + '[{#black}' . $item[1] . '$z] ' . $line); + if (++$lines > 14) { + $admin->msgs[] = $msg; + $lines = 0; + $msg = ''; + } + } + } + // add if last batch exists + if (!empty($msg)) + $admin->msgs[] = $msg; + + // display ManiaLink message + display_manialink_multi($admin); + } + } else { + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors('{#server}> {#error}No PM history found!'), $login); + } + + /** + * Executes direct server call + */ + } elseif ($command['params'][0] == 'call') { + global $method_results; + + // extra admin tier check + if (!$aseco->isMasterAdmin($admin)) { + $aseco->client->query('ChatSendToLogin', $aseco->formatColors('{#error}You don\'t have the required admin rights to do that!'), $login); + return; + } + + // check parameter(s) + if ($command['params'][1] != '') { + if ($command['params'][1] == 'help') { + if (isset($command['params'][2]) && $command['params'][2] != '') { + // generate help message for method + $method = $command['params'][2]; + $sign = $aseco->client->addCall('system.methodSignature', array($method)); + $help = $aseco->client->addCall('system.methodHelp', array($method)); + if (!$aseco->client->multiquery()) { + trigger_error('[' . $aseco->client->getErrorCode() . '] system.method - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + } else { + $response = $aseco->client->getResponse(); + if (isset($response[0]['faultCode'])) { + $message = '{#server}> {#error}No such method {#highlite}$i ' . $method . ' {#error}!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } else { + $sign = $response[$sign][0][0]; + $help = $response[$help][0]; + + // format signature & help + $params = ''; + for ($i = 1; $i < count($sign); $i++) + $params .= $sign[$i] . ', '; + $params = substr($params, 0, strlen($params)-2); // strip trailing ", " + $sign = $sign[0] . ' {#black}' . $method . '$g (' . $params . ')'; + $sign = explode(LF, wordwrap($sign, 58, LF)); + $help = str_replace(array('', ''), + array('$i', '$i'), $help); + $help = explode(LF, wordwrap($help, 58, LF)); + + // compile & display help message + if ($aseco->server->getGame() == 'TMN') { + $info = 'Server Method help for:' . LF . LF; + foreach ($sign as $line) + $info .= $line . LF; + $info .= LF; + foreach ($help as $line) + $info .= $line . LF; + + // display popup message + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $aseco->formatColors($info), 'OK', '', 0); + } elseif ($aseco->server->getGame() == 'TMF') { + $header = 'Server Method help for:'; + $info = array(); + foreach ($sign as $line) + $info[] = array($line); + $info[] = array(); + foreach ($help as $line) + $info[] = array($line); + + // display ManiaLink message + display_manialink($login, $header, array('Icons128x128_1', 'Advanced', 0.02), $info, array(1.05), 'OK'); + } + } + } + + } else { + // compile & display help message + if ($aseco->server->getGame() == 'TMN') { + $help = '{#black}/admin call$g executes server method:' . LF; + $help .= ' - {#black}help$g, displays this help information' . LF; + $help .= ' - {#black}help Method$g, displays help for method' . LF; + $help .= ' - {#black}list$g, lists all available methods' . LF; + $help .= ' - {#black}Method {params}$g, executes method & displays result' . LF; + + // display popup message + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $aseco->formatColors($help), 'OK', '', 0); + + } elseif ($aseco->server->getGame() == 'TMF') { + $header = '{#black}/admin call$g executes server method:'; + $help = array(); + $help[] = array('...', '{#black}help', + 'Displays this help information'); + $help[] = array('...', '{#black}help Method', + 'Displays help for method'); + $help[] = array('...', '{#black}list', + 'Lists all available methods'); + $help[] = array('...', '{#black}Method {params}', + 'Executes method & displays result'); + + // display ManiaLink message + display_manialink($login, $header, array('Icons64x64_1', 'TrackInfo', -0.01), $help, array(1.0, 0.05, 0.35, 0.6), 'OK'); + } + } + + } elseif ($command['params'][1] == 'list') { + // get list of methods + $aseco->client->query('system.listMethods'); + $methods = $aseco->client->getResponse(); + $admin->msgs = array(); + + if ($aseco->server->getGame() == 'TMN') { + $head = 'Available Methods on this Server:' . LF . 'Id Method' . LF; + $msg = ''; + $mid = 1; + $lines = 0; + $admin->msgs[0] = 1; + foreach ($methods as $method) { + $msg .= '$g' . str_pad($mid, 3, '0', STR_PAD_LEFT) . '. {#black}' + . $method . LF; + $mid++; + if (++$lines > 9) { + $admin->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + // add if last batch exists + if ($msg != '') + $admin->msgs[] = $aseco->formatColors($head . $msg); + + // display popup message + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + + } elseif ($aseco->server->getGame() == 'TMF') { + $head = 'Available Methods on this Server:'; + $msg = array(); + $msg[] = array('Id', 'Method'); + $mid = 1; + $lines = 0; + $admin->msgs[0] = array(1, $head, array(0.9, 0.15, 0.75), array('Icons128x128_1', 'Advanced', 0.02)); + foreach ($methods as $method) { + $msg[] = array(str_pad($mid, 2, '0', STR_PAD_LEFT) . '.', + '{#black}' . $method); + $mid++; + if (++$lines > 14) { + $admin->msgs[] = $msg; + $lines = 0; + $msg = array(); + $msg[] = array('Id', 'Method'); + } + } + // add if last batch exists + if (count($msg) > 1) + $admin->msgs[] = $msg; + + // display ManiaLink message + display_manialink_multi($admin); + } + + } else { // server method + $method = $command['params'][1]; + // collect parameters with correct types + $args = array(); + $multistr = ''; + $in_multi = false; + for ($i = 2; $i < count($command['params']); $i++) { + if (!$in_multi && strtolower($command['params'][$i]) == 'true') + $args[] = true; + elseif (!$in_multi && strtolower($command['params'][$i]) == 'false') + $args[] = false; + elseif (!$in_multi && is_numeric($command['params'][$i])) + $args[] = intval($command['params'][$i]); + else + // check for multi-word strings + if ($in_multi) { + if (substr($command['params'][$i], -1) == '"') { + $args[] = $multistr . ' ' . substr($command['params'][$i], 0, -1); + $multistr = ''; + $in_multi = false; + } else { + $multistr .= ' ' . $command['params'][$i]; + } + } else { + if (substr($command['params'][$i], 0, 1) == '"') { + $multistr = substr($command['params'][$i], 1); + $in_multi = true; + } else { + $args[] = $command['params'][$i]; + } + } + } + + // execute method + switch (count($args)) { + case 0: $res = $aseco->client->query($method); + break; + case 1: $res = $aseco->client->query($method, $args[0]); + break; + case 2: $res = $aseco->client->query($method, $args[0], $args[1]); + break; + case 3: $res = $aseco->client->query($method, $args[0], $args[1], $args[2]); + break; + case 4: $res = $aseco->client->query($method, $args[0], $args[1], $args[2], $args[3]); + break; + case 5: $res = $aseco->client->query($method, $args[0], $args[1], $args[2], $args[3], $args[4]); + break; + } + // process result + if ($res) { + $res = $aseco->client->getResponse(); + $admin->msgs = array(); + $method_results = array(); + collect_results($method, $res, ''); + + // compile & display result message + if ($aseco->server->getGame() == 'TMN') { + $head = 'Method results for:' . LF . LF; + $msg = ''; + $mid = 1; + $lines = 0; + $admin->msgs[0] = 1; + foreach ($method_results as $line) { + $msg .= $line . '$z' . LF; + $mid++; + if (++$lines > 14) { + $admin->msgs[] = $aseco->formatColors($head . $msg); + $lines = 0; + $msg = ''; + } + } + // add if last batch exists + if ($msg != '') + $admin->msgs[] = $aseco->formatColors($head . $msg); + + // display popup message + if (count($admin->msgs) == 2) { + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'OK', '', 0); + } else { // > 2 + $aseco->client->query('SendDisplayServerMessageToLogin', $login, $admin->msgs[1], 'Close', 'Next', 0); + } + + } elseif ($aseco->server->getGame() == 'TMF') { + $head = 'Method results for:'; + $msg = array(); + $mid = 1; + $lines = 0; + $admin->msgs[0] = array(1, $head, array(1.1), array('Icons128x128_1', 'Advanced', 0.02)); + foreach ($method_results as $line) { + $msg[] = array($line); + $mid++; + if (++$lines > 20) { + $admin->msgs[] = $msg; + $lines = 0; + $msg = array(); + } + } + // add if last batch exists + if (!empty($msg)) + $admin->msgs[] = $msg; + + // display ManiaLink message + display_manialink_multi($admin); + } + } else { + $message = '{#server}> {#error}Method error for {#highlite}$i ' . $method . '{#error}: [' . $aseco->client->getErrorCode() . '] ' . $aseco->client->getErrorMessage(); + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + } + } else { + $message = '{#server}> {#error}No call specified - see {#highlite}$i /admin call help{#error} and {#highlite}$i /admin call list{#error}!'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + /** + * Unlocks admin commands & features. + */ + } elseif ($command['params'][0] == 'unlock' && $command['params'][1] != '') { + + // check unlock password + if ($aseco->settings['lock_password'] == $command['params'][1]) { + $admin->unlocked = true; + $message = '{#server}> {#admin}Password accepted: admin commands unlocked!'; + } else { + $message = '{#server}> {#error}Invalid password!'; + } + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Toggle debug on/off. + */ + } elseif ($command['params'][0] == 'debug') { + + $aseco->debug = !$aseco->debug; + if ($aseco->debug) { + $message = '{#server}> Debug is now enabled'; + } else { + $message = '{#server}> Debug is now disabled'; + } + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + + /** + * Shuts down XASECO. + */ + } elseif ($command['params'][0] == 'shutdown') { + + trigger_error('Shutdown XASECO!', E_USER_ERROR); + + /** + * Shuts down Server & XASECO. + */ + } elseif ($command['params'][0] == 'shutdownall') { + + $message = '{#server}>> {#error}$wShutting down server now!'; + $aseco->client->query('ChatSendServerMessage', $aseco->formatColors($message)); + + $rtn = $aseco->client->query('StopServer'); + if (!$rtn) { + trigger_error('[' . $aseco->client->getErrorCode() . '] StopServer - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + } else { + // test for /noautoquit + sleep(2); + $autoquit = new IXR_ClientMulticall_Gbx(); + if ($autoquit->InitWithIp($aseco->server->ip, $aseco->server->port)) + $aseco->client->query('QuitGame'); + + trigger_error('Shutdown ' . $aseco->server->getGame() . ' server & XASECO!', E_USER_ERROR); + } + + /** + * Checks current version of XASECO. + */ + } elseif ($command['params'][0] == 'uptodate') { + + if (function_exists('admin_uptodate')) { + admin_uptodate($aseco, $command); // from plugin.uptodate.php + } else { + // show chat message + $message = '{#server}> {#admin}Version checking unavailable - include plugins.uptodate.php in plugins.xml'; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } + + } else { + $message = '{#server}> {#error}Unknown admin command or missing parameter(s): {#highlite}$i ' . $arglist[0] . ' ' . $arglist[1]; + $aseco->client->query('ChatSendServerMessageToLogin', $aseco->formatColors($message), $login); + } +} // chat_admin + + +function get_ignorelist($aseco) { + + $aseco->client->resetError(); + $newlist = array(); + $done = false; + $size = 300; + $i = 0; + while (!$done) { + $aseco->client->query('GetIgnoreList', $size, $i); + $players = $aseco->client->getResponse(); + if (!empty($players)) { + if ($aseco->client->isError()) { + trigger_error('[' . $aseco->client->getErrorCode() . '] GetIgnoreList - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $done = true; + break; + } + foreach ($players as $prow) { + // fetch nickname for this login + $lgn = $prow['Login']; + $nick = $aseco->getPlayerNick($lgn); + $newlist[$lgn] = array($lgn, $nick); + } + if (count($players) < $size) { + // got less than 300 players, might as well leave + $done = true; + } else { + $i += $size; + } + } else { + $done = true; + } + } + return $newlist; +} // get_ignorelist + +function get_banlist($aseco) { + + $aseco->client->resetError(); + $newlist = array(); + $done = false; + $size = 300; + $i = 0; + while (!$done) { + $aseco->client->query('GetBanList', $size, $i); + $players = $aseco->client->getResponse(); + if (!empty($players)) { + if ($aseco->client->isError()) { + trigger_error('[' . $aseco->client->getErrorCode() . '] GetBanList - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $done = true; + break; + } + foreach ($players as $prow) { + // fetch nickname for this login + $lgn = $prow['Login']; + $nick = $aseco->getPlayerNick($lgn); + $newlist[$lgn] = array($lgn, $nick, + preg_replace('/:\d+/', '', $prow['IPAddress'])); // strip port + } + if (count($players) < $size) { + // got less than 300 players, might as well leave + $done = true; + } else { + $i += $size; + } + } else { + $done = true; + } + } + return $newlist; +} // get_banlist + +function get_blacklist($aseco) { + + $aseco->client->resetError(); + $newlist = array(); + $done = false; + $size = 300; + $i = 0; + while (!$done) { + $aseco->client->query('GetBlackList', $size, $i); + $players = $aseco->client->getResponse(); + if (!empty($players)) { + if ($aseco->client->isError()) { + trigger_error('[' . $aseco->client->getErrorCode() . '] GetBlackList - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $done = true; + break; + } + foreach ($players as $prow) { + // fetch nickname for this login + $lgn = $prow['Login']; + $nick = $aseco->getPlayerNick($lgn); + $newlist[$lgn] = array($lgn, $nick); + } + if (count($players) < $size) { + // got less than 300 players, might as well leave + $done = true; + } else { + $i += $size; + } + } else { + $done = true; + } + } + return $newlist; +} // get_blacklist + +function get_guestlist($aseco) { + + $aseco->client->resetError(); + $newlist = array(); + $done = false; + $size = 300; + $i = 0; + while (!$done) { + $aseco->client->query('GetGuestList', $size, $i); + $players = $aseco->client->getResponse(); + if (!empty($players)) { + if ($aseco->client->isError()) { + trigger_error('[' . $aseco->client->getErrorCode() . '] GetGuestList - ' . $aseco->client->getErrorMessage(), E_USER_WARNING); + $done = true; + break; + } + foreach ($players as $prow) { + // fetch nickname for this login + $lgn = $prow['Login']; + $nick = $aseco->getPlayerNick($lgn); + $newlist[$lgn] = array($lgn, $nick); + } + if (count($players) < $size) { + // got less than 300 players, might as well leave + $done = true; + } else { + $i += $size; + } + } else { + $done = true; + } + } + return $newlist; +} // get_guestlist + +function collect_results($key, $val, $indent) { + global $method_results; + + if (is_array($val)) { + // recursively compile array results + $method_results[] = $indent . '*' . $key . ' :'; + foreach ($val as $key2 => $val2) { + collect_results($key2, $val2, ' ' . $indent); + } + } else { + if (!is_string($val)) + $val = strval($val); + // format result key/value pair + $val = explode(LF, wordwrap($val, 32, LF . $indent . ' ', true)); + $firstline = true; + foreach ($val as $line) { + if ($firstline) + $method_results[] = $indent . $key . ' = ' . $line; + else + $method_results[] = $line; + $firstline = false; + } + } +} // collect_results + + +// called @ onPlayerManialinkPageAnswer +// Handles ManiaLink admin responses +// [0]=PlayerUid, [1]=Login, [2]=Answer +function event_admin($aseco, $answer) { + + // leave actions outside 2201 - 5200 to other handlers + if ($answer[2] < 2201 && $answer[2] > 5200 && + $answer[2] < -8100 && $answer[2] > -7901) + return; + + // get player & possible parameter + $player = $aseco->server->players->getPlayer($answer[1]); + if (isset($player->panels['plyparam'])) + $param = $player->panels['plyparam']; + + // check for /admin warn command + if ($answer[2] >= 2201 && $answer[2] <= 2400) { + $target = $player->playerlist[$answer[2]-2201]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin warn {2}"', + $player->login, $target); + + // warn selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'warn ' . $target; + chat_admin($aseco, $command); + } + + // check for /admin ignore command + elseif ($answer[2] >= 2401 && $answer[2] <= 2600) { + $target = $player->playerlist[$answer[2]-2401]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin ignore {2}"', + $player->login, $target); + + // ignore selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'ignore ' . $target; + chat_admin($aseco, $command); + + // log clicked command + $aseco->console('player {1} clicked command "/admin players {2}"', + $player->login, $param); + + // refresh players window + $command['params'] = 'players ' . $param; + chat_admin($aseco, $command); + } + + // check for /admin unignore command + elseif ($answer[2] >= 2601 && $answer[2] <= 2800) { + $target = $player->playerlist[$answer[2]-2601]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin unignore {2}"', + $player->login, $target); + + // unignore selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'unignore ' . $target; + chat_admin($aseco, $command); + + // log clicked command + $aseco->console('player {1} clicked command "/admin players {2}"', + $player->login, $param); + + // refresh players window + $command['params'] = 'players ' . $param; + chat_admin($aseco, $command); + } + + // check for /admin kick command + elseif ($answer[2] >= 2801 && $answer[2] <= 3000) { + $target = $player->playerlist[$answer[2]-2801]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin kick {2}"', + $player->login, $target); + + // kick selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'kick ' . $target; + chat_admin($aseco, $command); + + // log clicked command + $aseco->console('player {1} clicked command "/admin players {2}"', + $player->login, $param); + + // refresh players window + $command['params'] = 'players ' . $param; + chat_admin($aseco, $command); + } + + // check for /admin ban command + elseif ($answer[2] >= 3001 && $answer[2] <= 3200) { + $target = $player->playerlist[$answer[2]-3001]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin ban {2}"', + $player->login, $target); + + // ban selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'ban ' . $target; + chat_admin($aseco, $command); + + // log clicked command + $aseco->console('player {1} clicked command "/admin players {2}"', + $player->login, $param); + + // refresh players window + $command['params'] = 'players ' . $param; + chat_admin($aseco, $command); + } + + // check for /admin unban command + elseif ($answer[2] >= 3201 && $answer[2] <= 3400) { + $target = $player->playerlist[$answer[2]-3201]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin unban {2}"', + $player->login, $target); + + // unban selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'unban ' . $target; + chat_admin($aseco, $command); + + // log clicked command + $aseco->console('player {1} clicked command "/admin players {2}"', + $player->login, $param); + + // refresh players window + $command['params'] = 'players ' . $param; + chat_admin($aseco, $command); + } + + // check for /admin black command + elseif ($answer[2] >= 3401 && $answer[2] <= 3600) { + $target = $player->playerlist[$answer[2]-3401]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin black {2}"', + $player->login, $target); + + // black selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'black ' . $target; + chat_admin($aseco, $command); + + // log clicked command + $aseco->console('player {1} clicked command "/admin players {2}"', + $player->login, $param); + + // refresh players window + $command['params'] = 'players ' . $param; + chat_admin($aseco, $command); + } + + // check for /admin unblack command + elseif ($answer[2] >= 3601 && $answer[2] <= 3800) { + $target = $player->playerlist[$answer[2]-3601]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin unblack {2}"', + $player->login, $target); + + // unblack selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'unblack ' . $target; + chat_admin($aseco, $command); + + // log clicked command + $aseco->console('player {1} clicked command "/admin players {2}"', + $player->login, $param); + + // refresh players window + $command['params'] = 'players ' . $param; + chat_admin($aseco, $command); + } + + // check for /admin addguest command + elseif ($answer[2] >= 3801 && $answer[2] <= 4000) { + $target = $player->playerlist[$answer[2]-3801]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin addguest {2}"', + $player->login, $target); + + // addguest selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'addguest ' . $target; + chat_admin($aseco, $command); + + // log clicked command + $aseco->console('player {1} clicked command "/admin players {2}"', + $player->login, $param); + + // refresh players window + $command['params'] = 'players ' . $param; + chat_admin($aseco, $command); + } + + // check for /admin removeguest command + elseif ($answer[2] >= 4001 && $answer[2] <= 4200) { + $target = $player->playerlist[$answer[2]-4001]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin removeguest {2}"', + $player->login, $target); + + // removeguest selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'removeguest ' . $target; + chat_admin($aseco, $command); + + // log clicked command + $aseco->console('player {1} clicked command "/admin players {2}"', + $player->login, $param); + + // refresh players window + $command['params'] = 'players ' . $param; + chat_admin($aseco, $command); + } + + // check for /admin forcespec command + elseif ($answer[2] >= 4201 && $answer[2] <= 4400) { + $target = $player->playerlist[$answer[2]-4201]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin forcespec {2}"', + $player->login, $target); + + // forcespec selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'forcespec ' . $target; + chat_admin($aseco, $command); + + // log clicked command + $aseco->console('player {1} clicked command "/admin players {2}"', + $player->login, $param); + + // refresh players window + $command['params'] = 'players ' . $param; + chat_admin($aseco, $command); + } + + // check for /admin unignore command in listignores + elseif ($answer[2] >= 4401 && $answer[2] <= 4600) { + $target = $player->playerlist[$answer[2]-4401]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin unignore {2}"', + $player->login, $target); + + // unignore selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'unignore ' . $target; + chat_admin($aseco, $command); + + // check whether last player was unignored + $ignores = get_ignorelist($aseco); + if (empty($ignores)) { + // close main window + mainwindow_off($aseco, $player->login); + } else { + // log clicked command + $aseco->console('player {1} clicked command "/admin listignores"', + $player->login); + + // refresh listignores window + $command['params'] = 'listignores'; + chat_admin($aseco, $command); + } + } + + // check for /admin unban command in listbans + elseif ($answer[2] >= 4601 && $answer[2] <= 4800) { + $target = $player->playerlist[$answer[2]-4601]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin unban {2}"', + $player->login, $target); + + // unban selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'unban ' . $target; + chat_admin($aseco, $command); + + // check whether last player was unbanned + $bans = get_banlist($aseco); + if (empty($bans)) { + // close main window + mainwindow_off($aseco, $player->login); + } else { + // log clicked command + $aseco->console('player {1} clicked command "/admin listbans"', + $player->login); + + // refresh listbans window + $command['params'] = 'listbans'; + chat_admin($aseco, $command); + } + } + + // check for /admin unblack command in listblacks + elseif ($answer[2] >= 4801 && $answer[2] <= 5000) { + $target = $player->playerlist[$answer[2]-4801]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin unblack {2}"', + $player->login, $target); + + // unblack selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'unblack ' . $target; + chat_admin($aseco, $command); + + // check whether last player was unblacked + $blacks = get_blacklist($aseco); + if (empty($blacks)) { + // close main window + mainwindow_off($aseco, $player->login); + } else { + // log clicked command + $aseco->console('player {1} clicked command "/admin listblacks"', + $player->login); + + // refresh listblacks window + $command['params'] = 'listblacks'; + chat_admin($aseco, $command); + } + } + + // check for /admin removeguest command in listguests + elseif ($answer[2] >= 5001 && $answer[2] <= 5200) { + $target = $player->playerlist[$answer[2]-5001]['login']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin removeguest {2}"', + $player->login, $target); + + // removeguest selected player + $command = array(); + $command['author'] = $player; + $command['params'] = 'removeguest ' . $target; + chat_admin($aseco, $command); + + // check whether last guest was removed + $guests = get_guestlist($aseco); + if (empty($guests)) { + // close main window + mainwindow_off($aseco, $player->login); + } else { + // log clicked command + $aseco->console('player {1} clicked command "/admin listguests"', + $player->login); + + // refresh listguests window + $command['params'] = 'listguests'; + chat_admin($aseco, $command); + } + } + + // check for /admin unbanip command + elseif ($answer[2] >= -8100 && $answer[2] <= -7901) { + $target = $player->playerlist[abs($answer[2])-7901]['ip']; + + // log clicked command + $aseco->console('player {1} clicked command "/admin unbanip {2}"', + $player->login, $target); + + // unbanip selected IP + $command = array(); + $command['author'] = $player; + $command['params'] = 'unbanip ' . $target; + chat_admin($aseco, $command); + + // check whether last IP was unbanned + if (!$empty = empty($aseco->bannedips)) { + $empty = true; + for ($i = 0; $i < count($aseco->bannedips); $i++) + if ($aseco->bannedips[$i] != '') { + $empty = false; + break; + } + } + if ($empty) { + // close main window + mainwindow_off($aseco, $player->login); + } else { + // log clicked command + $aseco->console('player {1} clicked command "/admin listips"', + $player->login); + + // refresh listips window + $command['params'] = 'listips'; + chat_admin($aseco, $command); + } + } +} // event_admin +?> diff --git a/docker-xaseco/xaseco/plugins/chat.dedimania.php b/docker-xaseco/xaseco/plugins/chat.dedimania.php new file mode 100644 index 0000000..a89c2c5 --- /dev/null +++ b/docker-xaseco/xaseco/plugins/chat.dedimania.php @@ -0,0 +1,941 @@ +server->getGame() == 'TMN') { + $help = '{#dedimsg}Dedimania$g is an online World Records database for {#black}all$g' . LF; + $help .= 'TrackMania games. See its official site at:' . LF; + $help .= '{#black}http://www.dedimania.com/SITE/$g and the records database:' . LF; + $help .= '{#black}http://www.dedimania.com/tmstats/?do=stat$g .' . LF . LF; + $help .= 'Dedimania records are stored per game (TMN, TMU, etc)' . LF; + $help .= 'and mode (TimeAttack, Rounds, etc) and shared between' . LF; + $help .= 'all servers that operate with Dedimania support.' . LF . LF; + $help .= 'The available Dedimania commands are similar to local' . LF; + $help .= 'record commands:' . LF; + $help .= '{#black}/dedirecs$g, {#black}/dedinew$g, {#black}/dedilive$g, {#black}/dedipb$g, {#black}/dedicps$g, {#black}/dedistats$g,' . LF; + $help .= '{#black}/dedifirst$g, {#black}/dedilast$g, {#black}/dedinext$g, {#black}/dedidiff$g, {#black}/dedirange$g' . LF; + $help .= 'See the {#black}/helpall$g command for detailed descriptions.'; + + // display popup message + $aseco->client->query('SendDisplayServerMessageToLogin', $command['author']->login, $aseco->formatColors($help), 'OK', '', 0); + + } elseif ($aseco->server->getGame() == 'TMF') { + $header = 'Dedimania information:'; + $data = array(); + $data[] = array('{#dedimsg}Dedimania$g is an online World Records database for {#black}all'); + $data[] = array('TrackMania games. See its official site at:'); + $data[] = array('{#black}$l[http://www.dedimania.com/SITE/]http://www.dedimania.com/SITE/$l$g and the records database:'); + $data[] = array('{#black}$l[http://www.dedimania.com/tmstats/?do=stat]http://www.dedimania.com/tmstats/?do=stat$l$g .'); + $data[] = array(); + $data[] = array('Dedimania records are stored per game (TMN, TMU, etc)'); + $data[] = array('and mode (TimeAttack, Rounds, etc) and shared between'); + $data[] = array('all servers that operate with Dedimania support.'); + $data[] = array(); + $data[] = array('The available Dedimania commands are similar to local'); + $data[] = array('record commands:'); + $data[] = array('{#black}/dedirecs$g, {#black}/dedinew$g, {#black}/dedilive$g, {#black}/dedipb$g, {#black}/dedicps$g, {#black}/dedistats$g,'); + $data[] = array('{#black}/dedifirst$g, {#black}/dedilast$g, {#black}/dedinext$g, {#black}/dedidiff$g, {#black}/dedirange$g'); + $data[] = array(); + $data[] = array('See the {#black}/helpall$g command for detailed descriptions.'); + + // display ManiaLink message + display_manialink($command['author']->login, $header, array('Icons64x64_1', 'TrackInfo', -0.01), $data, array(0.95), 'OK'); + } +} // chat_helpdedi + +function chat_dedirecs($aseco, $command) { + global $dedi_db; + + $player = $command['author']; + $login = $player->login; + $dedi_recs = $dedi_db['Challenge']['Records']; + + // split params into array + $arglist = explode(' ', strtolower(preg_replace('/ +/', ' ', $command['params']))); + + // process optional relations commands + if ($arglist[0] == 'help') { + if ($aseco->server->getGame() == 'TMN') { + $help = '{#black}/dedirecs
    + diff --git a/docker-xaseco/xaseco/styles/00README.txt b/docker-xaseco/xaseco/styles/00README.txt new file mode 100644 index 0000000..c1f9abe --- /dev/null +++ b/docker-xaseco/xaseco/styles/00README.txt @@ -0,0 +1,24 @@ +Styles +====== + +This directory holds window style templates, managed by plugin.style.php. +Templates define the window, header and body background containers, as +well as the header and body text fonts, and the clickable button style. + +To create a new template, copy an existing one to a new filename and +edit that, as the existing set will be overwritten in future releases. + +Use ManiaLink http://smurf1.free.fr/mle/index.xml +and webpage http://smurf1.free.fr/mle/list.php +to select styles and fonts. Note that not every (sub)style and font +fits everywhere due to size variations. Also, some layout aspects +are fixed in manialinks.inc.php and cannot be customized easily. + +Use to invert the text highlight color on dark backgrounds. +This color is used to replace any '{#black}' tags in window output. + +If you create a nice style template that's sufficiently distinct from +the standard ones, send it to me and I might include it in the next +XAseco release. :) + +Xymph diff --git a/docker-xaseco/xaseco/styles/Black.xml b/docker-xaseco/xaseco/styles/Black.xml new file mode 100644 index 0000000..24a2511 --- /dev/null +++ b/docker-xaseco/xaseco/styles/Black.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow1 + $ffc + + + +
    + + BgTitle2 + 0.07 + TextTitle2 +
    + + + + + BgList + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/BlackBlur.xml b/docker-xaseco/xaseco/styles/BlackBlur.xml new file mode 100644 index 0000000..fb6e751 --- /dev/null +++ b/docker-xaseco/xaseco/styles/BlackBlur.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow2 + $ffc + + + +
    + + BgTitle2 + 0.07 + TextTitle2 +
    + + + + + BgList + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/Blue.xml b/docker-xaseco/xaseco/styles/Blue.xml new file mode 100644 index 0000000..1463c12 --- /dev/null +++ b/docker-xaseco/xaseco/styles/Blue.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow1 + $ffc + + + +
    + + BgTitle3_3 + 0.07 + TextTitle2 +
    + + + + + BgList + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/BlueBlur.xml b/docker-xaseco/xaseco/styles/BlueBlur.xml new file mode 100644 index 0000000..34ecccd --- /dev/null +++ b/docker-xaseco/xaseco/styles/BlueBlur.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow2 + $ffc + + + +
    + + BgTitle3_3 + 0.07 + TextTitle2 +
    + + + + + BgList + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/Cyan.xml b/docker-xaseco/xaseco/styles/Cyan.xml new file mode 100644 index 0000000..862a0df --- /dev/null +++ b/docker-xaseco/xaseco/styles/Cyan.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow1 + $ffc + + + +
    + + BgTitle3_2 + 0.07 + TextTitle2 +
    + + + + + BgList + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/CyanBlur.xml b/docker-xaseco/xaseco/styles/CyanBlur.xml new file mode 100644 index 0000000..e987755 --- /dev/null +++ b/docker-xaseco/xaseco/styles/CyanBlur.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow2 + $ffc + + + +
    + + BgTitle3_2 + 0.07 + TextTitle2 +
    + + + + + BgList + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/DarkBlur.xml b/docker-xaseco/xaseco/styles/DarkBlur.xml new file mode 100644 index 0000000..3bc3667 --- /dev/null +++ b/docker-xaseco/xaseco/styles/DarkBlur.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow3 + $ffc + + + +
    + + BgPlayerName + 0.07 + TextTitle2 +
    + + + + + BgActivePlayerName + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/DarkTransp.xml b/docker-xaseco/xaseco/styles/DarkTransp.xml new file mode 100644 index 0000000..b304e1d --- /dev/null +++ b/docker-xaseco/xaseco/styles/DarkTransp.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow3 + $ffc + + + +
    + + BgPlayerName + 0.07 + TextTitle2 +
    + + + + + BgActivePlayerName + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/Gray.xml b/docker-xaseco/xaseco/styles/Gray.xml new file mode 100644 index 0000000..a9619f8 --- /dev/null +++ b/docker-xaseco/xaseco/styles/Gray.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow1 + $ffc + + + +
    + + BgTitle3 + 0.07 + TextTitle2 +
    + + + + + BgList + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/GrayBlur.xml b/docker-xaseco/xaseco/styles/GrayBlur.xml new file mode 100644 index 0000000..5ffbf00 --- /dev/null +++ b/docker-xaseco/xaseco/styles/GrayBlur.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow2 + $ffc + + + +
    + + BgTitle3 + 0.07 + TextTitle2 +
    + + + + + BgList + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/Green.xml b/docker-xaseco/xaseco/styles/Green.xml new file mode 100644 index 0000000..ef6e743 --- /dev/null +++ b/docker-xaseco/xaseco/styles/Green.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow1 + $ffc + + + +
    + + BgTitle3_4 + 0.07 + TextTitle2 +
    + + + + + BgList + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/GreenBlur.xml b/docker-xaseco/xaseco/styles/GreenBlur.xml new file mode 100644 index 0000000..d809bf2 --- /dev/null +++ b/docker-xaseco/xaseco/styles/GreenBlur.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow2 + $ffc + + + +
    + + BgTitle3_4 + 0.07 + TextTitle2 +
    + + + + + BgList + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/LightTransp.xml b/docker-xaseco/xaseco/styles/LightTransp.xml new file mode 100644 index 0000000..d2c854f --- /dev/null +++ b/docker-xaseco/xaseco/styles/LightTransp.xml @@ -0,0 +1,31 @@ + + + + + + BgRacePlayerName + $000 + + + +
    + + BgMediaTracker + 0.06 + TextButtonSmall +
    + + + + + BgRacePlayerName + 0.04 + TextChallengeNameSmall + + + + +
    diff --git a/docker-xaseco/xaseco/styles/LightTransp2.xml b/docker-xaseco/xaseco/styles/LightTransp2.xml new file mode 100644 index 0000000..a769ab0 --- /dev/null +++ b/docker-xaseco/xaseco/styles/LightTransp2.xml @@ -0,0 +1,31 @@ + + + + + + BgRacePlayerName + $000 + + + +
    + + BgMediaTracker + 0.06 + TextButtonSmall +
    + + + + + BgMediaTracker + 0.04 + TextChallengeNameSmall + + + + +
    diff --git a/docker-xaseco/xaseco/styles/LightTransp3.xml b/docker-xaseco/xaseco/styles/LightTransp3.xml new file mode 100644 index 0000000..4a300d5 --- /dev/null +++ b/docker-xaseco/xaseco/styles/LightTransp3.xml @@ -0,0 +1,31 @@ + + + + + + BgRacePlayerName + $000 + + + +
    + + BgActivePlayerScore + 0.06 + TextButtonSmall +
    + + + + + BgMediaTracker + 0.04 + TextChallengeNameSmall + + + + +
    diff --git a/docker-xaseco/xaseco/styles/Orange.xml b/docker-xaseco/xaseco/styles/Orange.xml new file mode 100644 index 0000000..6148f58 --- /dev/null +++ b/docker-xaseco/xaseco/styles/Orange.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow1 + $ffc + + + +
    + + BgTitle3_1 + 0.07 + TextTitle2 +
    + + + + + BgList + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/OrangeBlur.xml b/docker-xaseco/xaseco/styles/OrangeBlur.xml new file mode 100644 index 0000000..1997605 --- /dev/null +++ b/docker-xaseco/xaseco/styles/OrangeBlur.xml @@ -0,0 +1,31 @@ + + + + + + BgWindow2 + $ffc + + + +
    + + BgTitle3_1 + 0.07 + TextTitle2 +
    + + + + + BgList + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/ProgressBar.xml b/docker-xaseco/xaseco/styles/ProgressBar.xml new file mode 100644 index 0000000..dddf4ed --- /dev/null +++ b/docker-xaseco/xaseco/styles/ProgressBar.xml @@ -0,0 +1,32 @@ + + + + + + + BgWindow2 + $fff + + + +
    + + ProgressBar + 0.065 + TextSubTitle2 +
    + + + + + NavButtonBlink + 0.04 + TextTitle3 + + + + +
    diff --git a/docker-xaseco/xaseco/styles/WhiteCard.xml b/docker-xaseco/xaseco/styles/WhiteCard.xml new file mode 100644 index 0000000..7bae4a5 --- /dev/null +++ b/docker-xaseco/xaseco/styles/WhiteCard.xml @@ -0,0 +1,31 @@ + + + + + + BgCard1 + $000 + + + +
    + + BgListLine + 0.06 + TextButtonSmall +
    + + + + + BgMediaTracker + 0.04 + TextChallengeNameSmall + + + + +
    diff --git a/docker-xaseco/xaseco/styles/WhiteRect.xml b/docker-xaseco/xaseco/styles/WhiteRect.xml new file mode 100644 index 0000000..df23ce3 --- /dev/null +++ b/docker-xaseco/xaseco/styles/WhiteRect.xml @@ -0,0 +1,31 @@ + + + + + + BgIconBorder + $000 + + + +
    + + BgListLine + 0.06 + TextButtonSmall +
    + + + + + BgMediaTracker + 0.04 + TextChallengeNameSmall + + + + +
    diff --git a/docker-xaseco/xaseco/styles/WhiteRound.xml b/docker-xaseco/xaseco/styles/WhiteRound.xml new file mode 100644 index 0000000..979462c --- /dev/null +++ b/docker-xaseco/xaseco/styles/WhiteRound.xml @@ -0,0 +1,31 @@ + + + + + + BgButtonBig + $000 + + + +
    + + BgActivePlayerScore + 0.06 + TextButtonSmall +
    + + + + + NavButton + 0.04 + TextChallengeNameSmall + + + + +
    diff --git a/docker-xaseco/xaseco/text.tpl b/docker-xaseco/xaseco/text.tpl new file mode 100644 index 0000000..f409ff3 --- /dev/null +++ b/docker-xaseco/xaseco/text.tpl @@ -0,0 +1,11 @@ +Track: {TRACK} + Date: {DATE} - {TIME} +Rank Name + +{RANK}. {NICK} ({LAPTIME}) Team: {TEAM} Points: {POINTS} + + +Team Totals + +{TEAM} {POINTS} +