project_files/HedgewarsMobile/Classes/EngineProtocolNetwork.m
author Wuzzy <Wuzzy2@mail.ru>
Mon, 16 Sep 2019 17:33:49 +0200
changeset 15431 8504fee3b601
parent 12872 00215a7ec5f5
permissions -rw-r--r--
Racer: Fix weird water splashes after waypoint placement Does not affect official racer, as only waypoint placement is touched. The reason was that the air attack gear sometimes was not deleted fast enough so it might occassionally drop some air bombs (these are deleted now). Also, the airplane position was set to water level, which caused another water splash.

/*
 * Hedgewars-iOS, a Hedgewars port for iOS devices
 * Copyright (c) 2009-2012 Vittorio Giovara <vittorio.giovara@gmail.com>
 *
 * 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; version 2 of the License
 *
 * 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 St, Fifth Floor, Boston, MA  02110-1301  USA.
 */


#import "EngineProtocolNetwork.h"


#define BUFFER_SIZE 255     // like in original frontend

@implementation EngineProtocolNetwork
@synthesize delegate, stream, csd, enginePort;

- (id)initWithPort:(NSInteger)port {
    if ((self = [super init])) {
        self.delegate = nil;
        self.csd = NULL;
        self.stream = nil;
        self.enginePort = port;
    }
    return self;
}

- (id)init {
    return [self initWithPort:[HWUtils randomPort]];
}

#pragma mark -
#pragma mark Spawner functions
- (void)spawnThread:(NSString *)onSaveFile withOptions:(NSDictionary *)dictionary {
    self.stream = (onSaveFile) ? [[NSOutputStream alloc] initToFileAtPath:onSaveFile append:YES] : nil;
    [self.stream open];

    // +detachNewThreadSelector retain/release self automatically
    [NSThread detachNewThreadSelector:@selector(engineProtocol:)
                             toTarget:self
                           withObject:dictionary];
}

#pragma mark -
#pragma mark Provider functions
// unpacks team data from the selected team.plist to a sequence of engine commands
- (void)provideTeamData:(NSString *)teamName forHogs:(NSInteger)numberOfPlayingHogs withHealth:(NSInteger)initialHealth ofColor:(NSNumber *)teamColor {
    /*
     addteam <32charsMD5hash> <color> <team name>
     addhh <level> <health> <hedgehog name>
     <level> is 0 for human, 1-5 for bots (5 is the most stupid)
    */

    NSString *teamFile = [[NSString alloc] initWithFormat:@"%@/%@", TEAMS_DIRECTORY(), teamName];
    NSDictionary *teamData = [[NSDictionary alloc] initWithContentsOfFile:teamFile];

    NSString *teamHashColorAndName = [[NSString alloc] initWithFormat:@"eaddteam %@ %@ %@",
                                      [teamData objectForKey:@"hash"], [teamColor stringValue], [teamName stringByDeletingPathExtension]];
    [self sendToEngine: teamHashColorAndName];

    NSString *grave = [[NSString alloc] initWithFormat:@"egrave %@", [teamData objectForKey:@"grave"]];
    [self sendToEngine: grave];

    NSString *fort = [[NSString alloc] initWithFormat:@"efort %@", [teamData objectForKey:@"fort"]];
    [self sendToEngine: fort];

    NSString *voicepack = [[NSString alloc] initWithFormat:@"evoicepack %@", [teamData objectForKey:@"voicepack"]];
    [self sendToEngine: voicepack];

    NSString *flag = [[NSString alloc] initWithFormat:@"eflag %@", [teamData objectForKey:@"flag"]];
    [self sendToEngine: flag];

    NSArray *hogs = [teamData objectForKey:@"hedgehogs"];
    for (int i = 0; i < numberOfPlayingHogs; i++) {
        NSDictionary *hog = [hogs objectAtIndex:i];

        NSString *hogLevelHealthAndName = [[NSString alloc] initWithFormat:@"eaddhh %@ %ld %@",
                                           [hog objectForKey:@"level"], (long)initialHealth, [hog objectForKey:@"hogname"]];
        [self sendToEngine: hogLevelHealthAndName];

        NSString *hogHat = [[NSString alloc] initWithFormat:@"ehat %@", [hog objectForKey:@"hat"]];
        [self sendToEngine: hogHat];
    }
}

