Пример #1
0
void ConfigSaveJSON(const Config *config, const char *filename)
{
	FILE *f = fopen(filename, "w");
	char *text = NULL;
	json_t *root;

	if (f == NULL)
	{
		printf("Error saving config '%s'\n", filename);
		return;
	}

	setlocale(LC_ALL, "");

	root = json_new_object();
	json_insert_pair_into_object(root, "Version", json_new_number(VERSION));
	ConfigSaveVisit(config, root);

	json_tree_to_string(root, &text);
	char *formatText = json_format_string(text);
	fputs(formatText, f);

	// clean up
	CFREE(formatText);
	CFREE(text);
	json_free_value(&root);

	fclose(f);
}
Пример #2
0
void MissionTerminate(Mission *m)
{
	CFREE(m->Title);
	CFREE(m->Description);
	for (int i = 0; i < (int)m->Objectives.size; i++)
	{
		MissionObjective *mo = CArrayGet(&m->Objectives, i);
		CFREE(mo->Description);
	}
	CArrayTerminate(&m->Objectives);
	CArrayTerminate(&m->Enemies);
	CArrayTerminate(&m->SpecialChars);
	CArrayTerminate(&m->Items);
	CArrayTerminate(&m->ItemDensities);
	CArrayTerminate(&m->Weapons);
	switch (m->Type)
	{
	case MAPTYPE_CLASSIC:
		break;
	case MAPTYPE_STATIC:
		CArrayTerminate(&m->u.Static.Tiles);
		CArrayTerminate(&m->u.Static.Items);
		CArrayTerminate(&m->u.Static.Wrecks);
		CArrayTerminate(&m->u.Static.Characters);
		CArrayTerminate(&m->u.Static.Objectives);
		CArrayTerminate(&m->u.Static.Keys);
		break;
	}
}
Пример #3
0
static void CampaignIntroDraw(void *data)
{
	// This will only draw once
	const CampaignSetting *c = data;

	GraphicsBlitBkg(&gGraphicsDevice);
	const int w = gGraphicsDevice.cachedConfig.Res.x;
	const int h = gGraphicsDevice.cachedConfig.Res.y;
	const int y = h / 4;

	// Display title + author
	char *buf;
	CMALLOC(buf, strlen(c->Title) + strlen(c->Author) + 16);
	sprintf(buf, "%s by %s", c->Title, c->Author);
	FontOpts opts = FontOptsNew();
	opts.HAlign = ALIGN_CENTER;
	opts.Area = gGraphicsDevice.cachedConfig.Res;
	opts.Pad.y = y - 25;
	FontStrOpt(buf, Vec2iZero(), opts);
	CFREE(buf);

	// Display campaign description
	// allow some slack for newlines
	if (strlen(c->Description) > 0)
	{
		CMALLOC(buf, strlen(c->Description) * 2);
		// Pad about 1/6th of the screen width total (1/12th left and right)
		FontSplitLines(c->Description, buf, w * 5 / 6);
		FontStr(buf, Vec2iNew(w / 12, y));
		CFREE(buf);
	}
}
Пример #4
0
void UIObjectDestroy(UIObject *o)
{
	size_t i;
	UIObject **objs = o->Children.data;
	CFREE(o->Tooltip);
	for (i = 0; i < o->Children.size; i++, objs++)
	{
		UIObjectDestroy(*objs);
	}
	CArrayTerminate(&o->Children);
	if (o->IsDynamicData)
	{
		CFREE(o->Data);
	}
	switch (o->Type)
	{
	case UITYPE_TEXTBOX:
		CFREE(o->u.Textbox.Hint);
		break;
	case UITYPE_TAB:
		CArrayTerminate(&o->u.Tab.Labels);
		break;
	default:
		// do nothing
		break;
	}
	CFREE(o);
}
Пример #5
0
void ConvertCampaignSetting(CampaignSetting *dest, CampaignSettingOld *src)
{
	int i;
	CFREE(dest->Title);
	CSTRDUP(dest->Title, src->title);
	CFREE(dest->Author);
	CSTRDUP(dest->Author, src->author);
	CFREE(dest->Description);
	CSTRDUP(dest->Description, src->description);
	for (i = 0; i < src->missionCount; i++)
	{
		Mission m;
		MissionInit(&m);
		ConvertMission(&m, &src->missions[i]);
		CArrayPushBack(&dest->Missions, &m);
	}
	CharacterStoreTerminate(&dest->characters);
	CharacterStoreInit(&dest->characters);
	for (i = 0; i < src->characterCount; i++)
	{
		Character *ch = CharacterStoreAddOther(&dest->characters);
		ConvertCharacter(ch, &src->characters[i]);
		CharacterSetLooks(ch, &ch->looks);
	}
}
Пример #6
0
void UIObjectDestroy(UIObject *o)
{
	CFREE(o->Tooltip);
	CA_FOREACH(UIObject *, obj, o->Children)
		UIObjectDestroy(*obj);
	CA_FOREACH_END()
	CArrayTerminate(&o->Children);
	if (o->IsDynamicData)
	{
		CFREE(o->Data);
	}
	switch (o->Type)
	{
	case UITYPE_TEXTBOX:
		CFREE(o->u.Textbox.Hint);
		break;
	case UITYPE_TAB:
		CArrayTerminate(&o->u.Tab.Labels);
		break;
	default:
		// do nothing
		break;
	}
	CFREE(o);
}
Пример #7
0
void ConfigSaveJSON(Config *config, const char *filename)
{
	FILE *f = fopen(filename, "w");
	char *text = NULL;
	json_t *root;

	if (f == NULL)
	{
		printf("Error saving config '%s'\n", filename);
		return;
	}

	setlocale(LC_ALL, "");

	root = json_new_object();
	json_insert_pair_into_object(root, "Version", json_new_number(VERSION));
	AddGameConfigNode(&config->Game, root);
	AddGraphicsConfigNode(&config->Graphics, root);
	AddInputConfigNode(&config->Input, root);
	AddInterfaceConfigNode(&config->Interface, root);
	AddSoundConfigNode(&config->Sound, root);
	AddQuickPlayConfigNode(&config->QuickPlay, root);

	json_tree_to_string(root, &text);
	char *formatText = json_format_string(text);
	fputs(formatText, f);

	// clean up
	CFREE(formatText);
	CFREE(text);
	json_free_value(&root);

	fclose(f);
}
Пример #8
0
bool ScreenMissionBriefing(const struct MissionOptions *m)
{
	const int w = gGraphicsDevice.cachedConfig.Res.x;
	const int h = gGraphicsDevice.cachedConfig.Res.y;
	const int y = h / 4;
	MissionBriefingData mData;
	memset(&mData, 0, sizeof mData);
	mData.IsOK = true;

	// Title
	CMALLOC(mData.Title, strlen(m->missionData->Title) + 32);
	sprintf(mData.Title, "Mission %d: %s",
		m->index + 1, m->missionData->Title);
	mData.TitleOpts = FontOptsNew();
	mData.TitleOpts.HAlign = ALIGN_CENTER;
	mData.TitleOpts.Area = gGraphicsDevice.cachedConfig.Res;
	mData.TitleOpts.Pad.y = y - 25;

	// Password
	if (m->index > 0)
	{
		sprintf(
			mData.Password, "Password: %s", gAutosave.LastMission.Password);
		mData.PasswordOpts = FontOptsNew();
		mData.PasswordOpts.HAlign = ALIGN_CENTER;
		mData.PasswordOpts.Area = gGraphicsDevice.cachedConfig.Res;
		mData.PasswordOpts.Pad.y = y - 15;
	}

	// Split the description, and prepare it for typewriter effect
	mData.TypewriterCount = 0;
	// allow some slack for newlines
	CMALLOC(mData.Description, strlen(m->missionData->Description) * 2 + 1);
	CCALLOC(mData.TypewriterBuf, strlen(m->missionData->Description) * 2 + 1);
	// Pad about 1/6th of the screen width total (1/12th left and right)
	FontSplitLines(m->missionData->Description, mData.Description, w * 5 / 6);
	mData.DescriptionPos = Vec2iNew(w / 12, y);

	// Objectives
	mData.ObjectiveDescPos =
		Vec2iNew(w / 6, y + FontStrH(mData.Description) + h / 10);
	mData.ObjectiveInfoPos =
		Vec2iNew(w - (w / 6), mData.ObjectiveDescPos.y + FontH());
	mData.ObjectiveHeight = h / 12;
	mData.MissionOptions = m;

	GameLoopData gData = GameLoopDataNew(
		&mData, MissionBriefingUpdate,
		&mData, MissionBriefingDraw);
	GameLoop(&gData);
	if (mData.IsOK)
	{
		SoundPlay(&gSoundDevice, StrSound("mg"));
	}

	CFREE(mData.Title);
	CFREE(mData.Description);
	CFREE(mData.TypewriterBuf);
	return mData.IsOK;
}
Пример #9
0
bool TrySaveJSONFile(json_t *node, const char *filename)
{
	bool res = true;
	char *text;
	json_tree_to_string(node, &text);
	char *ftext = json_format_string(text);
	FILE *f = fopen(filename, "w");
	if (f == NULL)
	{
		LOG(LM_MAIN, LL_ERROR, "failed to open file(%s) for saving: %s",
			filename, strerror(errno));
		res = false;
		goto bail;
	}
	size_t writeLen = strlen(ftext);
	const size_t rc = fwrite(ftext, 1, writeLen, f);
	if (rc != writeLen)
	{
		LOG(LM_MAIN, LL_ERROR, "Wrote (%d) of (%d) bytes: %s",
			(int)rc, (int)writeLen, strerror(errno));
		res = false;
		goto bail;
	}

bail:
	CFREE(text);
	CFREE(ftext);
	if (f != NULL) fclose(f);
	return res;
}
Пример #10
0
static void PY_pdbdata_tp_dealloc(PY_pdbdata *self)
   {

/* XXX    PD_free(self->file, self->type, self->data) ;*/

/* Check if the data actually belongs to another object.
 * For example, by indexing.  In this case self->data
 * may be pointer into the middle of an array so
 * we can not delete it directly.  Instead decrement the parent.
 */
    if (self->parent == NULL)
       {if (self->data != NULL)
	   {_PP_rel_syment(self->dpobj->host_chart, self->data,
			   self->nitems, self->type);
            CFREE(self->data);};}
    else
       {Py_DECREF(self->parent);
        self->parent = NULL;
        self->data   = NULL;};

    CFREE(self->type);
    _PD_rl_dimensions(self->dims);
    _PD_rl_defstr(self->dp);
    Py_XDECREF(self->dpobj);

    PY_self_free(self);

    return;}
Пример #11
0
bool TrySaveJSONFile(json_t *node, const char *filename)
{
	bool res = true;
	char *text;
	json_tree_to_string(node, &text);
	char *ftext = json_format_string(text);
	FILE *f = fopen(filename, "w");
	if (f == NULL)
	{
		printf("failed to open. Reason: [%s].\n", strerror(errno));
		res = false;
		goto bail;
	}
	size_t writeLen = strlen(ftext);
	const size_t rc = fwrite(ftext, 1, writeLen, f);
	if (rc != writeLen)
	{
		printf("Wrote (%d) of (%d) bytes. Reason: [%s].\n",
			(int)rc, (int)writeLen, strerror(errno));
		res = false;
		goto bail;
	}

bail:
	CFREE(text);
	CFREE(ftext);
	if (f != NULL) fclose(f);
	return res;
}
Пример #12
0
static inline void VisitedNodesDestroy(VisitedNodes visitedNodes)
{
    CFREE(visitedNodes->nodeRecordsIndex);
	CFREE(visitedNodes->nodeRecords);
	CFREE(visitedNodes->openNodes);
	CFREE(visitedNodes);
}
Пример #13
0
void MenuSystemTerminate(MenuSystem *ms)
{
	MenuDestroySubmenus(ms->root);
	CFREE(ms->root);
	CFREE(ms->customDisplayFuncs);
	CFREE(ms->customDisplayDatas);
	memset(ms, 0, sizeof *ms);
}
Пример #14
0
static void makeh(void)
   {int i, j, l, sz, change, N_var;
    double t1, t2, *tt, **td, *pd, *time, **ldata;
    haelem **tab, *hp;
    source_record *sp;
    time_list *tp;
    pcons *pp, *pn;

/* order the times for each variable in srctab */
    sz    = srctab->size;
    tab   = srctab->table;
    N_var = 0;
    for (i = 0; i < sz; i++)
        for (hp = tab[i]; hp != NULL; hp = hp->next)
            {tp = (time_list *) (hp->def);
             l  = tp->length;

/* copy the list into arrays */
             tt = time = CMAKE_N(double, l);
             td = ldata = CMAKE_N(double *, l);
             for (pp = tp->list; pp != NULL; pp = pn)
                 {sp = (source_record *) pp->car;
                  *(tt++) = sp->time;
                  *(td++) = sp->data;
                  pn = (pcons *) pp->cdr;
                  CFREE(sp);
                  CFREE(pp);};

/* sort the arrays according to the times */
             change = TRUE;
             while (change)
                {change = FALSE;
                 for (j = 1; j < l; j++)
                     {t1 = time[j-1];
                      t2 = time[j];
                      if (t2 < t1)
                         {pd         = ldata[j-1];
                          ldata[j-1] = ldata[j];
                          ldata[j]   = pd;
                          time[j]    = t1;
                          time[j-1]  = t2;
                          change     = TRUE;};};};

/* write the variable and source_record array out */
             write_var(pdsf, hp->name, time, ldata, l);
             N_var++;

/* release the temporary storage for this variable */
             CFREE(time);
             CFREE(ldata);};

/* finish up */
    PD_write(pdsf, "n_variables", "integer", &N_var);
    PD_close(pdsf);
    pdsf = NULL;    

    return;}