// unpacks ammostore data from the selected ammo.plist to a sequence of engine commands
- (void)provideAmmoData:(NSString *)ammostoreName forPlayingTeams:(NSInteger)numberOfTeams {
    NSString *weaponPath = [[NSString alloc] initWithFormat:@"%@/%@",WEAPONS_DIRECTORY(),ammostoreName];
    NSDictionary *ammoData = [[NSDictionary alloc] initWithContentsOfFile:weaponPath];

    // if we're loading an older version of ammos fill the engine message with 0s
    int diff = HW_getNumberOfWeapons() - [[ammoData objectForKey:@"ammostore_initialqt"] length];
    NSString *update = @"";
    while ((int)[update length] < diff)
        update = [update stringByAppendingString:@"0"];

    NSString *ammloadt = [[NSString alloc] initWithFormat:@"eammloadt %@%@", [ammoData objectForKey:@"ammostore_initialqt"], update];
    [self sendToEngine: ammloadt];

    NSString *ammprob = [[NSString alloc] initWithFormat:@"eammprob %@%@", [ammoData objectForKey:@"ammostore_probability"], update];
    [self sendToEngine: ammprob];

    NSString *ammdelay = [[NSString alloc] initWithFormat:@"eammdelay %@%@", [ammoData objectForKey:@"ammostore_delay"], update];
    [self sendToEngine: ammdelay];

    NSString *ammreinf = [[NSString alloc] initWithFormat:@"eammreinf %@%@", [ammoData objectForKey:@"ammostore_crate"], update];
    [self sendToEngine: ammreinf];

    // send this for each team so it applies the same ammostore to all teams
    NSString *ammstore = [[NSString alloc] initWithString:@"eammstore"];
    for (int i = 0; i < numberOfTeams; i++) {
        [self sendToEngine: ammstore];
    }
}

// unpacks scheme data from the selected scheme.plist to a sequence of engine commands
- (NSInteger)provideScheme:(NSString *)schemeName {
    NSString *schemePath = [[NSString alloc] initWithFormat:@"%@/%@",SCHEMES_DIRECTORY(),schemeName];
    NSDictionary *schemeDictionary = [[NSDictionary alloc] initWithContentsOfFile:schemePath];

    NSArray *basicArray = [schemeDictionary objectForKey:@"basic"];
    NSArray *gamemodArray = [schemeDictionary objectForKey:@"gamemod"];
    int result = 0;
    int mask = 0x00000004;

    // pack the game modifiers in a single var and send it
    for (NSNumber *value in gamemodArray) {
        if ([value boolValue] == YES)
            result |= mask;
        mask <<= 1;
    }
    NSString *flags = [[NSString alloc] initWithFormat:@"e$gmflags %d",result];
    [self sendToEngine:flags];

    // basic game flags
    result = [[basicArray objectAtIndex:0] intValue];
    NSArray *basic = [[NSArray alloc] initWithContentsOfFile:BASICFLAGS_FILE()];

    for (NSUInteger i = 1; i < [basicArray count]; i++) {
        NSDictionary *dict = [basic objectAtIndex:i];
        NSString *command = [dict objectForKey:@"command"];
        NSInteger value = [[basicArray objectAtIndex:i] intValue];
        if ([[dict objectForKey:@"checkOverMax"] boolValue] && value >= [[dict objectForKey:@"max"] intValue])
            value = 9999;
        if ([[dict objectForKey:@"times1000"] boolValue])
            value = value * 1000;
        NSString *strToSend = [[NSString alloc] initWithFormat:@"%@ %d",command,value];
        [self sendToEngine:strToSend];
    }

    return result;
}

#pragma mark -
#pragma mark Network relevant code
- (void)dumpRawData:(const char *)buffer ofSize:(uint8_t) length {
    [self.stream write:&length maxLength:1];
    [self.stream write:(const uint8_t *)buffer maxLength:length];
}

// wrapper that computes the length of the message and then sends the command string, saving the command on a file
-(int) sendToEngine:(NSString *)string {
    uint8_t length = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding];

    [self dumpRawData:[string UTF8String] ofSize:length];
    SDLNet_TCP_Send(csd, &length, 1);
    return SDLNet_TCP_Send(csd, [string UTF8String], length);
}

// wrapper that computes the length of the message and then sends the command string, skipping file writing
-(int) sendToEngineNoSave:(NSString *)string {
    uint8_t length = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding];

    SDLNet_TCP_Send(csd, &length, 1);
    return SDLNet_TCP_Send(csd, [string UTF8String], length);
}

// this is launched as thread and handles all IPC with engine
- (void)engineProtocol:(id)object {
    @autoreleasepool {
    
    NSDictionary *gameConfig = (NSDictionary *)object;
    NSMutableArray *statsArray = nil;
    TCPsocket sd;
    IPaddress ip;
    int eProto;
    BOOL clientQuit;
    char const buffer[BUFFER_SIZE];
    uint8_t msgSize;

    clientQuit = NO;
    csd = NULL;

    if (SDLNet_Init() < 0) {
        DLog(@"SDLNet_Init: %s", SDLNet_GetError());
        clientQuit = YES;
    }

    // Resolving the host using NULL make network interface to listen
    if (SDLNet_ResolveHost(&ip, NULL, self.enginePort) < 0 && !clientQuit) {
        DLog(@"SDLNet_ResolveHost: %s\n", SDLNet_GetError());
        clientQuit = YES;
    }

    // Open a connection with the IP provided (listen on the host's port)
    if (!(sd = SDLNet_TCP_Open(&ip)) && !clientQuit) {
        DLog(@"SDLNet_TCP_Open: %s %d\n", SDLNet_GetError(), self.enginePort);
        clientQuit = YES;
    }

    DLog(@"Waiting for a client on port %d", self.enginePort);
    while (csd == NULL)
        csd = SDLNet_TCP_Accept(sd);
    SDLNet_TCP_Close(sd);

    while (!clientQuit) {
        msgSize = 0;
        memset((void *)buffer, '\0', BUFFER_SIZE);
        if (SDLNet_TCP_Recv(csd, &msgSize, sizeof(uint8_t)) <= 0)
            break;
        if (SDLNet_TCP_Recv(csd, (void *)buffer, msgSize) <= 0)
            break;

        switch (buffer[0]) {
            case 'C': {
                DLog(@"Sending game config...\n%@", gameConfig);

                /*if (isNetGame == YES)
                    [self sendToEngineNoSave:@"TN"];
                else*/
                    [self sendToEngineNoSave:@"TL"];
                NSString *saveHeader = @"TS";
                [self dumpRawData:[saveHeader UTF8String] ofSize:[saveHeader length]];

                // lua script (if set)
                NSString *script = [gameConfig objectForKey:@"mission_command"];
                if ([script length] != 0)
                    [self sendToEngine:script];
                
                // seed info
                [self sendToEngine:[gameConfig objectForKey:@"seed_command"]];

                // missions/tranings/campaign only need the script configuration set and seed
                TGameType currentGameType = [HWUtils gameType];
                if (currentGameType == gtMission || currentGameType == gtCampaign)
                    break;
                
                // dimension of the map
                [self sendToEngine:[gameConfig objectForKey:@"templatefilter_command"]];
                [self sendToEngine:[gameConfig objectForKey:@"mapgen_command"]];
                [self sendToEngine:[gameConfig objectForKey:@"mazesize_command"]];

                // static land (if set)
                NSString *staticMap = [gameConfig objectForKey:@"staticmap_command"];
                if ([staticMap length] != 0)
                    [self sendToEngine:staticMap];

                // theme info
                [self sendToEngine:[gameConfig objectForKey:@"theme_command"]];

                // scheme (returns initial health)
                NSInteger health = [self provideScheme:[gameConfig objectForKey:@"scheme"]];

                // send an ammostore for each team
                NSArray *teamsConfig = [gameConfig objectForKey:@"teams_list"];
                [self provideAmmoData:[gameConfig objectForKey:@"weapon"] forPlayingTeams:[teamsConfig count]];

                // finally add hogs
                for (NSDictionary *teamData in teamsConfig) {
                    [self provideTeamData:[teamData objectForKey:@"team"]
                                  forHogs:[[teamData objectForKey:@"number"] intValue]
                               withHealth:health
                                  ofColor:[teamData objectForKey:@"color"]];
                }
                break;
            }
            case '?': {
                DLog(@"Ping? Pong!");
                [self sendToEngine:@"!"];
                break;
            }
            case 'E': {
                DLog(@"ERROR - last console line: [%s]", &buffer[1]);
                clientQuit = YES;
                break;
            }
            case 'e': {
                [self dumpRawData:buffer ofSize:msgSize];

                sscanf((char *)buffer, "%*s %d", &eProto);
                int netProto;
                char *versionStr;

                HW_versionInfo(&netProto, &versionStr);
                if (netProto == eProto) {
                    DLog(@"Setting protocol version %d (%s)", eProto, versionStr);
                } else {
                    DLog(@"ERROR - wrong protocol number: %d (expecting %d)", netProto, eProto);
                    clientQuit = YES;
                }
                break;
            }
            case 'i': {
                if (statsArray == nil) {
                    statsArray = [[NSMutableArray alloc] initWithCapacity:10 - 2];
                    NSMutableArray *ranking = [[NSMutableArray alloc] initWithCapacity:4];
                    [statsArray insertObject:ranking atIndex:0];
                }
                NSString *tempStr = [NSString stringWithUTF8String:&buffer[2]];
                NSArray *info = [tempStr componentsSeparatedByString:@" "];
                NSString *arg = [info objectAtIndex:0];
                int index = [arg lengthOfBytesUsingEncoding:NSUTF8StringEncoding] + 3;
                switch (buffer[1]) {
                    case 'r': {           // winning team
                        [statsArray insertObject:[NSString stringWithUTF8String:&buffer[2]] atIndex:1];
                        break;
                    }
                    case 'D':           // best shot
                    {
                        NSString *hogName = [NSString stringWithUTF8String:&buffer[index]];
                        [statsArray addObject:[NSString stringWithFormat:NSLocalizedString(@"The best shot award won by %@ (with %@ points)", nil), hogName, arg]];
                        break;
                    }
                    case 'k':           // best hedgehog
                    {
                        NSString *hogName = [NSString stringWithUTF8String:&buffer[index]];
                        [statsArray addObject:[NSString stringWithFormat:NSLocalizedString(@"The best killer is %@ with %@ kill(s) in a turn", nil), hogName, arg]];
                        break;
                    }
                    case 'K':           // number of hogs killed
                        [statsArray addObject:[NSString stringWithFormat:NSLocalizedString(@"%@ hedgehog(s) were killed during this round", nil), arg]];
                        break;
                    case 'H':           // team health/graph
                        break;
                    case 'T':           // local team stats
                        // still WIP in statsPage.cpp
                        break;
                    case 'P':           // teams ranking
                        [[statsArray objectAtIndex:0] addObject:tempStr];
                        break;
                    case 's':           // self damage
                    {
                        NSString *hogName = [NSString stringWithUTF8String:&buffer[index]];
                        [statsArray addObject:[NSString stringWithFormat:NSLocalizedString(@"%@ thought it's good to shoot his own hedgehogs with %@ points", nil), hogName, arg]];
                        break;
                    }
                    case 'S':           // friendly fire
                    {
                        NSString *hogName = [NSString stringWithUTF8String:&buffer[index]];
                        [statsArray addObject:[NSString stringWithFormat:NSLocalizedString(@"%@ killed %@ of his own hedgehogs", nil), hogName, arg]];
                        break;
                    }
                    case 'B':           // turn skipped
                    {
                        NSString *hogName = [NSString stringWithUTF8String:&buffer[index]];
                        [statsArray addObject:[NSString stringWithFormat:NSLocalizedString(@"%@ was scared and skipped turn %@ times", nil), hogName, arg]];
                        break;
                    }
                    default:
                        DLog(@"Unhandled stat message, see statsPage.cpp");
                        break;
                }
                break;
            }
            case 'q': {
                // game ended and match finished, statsArray is full of delicious statistics
                if (self.delegate != nil && [self.delegate respondsToSelector:@selector(gameEndedWithStatistics:)])
                    [self.delegate gameEndedWithStatistics:statsArray];
                [HWUtils setGameStatus:gsEnded];
                // closing connection here would trigger a "IPC connection lost" error, so we have to wait until recv fails
                break;
            }
            case 'Q': {
                // game exited but not completed, skip this message in the savefile
                [HWUtils setGameStatus:gsInterrupted];
                // same here, don't set clientQuit to YES
                break;
            }
            default:
                [self dumpRawData:buffer ofSize:msgSize];
                break;
        }
    }
    DLog(@"Engine exited, ending thread");

    [self.stream close];

    // Close the client socket
    [HWUtils freePort:self.enginePort];
    SDLNet_TCP_Close(csd);
    SDLNet_Quit();
    
    }

    // Invoking this method should be avoided as it does not give your thread a chance
    // to clean up any resources it allocated during its execution.
    //[NSThread exit];
}

@end