Пример #15
0
static void PlayerListTerminate(GameLoopData *data)
{
	PlayerList *pl = data->Data;

	CArrayTerminate(&pl->playerUIDs);
	MenuSystemTerminate(&pl->ms);
	CFREE(pl->data);
	CFREE(pl);
}
Пример #16
0
void GraphicsTerminate(GraphicsDevice *device)
{
	debug(D_NORMAL, "Shutting down video...\n");
	SDL_FreeSurface(device->icon);
	SDL_FreeSurface(device->screen);
	SDL_VideoQuit();
	CFREE(device->buf);
	CFREE(device->bkg);
}
Пример #17
0
void MapNewLoadCampaignJSON(json_t *root, CampaignSetting *c)
{
	CFREE(c->Title);
	c->Title = GetString(root, "Title");
	CFREE(c->Author);
	c->Author = GetString(root, "Author");
	CFREE(c->Description);
	c->Description = GetString(root, "Description");
}
Пример #18
0
void AmmoClassesClear(CArray *ammo)
{
	CA_FOREACH(Ammo, a, *ammo)
		CFREE(a->Name);
		CFREE(a->Sound);
		CFREE(a->DefaultGun);
	CA_FOREACH_END()
	CArrayClear(ammo);
}
Пример #19
0
void MenuDestroy(MenuSystem *menu)
{
	if (menu == NULL || menu->root == NULL)
	{
		return;
	}
	MenuDestroySubmenus(menu->root);
	CFREE(menu->root);
	CFREE(menu);
}
Пример #20
0
void AmmoClassesClear(CArray *ammo)
{
	for (int i = 0; i < (int)ammo->size; i++)
	{
		Ammo *a = CArrayGet(ammo, i);
		CFREE(a->Name);
		CFREE(a->Sound);
	}
	CArrayClear(ammo);
}
Пример #21
0
void FreeTrigger(TTrigger * t)
{
	if (!t)
		return;

	FreeTrigger(t->left);
	FreeTrigger(t->right);
	CFREE(t->actions);
	CFREE(t);
}
Пример #22
0
void WeaponClassesClear(CArray *classes)
{
	for (int i = 0; i < (int)classes->size; i++)
	{
		GunDescription *gd = CArrayGet(classes, i);
		CFREE(gd->name);
		CFREE(gd->Description);
	}
	CArrayClear(classes);
}
Пример #23
0
static void BulletClassFree(BulletClass *b)
{
	CFREE(b->Name);
	CFREE(b->HitSound.Object);
	CFREE(b->HitSound.Flesh);
	CFREE(b->HitSound.Wall);
	CArrayTerminate(&b->OutOfRangeGuns);
	CArrayTerminate(&b->HitGuns);
	CArrayTerminate(&b->Falling.DropGuns);
	CArrayTerminate(&b->ProximityGuns);
}
Пример #24
0
static void LoadArchivePics(
	PicManager *pm, const char *archive, const char *dirname)
{
	char *buf = NULL;

	char path[CDOGS_PATH_MAX];
	sprintf(path, "%s/%s", archive, dirname);
	tinydir_dir dir;
	if (tinydir_open(&dir, path) != 0)
	{
		LOG(LM_MAP, LL_DEBUG, "no pic dir(%s): %s", path, strerror(errno));
		goto bail;
	}
	while (dir.has_next)
	{
		tinydir_file file;
		if (tinydir_readfile(&dir, &file) != 0)
		{
			LOG(LM_MAP, LL_WARN, "cannot read file: %s", strerror(errno));
			break;
		}
		if (!file.is_reg) goto nextFile;
		long len;
		buf = ReadFileIntoBuf(file.path, "rb", &len);
		if (buf == NULL) goto nextFile;
		SDL_RWops *rwops = SDL_RWFromMem(buf, len);
		bool isPng = IMG_isPNG(rwops);
		if (isPng)
		{
			SDL_Surface *data = IMG_Load_RW(rwops, 0);
			if (data != NULL)
			{
				char nameBuf[CDOGS_FILENAME_MAX];
				PathGetBasenameWithoutExtension(nameBuf, file.path);
				PicManagerAdd(&pm->customPics, &pm->customSprites, nameBuf, data);
			}
		}
		rwops->close(rwops);
	nextFile:
		CFREE(buf);
		buf = NULL;
		if (tinydir_next(&dir) != 0)
		{
			printf(
				"Could not go to next file in dir %s: %s\n",
				path, strerror(errno));
			goto bail;
		}
	}

bail:
	CFREE(buf);
	tinydir_close(&dir);
}
Пример #25
0
void CampaignSettingTerminate(CampaignSetting *setting)
{
	CFREE(setting->Title);
	CFREE(setting->Author);
	CFREE(setting->Description);
	CA_FOREACH(Mission, m, setting->Missions)
		MissionTerminate(m);
	CA_FOREACH_END()
	CArrayTerminate(&setting->Missions);
	CharacterStoreTerminate(&setting->characters);
	memset(setting, 0, sizeof *setting);
}
Пример #26
0
static void LoadArchiveSounds(
	SoundDevice *device, const char *archive, const char *dirname)
{
	char *buf = NULL;

	char path[CDOGS_PATH_MAX];
	sprintf(path, "%s/%s", archive, dirname);
	tinydir_dir dir;
	if (tinydir_open(&dir, path) != 0)
	{
		LOG(LM_MAP, LL_DEBUG, "no sound dir(%s): %s", path, strerror(errno));
		goto bail;
	}
	while (dir.has_next)
	{
		tinydir_file file;
		tinydir_readfile(&dir, &file);
		if (!file.is_reg) goto nextFile;
		long len;
		buf = ReadFileIntoBuf(file.path, "rb", &len);
		if (buf == NULL) goto nextFile;
		SDL_RWops *rwops = SDL_RWFromMem(buf, len);
		Mix_Chunk *data = Mix_LoadWAV_RW(rwops, 0);
		if (data != NULL)
		{
			char nameBuf[CDOGS_FILENAME_MAX];
			strcpy(nameBuf, file.name);
			// Remove extension
			char *dot = strrchr(nameBuf, '.');
			if (dot != NULL)
			{
				*dot = '\0';
			}
			SoundAdd(&device->customSounds, nameBuf, data);
		}
		rwops->close(rwops);
	nextFile:
		CFREE(buf);
		buf = NULL;
		if (tinydir_next(&dir) != 0)
		{
			printf(
				"Could not go to next file in dir %s: %s\n",
				path, strerror(errno));
			goto bail;
		}
	}

bail:
	CFREE(buf);
	tinydir_close(&dir);
}
Пример #27
0
void ObjectiveLoadJSON(Objective *o, json_t *node, const int version)
{
	memset(o, 0, sizeof *o);
	o->Description = GetString(node, "Description");
	JSON_UTILS_LOAD_ENUM(o->Type, node, "Type", StrObjectiveType);
	// Set objective colours based on type
	o->color = ObjectiveTypeColor(o->Type);
	if (version < 8)
	{
		// Index numbers used for all objective classes; convert them
		// to their class handles
		LoadInt(&o->u.Index, node, "Index");
		switch (o->Type)
		{
		case OBJECTIVE_COLLECT:
			o->u.Pickup = IntPickupClass(o->u.Index);
			break;
		case OBJECTIVE_DESTROY:
			o->u.MapObject = IntMapObject(o->u.Index);
			break;
		default:
			// do nothing
			break;
		}
	}
	else
	{
		char *tmp;
		switch (o->Type)
		{
		case OBJECTIVE_COLLECT:
			tmp = GetString(node, "Pickup");
			o->u.Pickup = StrPickupClass(tmp);
			CFREE(tmp);
			break;
		case OBJECTIVE_DESTROY:
			tmp = GetString(node, "MapObject");
			o->u.MapObject = StrMapObject(tmp);
			CFREE(tmp);
			break;
		default:
			LoadInt(&o->u.Index, node, "Index");
			break;
		}
	}
	LoadInt(&o->Count, node, "Count");
	LoadInt(&o->Required, node, "Required");
	LoadInt(&o->Flags, node, "Flags");
}
Пример #28
0
void MapObjectsTerminate(MapObjects *classes)
{
	MapObjectsClear(&classes->Classes);
	CArrayTerminate(&classes->Classes);
	MapObjectsClear(&classes->CustomClasses);
	CArrayTerminate(&classes->CustomClasses);
	CA_FOREACH(char *, s, classes->Destructibles)
		CFREE(*s);
	CA_FOREACH_END()
	CArrayTerminate(&classes->Destructibles);
	CA_FOREACH(char *, s, classes->Bloods)
		CFREE(*s);
	CA_FOREACH_END()
	CArrayTerminate(&classes->Bloods);
}
Пример #29
0
void LoadCharacters(
	CharacterStore *c, json_t *charactersNode, const int version)
{
	json_t *child = charactersNode->child;
	CharacterStoreTerminate(c);
	CharacterStoreInit(c);
	while (child)
	{
		Character *ch = CharacterStoreAddOther(c);
		char *tmp;
		if (version < 7)
		{
			// Old version stored character looks as palette indices
			int face;
			LoadInt(&face, child, "face");
			ch->Class = IntCharacterClass(face);
			int skin, arm, body, leg, hair;
			LoadInt(&skin, child, "skin");
			LoadInt(&arm, child, "arm");
			LoadInt(&body, child, "body");
			LoadInt(&leg, child, "leg");
			LoadInt(&hair, child, "hair");
			ConvertCharacterColors(skin, arm, body, leg, hair, &ch->Colors);
		}
		else
		{
			tmp = GetString(child, "Class");
			ch->Class = StrCharacterClass(tmp);
			CFREE(tmp);
			LoadColor(&ch->Colors.Skin, child, "Skin");
			LoadColor(&ch->Colors.Arms, child, "Arms");
			LoadColor(&ch->Colors.Body, child, "Body");
			LoadColor(&ch->Colors.Legs, child, "Legs");
			LoadColor(&ch->Colors.Hair, child, "Hair");
		}
		LoadInt(&ch->speed, child, "speed");
		tmp = GetString(child, "Gun");
		ch->Gun = StrGunDescription(tmp);
		CFREE(tmp);
		LoadInt(&ch->maxHealth, child, "maxHealth");
		LoadInt(&ch->flags, child, "flags");
		LoadInt(&ch->bot->probabilityToMove, child, "probabilityToMove");
		LoadInt(&ch->bot->probabilityToTrack, child, "probabilityToTrack");
		LoadInt(&ch->bot->probabilityToShoot, child, "probabilityToShoot");
		LoadInt(&ch->bot->actionDelay, child, "actionDelay");
		child = child->next;
	}
}
Пример #30
0
void NetInputClientConnect(NetInputClient *n, Uint32 host)
{
	if (!NetInputChannelTryOpen(&n->channel, 0, host))
	{
		printf("Failed to open channel\n");
		return;
	}
	n->channel.otherPort = NET_INPUT_UDP_PORT;

	// Handshake

	// Send SYN
	UDPpacket packet = NetInputNewPacket(&n->channel, sizeof(NetMsgSyn));
	NetMsgSyn *packetSyn = (NetMsgSyn *)packet.data;
	n->channel.seq = rand() & (Uint16)-1;
	SDLNet_Write16(n->channel.seq, &packetSyn->seq);
	if (!NetInputTrySendPacket(&n->channel, packet))
	{
		printf("Failed to send SYN\n");
		goto bail;
	}
	n->channel.seq++;

	// Wait for SYN-ACK
	// TODO: don't wait forever
	n->channel.state = CHANNEL_STATE_WAIT_HANDSHAKE;
	while (!NetInputRecvBlocking(&n->channel, TryParseSynAck, &n->channel))
	{
		// Spin
	}

	// Send ACK
	CFREE(packet.data);
	packet = NetInputNewPacket(&n->channel, sizeof(NetMsgAck));
	NetMsgAck *packetAck = (NetMsgAck *)packet.data;
	SDLNet_Write16(n->channel.ack, &packetAck->ack);
	if (!NetInputTrySendPacket(&n->channel, packet))
	{
		printf("Failed to send ACK\n");
		goto bail;
	}

	n->channel.state = CHANNEL_STATE_CONNECTED;
	return;

bail:
	CFREE(packet.data);
}