/* See gl_terrain.h for terminology, networking notes, etc. */ #include "quakedef.h" #ifdef TERRAIN #include "glquake.h" #include "shader.h" #include "pr_common.h" #include "gl_terrain.h" static terrainfuncs_t terrainfuncs; cvar_t mod_terrain_networked = CVARD("mod_terrain_networked", "0", "Terrain edits are networked. Clients will download sections on demand, and servers will notify clients of changes."); cvar_t mod_terrain_defaulttexture = CVARD("mod_terrain_defaulttexture", "", "Newly created terrain tiles will use this texture. This should generally be updated by the terrain editor."); cvar_t mod_terrain_savever = CVARD("mod_terrain_savever", "", "Which terrain section version to write if terrain was edited."); enum { hmcmd_brush_delete, hmcmd_brush_insert, hmcmd_prespawning, //sent before initial inserts hmcmd_prespawned, //sent just after initial inserts hmcmd_ent_edit = 0x40, hmcmd_ent_remove }; void validatelinks(link_t *firstnode) { /* link_t *node; COM_AssertMainThread("foo"); for (node = firstnode->next; node; node = node->next) if (firstnode == node) break; for (node = firstnode->prev; node; node = node->prev) if (firstnode == node) break; return;*/ } void validatelinks2(link_t *firstnode, link_t *panic) { /* link_t *node; COM_AssertMainThread("foo"); for (node = firstnode->next; node; node = node->next) { if (node == panic) Sys_Error("Panic\n"); if (firstnode == node) break; } for (node = firstnode->prev; node; node = node->prev) { if (node == panic) Sys_Error("Panic\n"); if (firstnode == node) break; } return;*/ } //FIXME int Surf_NewLightmaps(int count, int width, int height, qboolean deluxe); #ifndef SERVERONLY static void ted_dorelight(heightmap_t *hm); static void Terr_WorkerLoadedSectionLightmap(void *ctx, void *data, size_t a, size_t b); static qboolean Terr_Collect(heightmap_t *hm); #endif static hmsection_t *QDECL Terr_GetSection(heightmap_t *hm, int x, int y, unsigned int flags); static void Terr_LoadSectionWorker(void *ctx, void *data, size_t a, size_t b); static void Terr_WorkerLoadedSection(void *ctx, void *data, size_t a, size_t b); static void Terr_WorkerFailedSection(void *ctx, void *data, size_t a, size_t b); static void Terr_Brush_DeleteIdx(heightmap_t *hm, size_t idx); #ifndef SERVERONLY static void Terr_Brush_Draw(heightmap_t *hm, batch_t **batches, entity_t *e); #endif #ifndef SERVERONLY static texid_t Terr_LoadTexture(char *name) { extern texid_t missing_texture; texid_t id; if (*name) { id = R_LoadHiResTexture(name, NULL, 0); if (!TEXVALID(id)) { id = missing_texture; Con_Printf("Unable to load texture %s\n", name); } } else id = missing_texture; return id; } #endif static void QDECL Terr_LoadSectionTextures(hmsection_t *s) { #ifndef SERVERONLY extern texid_t missing_texture; struct hmwater_s *w; if (isDedicated) return; //CL_CheckOrEnqueDownloadFile(s->texname[0], NULL, 0); //CL_CheckOrEnqueDownloadFile(s->texname[1], NULL, 0); //CL_CheckOrEnqueDownloadFile(s->texname[2], NULL, 0); //CL_CheckOrEnqueDownloadFile(s->texname[3], NULL, 0); switch(s->hmmod->mode) { case HMM_BLOCKS: s->textures.base = Terr_LoadTexture(va("maps/%s/atlas.tga", s->hmmod->path)); s->textures.fullbright = Terr_LoadTexture(va("maps/%s/atlas_luma.tga", s->hmmod->path)); s->textures.bump = Terr_LoadTexture(va("maps/%s/atlas_norm.tga", s->hmmod->path)); s->textures.specular = Terr_LoadTexture(va("maps/%s/atlas_spec.tga", s->hmmod->path)); s->textures.upperoverlay = missing_texture; s->textures.loweroverlay = missing_texture; break; case HMM_TERRAIN: s->textures.base = Terr_LoadTexture(s->texname[0]); s->textures.upperoverlay = Terr_LoadTexture(s->texname[1]); s->textures.loweroverlay = Terr_LoadTexture(s->texname[2]); s->textures.fullbright = Terr_LoadTexture(s->texname[3]); s->textures.bump = *s->texname[0]?R_LoadHiResTexture(va("%s_norm", s->texname[0]), NULL, 0):r_nulltex; s->textures.specular = *s->texname[0]?R_LoadHiResTexture(va("%s_spec", s->texname[0]), NULL, 0):r_nulltex; break; } for (w = s->water; w; w = w->next) { w->shader = R_RegisterCustom (w->shadername, SUF_NONE, Shader_DefaultWaterShader, NULL); R_BuildDefaultTexnums(NULL, w->shader); //this might get expensive. hideously so. } #endif } static qboolean QDECL Terr_InitLightmap(hmsection_t *s, qboolean initialise) { #ifdef SERVERONLY return false; #else heightmap_t *hm = s->hmmod; if (s->lightmap < 0) { struct lmsect_s *lms; Sys_LockMutex(com_resourcemutex); while (!hm->unusedlmsects) { int lm; int i; Sys_UnlockMutex(com_resourcemutex); lm = Surf_NewLightmaps(1, SECTTEXSIZE*LMCHUNKS, SECTTEXSIZE*LMCHUNKS, false); Sys_LockMutex(com_resourcemutex); for (i = 0; i < LMCHUNKS*LMCHUNKS; i++) { lms = BZ_Malloc(sizeof(*lms)); lms->lm = lm; lms->x = (i & (LMCHUNKS-1))*SECTTEXSIZE; lms->y = (i / LMCHUNKS)*SECTTEXSIZE; lms->next = hm->unusedlmsects; hm->unusedlmsects = lms; hm->numunusedlmsects++; } } lms = hm->unusedlmsects; hm->unusedlmsects = lms->next; s->lightmap = lms->lm; s->lmx = lms->x; s->lmy = lms->y; hm->numunusedlmsects--; hm->numusedlmsects++; Sys_UnlockMutex(com_resourcemutex); Z_Free(lms); initialise = true; } if (initialise && s->lightmap >= 0) { int x, y; unsigned char *lm; lm = lightmap[s->lightmap]->lightmaps; lm += (s->lmy * HMLMSTRIDE + s->lmx) * lightmap_bytes; for (y = 0; y < SECTTEXSIZE; y++) { for (x = 0; x < SECTTEXSIZE; x++) { lm[x*4+0] = 0; lm[x*4+1] = 0; lm[x*4+2] = 0; lm[x*4+3] = 255; } lm += (HMLMSTRIDE)*lightmap_bytes; } } if (s->lightmap >= 0) { lightmap[s->lightmap]->modified = true; lightmap[s->lightmap]->rectchange.l = 0; lightmap[s->lightmap]->rectchange.t = 0; lightmap[s->lightmap]->rectchange.r = HMLMSTRIDE; lightmap[s->lightmap]->rectchange.b = HMLMSTRIDE; } return s->lightmap>=0; #endif } static char *genextendedhex(int n, char *buf) { char *ret; static char nibble[16] = "0123456789abcdef"; unsigned int m; int i; for (i = 7; i >= 1; i--) //>=1 ensures at least two nibbles appear. { m = 0xfffffff8<<(i*4); if ((n & m) != m && (n & m) != 0) break; } ret = buf; for(i++; i >= 0; i--) *buf++ = nibble[(n>>i*4) & 0xf]; *buf++ = 0; return ret; } static char *Terr_DiskBlockName(heightmap_t *hm, int sx, int sy, char *out, size_t outsize) { char xpart[9]; char ypart[9]; //using a naming scheme centered around 0 means we can gracefully expand the map away from 0,0 sx -= CHUNKBIAS; sy -= CHUNKBIAS; //wrap cleanly sx &= CHUNKLIMIT-1; sy &= CHUNKLIMIT-1; sx /= SECTIONSPERBLOCK; sy /= SECTIONSPERBLOCK; if (sx >= CHUNKBIAS/SECTIONSPERBLOCK) sx |= 0xffffff00; if (sy >= CHUNKBIAS/SECTIONSPERBLOCK) sy |= 0xffffff00; Q_snprintfz(out, outsize, "maps/%s/block_%s_%s.hms", hm->path, genextendedhex(sx, xpart), genextendedhex(sy, ypart)); return out; } static char *Terr_DiskSectionName(heightmap_t *hm, int sx, int sy, char *out, size_t outsize) { sx -= CHUNKBIAS; sy -= CHUNKBIAS; //wrap cleanly sx &= CHUNKLIMIT-1; sy &= CHUNKLIMIT-1; Q_snprintfz(out, outsize, "maps/%s/sect_%03x_%03x.hms", hm->path, sx, sy); return out; } #ifndef SERVERONLY static char *Terr_TempDiskSectionName(heightmap_t *hm, int sx, int sy) { sx -= CHUNKBIAS; sy -= CHUNKBIAS; //wrap cleanly sx &= CHUNKLIMIT-1; sy &= CHUNKLIMIT-1; return va("temp/%s/sect_%03x_%03x.hms", hm->path, sx, sy); } #endif static int dehex_e(int i, qboolean *error) { if (i >= '0' && i <= '9') return (i-'0'); else if (i >= 'A' && i <= 'F') return (i-'A'+10); else if (i >= 'a' && i <= 'f') return (i-'a'+10); else *error = true; return 0; } static qboolean Terr_IsSectionFName(heightmap_t *hm, const char *fname, int *sx, int *sy) { int l; qboolean error = false; *sx = 0xdeafbeef; //something clearly invalid *sy = 0xdeafbeef; //not this model... if (!hm) return false; //expect the first 5 chars to be maps/ or temp/ fname += 5; l = strlen(hm->path); if (strncmp(fname, hm->path, l) || fname[l] != '/') return false; fname += l+1; //fname now has a fixed length. if (strlen(fname) != 16) return false; if (strncmp(fname, "sect_", 5) || fname[8] != '_' || (strcmp(fname+12, ".hms") && strcmp(fname+12, ".tmp"))) return false; *sx = 0; *sx += dehex_e(fname[5], &error)<<8; *sx += dehex_e(fname[6], &error)<<4; *sx += dehex_e(fname[7], &error)<<0; *sy = 0; *sy += dehex_e(fname[9], &error)<<8; *sy += dehex_e(fname[10], &error)<<4; *sy += dehex_e(fname[11], &error)<<0; *sx += CHUNKBIAS; *sy += CHUNKBIAS; if ((unsigned)*sx >= CHUNKLIMIT) *sx -= CHUNKLIMIT; if ((unsigned)*sy >= CHUNKLIMIT) *sy -= CHUNKLIMIT; //make sure its a valid section index. if ((unsigned)*sx >= CHUNKLIMIT) return false; if ((unsigned)*sy >= CHUNKLIMIT) return false; return true; } static int QDECL Terr_GenerateSections(heightmap_t *hm, int sx, int sy, int count, hmsection_t **sects) { //a worker is trying to load multiple sections at once. //lock ALL of them atomically, so that we don't end up with too many workers all doing stuff at once. int x, y; hmsection_t *s; hmcluster_t *cluster; int numgen = 0; Sys_LockMutex(com_resourcemutex); for (y = 0; y < count; y++) for (x = 0; x < count; x++) { int clusternum = ((sx+x)/MAXSECTIONS) + ((sy+y)/MAXSECTIONS)*MAXCLUSTERS; cluster = hm->cluster[clusternum]; if (!cluster) cluster = hm->cluster[clusternum] = Z_Malloc(sizeof(*cluster)); s = cluster->section[((sx+x)%MAXSECTIONS) + ((sy+y)%MAXSECTIONS)*MAXSECTIONS]; if (!s) { s = Z_Malloc(sizeof(*s)); s->loadstate = TSLS_LOADING0; #ifndef SERVERONLY s->lightmap = -1; #endif s->numents = 0; s->sx = sx + x; s->sy = sy + y; cluster->section[(s->sx%MAXSECTIONS) + (s->sy%MAXSECTIONS)*MAXSECTIONS] = s; hm->activesections++; s->hmmod = hm; s->flags = TSF_DIRTY; hm->loadingsections+=1; } #ifndef SERVERONLY else if (s->loadstate == TSLS_LOADED && s->lightmap < 0) ; //it lost its lightmap. the main thread won't be drawing with it, nor do any loaders. //FIXME: might be used by tracelines on a worker (eg lightmap generation) #endif else if (s->loadstate != TSLS_LOADING0) { //this one is already active. sects[x + y*count] = NULL; continue; } s->loadstate = TSLS_LOADING1; sects[x + y*count] = s; numgen++; } Sys_UnlockMutex(com_resourcemutex); return numgen; } static hmsection_t *QDECL Terr_GenerateSection(heightmap_t *hm, int sx, int sy, qboolean scheduleload) { hmsection_t *s; hmcluster_t *cluster; int clusternum = (sx/MAXSECTIONS) + (sy/MAXSECTIONS)*MAXCLUSTERS; #ifdef LOADERTHREAD Sys_LockMutex(com_resourcemutex); #endif cluster = hm->cluster[clusternum]; if (!cluster) cluster = hm->cluster[clusternum] = Z_Malloc(sizeof(*cluster)); s = cluster->section[(sx%MAXSECTIONS) + (sy%MAXSECTIONS)*MAXSECTIONS]; if (!s) { s = Z_Malloc(sizeof(*s)); if (!s) { #ifdef LOADERTHREAD Sys_UnlockMutex(com_resourcemutex); #endif return NULL; } #ifndef SERVERONLY s->lightmap = -1; #endif #ifndef SERVERONLY s->numents = 0; #endif s->sx = sx; s->sy = sy; cluster->section[(sx%MAXSECTIONS) + (sy%MAXSECTIONS)*MAXSECTIONS] = s; hm->activesections++; s->hmmod = hm; s->flags = TSF_DIRTY; hm->loadingsections+=1; if (!scheduleload) { //no scheduling means that we're loading it NOW, on this thread. s->loadstate = TSLS_LOADING1; #ifdef LOADERTHREAD Sys_UnlockMutex(com_resourcemutex); #endif return s; } s->loadstate = TSLS_LOADING0; #ifdef LOADERTHREAD Sys_UnlockMutex(com_resourcemutex); #endif COM_AddWork(WG_LOADER, Terr_LoadSectionWorker, s, hm, sx, sy); return s; } if (!scheduleload) { if (s->loadstate == TSLS_LOADING0) s->loadstate = TSLS_LOADING1; else s = NULL; } #ifdef LOADERTHREAD Sys_UnlockMutex(com_resourcemutex); #endif return s; } //generates some water static void *QDECL Terr_GenerateWater(hmsection_t *s, float maxheight) { int i; struct hmwater_s *w; w = Z_Malloc(sizeof(*s->water)); w->next = s->water; s->water = w; Q_strncpyz(w->shadername, s->hmmod->defaultwatershader, sizeof(w->shadername)); w->simple = true; w->contentmask = FTECONTENTS_WATER; memset(w->holes, 0, sizeof(w->holes)); for (i = 0; i < 9*9; i++) w->heights[i] = maxheight; w->maxheight = w->minheight = maxheight; if (s->maxh_cull < w->maxheight) s->maxh_cull = w->maxheight; return w; } //embeds a mesh static void QDECL Terr_AddMesh(heightmap_t *hm, int loadflags, model_t *mod, const char *modelname, vec3_t epos, vec3_t axis[3], float scale) { #ifndef SERVERONLY struct hmentity_s *e, *f = NULL; hmsection_t *s; int min[2], max[2], coord[2]; int i; if (!mod) { if (modelname) mod = Mod_ForName(modelname, MLV_WARN); if (!mod) return; } if (!scale) scale = 1; if (mod->loadstate != MLS_LOADED) Con_DPrintf("Terr_AddMesh: model is not loaded yet\n"); //I do NOT like that this depends on the size of the model. if (axis[0][0] != 1 || axis[0][1] != 0 || axis[0][2] != 0 || axis[1][0] != 0 || axis[1][1] != 1 || axis[1][2] != 0 || axis[2][0] != 0 || axis[2][1] != 0 || axis[2][2] != 1) { min[0] = floor((epos[0]-mod->radius*scale) / hm->sectionsize) + CHUNKBIAS; min[1] = floor((epos[1]-mod->radius*scale) / hm->sectionsize) + CHUNKBIAS; min[0] = bound(hm->firstsegx, min[0], hm->maxsegx-1); min[1] = bound(hm->firstsegy, min[1], hm->maxsegy-1); max[0] = floor((epos[0]+mod->radius*scale) / hm->sectionsize) + CHUNKBIAS; max[1] = floor((epos[1]+mod->radius*scale) / hm->sectionsize) + CHUNKBIAS; max[0] = bound(hm->firstsegx, max[0], hm->maxsegx-1); max[1] = bound(hm->firstsegy, max[1], hm->maxsegy-1); } else { min[0] = floor((epos[0]+mod->mins[0]*scale) / hm->sectionsize) + CHUNKBIAS; min[1] = floor((epos[1]+mod->mins[1]*scale) / hm->sectionsize) + CHUNKBIAS; min[0] = bound(hm->firstsegx, min[0], hm->maxsegx-1); min[1] = bound(hm->firstsegy, min[1], hm->maxsegy-1); max[0] = floor((epos[0]+mod->maxs[0]*scale) / hm->sectionsize) + CHUNKBIAS; max[1] = floor((epos[1]+mod->maxs[1]*scale) / hm->sectionsize) + CHUNKBIAS; max[0] = bound(hm->firstsegx, max[0], hm->maxsegx-1); max[1] = bound(hm->firstsegy, max[1], hm->maxsegy-1); } Sys_LockMutex(hm->entitylock); //try to find the ent if it already exists (don't do dupes) for (e = hm->entities; e; e = e->next) { if (!e->refs) f = e; else { if (e->ent.origin[0] != epos[0] || e->ent.origin[1] != epos[1] || e->ent.origin[2] != epos[2]) continue; if (e->ent.model != mod || e->ent.scale != scale) continue; if (memcmp(axis, e->ent.axis, sizeof(e->ent.axis))) continue; break; //looks like a match. } } //allocate it if needed if (!e) { if (f) e = f; //can reuse a released one else { //allocate one e = Z_Malloc(sizeof(*e)); e->next = hm->entities; hm->entities = e; } #ifdef HEXEN2 e->ent.drawflags = SCALE_ORIGIN_ORIGIN; #endif e->ent.scale = scale; e->ent.playerindex = -1; e->ent.framestate.g[FS_REG].lerpweight[0] = 1; e->ent.topcolour = TOP_DEFAULT; e->ent.bottomcolour = BOTTOM_DEFAULT; e->ent.shaderRGBAf[0] = 1; e->ent.shaderRGBAf[1] = 1; e->ent.shaderRGBAf[2] = 1; e->ent.shaderRGBAf[3] = 1; VectorCopy(epos, e->ent.origin); memcpy(e->ent.axis, axis, sizeof(e->ent.axis)); e->ent.model = mod; } for (coord[0] = min[0]; coord[0] <= max[0]; coord[0]++) { for (coord[1] = min[1]; coord[1] <= max[1]; coord[1]++) { s = Terr_GetSection(hm, coord[0], coord[1], loadflags|TGS_ANYSTATE); if (!s) continue; //don't add pointless dupes for (i = 0; i < s->numents; i++) { if (s->ents[i] == e) break; } if (i < s->numents) continue; //FIXME: while technically correct, this causes issues with the v1 format. s->flags |= TSF_EDITED; //FIXME: race condition - main thread might be walking the entity list. //FIXME: even worse: the editor might be running through this routine adding/removing entities at the same time as the loader. if (s->maxents == s->numents) { s->maxents++; s->ents = realloc(s->ents, sizeof(*s->ents)*(s->maxents)); } s->ents[s->numents++] = e; e->refs++; } } Sys_UnlockMutex(hm->entitylock); #endif } static void *Terr_ReadV1(heightmap_t *hm, hmsection_t *s, void *ptr, int len) { #ifndef SERVERONLY dsmesh_v1_t *dm; float *colours; qbyte *lmstart; #endif dsection_v1_t *ds = ptr; int i; unsigned int flags = LittleLong(ds->flags); s->flags |= flags & ~(TSF_INTERNAL|TSF_HASWATER_V0); for (i = 0; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++) { s->heights[i] = LittleFloat(ds->heights[i]); } s->minh = ds->minh; s->maxh = ds->maxh; if (flags & TSF_HASWATER_V0) Terr_GenerateWater(s, ds->waterheight); memset(s->holes, 0, sizeof(s->holes)); for (i = 0; i < 8*8; i++) { int x = (i & 7); int y = (i>>3); int b = (1u<<(x>>1)) << ((y>>1)<<2); if (ds->holes & b) s->holes[y] |= 1u<texname[0], ds->texname[0], sizeof(s->texname[0])); Q_strncpyz(s->texname[1], ds->texname[1], sizeof(s->texname[1])); Q_strncpyz(s->texname[2], ds->texname[2], sizeof(s->texname[2])); Q_strncpyz(s->texname[3], ds->texname[3], sizeof(s->texname[3])); /*load in the mixture/lighting*/ lmstart = BZ_Malloc(SECTTEXSIZE*SECTTEXSIZE*4); memcpy(lmstart, ds->texmap, SECTTEXSIZE*SECTTEXSIZE*4); COM_AddWork(WG_MAIN, Terr_WorkerLoadedSectionLightmap, hm, lmstart, s->sx, s->sy); s->mesh.colors4f_array[0] = s->colours; if (flags & TSF_HASCOLOURS) { for (i = 0, colours = (float*)ptr; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++, colours+=4) { s->colours[i][0] = LittleFloat(colours[0]); s->colours[i][1] = LittleFloat(colours[1]); s->colours[i][2] = LittleFloat(colours[2]); s->colours[i][3] = LittleFloat(colours[3]); } ptr = colours; } else { for (i = 0; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++) { s->colours[i][0] = 1; s->colours[i][1] = 1; s->colours[i][2] = 1; s->colours[i][3] = 1; } } /*load any static ents*/ for (i = 0, dm = (dsmesh_v1_t*)ptr; i < ds->ents_num; i++, dm = (dsmesh_v1_t*)((qbyte*)dm + dm->size)) { vec3_t org; org[0] = dm->axisorg[3][0] + (s->sx-CHUNKBIAS)*hm->sectionsize; org[1] = dm->axisorg[3][1] + (s->sy-CHUNKBIAS)*hm->sectionsize; org[2] = dm->axisorg[3][2]; Terr_AddMesh(hm, TGS_NOLOAD, NULL, (char*)(dm + 1), org, dm->axisorg, dm->scale); } #endif return ptr; } struct terrstream_s { qbyte *buffer; int maxsize; int pos; }; //I really hope these get inlined properly. static int Terr_Read_SInt(struct terrstream_s *strm) { int val; strm->pos = (strm->pos + sizeof(val)-1) & ~(sizeof(val)-1); val = *(int*)(strm->buffer+strm->pos); strm->pos += sizeof(val); return LittleLong(val); } static qbyte Terr_Read_Byte(struct terrstream_s *strm) { qbyte val; val = *(qbyte*)(strm->buffer+strm->pos); strm->pos += sizeof(val); return val; } static float Terr_Read_Float(struct terrstream_s *strm) { float val; strm->pos = (strm->pos + sizeof(val)-1) & ~(sizeof(val)-1); val = *(float*)(strm->buffer+strm->pos); strm->pos += sizeof(val); return LittleFloat(val); } static char *Terr_Read_String(struct terrstream_s *strm, char *val, int maxlen) { int len = strlen(strm->buffer + strm->pos); maxlen = min(len, maxlen-1); //truncate memcpy(val, strm->buffer + strm->pos, maxlen); val[maxlen] = 0; strm->pos += len+1; return val; } #ifndef SERVERONLY static void Terr_Write_SInt(struct terrstream_s *strm, int val) { val = LittleLong(val); strm->pos = (strm->pos + sizeof(val)-1) & ~(sizeof(val)-1); *(int*)(strm->buffer+strm->pos) = val; strm->pos += sizeof(val); } static void Terr_Write_Byte(struct terrstream_s *strm, qbyte val) { *(qbyte*)(strm->buffer+strm->pos) = val; strm->pos += sizeof(val); } static void Terr_Write_Float(struct terrstream_s *strm, float val) { val = LittleFloat(val); strm->pos = (strm->pos + sizeof(val)-1) & ~(sizeof(val)-1); *(float*)(strm->buffer+strm->pos) = val; strm->pos += sizeof(val); } static void Terr_Write_String(struct terrstream_s *strm, char *val) { int len = strlen(val)+1; memcpy(strm->buffer + strm->pos, val, len); strm->pos += len; } static void Terr_TrimWater(hmsection_t *s) { int i; struct hmwater_s *w, **link; for (link = &s->water; (w = *link); ) { //one has a height above the terrain? for (i = 0; i < 9*9; i++) if (w->heights[i] > s->minh) break; if (i == 9*9) { *link = w->next; Z_Free(w); continue; } else link = &(*link)->next; } } static void Terr_SaveV2(heightmap_t *hm, hmsection_t *s, vfsfile_t *f, int sx, int sy) { qbyte buffer[65536], last, delta, *lm; struct terrstream_s strm = {buffer, sizeof(buffer), 0}; unsigned int flags = s->flags; int i, j, x, y; struct hmwater_s *w; flags &= ~(TSF_INTERNAL); flags &= ~(TSF_HASCOLOURS|TSF_HASHEIGHTS|TSF_HASSHADOW); for (i = 0; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++) { if (s->colours[i][0] != 1 || s->colours[i][1] != 1 || s->colours[i][2] != 1 || s->colours[i][3] != 1) { flags |= TSF_HASCOLOURS; break; } } for (i = 0; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++) { if (s->heights[i] != s->heights[0]) { flags |= TSF_HASHEIGHTS; break; } } lm = lightmap[s->lightmap]->lightmaps; lm += (s->lmy * HMLMSTRIDE + s->lmx) * lightmap_bytes; for (y = 0; y < SECTTEXSIZE; y++) { for (x = 0; x < SECTTEXSIZE; x++) { if (lm[x*4+3] != 255) { flags |= TSF_HASSHADOW; y = SECTTEXSIZE; break; } } lm += (HMLMSTRIDE)*lightmap_bytes; } //write the flags so the loader knows what to load Terr_Write_SInt(&strm, flags); //if heights are compressed, only the first is present. if (!(flags & TSF_HASHEIGHTS)) Terr_Write_Float(&strm, s->heights[0]); else { for (i = 0; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++) Terr_Write_Float(&strm, s->heights[i]); } for (i = 0; i < sizeof(s->holes); i++) Terr_Write_Byte(&strm, s->holes[i]); Terr_TrimWater(s); for (j = 0, w = s->water; w; j++) w = w->next; Terr_Write_SInt(&strm, j); for (i = 0, w = s->water; i < j; i++, w = w->next) { char *shadername = w->shader->name; int fl = 0; if (strcmp(shadername, hm->defaultwatershader)) fl |= 1; for (x = 0; x < 8; x++) if (w->holes[x]) break; fl |= ((x==8)?0:2); for (x = 0; x < 9*9; x++) if (w->heights[x] != w->heights[0]) break; fl |= ((x==9*9)?0:4); Terr_Write_SInt(&strm, fl); Terr_Write_SInt(&strm, w->contentmask); if (fl & 1) Terr_Write_String(&strm, shadername); if (fl & 2) { for (x = 0; x < 8; x++) Terr_Write_Byte(&strm, w->holes[x]); } if (fl & 4) { for (x = 0; x < 9*9; x++) Terr_Write_Float(&strm, w->heights[x]); } else Terr_Write_Float(&strm, w->heights[0]); } if (flags & TSF_HASCOLOURS) { //FIXME: bytes? channels? for (i = 0; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++) { Terr_Write_Float(&strm, s->colours[i][0]); Terr_Write_Float(&strm, s->colours[i][1]); Terr_Write_Float(&strm, s->colours[i][2]); Terr_Write_Float(&strm, s->colours[i][3]); } } for (j = 0; j < 4; j++) Terr_Write_String(&strm, s->texname[j]); for (j = 0; j < 4; j++) { if (j == 3) { //only write the channel if it has actual data if (!(flags & TSF_HASSHADOW)) continue; } else { //only write the data if there's actually a texture. //its not meant to be possible to delete a texture without deleting its data too. // if (!*s->texname[2-j]) continue; } //write the channel last = 0; lm = lightmap[s->lightmap]->lightmaps; lm += (s->lmy * HMLMSTRIDE + s->lmx) * lightmap_bytes; for (y = 0; y < SECTTEXSIZE; y++) { for (x = 0; x < SECTTEXSIZE; x++) { delta = lm[x*4+j] - last; last = lm[x*4+j]; Terr_Write_Byte(&strm, delta); } lm += (HMLMSTRIDE)*lightmap_bytes; } } Sys_LockMutex(hm->entitylock); Terr_Write_SInt(&strm, s->numents); for (i = 0; i < s->numents; i++) { unsigned int mf; //make sure we don't overflow. we should always be aligned at this point. if (strm.pos > strm.maxsize/2) { VFS_WRITE(f, strm.buffer, strm.pos); strm.pos = 0; } mf = 0; if (s->ents[i]->ent.scale != 1) mf |= TMF_SCALE; Terr_Write_SInt(&strm, mf); if (s->ents[i]->ent.model) Terr_Write_String(&strm, s->ents[i]->ent.model->name); else Terr_Write_String(&strm, "*invalid"); Terr_Write_Float(&strm, s->ents[i]->ent.origin[0]+(CHUNKBIAS-sx)*hm->sectionsize); Terr_Write_Float(&strm, s->ents[i]->ent.origin[1]+(CHUNKBIAS-sy)*hm->sectionsize); Terr_Write_Float(&strm, s->ents[i]->ent.origin[2]); Terr_Write_Float(&strm, s->ents[i]->ent.axis[0][0]); Terr_Write_Float(&strm, s->ents[i]->ent.axis[0][1]); Terr_Write_Float(&strm, s->ents[i]->ent.axis[0][2]); Terr_Write_Float(&strm, s->ents[i]->ent.axis[1][0]); Terr_Write_Float(&strm, s->ents[i]->ent.axis[1][1]); Terr_Write_Float(&strm, s->ents[i]->ent.axis[1][2]); Terr_Write_Float(&strm, s->ents[i]->ent.axis[2][0]); Terr_Write_Float(&strm, s->ents[i]->ent.axis[2][1]); Terr_Write_Float(&strm, s->ents[i]->ent.axis[2][2]); if (mf & TMF_SCALE) Terr_Write_Float(&strm, s->ents[i]->ent.scale); } Sys_UnlockMutex(hm->entitylock); //reset it in case the buffer is getting a little full strm.pos = (strm.pos + sizeof(int)-1) & ~(sizeof(int)-1); VFS_WRITE(f, strm.buffer, strm.pos); strm.pos = 0; } #ifndef SERVERONLY static void Terr_WorkerLoadedSectionLightmap(void *ctx, void *data, size_t a, size_t b) { heightmap_t *hm = ctx; hmsection_t *s = Terr_GetSection(hm, a, b, TGS_NOLOAD|TGS_ANYSTATE); qbyte *inlm = data; qbyte *outlm; int y; if (s) if (lightmap_bytes == 4 && Terr_InitLightmap(s, false)) { outlm = lightmap[s->lightmap]->lightmaps; outlm += (s->lmy * HMLMSTRIDE + s->lmx) * lightmap_bytes; for (y = 0; y < SECTTEXSIZE; y++) { memcpy(outlm, inlm, SECTTEXSIZE*4); inlm += SECTTEXSIZE*4; outlm += (HMLMSTRIDE)*lightmap_bytes; } } BZ_Free(data); } #endif #endif static void *Terr_ReadV2(heightmap_t *hm, hmsection_t *s, void *ptr, int len) { #ifndef SERVERONLY char modelname[MAX_QPATH]; qbyte last; int y; qboolean present; qbyte *lmstart = NULL, *lm, delta; #endif struct terrstream_s strm = {ptr, len, 0}; float f; int i, j, x; unsigned int flags = Terr_Read_SInt(&strm); s->flags |= flags & ~TSF_INTERNAL; if (flags & TSF_HASHEIGHTS) { s->minh = s->maxh = s->heights[0] = Terr_Read_Float(&strm); for (i = 1; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++) { f = Terr_Read_Float(&strm); if (s->minh > f) s->minh = f; if (s->maxh < f) s->maxh = f; s->heights[i] = f; } } else { s->minh = s->maxh = f = Terr_Read_Float(&strm); for (i = 0; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++) s->heights[i] = f; } for (i = 0; i < sizeof(s->holes); i++) s->holes[i] = Terr_Read_Byte(&strm); j = Terr_Read_SInt(&strm); for (i = 0; i < j; i++) { struct hmwater_s *w = Z_Malloc(sizeof(*w)); int fl = Terr_Read_SInt(&strm); w->next = s->water; s->water = w; w->simple = true; w->contentmask = Terr_Read_SInt(&strm); if (fl & 1) Terr_Read_String(&strm, w->shadername, sizeof(w->shadername)); else Q_strncpyz(w->shadername, hm->defaultwatershader, sizeof(w->shadername)); if (fl & 2) { for (x = 0; x < 8; x++) w->holes[i] = Terr_Read_Byte(&strm); w->simple = false; } if (fl & 4) { for (x = 0; x < 9*9; x++) { w->heights[x] = Terr_Read_Float(&strm); } w->simple = false; } else { //all heights the same can be used as a way to compress the data w->minheight = w->maxheight = Terr_Read_Float(&strm); for (x = 0; x < 9*9; x++) w->heights[x] = w->minheight = w->maxheight; } } //dedicated server can stop reading here. #ifndef SERVERONLY if (flags & TSF_HASCOLOURS) { for (i = 0; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++) { s->colours[i][0] = Terr_Read_Float(&strm); s->colours[i][1] = Terr_Read_Float(&strm); s->colours[i][2] = Terr_Read_Float(&strm); s->colours[i][3] = Terr_Read_Float(&strm); } } else { for (i = 0; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++) { s->colours[i][0] = 1; s->colours[i][1] = 1; s->colours[i][2] = 1; s->colours[i][3] = 1; } } for (j = 0; j < 4; j++) Terr_Read_String(&strm, s->texname[j], sizeof(s->texname[j])); for (j = 0; j < 4; j++) { if (j == 3) present = !!(flags & TSF_HASSHADOW); else present = !!(*s->texname[2-j]); //should be able to skip this if no shadows or textures if (!lmstart) lmstart = BZ_Malloc(SECTTEXSIZE*SECTTEXSIZE*4); if (present) { //read the channel last = 0; lm = lmstart; for (y = 0; y < SECTTEXSIZE; y++) { for (x = 0; x < SECTTEXSIZE; x++) { delta = Terr_Read_Byte(&strm); last = (last+delta)&0xff; lm[x*4+j] = last; } lm += x*4; } } else { last = ((j==3)?255:0); lm = lmstart; for (y = 0; y < SECTTEXSIZE; y++) { for (x = 0; x < SECTTEXSIZE; x++) lm[x*4+j] = last; lm += x*4; } } } if (lmstart) COM_AddWork(WG_MAIN, Terr_WorkerLoadedSectionLightmap, hm, lmstart, s->sx, s->sy); /*load any static ents*/ j = Terr_Read_SInt(&strm); for (i = 0; i < j; i++) { vec3_t axis[3]; vec3_t org; unsigned int mf; model_t *mod; float scale; mf = Terr_Read_SInt(&strm); mod = Mod_FindName(Terr_Read_String(&strm, modelname, sizeof(modelname))); org[0] = Terr_Read_Float(&strm); org[1] = Terr_Read_Float(&strm); org[2] = Terr_Read_Float(&strm); axis[0][0] = Terr_Read_Float(&strm); axis[0][1] = Terr_Read_Float(&strm); axis[0][2] = Terr_Read_Float(&strm); axis[1][0] = Terr_Read_Float(&strm); axis[1][1] = Terr_Read_Float(&strm); axis[1][2] = Terr_Read_Float(&strm); axis[2][0] = Terr_Read_Float(&strm); axis[2][1] = Terr_Read_Float(&strm); axis[2][2] = Terr_Read_Float(&strm); scale = (mf&TMF_SCALE)?Terr_Read_Float(&strm):1; org[0] += (s->sx-CHUNKBIAS)*hm->sectionsize; org[1] += (s->sy-CHUNKBIAS)*hm->sectionsize; Terr_AddMesh(hm, TGS_NOLOAD, mod, NULL, org, axis, scale); } #endif return ptr; } static void Terr_ClearSection(hmsection_t *s) { struct hmwater_s *w; int i; Sys_LockMutex(s->hmmod->entitylock); for (i = 0; i < s->numents; i++) s->ents[i]->refs-=1; s->numents = 0; Sys_UnlockMutex(s->hmmod->entitylock); while(s->water) { w = s->water; s->water = w->next; Z_Free(w); } } static void Terr_GenerateDefault(heightmap_t *hm, hmsection_t *s) { int i; memset(s->holes, 0, sizeof(s->holes)); #ifndef SERVERONLY Q_strncpyz(s->texname[0], "", sizeof(s->texname[0])); Q_strncpyz(s->texname[1], "", sizeof(s->texname[1])); Q_strncpyz(s->texname[2], "", sizeof(s->texname[2])); Q_strncpyz(s->texname[3], hm->defaultgroundtexture, sizeof(s->texname[3])); if (s->lightmap >= 0) { int j; qbyte *lm; lm = lightmap[s->lightmap]->lightmaps; lm += (s->lmy * HMLMSTRIDE + s->lmx) * lightmap_bytes; for (i = 0; i < SECTTEXSIZE; i++) { for (j = 0; j < SECTTEXSIZE; j++) { lm[j*4+0] = 0; lm[j*4+0] = 0; lm[j*4+0] = 0; lm[j*4+3] = 255; } lm += (HMLMSTRIDE)*lightmap_bytes; } lightmap[s->lightmap]->modified = true; lightmap[s->lightmap]->rectchange.l = 0; lightmap[s->lightmap]->rectchange.t = 0; lightmap[s->lightmap]->rectchange.r = HMLMSTRIDE; lightmap[s->lightmap]->rectchange.b = HMLMSTRIDE; } for (i = 0; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++) { s->colours[i][0] = 1; s->colours[i][1] = 1; s->colours[i][2] = 1; s->colours[i][3] = 1; } s->mesh.colors4f_array[0] = s->colours; #endif for (i = 0; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++) s->heights[i] = hm->defaultgroundheight; if (hm->defaultwaterheight > hm->defaultgroundheight) Terr_GenerateWater(s, hm->defaultwaterheight); #if 0//def DEBUG void *f; if (lightmap_bytes == 4 && lightmap_bgra && FS_LoadFile(va("maps/%s/splatt.png", hm->path), &f) != (qofs_t)-1) { //temp int vx, vy; int x, y; extern qbyte *Read32BitImageFile(qbyte *buf, int len, int *width, int *height, qboolean *hasalpha, const char *fname); int sw, sh; qboolean hasalpha; unsigned char *splatter = Read32BitImageFile(f, com_filesize, &sw, &sh, &hasalpha, "splattermap"); if (splatter) { lm = lightmap[s->lightmap]->lightmaps; lm += (s->lmy * HMLMSTRIDE + s->lmx) * lightmap_bytes; for (vx = 0; vx < SECTTEXSIZE; vx++) { x = sw * (((float)sy) + ((float)vx / (SECTTEXSIZE-1))) / hm->numsegsx; if (x > sw-1) x = sw-1; for (vy = 0; vy < SECTTEXSIZE; vy++) { y = sh * (((float)sx) + ((float)vy / (SECTTEXSIZE-1))) / hm->numsegsy; if (y > sh-1) y = sh-1; lm[2] = splatter[(y + x*sh)*4+0]; lm[1] = splatter[(y + x*sh)*4+1]; lm[0] = splatter[(y + x*sh)*4+2]; lm[3] = splatter[(y + x*sh)*4+3]; lm += 4; } lm += (HMLMSTRIDE - SECTTEXSIZE)*lightmap_bytes; } BZ_Free(splatter); lightmap[s->lightmap]->modified = true; lightmap[s->lightmap]->rectchange.l = 0; lightmap[s->lightmap]->rectchange.t = 0; lightmap[s->lightmap]->rectchange.w = HMLMSTRIDE; lightmap[s->lightmap]->rectchange.h = HMLMSTRIDE; } FS_FreeFile(f); } if (lightmap_bytes == 4 && lightmap_bgra && !qofs_Error(FS_LoadFile(va("maps/%s/heightmap.png", hm->path), &f))) { //temp int vx, vy; int x, y; extern qbyte *Read32BitImageFile(qbyte *buf, int len, int *width, int *height, qboolean *hasalpha, const char *fname); int sw, sh; float *h; qboolean hasalpha; unsigned char *hmimage = Read32BitImageFile(f, com_filesize, &sw, &sh, &hasalpha, "heightmap"); if (hmimage) { h = s->heights; for (vx = 0; vx < SECTHEIGHTSIZE; vx++) { x = sw * (((float)sy) + ((float)vx / (SECTHEIGHTSIZE-1))) / hm->numsegsx; if (x > sw-1) x = sw-1; for (vy = 0; vy < SECTHEIGHTSIZE; vy++) { y = sh * (((float)sx) + ((float)vy / (SECTHEIGHTSIZE-1))) / hm->numsegsy; if (y > sh-1) y = sh-1; *h = 0; *h += hmimage[(y + x*sh)*4+0]; *h += hmimage[(y + x*sh)*4+1]<<8; *h += hmimage[(y + x*sh)*4+2]<<16; *h *= 4.0f/(1<<16); h++; } } BZ_Free(hmimage); } FS_FreeFile(f); } #endif } static void Terr_WorkerLoadedSection(void *ctx, void *data, size_t a, size_t b) { hmsection_t *s = ctx; validatelinks(&s->hmmod->recycle); Terr_LoadSectionTextures(s); validatelinks2(&s->hmmod->recycle, &s->recycle); InsertLinkBefore(&s->recycle, &s->hmmod->recycle); validatelinks(&s->hmmod->recycle); s->hmmod->loadingsections-=1; s->flags &= ~TSF_EDITED; s->loadstate = TSLS_LOADED; s->timestamp = realtime; validatelinks(&s->hmmod->recycle); } static void Terr_WorkerFailedSection(void *ctx, void *data, size_t a, size_t b) { hmsection_t *s = ctx; Terr_WorkerLoadedSection(ctx, data, a, b); s->flags &= ~TSF_EDITED; s->loadstate = TSLS_FAILED; validatelinks(&s->hmmod->recycle); } void QDECL Terr_FinishedSection(hmsection_t *s, qboolean success) { s->flags &= ~TSF_EDITED; //its just been loaded (and was probably edited by the loader), make sure it doesn't get saved or whatever s->loadstate = TSLS_LOADING2; if (!success) COM_AddWork(WG_MAIN, Terr_WorkerFailedSection, s, NULL, s->sx, s->sy); else COM_AddWork(WG_MAIN, Terr_WorkerLoadedSection, s, NULL, s->sx, s->sy); } static hmsection_t *Terr_ReadSection(heightmap_t *hm, hmsection_t *s, int ver, void *filebase, unsigned int filelen) { qboolean failed = false; void *ptr = filebase; if (ptr && ver == 1) Terr_ReadV1(hm, s, ptr, filelen); else if (ptr && ver == 2) Terr_ReadV2(hm, s, ptr, filelen); else { // s->flags |= TSF_RELIGHT; Terr_GenerateDefault(hm, s); failed = true; } Terr_FinishedSection(s, !failed); return s; } #ifndef SERVERONLY qboolean Terr_DownloadedSection(char *fname) { /* qofs_t len; dsection_t *fileptr; int x, y; heightmap_t *hm; int ver = 0; if (!cl.worldmodel) return false; hm = cl.worldmodel->terrain; if (Terr_IsSectionFName(hm, fname, &x, &y)) { fileptr = NULL; len = FS_LoadFile(fname, (void**)&fileptr); if (!qofs_Error(len) && len >= sizeof(*fileptr) && fileptr->magic == SECTION_MAGIC) Terr_ReadSection(hm, ver, x, y, fileptr+1, len - sizeof(*fileptr)); else Terr_ReadSection(hm, ver, x, y, NULL, 0); if (fileptr) FS_FreeFile(fileptr); return true; } */ return false; } #endif #ifndef SERVERONLY static void Terr_LoadSection(heightmap_t *hm, hmsection_t *s, int sx, int sy, unsigned int flags) { //when using networked terrain, the client will never load a section from disk, but will only load it from the server //one section at a time. if (mod_terrain_networked.ival && !sv.state) { char fname[MAX_QPATH]; if (flags & TGS_NODOWNLOAD) return; //try to download it now... if (!cl.downloadlist) CL_CheckOrEnqueDownloadFile(Terr_DiskSectionName(hm, sx, sy, fname, sizeof(fname)), Terr_TempDiskSectionName(hm, sx, sy), DLLF_OVERWRITE|DLLF_TEMPORARY); return; } if (!s) { Terr_GenerateSection(hm, sx, sy, true); } } #endif static void Terr_LoadSectionWorker(void *ctx, void *data, size_t a, size_t b) { heightmap_t *hm = data; hmsection_t *s = ctx; int sx = a; int sy = b; void *diskimage; qofs_t len; char fname[MAX_QPATH]; //already processed, or not otherwise valid if (s->loadstate != TSLS_LOADING0) return; #if SECTIONSPERBLOCK > 1 len = FS_LoadFile(Terr_DiskBlockName(hm, sx, sy, fname, sizeof(fname)), (void**)&diskimage); if (!qofs_Error(len)) { int offset; int x, y; int ver; dblock_t *block = diskimage; if (block->magic != SECTION_MAGIC || !(block->ver & 0x80000000)) { s = Terr_GenerateSection(hm, sx, sy, false); //give it a dummy so we don't constantly hit the disk Terr_ReadSection(hm, s, 0, NULL, 0); } else { hmsection_t *sects[SECTIONSPERBLOCK*SECTIONSPERBLOCK]; sx&=~(SECTIONSPERBLOCK-1); sy&=~(SECTIONSPERBLOCK-1); ver = block->ver & ~0x80000000; if (Terr_GenerateSections(hm, sx, sy, SECTIONSPERBLOCK, sects)) { for (y = 0; y < SECTIONSPERBLOCK; y++) for (x = 0; x < SECTIONSPERBLOCK; x++) { //noload avoids recursion. s = sects[x+y*SECTIONSPERBLOCK]; if (s) { offset = block->offset[x + y*SECTIONSPERBLOCK]; if (!offset) Terr_ReadSection(hm, s, ver, NULL, 0); //no data in the file for this section else Terr_ReadSection(hm, s, ver, (char*)diskimage + offset, len - offset); } } } } FS_FreeFile(diskimage); return; } #endif //legacy one-section-per-file format. len = FS_LoadFile(Terr_DiskSectionName(hm, sx, sy, fname, sizeof(fname)), (void**)&diskimage); if (!qofs_Error(len)) { dsection_t *h = diskimage; if (len >= sizeof(*h) && h->magic == SECTION_MAGIC) { s = Terr_GenerateSection(hm, sx, sy, false); if (!s) return; Terr_ReadSection(hm, s, h->ver, h+1, len-sizeof(*h)); FS_FreeFile(diskimage); return; } if (diskimage) FS_FreeFile(diskimage); } if (terrainfuncs.AutogenerateSection && terrainfuncs.AutogenerateSection(hm, sx, sy, 0)) return; s = Terr_GenerateSection(hm, sx, sy, false); if (!s) return; //generate a dummy one Terr_ReadSection(hm, s, 0, NULL, 0); } #ifndef SERVERONLY static void Terr_SaveV1(heightmap_t *hm, hmsection_t *s, vfsfile_t *f, int sx, int sy) { int i; dsmesh_v1_t dm; qbyte *lm; dsection_v1_t ds; vec4_t dcolours[SECTHEIGHTSIZE*SECTHEIGHTSIZE]; int nothing = 0; struct hmwater_s *w = s->water; memset(&ds, 0, sizeof(ds)); memset(&dm, 0, sizeof(dm)); //mask off the flags which are only valid in memory ds.flags = s->flags & ~(TSF_INTERNAL|TSF_HASWATER_V0); //kill the haswater flag if its entirely above any possible water anyway. if (w) ds.flags |= TSF_HASWATER_V0; ds.flags &= ~TSF_HASCOLOURS; //recalculated Q_strncpyz(ds.texname[0], s->texname[0], sizeof(ds.texname[0])); Q_strncpyz(ds.texname[1], s->texname[1], sizeof(ds.texname[1])); Q_strncpyz(ds.texname[2], s->texname[2], sizeof(ds.texname[2])); Q_strncpyz(ds.texname[3], s->texname[3], sizeof(ds.texname[3])); for (i = 0; i < 8*8; i++) { int x = (i & 7); int y = (i>>3); int b = (1u<<(x>>1)) << ((y>>1)<<2); if (s->holes[y] & (1u<holes, 0, sizeof(s->holes)); for (i = 0; i < 8*8; i++) { int x = (i & 7); int y = (i>>3); int b = (1u<<(x>>1)) << ((y>>1)<<2); if (ds.holes & b) s->holes[y] |= 1u<flags |= TSF_DIRTY; lm = lightmap[s->lightmap]->lightmaps; lm += (s->lmy * HMLMSTRIDE + s->lmx) * lightmap_bytes; for (i = 0; i < SECTTEXSIZE; i++) { memcpy(ds.texmap + i, lm, sizeof(ds.texmap[0])); lm += (HMLMSTRIDE)*lightmap_bytes; } for (i = 0; i < SECTHEIGHTSIZE*SECTHEIGHTSIZE; i++) { ds.heights[i] = LittleFloat(s->heights[i]); if (s->colours[i][0] != 1 || s->colours[i][1] != 1 || s->colours[i][2] != 1 || s->colours[i][3] != 1) { ds.flags |= TSF_HASCOLOURS; dcolours[i][0] = LittleFloat(s->colours[i][0]); dcolours[i][1] = LittleFloat(s->colours[i][1]); dcolours[i][2] = LittleFloat(s->colours[i][2]); dcolours[i][3] = LittleFloat(s->colours[i][3]); } else { dcolours[i][0] = dcolours[i][1] = dcolours[i][2] = dcolours[i][3] = LittleFloat(1); } } ds.waterheight = w?w->heights[4*8+4]:s->minh; ds.minh = s->minh; ds.maxh = s->maxh; Sys_LockMutex(hm->entitylock); ds.ents_num = s->numents; VFS_WRITE(f, &ds, sizeof(ds)); if (ds.flags & TSF_HASCOLOURS) VFS_WRITE(f, dcolours, sizeof(dcolours)); for (i = 0; i < s->numents; i++) { int pad; dm.scale = s->ents[i]->ent.scale; VectorCopy(s->ents[i]->ent.axis[0], dm.axisorg[0]); VectorCopy(s->ents[i]->ent.axis[1], dm.axisorg[1]); VectorCopy(s->ents[i]->ent.axis[2], dm.axisorg[2]); VectorCopy(s->ents[i]->ent.origin, dm.axisorg[3]); dm.axisorg[3][0] += (CHUNKBIAS-sx)*hm->sectionsize; dm.axisorg[3][1] += (CHUNKBIAS-sy)*hm->sectionsize; dm.size = sizeof(dm) + strlen(s->ents[i]->ent.model->name) + 1; if (dm.size & 3) pad = 4 - (dm.size&3); else pad = 0; dm.size += pad; VFS_WRITE(f, &dm, sizeof(dm)); VFS_WRITE(f, s->ents[i]->ent.model->name, strlen(s->ents[i]->ent.model->name)+1); if (pad) VFS_WRITE(f, ¬hing, pad); } Sys_UnlockMutex(hm->entitylock); } static void Terr_Save(heightmap_t *hm, hmsection_t *s, vfsfile_t *f, int sx, int sy, int ver) { if (ver == 1) Terr_SaveV1(hm, s, f, sx, sy); else if (ver == 2) Terr_SaveV2(hm, s, f, sx, sy); } #endif //doesn't clear edited/dirty flags or anything static qboolean Terr_SaveSection(heightmap_t *hm, hmsection_t *s, int sx, int sy, qboolean blocksave) { #ifdef SERVERONLY return true; #else vfsfile_t *f; char fname[MAX_QPATH]; int x, y; int writever = mod_terrain_savever.ival; if (!writever) writever = SECTION_VER_DEFAULT; //if its invalid or doesn't contain all the data... if (!s || s->lightmap < 0) return true; #if SECTIONSPERBLOCK > 1 if (blocksave) { dblock_t dbh; sx = sx & ~(SECTIONSPERBLOCK-1); sy = sy & ~(SECTIONSPERBLOCK-1); //make sure its loaded before we replace the file for (y = 0; y < SECTIONSPERBLOCK; y++) { for (x = 0; x < SECTIONSPERBLOCK; x++) { s = Terr_GetSection(hm, sx+x, sy+y, TGS_WAITLOAD|TGS_NODOWNLOAD); if (s) s->flags |= TSF_EDITED; //stop them from getting reused for something else. } } //make sure all lightmap info was loaded. COM_WorkerFullSync(); Terr_DiskBlockName(hm, sx, sy, fname, sizeof(fname)); FS_CreatePath(fname, FS_GAMEONLY); f = FS_OpenVFS(fname, "wb", FS_GAMEONLY); if (!f) { Con_Printf("Failed to open %s\n", fname); return false; } memset(&dbh, 0, sizeof(dbh)); dbh.magic = LittleLong(SECTION_MAGIC); dbh.ver = LittleLong(writever | 0x80000000); VFS_WRITE(f, &dbh, sizeof(dbh)); for (y = 0; y < SECTIONSPERBLOCK; y++) { for (x = 0; x < SECTIONSPERBLOCK; x++) { s = Terr_GetSection(hm, sx+x, sy+y, TGS_WAITLOAD|TGS_NODOWNLOAD); if (s && s->loadstate == TSLS_LOADED && Terr_InitLightmap(s, false)) { dbh.offset[y*SECTIONSPERBLOCK + x] = VFS_TELL(f); Terr_Save(hm, s, f, sx+x, sy+y, writever); s->flags &= ~TSF_EDITED; } else dbh.offset[y*SECTIONSPERBLOCK + x] = 0; } } VFS_SEEK(f, 0); VFS_WRITE(f, &dbh, sizeof(dbh)); VFS_CLOSE(f); FS_FlushFSHashWritten(fname); } else #endif { dsection_t dsh; Terr_DiskSectionName(hm, sx, sy, fname, sizeof(fname)); // if (s && (s->flags & (TSF_EDITED|TSF_FAILEDLOAD)) != TSF_FAILEDLOAD) // return FS_Remove(fname, FS_GAMEONLY); //delete the file if the section got reverted to default, and wasn't later modified. //make sure all lightmap info was loaded. COM_WorkerFullSync(); FS_CreatePath(fname, FS_GAMEONLY); f = FS_OpenVFS(fname, "wb", FS_GAMEONLY); if (!f) { Con_Printf("Failed to open %s\n", fname); return false; } memset(&dsh, 0, sizeof(dsh)); dsh.magic = SECTION_MAGIC; dsh.ver = writever; VFS_WRITE(f, &dsh, sizeof(dsh)); Terr_Save(hm, s, f, sx, sy, writever); VFS_CLOSE(f); FS_FlushFSHashWritten(fname); } return true; #endif } /*convienience function*/ static hmsection_t *QDECL Terr_GetSection(heightmap_t *hm, int x, int y, unsigned int flags) { hmcluster_t *cluster; hmsection_t *section; int cx = x / MAXSECTIONS; int cy = y / MAXSECTIONS; int sx = x & (MAXSECTIONS-1); int sy = y & (MAXSECTIONS-1); cluster = hm->cluster[cx + cy*MAXCLUSTERS]; if (!cluster) section = NULL; else section = cluster->section[sx + sy*MAXSECTIONS]; if (!section) { if (flags & (TGS_LAZYLOAD|TGS_TRYLOAD|TGS_WAITLOAD)) { if ((flags & TGS_LAZYLOAD) && hm->loadingsections) return NULL; section = Terr_GenerateSection(hm, x, y, true); } } #ifndef SERVERONLY //when using networked terrain, the client will never load a section from disk, but only loading it from the server //this means we need to send a new request to download the section if it was flagged as modified. if (!(flags & TGS_NODOWNLOAD)) if (section && (section->flags & TSF_NOTIFY) && mod_terrain_networked.ival && !sv.state) { //try to download it now... if (!cl.downloadlist) { char fname[MAX_QPATH]; CL_CheckOrEnqueDownloadFile(Terr_DiskSectionName(hm, x, y, fname, sizeof(fname)), Terr_TempDiskSectionName(hm, x, y), DLLF_OVERWRITE|DLLF_TEMPORARY); section->flags &= ~TSF_NOTIFY; } } #endif if (section && section->loadstate != TSLS_LOADED) { //wait for it to load if we're meant to be doing that. if (flags & TGS_WAITLOAD) { //wait for it to load if we're meant to be doing that. if (section->loadstate == TSLS_LOADING0) COM_WorkerPartialSync(section, §ion->loadstate, TSLS_LOADING0); if (section->loadstate == TSLS_LOADING1) COM_WorkerPartialSync(section, §ion->loadstate, TSLS_LOADING1); if (section->loadstate == TSLS_LOADING2) COM_MainThreadFlush(); //make sure any associated lightmaps also got read+handled } //if it failed, generate a default (for editing) if (section->loadstate == TSLS_FAILED && ((flags & TGS_DEFAULTONFAIL) || hm->forcedefault)) { section->flags = (section->flags & ~TSF_EDITED); section->loadstate = TSLS_LOADED; Terr_ClearSection(section); Terr_GenerateDefault(hm, section); } if ((section->loadstate != TSLS_LOADED) && !(flags & TGS_ANYSTATE)) section = NULL; } if (section) section->timestamp = realtime; return section; } /*save all currently loaded sections*/ int Heightmap_Save(heightmap_t *hm) { hmsection_t *s; int x, y; int sectionssaved = 0; for (x = hm->firstsegx; x < hm->maxsegx; x++) { for (y = hm->firstsegy; y < hm->maxsegy; y++) { s = Terr_GetSection(hm, x, y, TGS_NOLOAD); if (!s) continue; if (s->flags & TSF_EDITED) { /* //make sure all the parts are loaded before trying to write them, so we don't try reading partial files, which would be bad, mmkay? for (sy = y&~(SECTIONSPERBLOCK-1); sy < y+SECTIONSPERBLOCK && sy < hm->maxsegy; sy++) { for (sx = x&~(SECTIONSPERBLOCK-1); sx < x+SECTIONSPERBLOCK && sx < hm->maxsegx; sx++) { os = Terr_GetSection(hm, sx, sy, TGS_WAITLOAD|TGS_NODOWNLOAD|TGS_NORENDER); if (os) os->flags |= TSF_EDITED; } } */ if (Terr_SaveSection(hm, s, x, y, true)) { s->flags &= ~TSF_EDITED; sectionssaved++; } } } } return sectionssaved; } #ifndef CLIENTONLY //on servers, we can get requests to download current map sections. if so, give them it. qboolean Terrain_LocateSection(const char *name, flocation_t *loc) { heightmap_t *hm; hmsection_t *s; int x, y; char fname[MAX_QPATH]; //reject if its not in maps if (Q_strncasecmp(name, "maps/", 5)) return false; if (!sv.world.worldmodel) return false; hm = sv.world.worldmodel->terrain; if (!Terr_IsSectionFName(hm, name, &x, &y)) return false; //verify that its valid if (strcmp(name, Terr_DiskSectionName(hm, x, y, fname, sizeof(fname)))) return false; s = Terr_GetSection(hm, x, y, TGS_NOLOAD); if (!s || !(s->flags & TSF_EDITED)) return false; //its not been edited, might as well just use the regular file if (!Terr_SaveSection(hm, s, x, y, false)) return false; return FS_FLocateFile(name, FSLF_IFFOUND, loc); } #endif void Terr_DestroySection(heightmap_t *hm, hmsection_t *s, qboolean lightmapreusable) { if (s && s->loadstate == TSLS_LOADING0) COM_WorkerPartialSync(s, &s->loadstate, TSLS_LOADING0); if (s && s->loadstate == TSLS_LOADING1) COM_WorkerPartialSync(s, &s->loadstate, TSLS_LOADING1); if (s && s->loadstate == TSLS_LOADING2) COM_MainThreadFlush(); //make sure any associated lightmaps also got read+handled if (!s || s->loadstate < TSLS_LOADING2) return; validatelinks(&hm->recycle); RemoveLink(&s->recycle); validatelinks(&s->hmmod->recycle); Terr_ClearSection(s); #ifndef SERVERONLY if (s->lightmap >= 0) { struct lmsect_s *lms; if (lightmapreusable) { lms = BZ_Malloc(sizeof(*lms)); lms->lm = s->lightmap; lms->x = s->lmx; lms->y = s->lmy; lms->next = hm->unusedlmsects; hm->unusedlmsects = lms; hm->numunusedlmsects++; } hm->numusedlmsects--; } if (hm->relight == s) hm->relight = NULL; #ifdef GLQUAKE if (qrenderer == QR_OPENGL) { if (qglDeleteBuffersARB) { qglDeleteBuffersARB(1, &s->vbo.coord.gl.vbo); s->vbo.coord.gl.vbo = 0; qglDeleteBuffersARB(1, &s->vbo.indicies.gl.vbo); s->vbo.indicies.gl.vbo = 0; } } else #endif { BE_ClearVBO(&s->vbo, true); } Z_Free(s->ents); Z_Free(s->mesh.xyz_array); Z_Free(s->mesh.indexes); #endif Z_Free(s); hm->activesections--; validatelinks(&hm->recycle); } #ifndef SERVERONLY //dedicated servers do not support editing. no lightmap info causes problems. //when a terrain section has the notify flag set on the server, the server needs to go through and set out notifications to replicate it to the various clients //so the clients know to re-download the section. static void Terr_DoEditNotify(heightmap_t *hm) { int i; char *cmd; hmsection_t *s; link_t *ln = &hm->recycle; if (!sv.state) return; for (i = 0; i < sv.allocated_client_slots; i++) { if (svs.clients[i].state >= cs_connected && svs.clients[i].netchan.remote_address.type != NA_LOOPBACK) { if (svs.clients[i].backbuf.cursize) return; } } for (ln = &hm->recycle; ln->next != &hm->recycle; ln = &s->recycle) { s = (hmsection_t*)ln->next; if (s->flags & TSF_NOTIFY) { s->flags &= ~TSF_NOTIFY; cmd = va("mod_terrain_reload %s %i %i\n", hm->path, s->sx - CHUNKBIAS, s->sy - CHUNKBIAS); for (i = 0; i < sv.allocated_client_slots; i++) { if (svs.clients[i].state >= cs_connected && svs.clients[i].netchan.remote_address.type != NA_LOOPBACK) { SV_StuffcmdToClient(&svs.clients[i], cmd); } } return; } } } #ifndef SERVERONLY //garbage collect the oldest section, to make space for another static qboolean Terr_Collect(heightmap_t *hm) { hmcluster_t *c; hmsection_t *s; int cx, cy; int sx, sy; float timeout = realtime-2; //must used no later than 2 seconds in the past link_t *ln = &hm->recycle; validatelinks(&hm->recycle); for (ln = &hm->recycle; ln->next != &hm->recycle; ) { s = (hmsection_t*)ln->next; if ((s->flags & TSF_EDITED) || s->loadstate <= TSLS_LOADING2 || s->timestamp > timeout) ln = &s->recycle; else { cx = s->sx/MAXSECTIONS; cy = s->sy/MAXSECTIONS; c = hm->cluster[cx + cy*MAXCLUSTERS]; sx = s->sx & (MAXSECTIONS-1); sy = s->sy & (MAXSECTIONS-1); if (c->section[sx+sy*MAXSECTIONS] != s) Sys_Error("invalid section collection"); c->section[sx+sy*MAXSECTIONS] = NULL; #if 0 if (hm->relight == s) hm->relight = NULL; RemoveLink(&s->recycle); InsertLinkAfter(&s->recycle, &hm->collected); hm->activesections--; #else Terr_DestroySection(hm, s, true); #endif validatelinks(&hm->recycle); return true; } } return false; } #endif #endif /*purge all sections, but not root lightmaps only are purged whenever the client rudely kills lightmaps (purges all lightmaps on map changes, to cope with models/maps potentially being unloaded) we'll reload those when its next seen. (lightmaps will already have been destroyed, so no poking them) */ void Terr_PurgeTerrainModel(model_t *mod, qboolean lightmapsonly, qboolean lightmapreusable) { heightmap_t *hm = mod->terrain; hmcluster_t *c; hmsection_t *s; int cx, cy; int sx, sy; COM_WorkerFullSync(); //should probably be inside the caller or something. make sure there's no loaders still loading lightmaps when lightmaps are going to be nuked. validatelinks(&hm->recycle); // Con_Printf("PrePurge: %i lm chunks used, %i unused\n", hm->numusedlmsects, hm->numunusedlmsects); for (cy = 0; cy < MAXCLUSTERS; cy++) for (cx = 0; cx < MAXCLUSTERS; cx++) { int numremaining = 0; c = hm->cluster[cx + cy*MAXCLUSTERS]; if (!c) continue; for (sy = 0; sy < MAXSECTIONS; sy++) for (sx = 0; sx < MAXSECTIONS; sx++) { s = c->section[sx + sy*MAXSECTIONS]; if (!s) { } else if (lightmapsonly) { numremaining++; #ifndef SERVERONLY s->lightmap = -1; #endif } else { c->section[sx+sy*MAXSECTIONS] = NULL; validatelinks(&hm->recycle); Terr_DestroySection(hm, s, lightmapreusable); validatelinks(&hm->recycle); } } if (!numremaining) { hm->cluster[cx + cy*MAXSECTIONS] = NULL; BZ_Free(c); validatelinks(&hm->recycle); } } validatelinks(&hm->recycle); #ifndef SERVERONLY if (!lightmapreusable) { while (hm->unusedlmsects) { struct lmsect_s *lms; lms = hm->unusedlmsects; hm->unusedlmsects = lms->next; BZ_Free(lms); hm->numunusedlmsects--; } hm->recalculatebrushlighting = true; BZ_Free(hm->brushlmremaps); hm->brushlmremaps = NULL; hm->brushmaxlms = 0; } #endif validatelinks(&hm->recycle); // Con_Printf("PostPurge: %i lm chunks used, %i unused\n", hm->numusedlmsects, hm->numunusedlmsects); } void Terr_FreeModel(model_t *mod) { heightmap_t *hm = mod->terrain; if (hm) { validatelinks(&hm->recycle); while(hm->numbrushes) Terr_Brush_DeleteIdx(hm, hm->numbrushes-1); while(hm->brushtextures) { brushtex_t *bt = hm->brushtextures; #ifndef SERVERONLY brushbatch_t *bb; while((bb = bt->batches)) { bt->batches = bb->next; BE_VBO_Destroy(&bb->vbo.coord, bb->vbo.vbomem); BE_VBO_Destroy(&bb->vbo.indicies, bb->vbo.ebomem); BZ_Free(bb); } #endif hm->brushtextures = bt->next; BZ_Free(bt); } #ifdef RUNTIMELIGHTING if (hm->relightcontext) LightShutdown(hm->relightcontext, mod); if (hm->lightthreadmem && !hm->inheritedlightthreadmem) BZ_Free(hm->lightthreadmem); #endif BZ_Free(hm->wbrushes); Terr_PurgeTerrainModel(mod, false, false); while(hm->entities) { struct hmentity_s *n = hm->entities->next; Z_Free(hm->entities); hm->entities = n; } Sys_DestroyMutex(hm->entitylock); Z_Free(hm->seed); Z_Free(hm); mod->terrain = NULL; } } #ifndef SERVERONLY void Terr_DrawTerrainWater(heightmap_t *hm, float *mins, float *maxs, struct hmwater_s *w) { scenetris_t *t; int flags = BEF_NOSHADOWS; int firstv; int y, x; //need to filter by height too, or reflections won't work properly. if (cl_numstris && cl_stris[cl_numstris-1].shader == w->shader && cl_stris[cl_numstris-1].flags == flags && cl_strisvertv[cl_stris[cl_numstris-1].firstvert][2] == w->maxheight) { t = &cl_stris[cl_numstris-1]; } else { if (cl_numstris == cl_maxstris) { cl_maxstris+=8; cl_stris = BZ_Realloc(cl_stris, sizeof(*cl_stris)*cl_maxstris); } t = &cl_stris[cl_numstris++]; t->shader = w->shader; t->flags = flags; t->firstidx = cl_numstrisidx; t->firstvert = cl_numstrisvert; t->numvert = 0; t->numidx = 0; } if (!w->simple) { float step = (maxs[0] - mins[0]) / 8; if (cl_numstrisidx+9*9*6 > cl_maxstrisidx) { cl_maxstrisidx=cl_numstrisidx+12 + 9*9*6*4; cl_strisidx = BZ_Realloc(cl_strisidx, sizeof(*cl_strisidx)*cl_maxstrisidx); } if (cl_numstrisvert+9*9 > cl_maxstrisvert) { cl_maxstrisvert+=9*9+64; cl_strisvertv = BZ_Realloc(cl_strisvertv, sizeof(*cl_strisvertv)*cl_maxstrisvert); cl_strisvertt = BZ_Realloc(cl_strisvertt, sizeof(*cl_strisvertt)*cl_maxstrisvert); cl_strisvertc = BZ_Realloc(cl_strisvertc, sizeof(*cl_strisvertc)*cl_maxstrisvert); } firstv = t->numvert; for (y = 0; y < 9; y++) { for (x = 0; x < 9; x++) { cl_strisvertv[cl_numstrisvert][0] = mins[0] + step*x; cl_strisvertv[cl_numstrisvert][1] = mins[1] + step*y; cl_strisvertv[cl_numstrisvert][2] = w->heights[x + y*9]; cl_strisvertt[cl_numstrisvert][0] = cl_strisvertv[cl_numstrisvert][0]/64; cl_strisvertt[cl_numstrisvert][1] = cl_strisvertv[cl_numstrisvert][1]/64; Vector4Set(cl_strisvertc[cl_numstrisvert], 1,1,1,1) cl_numstrisvert++; } } for (y = 0; y < 8; y++) { for (x = 0; x < 8; x++) { if (w->holes[y] & (1u<numidx = cl_numstrisidx - t->firstidx; t->numvert = cl_numstrisvert - t->firstvert; } else { if (cl_numstrisidx+12 > cl_maxstrisidx) { cl_maxstrisidx=cl_numstrisidx+12 + 64; cl_strisidx = BZ_Realloc(cl_strisidx, sizeof(*cl_strisidx)*cl_maxstrisidx); } if (cl_numstrisvert+4 > cl_maxstrisvert) { cl_maxstrisvert+=64; cl_strisvertv = BZ_Realloc(cl_strisvertv, sizeof(*cl_strisvertv)*cl_maxstrisvert); cl_strisvertt = BZ_Realloc(cl_strisvertt, sizeof(*cl_strisvertt)*cl_maxstrisvert); cl_strisvertc = BZ_Realloc(cl_strisvertc, sizeof(*cl_strisvertc)*cl_maxstrisvert); } { VectorSet(cl_strisvertv[cl_numstrisvert], mins[0], mins[1], w->maxheight); Vector4Set(cl_strisvertc[cl_numstrisvert], 1,1,1,1) Vector2Set(cl_strisvertt[cl_numstrisvert], mins[0]/64, mins[1]/64); cl_numstrisvert++; VectorSet(cl_strisvertv[cl_numstrisvert], mins[0], maxs[1], w->maxheight); Vector4Set(cl_strisvertc[cl_numstrisvert], 1,1,1,1) Vector2Set(cl_strisvertt[cl_numstrisvert], mins[0]/64, maxs[1]/64); cl_numstrisvert++; VectorSet(cl_strisvertv[cl_numstrisvert], maxs[0], maxs[1], w->maxheight); Vector4Set(cl_strisvertc[cl_numstrisvert], 1,1,1,1) Vector2Set(cl_strisvertt[cl_numstrisvert], maxs[0]/64, maxs[1]/64); cl_numstrisvert++; VectorSet(cl_strisvertv[cl_numstrisvert], maxs[0], mins[1], w->maxheight); Vector4Set(cl_strisvertc[cl_numstrisvert], 1,1,1,1) Vector2Set(cl_strisvertt[cl_numstrisvert], maxs[0]/64, mins[1]/64); cl_numstrisvert++; } firstv = t->numvert; /*build the triangles*/ cl_strisidx[cl_numstrisidx++] = firstv + 0; cl_strisidx[cl_numstrisidx++] = firstv + 1; cl_strisidx[cl_numstrisidx++] = firstv + 2; cl_strisidx[cl_numstrisidx++] = firstv + 0; cl_strisidx[cl_numstrisidx++] = firstv + 2; cl_strisidx[cl_numstrisidx++] = firstv + 3; cl_strisidx[cl_numstrisidx++] = firstv + 3; cl_strisidx[cl_numstrisidx++] = firstv + 2; cl_strisidx[cl_numstrisidx++] = firstv + 1; cl_strisidx[cl_numstrisidx++] = firstv + 3; cl_strisidx[cl_numstrisidx++] = firstv + 1; cl_strisidx[cl_numstrisidx++] = firstv + 0; t->numidx = cl_numstrisidx - t->firstidx; t->numvert = cl_numstrisvert - t->firstvert; } } static void Terr_RebuildMesh(model_t *model, hmsection_t *s, int x, int y) { int vx, vy; int v; mesh_t *mesh = &s->mesh; heightmap_t *hm = s->hmmod; Terr_InitLightmap(s, false); s->minh = 9999999999999999.f; s->maxh = -9999999999999999.f; switch(hm->mode) { case HMM_BLOCKS: //tiles, like dungeon keeper if (mesh->xyz_array) BZ_Free(mesh->xyz_array); { mesh->xyz_array = BZ_Malloc((sizeof(vecV_t)+sizeof(vec2_t)+sizeof(vec2_t)) * (SECTHEIGHTSIZE-1)*(SECTHEIGHTSIZE-1)*4*3); mesh->st_array = (void*) (mesh->xyz_array + (SECTHEIGHTSIZE-1)*(SECTHEIGHTSIZE-1)*4*3); mesh->lmst_array[0] = (void*) (mesh->st_array + (SECTHEIGHTSIZE-1)*(SECTHEIGHTSIZE-1)*4*3); } mesh->numvertexes = 0; if (mesh->indexes) BZ_Free(mesh->indexes); mesh->indexes = BZ_Malloc(sizeof(index_t) * SECTHEIGHTSIZE*SECTHEIGHTSIZE*6*3); mesh->numindexes = 0; mesh->colors4f_array[0] = NULL; for (vy = 0; vy < SECTHEIGHTSIZE-1; vy++) { for (vx = 0; vx < SECTHEIGHTSIZE-1; vx++) { float st[2], inst[2]; #if SECTHEIGHTSIZE == 17 int holebit; int holerow; //skip generation of the mesh above holes holerow = ((vy<<3)/(SECTHEIGHTSIZE-1)); holebit = 1u<<((vx<<3)/(SECTHEIGHTSIZE-1)); if (s->holes[holerow] & holebit) continue; #endif //top face v = mesh->numvertexes; mesh->numvertexes += 4; mesh->xyz_array[v+0][0] = (x-CHUNKBIAS + (vx+0)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+0][1] = (y-CHUNKBIAS + (vy+0)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+0][2] = s->heights[vx + vy*SECTHEIGHTSIZE]; mesh->xyz_array[v+1][0] = (x-CHUNKBIAS + (vx+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+1][1] = (y-CHUNKBIAS + (vy+0)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+1][2] = s->heights[vx + vy*SECTHEIGHTSIZE]; mesh->xyz_array[v+2][0] = (x-CHUNKBIAS + (vx+0)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+2][1] = (y-CHUNKBIAS + (vy+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+2][2] = s->heights[vx + vy*SECTHEIGHTSIZE]; mesh->xyz_array[v+3][0] = (x-CHUNKBIAS + (vx+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+3][1] = (y-CHUNKBIAS + (vy+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+3][2] = s->heights[vx + vy*SECTHEIGHTSIZE]; if (s->maxh < mesh->xyz_array[v][2]) s->maxh = mesh->xyz_array[v][2]; if (s->minh > mesh->xyz_array[v][2]) s->minh = mesh->xyz_array[v][2]; st[0] = 1.0f/hm->tilecount[0] * vx; st[1] = 1.0f/hm->tilecount[1] * vy; inst[0] = 0.5f/(hm->tilecount[0]*hm->tilepixcount[0]); inst[1] = 0.5f/(hm->tilecount[1]*hm->tilepixcount[1]); mesh->st_array[v+0][0] = st[0]+inst[0]; mesh->st_array[v+0][1] = st[1]+inst[1]; mesh->st_array[v+1][0] = st[0]-inst[0]+1.0f/hm->tilecount[0]; mesh->st_array[v+1][1] = st[1]+inst[1]; mesh->st_array[v+2][0] = st[0]+inst[0]; mesh->st_array[v+2][1] = st[1]-inst[1]+1.0f/hm->tilecount[1]; mesh->st_array[v+3][0] = st[0]-inst[0]+1.0f/hm->tilecount[0]; mesh->st_array[v+3][1] = st[1]-inst[1]+1.0f/hm->tilecount[1]; //calc the position in the range -0.5 to 0.5 mesh->lmst_array[0][v][0] = (((float)vx / (SECTHEIGHTSIZE-1))-0.5); mesh->lmst_array[0][v][1] = (((float)vy / (SECTHEIGHTSIZE-1))-0.5); //scale down to a half-texel mesh->lmst_array[0][v][0] *= (SECTTEXSIZE-1.0f)/HMLMSTRIDE; mesh->lmst_array[0][v][1] *= (SECTTEXSIZE-1.0f)/HMLMSTRIDE; //bias it mesh->lmst_array[0][v][0] += ((float)SECTTEXSIZE/(HMLMSTRIDE*2)) + ((float)(s->lmx) / HMLMSTRIDE); mesh->lmst_array[0][v][1] += ((float)SECTTEXSIZE/(HMLMSTRIDE*2)) + ((float)(s->lmy) / HMLMSTRIDE); mesh->indexes[mesh->numindexes++] = v+0; mesh->indexes[mesh->numindexes++] = v+2; mesh->indexes[mesh->numindexes++] = v+1; mesh->indexes[mesh->numindexes++] = v+1; mesh->indexes[mesh->numindexes++] = v+2; mesh->indexes[mesh->numindexes++] = v+1+2; //x boundary v = mesh->numvertexes; mesh->numvertexes += 4; mesh->xyz_array[v+0][0] = (x-CHUNKBIAS + (vx+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+0][1] = (y-CHUNKBIAS + (vy+0)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+0][2] = s->heights[vx+0 + vy*SECTHEIGHTSIZE]; mesh->xyz_array[v+1][0] = (x-CHUNKBIAS + (vx+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+1][1] = (y-CHUNKBIAS + (vy+0)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+1][2] = s->heights[(vx+1) + vy*SECTHEIGHTSIZE]; mesh->xyz_array[v+2][0] = (x-CHUNKBIAS + (vx+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+2][1] = (y-CHUNKBIAS + (vy+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+2][2] = s->heights[(vx+0) + vy*SECTHEIGHTSIZE]; mesh->xyz_array[v+3][0] = (x-CHUNKBIAS + (vx+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+3][1] = (y-CHUNKBIAS + (vy+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+3][2] = s->heights[(vx+1) + vy*SECTHEIGHTSIZE]; if (s->maxh < mesh->xyz_array[v][2]) s->maxh = mesh->xyz_array[v][2]; if (s->minh > mesh->xyz_array[v][2]) s->minh = mesh->xyz_array[v][2]; st[0] = 1.0f/hm->tilecount[0] * vx; st[1] = 1.0f/hm->tilecount[1] * vy; inst[0] = 0.5f/(hm->tilecount[0]*hm->tilepixcount[0]); inst[1] = 0.5f/(hm->tilecount[1]*hm->tilepixcount[1]); mesh->st_array[v+0][0] = st[0]+inst[0]; mesh->st_array[v+0][1] = st[1]+inst[1]; mesh->st_array[v+1][0] = st[0]+inst[0]; mesh->st_array[v+1][1] = st[1]-inst[1]+1.0f/hm->tilecount[1]; mesh->st_array[v+2][0] = st[0]-inst[0]+1.0f/hm->tilecount[0]; mesh->st_array[v+2][1] = st[1]+inst[1]; mesh->st_array[v+3][0] = st[0]-inst[0]+1.0f/hm->tilecount[0]; mesh->st_array[v+3][1] = st[1]-inst[1]+1.0f/hm->tilecount[1]; //calc the position in the range -0.5 to 0.5 mesh->lmst_array[0][v][0] = (((float)vx / (SECTHEIGHTSIZE-1))-0.5); mesh->lmst_array[0][v][1] = (((float)vy / (SECTHEIGHTSIZE-1))-0.5); //scale down to a half-texel mesh->lmst_array[0][v][0] *= (SECTTEXSIZE-1.0f)/HMLMSTRIDE; mesh->lmst_array[0][v][1] *= (SECTTEXSIZE-1.0f)/HMLMSTRIDE; //bias it mesh->lmst_array[0][v][0] += ((float)SECTTEXSIZE/(HMLMSTRIDE*2)) + ((float)(s->lmx) / HMLMSTRIDE); mesh->lmst_array[0][v][1] += ((float)SECTTEXSIZE/(HMLMSTRIDE*2)) + ((float)(s->lmy) / HMLMSTRIDE); mesh->indexes[mesh->numindexes++] = v+0; mesh->indexes[mesh->numindexes++] = v+2; mesh->indexes[mesh->numindexes++] = v+1; mesh->indexes[mesh->numindexes++] = v+1; mesh->indexes[mesh->numindexes++] = v+2; mesh->indexes[mesh->numindexes++] = v+1+2; //y boundary v = mesh->numvertexes; mesh->numvertexes += 4; mesh->xyz_array[v+0][0] = (x-CHUNKBIAS + (vx+0)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+0][1] = (y-CHUNKBIAS + (vy+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+0][2] = s->heights[vx + (vy+0)*SECTHEIGHTSIZE]; mesh->xyz_array[v+1][0] = (x-CHUNKBIAS + (vx+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+1][1] = (y-CHUNKBIAS + (vy+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+1][2] = s->heights[vx + (vy+0)*SECTHEIGHTSIZE]; mesh->xyz_array[v+2][0] = (x-CHUNKBIAS + (vx+0)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+2][1] = (y-CHUNKBIAS + (vy+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+2][2] = s->heights[vx + (vy+1)*SECTHEIGHTSIZE]; mesh->xyz_array[v+3][0] = (x-CHUNKBIAS + (vx+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+3][1] = (y-CHUNKBIAS + (vy+1)/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v+3][2] = s->heights[vx + (vy+1)*SECTHEIGHTSIZE]; if (s->maxh < mesh->xyz_array[v][2]) s->maxh = mesh->xyz_array[v][2]; if (s->minh > mesh->xyz_array[v][2]) s->minh = mesh->xyz_array[v][2]; st[0] = 1.0f/hm->tilecount[0] * vx; st[1] = 1.0f/hm->tilecount[1] * vy; inst[0] = 0.5f/(hm->tilecount[0]*hm->tilepixcount[0]); inst[1] = 0.5f/(hm->tilecount[1]*hm->tilepixcount[1]); mesh->st_array[v+0][0] = st[0]+inst[0]; mesh->st_array[v+0][1] = st[1]+inst[1]; mesh->st_array[v+1][0] = st[0]-inst[0]+1.0f/hm->tilecount[0]; mesh->st_array[v+1][1] = st[1]+inst[1]; mesh->st_array[v+2][0] = st[0]+inst[0]; mesh->st_array[v+2][1] = st[1]-inst[1]+1.0f/hm->tilecount[1]; mesh->st_array[v+3][0] = st[0]-inst[0]+1.0f/hm->tilecount[0]; mesh->st_array[v+3][1] = st[1]-inst[1]+1.0f/hm->tilecount[1]; //calc the position in the range -0.5 to 0.5 mesh->lmst_array[0][v][0] = (((float)vx / (SECTHEIGHTSIZE-1))-0.5); mesh->lmst_array[0][v][1] = (((float)vy / (SECTHEIGHTSIZE-1))-0.5); //scale down to a half-texel mesh->lmst_array[0][v][0] *= (SECTTEXSIZE-1.0f)/HMLMSTRIDE; mesh->lmst_array[0][v][1] *= (SECTTEXSIZE-1.0f)/HMLMSTRIDE; //bias it mesh->lmst_array[0][v][0] += ((float)SECTTEXSIZE/(HMLMSTRIDE*2)) + ((float)(s->lmx) / HMLMSTRIDE); mesh->lmst_array[0][v][1] += ((float)SECTTEXSIZE/(HMLMSTRIDE*2)) + ((float)(s->lmy) / HMLMSTRIDE); mesh->indexes[mesh->numindexes++] = v+0; mesh->indexes[mesh->numindexes++] = v+2; mesh->indexes[mesh->numindexes++] = v+1; mesh->indexes[mesh->numindexes++] = v+1; mesh->indexes[mesh->numindexes++] = v+2; mesh->indexes[mesh->numindexes++] = v+1+2; } } break; case HMM_TERRAIN: //smooth terrain if (!mesh->xyz_array) { mesh->xyz_array = BZ_Malloc((sizeof(vecV_t)+sizeof(vec2_t)+sizeof(vec2_t)) * (SECTHEIGHTSIZE)*(SECTHEIGHTSIZE)); mesh->st_array = (void*) (mesh->xyz_array + (SECTHEIGHTSIZE)*(SECTHEIGHTSIZE)); mesh->lmst_array[0] = (void*) (mesh->st_array + (SECTHEIGHTSIZE)*(SECTHEIGHTSIZE)); } mesh->colors4f_array[0] = s->colours; mesh->numvertexes = 0; /*64 quads across requires 65 verticies*/ for (vy = 0; vy < SECTHEIGHTSIZE; vy++) { for (vx = 0; vx < SECTHEIGHTSIZE; vx++) { v = mesh->numvertexes++; mesh->xyz_array[v][0] = (x-CHUNKBIAS + vx/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v][1] = (y-CHUNKBIAS + vy/(SECTHEIGHTSIZE-1.0f)) * hm->sectionsize; mesh->xyz_array[v][2] = s->heights[vx + vy*SECTHEIGHTSIZE]; if (s->maxh < mesh->xyz_array[v][2]) s->maxh = mesh->xyz_array[v][2]; if (s->minh > mesh->xyz_array[v][2]) s->minh = mesh->xyz_array[v][2]; mesh->st_array[v][0] = mesh->xyz_array[v][0] / 128; mesh->st_array[v][1] = mesh->xyz_array[v][1] / 128; //calc the position in the range -0.5 to 0.5 mesh->lmst_array[0][v][0] = (((float)vx / (SECTHEIGHTSIZE-1))-0.5); mesh->lmst_array[0][v][1] = (((float)vy / (SECTHEIGHTSIZE-1))-0.5); //scale down to a half-texel mesh->lmst_array[0][v][0] *= (SECTTEXSIZE-1.0f)/HMLMSTRIDE; mesh->lmst_array[0][v][1] *= (SECTTEXSIZE-1.0f)/HMLMSTRIDE; //bias it mesh->lmst_array[0][v][0] += ((float)SECTTEXSIZE/(HMLMSTRIDE*2)) + ((float)(s->lmx) / HMLMSTRIDE); mesh->lmst_array[0][v][1] += ((float)SECTTEXSIZE/(HMLMSTRIDE*2)) + ((float)(s->lmy) / HMLMSTRIDE); } } if (!mesh->indexes) mesh->indexes = BZ_Malloc(sizeof(index_t) * SECTHEIGHTSIZE*SECTHEIGHTSIZE*6); mesh->numindexes = 0; for (vy = 0; vy < SECTHEIGHTSIZE-1; vy++) { for (vx = 0; vx < SECTHEIGHTSIZE-1; vx++) { #ifndef STRICTEDGES float d1,d2; #endif #if SECTHEIGHTSIZE == 17 int holerow; int holebit; //skip generation of the mesh above holes holerow = ((vy<<3)/(SECTHEIGHTSIZE-1)); holebit = 1u<<((vx<<3)/(SECTHEIGHTSIZE-1)); if (s->holes[holerow] & holebit) continue; #endif v = vx + vy*(SECTHEIGHTSIZE); #ifndef STRICTEDGES d1 = fabs(mesh->xyz_array[v][2] - mesh->xyz_array[v+1+SECTHEIGHTSIZE][2]); d2 = fabs(mesh->xyz_array[v+1][2] - mesh->xyz_array[v+SECTHEIGHTSIZE][2]); if (d1 < d2) { mesh->indexes[mesh->numindexes++] = v+0; mesh->indexes[mesh->numindexes++] = v+1+SECTHEIGHTSIZE; mesh->indexes[mesh->numindexes++] = v+1; mesh->indexes[mesh->numindexes++] = v+0; mesh->indexes[mesh->numindexes++] = v+SECTHEIGHTSIZE; mesh->indexes[mesh->numindexes++] = v+1+SECTHEIGHTSIZE; } else #endif { mesh->indexes[mesh->numindexes++] = v+0; mesh->indexes[mesh->numindexes++] = v+SECTHEIGHTSIZE; mesh->indexes[mesh->numindexes++] = v+1; mesh->indexes[mesh->numindexes++] = v+1; mesh->indexes[mesh->numindexes++] = v+SECTHEIGHTSIZE; mesh->indexes[mesh->numindexes++] = v+1+SECTHEIGHTSIZE; } } } break; } //pure holes if (!mesh->numindexes) { memset(&s->pvscache, 0, sizeof(s->pvscache)); return; } if (s->maxh_cull < s->maxh) s->maxh_cull = s->maxh; { vec3_t mins, maxs; mins[0] = (x-CHUNKBIAS) * hm->sectionsize; mins[1] = (y-CHUNKBIAS) * hm->sectionsize; mins[2] = s->minh; maxs[0] = (x+1-CHUNKBIAS) * hm->sectionsize; maxs[1] = (y+1-CHUNKBIAS) * hm->sectionsize; maxs[2] = s->maxh_cull; model->funcs.FindTouchedLeafs(model, &s->pvscache, mins, maxs); } #ifdef GLQUAKE if (qrenderer == QR_OPENGL && qglGenBuffersARB) { if (!s->vbo.coord.gl.vbo) { qglGenBuffersARB(1, &s->vbo.coord.gl.vbo); GL_SelectVBO(s->vbo.coord.gl.vbo); } else GL_SelectVBO(s->vbo.coord.gl.vbo); qglBufferDataARB(GL_ARRAY_BUFFER_ARB, (sizeof(vecV_t)+sizeof(vec2_t)+sizeof(vec2_t)+sizeof(vec4_t)) * (mesh->numvertexes), NULL, GL_STATIC_DRAW_ARB); qglBufferSubDataARB(GL_ARRAY_BUFFER_ARB, 0, (sizeof(vecV_t)+sizeof(vec2_t)+sizeof(vec2_t)) * mesh->numvertexes, mesh->xyz_array); if (mesh->colors4f_array[0]) qglBufferSubDataARB(GL_ARRAY_BUFFER_ARB, (sizeof(vecV_t)+sizeof(vec2_t)+sizeof(vec2_t)) * mesh->numvertexes, sizeof(vec4_t)*mesh->numvertexes, mesh->colors4f_array[0]); GL_SelectVBO(0); s->vbo.coord.gl.addr = 0; s->vbo.texcoord.gl.addr = (void*)((char*)mesh->st_array - (char*)mesh->xyz_array); s->vbo.texcoord.gl.vbo = s->vbo.coord.gl.vbo; s->vbo.lmcoord[0].gl.addr = (void*)((char*)mesh->lmst_array[0] - (char*)mesh->xyz_array); s->vbo.lmcoord[0].gl.vbo = s->vbo.coord.gl.vbo; s->vbo.colours[0].gl.addr = (void*)((sizeof(vecV_t)+sizeof(vec2_t)+sizeof(vec2_t)) * mesh->numvertexes); s->vbo.colours[0].gl.vbo = s->vbo.coord.gl.vbo; if (!s->vbo.indicies.gl.vbo) qglGenBuffersARB(1, &s->vbo.indicies.gl.vbo); s->vbo.indicies.gl.addr = 0; GL_SelectEBO(s->vbo.indicies.gl.vbo); qglBufferDataARB(GL_ELEMENT_ARRAY_BUFFER_ARB, sizeof(index_t) * mesh->numindexes, mesh->indexes, GL_STATIC_DRAW_ARB); GL_SelectEBO(0); #if 1 Z_Free(mesh->xyz_array); mesh->xyz_array = NULL; mesh->st_array = NULL; mesh->lmst_array[0] = NULL; Z_Free(mesh->indexes); mesh->indexes = NULL; #endif } #endif #ifdef VKQUAKE if (qrenderer == QR_VULKAN) { void VKBE_GenBatchVBOs(vbo_t **vbochain, batch_t *firstbatch, batch_t *stopbatch); batch_t batch = {0}; mesh_t *meshes = &s->mesh; vbo_t *vbo = NULL; batch.maxmeshes = 1; batch.mesh = &meshes; VKBE_GenBatchVBOs(&vbo, &batch, NULL); s->vbo = *vbo; } #endif #ifdef D3D9QUAKE if (qrenderer == QR_DIRECT3D9) { void D3D9BE_GenBatchVBOs(vbo_t **vbochain, batch_t *firstbatch, batch_t *stopbatch); batch_t batch = {0}; mesh_t *meshes = &s->mesh; vbo_t *vbo = NULL; batch.maxmeshes = 1; batch.mesh = &meshes; //BE_ClearVBO(&s->vbo); D3D9BE_GenBatchVBOs(&vbo, &batch, NULL); s->vbo = *vbo; } #endif #ifdef D3D11QUAKE if (qrenderer == QR_DIRECT3D11) { void D3D11BE_GenBatchVBOs(vbo_t **vbochain, batch_t *firstbatch, batch_t *stopbatch); batch_t batch = {0}; mesh_t *meshes = &s->mesh; vbo_t *vbo = NULL; batch.maxmeshes = 1; batch.mesh = &meshes; //BE_ClearVBO(&s->vbo); D3D11BE_GenBatchVBOs(&vbo, &batch, NULL); s->vbo = *vbo; } #endif } struct tdibctx { heightmap_t *hm; int vx; int vy; entity_t *ent; batch_t **batches; qbyte *pvs; model_t *wmodel; }; void Terr_DrawInBounds(struct tdibctx *ctx, int x, int y, int w, int h) { vec3_t mins, maxs; hmsection_t *s; struct hmwater_s *wa; int i, j; batch_t *b; heightmap_t *hm = ctx->hm; mins[0] = (x+0 - CHUNKBIAS)*hm->sectionsize; maxs[0] = (x+w - CHUNKBIAS)*hm->sectionsize; mins[1] = (y+0 - CHUNKBIAS)*hm->sectionsize; maxs[1] = (y+h - CHUNKBIAS)*hm->sectionsize; mins[2] = r_origin[2]-999999; maxs[2] = r_origin[2]+999999; if (w == 1 && h == 1) { // if (R_CullBox(mins, maxs)) // return; s = Terr_GetSection(hm, x, y, TGS_LAZYLOAD); if (!s) return; /*move to head*/ validatelinks(&hm->recycle); RemoveLink(&s->recycle); validatelinks(&hm->recycle); InsertLinkBefore(&s->recycle, &hm->recycle); validatelinks(&hm->recycle); if (s->lightmap < 0) Terr_LoadSection(hm, s, x, y, TGS_NODOWNLOAD); if (s->flags & TSF_RELIGHT) { if (!hm->relight) { hm->relight = s; hm->relightidx = 0; hm->relightmin[0] = mins[0]; hm->relightmin[1] = mins[1]; } } if (s->flags & TSF_DIRTY) { s->flags &= ~TSF_DIRTY; Terr_RebuildMesh(ctx->wmodel, s, x, y); } if (ctx->pvs && !ctx->wmodel->funcs.EdictInFatPVS(ctx->wmodel, &s->pvscache, ctx->pvs)) return; //this section isn't in any visible bsp leafs if (s->numents) { Sys_LockMutex(hm->entitylock); //chuck out any batches for models in this section for (i = 0; i < s->numents; i++) { struct hmentity_s *e = s->ents[i]; vec3_t dist; float a, dmin, dmax; model_t *model; //skip the entity if its already been added to some batch this frame. if (e->drawnframe == hm->drawnframe) continue; e->drawnframe = hm->drawnframe; model = e->ent.model; if (!model) continue; if (model->loadstate == MLS_NOTLOADED) { // if (hm->beinglazy) // continue; // hm->beinglazy = true; Mod_LoadModel(model, MLV_WARN); } if (model->loadstate != MLS_LOADED) continue; VectorSubtract(e->ent.origin, r_origin, dist); a = VectorLength(dist); dmin = 1024 + model->radius*160; dmax = dmin + 1024; a = (a - dmin) / (dmax - dmin); a = 1-a; if (a < 0) continue; if (R_CullSphere(e->ent.origin, model->radius)) continue; if (a >= 1) { a = 1; e->ent.flags &= ~RF_TRANSLUCENT; } else e->ent.flags |= RF_TRANSLUCENT; e->ent.shaderRGBAf[3] = a; switch(model->type) { case mod_alias: R_GAlias_GenerateBatches(&e->ent, ctx->batches); break; case mod_brush: Surf_GenBrushBatches(ctx->batches, &e->ent); break; default: //FIXME: no sprites! oh noes! break; } } Sys_UnlockMutex(hm->entitylock); } for (wa = s->water; wa; wa = wa->next) { mins[2] = wa->minheight; maxs[2] = wa->maxheight; if (!R_CullBox(mins, maxs)) { Terr_DrawTerrainWater(hm, mins, maxs, wa); } } mins[2] = s->minh; maxs[2] = s->maxh; // if (!BoundsIntersect(mins, maxs, r_refdef.vieworg, r_refdef.vieworg)) if (R_CullBox(mins, maxs)) return; if (hm->texmask) { for (i = 0; i < 4; i++) { if (!*s->texname[i]) break; if (!strcmp(s->texname[i], hm->texmask)) break; } if (i == 4) { //flicker if the surface cannot accept the named texture int xor = (x&1)^(y&1); if (((int)(realtime*10) & 1) ^ xor) return; } } b = BE_GetTempBatch(); if (!b) return; b->ent = ctx->ent; b->shader = hm->shader; b->flags = 0; b->mesh = &s->amesh; b->mesh[0] = &s->mesh; b->meshes = 1; b->buildmeshes = NULL; b->skin = &s->textures; b->texture = NULL; b->vbo = &s->vbo; b->lightmap[0] = s->lightmap; for (j = 1; j < MAXRLIGHTMAPS; j++) b->lightmap[j] = -1; b->next = ctx->batches[b->shader->sort]; ctx->batches[b->shader->sort] = b; } else if (w && h) { //divide and conquer, radiating outwards from the view. if (w > h) { i = x + w; w = x + w/2; if (ctx->vx >= w) { Terr_DrawInBounds(ctx, w, y, i-w, h); Terr_DrawInBounds(ctx, x, y, w-x, h); } else { Terr_DrawInBounds(ctx, x, y, w-x, h); Terr_DrawInBounds(ctx, w, y, i-w, h); } } else { i = y + h; h = y + h/2; if (ctx->vy >= h) { Terr_DrawInBounds(ctx, x, h, w, i-h); Terr_DrawInBounds(ctx, x, y, w, h-y); } else { Terr_DrawInBounds(ctx, x, y, w, h-y); Terr_DrawInBounds(ctx, x, h, w, i-h); } } } } void Terr_DrawTerrainModel (batch_t **batches, entity_t *e) { extern qbyte *frustumvis; model_t *m = e->model; heightmap_t *hm = m->terrain; batch_t *b; int bounds[4], j; struct tdibctx tdibctx; if (!r_refdef.recurse) { Terr_DoEditNotify(hm); // while (hm->activesections > 0) // if (!Terr_Collect(hm)) // break; while (hm->activesections > TERRAINACTIVESECTIONS) { if (!Terr_Collect(hm)) break; break; } } // hm->beinglazy = false; if (hm->relight) ted_dorelight(hm); if (e->model == cl.worldmodel && hm->skyshader) { b = BE_GetTempBatch(); if (b) { for (j = 0; j < MAXRLIGHTMAPS; j++) b->lightmap[j] = -1; b->ent = e; b->shader = hm->skyshader; b->flags = 0; b->mesh = &hm->askymesh; b->mesh[0] = &hm->skymesh; b->meshes = 1; b->buildmeshes = NULL; b->skin = NULL; b->texture = NULL; // vbo = b->vbo = hm->vbo[x+y*MAXSECTIONS]; b->vbo = NULL; b->next = batches[b->shader->sort]; batches[b->shader->sort] = b; } } Terr_Brush_Draw(hm, batches, e); if (r_refdef.globalfog.density || r_refdef.maxdist>0) { float culldist; extern cvar_t r_fog_exp2; if (r_refdef.globalfog.density) { //figure out the eyespace distance required to reach that fog value culldist = log(0.5/255.0f); if (r_fog_exp2.ival) culldist = sqrt(culldist / (-r_refdef.globalfog.density * r_refdef.globalfog.density)); else culldist = culldist / (-r_refdef.globalfog.density); //anything drawn beyond this point is fully obscured by fog culldist += 4096; } else culldist = 999999999999999.f; if (culldist < hm->maxdrawdist) culldist = hm->maxdrawdist; if (culldist > r_refdef.maxdist && r_refdef.maxdist>0) culldist = r_refdef.maxdist; bounds[0] = bound(hm->firstsegx, (r_refdef.vieworg[0] + (CHUNKBIAS + 0)*hm->sectionsize - culldist) / hm->sectionsize, hm->maxsegx); bounds[1] = bound(hm->firstsegx, (r_refdef.vieworg[0] + (CHUNKBIAS + 1)*hm->sectionsize + culldist) / hm->sectionsize, hm->maxsegx); bounds[2] = bound(hm->firstsegy, (r_refdef.vieworg[1] + (CHUNKBIAS + 0)*hm->sectionsize - culldist) / hm->sectionsize, hm->maxsegy); bounds[3] = bound(hm->firstsegy, (r_refdef.vieworg[1] + (CHUNKBIAS + 1)*hm->sectionsize + culldist) / hm->sectionsize, hm->maxsegy); } else { bounds[0] = hm->firstsegx; bounds[1] = hm->maxsegx; bounds[2] = hm->firstsegy; bounds[3] = hm->maxsegy; } //FIXME: project the near+far clip planes onto the screen, generate bounds from those, instead of the above overkill code. hm->drawnframe+=1; tdibctx.hm = hm; tdibctx.batches = batches; tdibctx.ent = e; tdibctx.vx = (r_refdef.vieworg[0] + CHUNKBIAS*hm->sectionsize) / hm->sectionsize; tdibctx.vy = (r_refdef.vieworg[1] + CHUNKBIAS*hm->sectionsize) / hm->sectionsize; tdibctx.wmodel = e->model; tdibctx.pvs = (e->model == cl.worldmodel)?frustumvis:NULL; validatelinks(&hm->recycle); Terr_DrawInBounds(&tdibctx, bounds[0], bounds[2], bounds[1]-bounds[0], bounds[3]-bounds[2]); validatelinks(&hm->recycle); /*{ trace_t trace; vec3_t player_mins = {-16, -16, -24}; vec3_t player_maxs = {16, 16, 32}; vec3_t start, end; VectorCopy(cl.playerview[0].simorg, start); VectorCopy(start, end); start[0] += 5; end[2] -= 100; Heightmap_Trace(cl.worldmodel, 0, 0, NULL, start, end, player_mins, player_maxs, false, ~0, &trace); }*/ } void Terrain_ClipDecal(fragmentdecal_t *dec, float *center, float radius, model_t *model) { int min[2], max[2], mint[2], maxt[2]; int x, y, tx, ty; vecV_t vert[6]; hmsection_t *s; heightmap_t *hm = model->terrain; min[0] = floor((center[0] - radius)/(hm->sectionsize)) + CHUNKBIAS; min[1] = floor((center[1] - radius)/(hm->sectionsize)) + CHUNKBIAS; max[0] = ceil((center[0] + radius)/(hm->sectionsize)) + CHUNKBIAS; max[1] = ceil((center[1] + radius)/(hm->sectionsize)) + CHUNKBIAS; min[0] = bound(hm->firstsegx, min[0], hm->maxsegx); min[1] = bound(hm->firstsegy, min[1], hm->maxsegy); max[0] = bound(hm->firstsegx, max[0], hm->maxsegx); max[1] = bound(hm->firstsegy, max[1], hm->maxsegy); for (y = min[1]; y < max[1]; y++) { for (x = min[0]; x < max[0]; x++) { s = Terr_GetSection(hm, x, y, TGS_WAITLOAD); if (!s) continue; mint[0] = floor((center[0] - radius)*(SECTHEIGHTSIZE-1)/(hm->sectionsize) + (CHUNKBIAS - x)*(SECTHEIGHTSIZE-1)); mint[1] = floor((center[1] - radius)*(SECTHEIGHTSIZE-1)/(hm->sectionsize) + (CHUNKBIAS - y)*(SECTHEIGHTSIZE-1)); maxt[0] = ceil((center[0] + radius)*(SECTHEIGHTSIZE-1)/(hm->sectionsize) + (CHUNKBIAS - x)*(SECTHEIGHTSIZE-1)); maxt[1] = ceil((center[1] + radius)*(SECTHEIGHTSIZE-1)/(hm->sectionsize) + (CHUNKBIAS - y)*(SECTHEIGHTSIZE-1)); mint[0] = bound(0, mint[0], (SECTHEIGHTSIZE-1)); mint[1] = bound(0, mint[1], (SECTHEIGHTSIZE-1)); maxt[0] = bound(0, maxt[0], (SECTHEIGHTSIZE-1)); maxt[1] = bound(0, maxt[1], (SECTHEIGHTSIZE-1)); for (ty = mint[1]; ty < maxt[1]; ty++) { for (tx = mint[0]; tx < maxt[0]; tx++) { #ifndef STRICTEDGES float d1, d2; d1 = fabs(s->heights[(tx+0) + (ty+0)*SECTHEIGHTSIZE] - s->heights[(tx+1) + (ty+1)*SECTHEIGHTSIZE]); d2 = fabs(s->heights[(tx+1) + (ty+0)*SECTHEIGHTSIZE] - s->heights[(tx+0) + (ty+1)*SECTHEIGHTSIZE]); if (d1 < d2) { vert[0][0] = (x-CHUNKBIAS)*hm->sectionsize + (tx+0)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize;vert[0][1] = (y-CHUNKBIAS)*hm->sectionsize + (ty+0)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize; vert[1][0] = (x-CHUNKBIAS)*hm->sectionsize + (tx+1)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize;vert[1][1] = (y-CHUNKBIAS)*hm->sectionsize + (ty+1)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize; vert[2][0] = (x-CHUNKBIAS)*hm->sectionsize + (tx+1)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize;vert[2][1] = (y-CHUNKBIAS)*hm->sectionsize + (ty+0)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize; vert[3][0] = (x-CHUNKBIAS)*hm->sectionsize + (tx+0)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize;vert[3][1] = (y-CHUNKBIAS)*hm->sectionsize + (ty+0)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize; vert[4][0] = (x-CHUNKBIAS)*hm->sectionsize + (tx+0)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize;vert[4][1] = (y-CHUNKBIAS)*hm->sectionsize + (ty+1)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize; vert[5][0] = (x-CHUNKBIAS)*hm->sectionsize + (tx+1)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize;vert[5][1] = (y-CHUNKBIAS)*hm->sectionsize + (ty+1)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize; vert[0][2] = s->heights[(tx+0) + (ty+0)*SECTHEIGHTSIZE]; vert[1][2] = s->heights[(tx+1) + (ty+1)*SECTHEIGHTSIZE]; vert[2][2] = s->heights[(tx+1) + (ty+0)*SECTHEIGHTSIZE]; vert[3][2] = s->heights[(tx+0) + (ty+0)*SECTHEIGHTSIZE]; vert[4][2] = s->heights[(tx+0) + (ty+1)*SECTHEIGHTSIZE]; vert[5][2] = s->heights[(tx+1) + (ty+1)*SECTHEIGHTSIZE]; } else #endif { vert[0][0] = (x-CHUNKBIAS)*hm->sectionsize + (tx+0)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize;vert[0][1] = (y-CHUNKBIAS)*hm->sectionsize + (ty+0)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize; vert[1][0] = (x-CHUNKBIAS)*hm->sectionsize + (tx+0)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize;vert[1][1] = (y-CHUNKBIAS)*hm->sectionsize + (ty+1)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize; vert[2][0] = (x-CHUNKBIAS)*hm->sectionsize + (tx+1)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize;vert[2][1] = (y-CHUNKBIAS)*hm->sectionsize + (ty+0)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize; vert[3][0] = (x-CHUNKBIAS)*hm->sectionsize + (tx+1)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize;vert[3][1] = (y-CHUNKBIAS)*hm->sectionsize + (ty+0)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize; vert[4][0] = (x-CHUNKBIAS)*hm->sectionsize + (tx+0)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize;vert[4][1] = (y-CHUNKBIAS)*hm->sectionsize + (ty+1)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize; vert[5][0] = (x-CHUNKBIAS)*hm->sectionsize + (tx+1)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize;vert[5][1] = (y-CHUNKBIAS)*hm->sectionsize + (ty+1)/(float)(SECTHEIGHTSIZE-1)*hm->sectionsize; vert[0][2] = s->heights[(tx+0) + (ty+0)*SECTHEIGHTSIZE]; vert[1][2] = s->heights[(tx+0) + (ty+1)*SECTHEIGHTSIZE]; vert[2][2] = s->heights[(tx+1) + (ty+0)*SECTHEIGHTSIZE]; vert[3][2] = s->heights[(tx+1) + (ty+0)*SECTHEIGHTSIZE]; vert[4][2] = s->heights[(tx+0) + (ty+1)*SECTHEIGHTSIZE]; vert[5][2] = s->heights[(tx+1) + (ty+1)*SECTHEIGHTSIZE]; } //fixme: per-section shaders for clutter info. this kinda sucks. Fragment_ClipPoly(dec, 3, &vert[0][0], hm->shader); Fragment_ClipPoly(dec, 3, &vert[3][0], hm->shader); } } } } } #endif unsigned int Heightmap_PointContentsHM(heightmap_t *hm, float clipmipsz, vec3_t org) { float x, y; float z, tz; int sx, sy; unsigned int holerow; unsigned int holebit; hmsection_t *s; struct hmwater_s *w; unsigned int contents; const float wbias = CHUNKBIAS * hm->sectionsize; sx = (org[0]+wbias)/hm->sectionsize; sy = (org[1]+wbias)/hm->sectionsize; if (sx < hm->firstsegx || sy < hm->firstsegy) return hm->exteriorcontents; if (sx >= hm->maxsegx || sy >= hm->maxsegy) return hm->exteriorcontents; s = Terr_GetSection(hm, sx, sy, TGS_TRYLOAD | TGS_ANYSTATE); if (!s || s->loadstate != TSLS_LOADED) { if (s && s->loadstate == TSLS_FAILED) return hm->exteriorcontents; return FTECONTENTS_SOLID; } x = (org[0]+wbias - (sx*hm->sectionsize))*(SECTHEIGHTSIZE-1)/hm->sectionsize; y = (org[1]+wbias - (sy*hm->sectionsize))*(SECTHEIGHTSIZE-1)/hm->sectionsize; z = (org[2]+clipmipsz); if (z < s->minh-16) return hm->exteriorcontents; sx = x; x-=sx; sy = y; y-=sy; holerow = ((sy<<3)/(SECTHEIGHTSIZE-1)); holebit = 1u<<((sx<<3)/(SECTHEIGHTSIZE-1)); if (s->holes[holerow] & (1u<1) //the 1, 1 triangle { float v1, v2, v3; v3 = 1-y; v2 = x+y-1; v1 = 1-x; //0, 1 //1, 1 //1, 0 tz = (s->heights[(sx+0)+(sy+1)*SECTHEIGHTSIZE]*v1 + s->heights[(sx+1)+(sy+1)*SECTHEIGHTSIZE]*v2 + s->heights[(sx+1)+(sy+0)*SECTHEIGHTSIZE]*v3); } else { float v1, v2, v3; v1 = y; v2 = x; v3 = 1-y-x; //0, 1 //1, 0 //0, 0 tz = (s->heights[(sx+0)+(sy+1)*SECTHEIGHTSIZE]*v1 + s->heights[(sx+1)+(sy+0)*SECTHEIGHTSIZE]*v2 + s->heights[(sx+0)+(sy+0)*SECTHEIGHTSIZE]*v3); } if (z <= tz) return FTECONTENTS_SOLID; //contained within contents = FTECONTENTS_EMPTY; for (w = s->water; w; w = w->next) { if (w->holes[holerow] & (1u<maxheight) //FIXME contents |= w->contentmask; } return contents; } unsigned int Heightmap_PointContents(model_t *model, vec3_t axis[3], vec3_t org) { heightmap_t *hm = model->terrain; unsigned int cont; brushes_t *br; unsigned int i, j; float dist; cont = Heightmap_PointContentsHM(hm, 0, org); if (cont & FTECONTENTS_SOLID) return cont; for (i = 0; i < hm->numbrushes; i++) { br = &hm->wbrushes[i]; for (j = 0; j < br->numplanes; j++) { /* for (k=0 ; k<3 ; k++) { if (in_normals[j][k] < 0) best[k] = br->maxs[k]; else best[k] = br->mins[k]; } */ dist = DotProduct (org/*best*/, br->planes[j]); dist = br->planes[j][3] - dist; if (dist < 0) break; } if (j == br->numplanes) { cont |= br->contents; } } return cont; } unsigned int Heightmap_NativeBoxContents(model_t *model, int hulloverride, framestate_t *framestate, vec3_t axis[3], vec3_t org, vec3_t mins, vec3_t maxs) { heightmap_t *hm = model->terrain; return Heightmap_PointContentsHM(hm, mins[2], org); } void Heightmap_Normal(heightmap_t *hm, vec3_t org, vec3_t norm) { #if 0 norm[0] = 0; norm[1] = 0; norm[2] = 1; #else float x, y; int sx, sy; vec3_t d1, d2; const float wbias = CHUNKBIAS * hm->sectionsize; hmsection_t *s; norm[0] = 0; norm[1] = 0; norm[2] = 1; sx = (org[0]+wbias)/hm->sectionsize; sy = (org[1]+wbias)/hm->sectionsize; if (sx < hm->firstsegx || sy < hm->firstsegy) return; if (sx >= hm->maxsegx || sy >= hm->maxsegy) return; s = Terr_GetSection(hm, sx, sy, TGS_TRYLOAD); if (!s) return; x = (org[0]+wbias - (sx*hm->sectionsize))*(SECTHEIGHTSIZE-1)/hm->sectionsize; y = (org[1]+wbias - (sy*hm->sectionsize))*(SECTHEIGHTSIZE-1)/hm->sectionsize; sx = x; x-=sx; sy = y; y-=sy; if (x+y>1) //the 1, 1 triangle { //0, 1 //1, 1 //1, 0 d1[0] = (hm->sectionsize / SECTHEIGHTSIZE); d1[1] = 0; d1[2] = (s->heights[(sx+1)+(sy+1)*SECTHEIGHTSIZE] - s->heights[(sx+0)+(sy+1)*SECTHEIGHTSIZE]); d2[0] = 0; d2[1] = (hm->sectionsize / SECTHEIGHTSIZE); d2[2] = (s->heights[(sx+1)+(sy+1)*SECTHEIGHTSIZE] - s->heights[(sx+1)+(sy+0)*SECTHEIGHTSIZE]); } else { //the 0,0 triangle //0, 1 //1, 0 //0, 0 d1[0] = (hm->sectionsize / SECTHEIGHTSIZE); d1[1] = 0; d1[2] = (s->heights[(sx+1)+(sy+0)*SECTHEIGHTSIZE] - s->heights[(sx+0)+(sy+0)*SECTHEIGHTSIZE]); d2[0] = 0; d2[1] = (hm->sectionsize / SECTHEIGHTSIZE); d2[2] = (s->heights[(sx+0)+(sy+1)*SECTHEIGHTSIZE] - s->heights[(sx+0)+(sy+0)*SECTHEIGHTSIZE]); } VectorNormalize(d1); VectorNormalize(d2); CrossProduct(d1, d2, norm); VectorNormalize(norm); #endif } typedef struct { vec3_t start; vec3_t end; vec3_t impact; vec4_t plane; vec3_t mins; vec3_t maxs; vec3_t absmins; vec3_t absmaxs; vec3_t up; vec3_t capsulesize; enum {ispoint, iscapsule, isbox} shape; qboolean startsolid; double nearfrac; float truefrac; float htilesize; heightmap_t *hm; int contents; int hitcontentsmask; trace_t *result; #ifdef _DEBUG qboolean debug; #endif } hmtrace_t; static int Heightmap_Trace_Brush(hmtrace_t *tr, vec4_t *planes, int numplanes, brushes_t *brushinfo) { qboolean startout; float *enterplane; double enterfrac, exitfrac, nearfrac=0; double enterdist=0; double dist, d1, d2, f; unsigned int i, j; vec3_t ofs; startout = false; enterplane= NULL; enterfrac = -1; exitfrac = 10; for (i = 0; i < numplanes; i++) { /*calculate the distance based upon the shape of the object we're tracing for*/ switch(tr->shape) { default: case isbox: // general box case // push the plane out apropriately for mins/maxs // FIXME: use signbits into 8 way lookup for each mins/maxs for (j=0 ; j<3 ; j++) { if (planes[i][j] < 0) ofs[j] = tr->maxs[j]; else ofs[j] = tr->mins[j]; } dist = DotProduct (ofs, planes[i]); dist = planes[i][3] - dist; break; case iscapsule: dist = DotProduct(tr->up, planes[i]); dist = dist*(tr->capsulesize[(dist<0)?1:2]) - tr->capsulesize[0]; dist = planes[i][3] - dist; break; case ispoint: // special point case dist = planes[i][3]; break; } d1 = DotProduct (tr->start, planes[i]) - dist; d2 = DotProduct (tr->end, planes[i]) - dist; //if we're fully outside any plane, then we cannot possibly enter the brush, skip to the next one if (d1 > 0 && d2 >= d1) return false; if (d1 > 0) startout = true; //if we're fully inside the plane, then whatever is happening is not relevent for this plane if (d1 < 0 && d2 <= 0) continue; f = (d1) / (d1-d2); if (d1 > d2) { //entered the brush. favour the furthest fraction to avoid extended edges (yay for convex shapes) if (enterfrac < f) { enterfrac = f; nearfrac = (d1 - (0.03125)) / (d1-d2); enterplane = planes[i]; enterdist = dist; } } else { //left the brush, favour the nearest plane (smallest frac) if (exitfrac > f) { exitfrac = f; } } } //non-point traces need to clip against the brush's edges if (brushinfo && tr->shape != ispoint && brushinfo->axialplanes != 0x3f) { static vec3_t axis[] = {{1,0,0},{0,1,0},{0,0,1},{-1,0,0},{0,-1,0},{0,0,-1}}; for (i = 0; i < 6; i++) { // if (brushinfo->axialplanes & (1u<= 3) { /*calculate the distance based upon the shape of the object we're tracing for*/ switch(tr->shape) { default: case isbox: dist = -tr->maxs[i-3]; dist = -brushinfo->mins[i-3] - dist; break; case iscapsule: dist = -tr->up[i-3]; dist = dist*(tr->capsulesize[(dist<0)?1:2]) - tr->capsulesize[0]; dist = -brushinfo->mins[i-3] - dist; break; case ispoint: dist = -brushinfo->mins[i-3]; break; } d1 = -tr->start[i-3] - dist; d2 = -tr->end[i-3] - dist; } else { switch(tr->shape) { default: case isbox: dist = brushinfo->maxs[i] - tr->mins[i]; break; case iscapsule: dist = tr->up[i]; dist = dist*(tr->capsulesize[(dist<0)?1:2]) - tr->capsulesize[0]; dist = brushinfo->maxs[i] - dist; break; case ispoint: dist = brushinfo->maxs[i]; break; } d1 = (tr->start[i]) - dist; d2 = (tr->end[i]) - dist; } //if we're fully outside any plane, then we cannot possibly enter the brush, skip to the next one if (d1 > 0 && d2 >= d1) return false; if (d1 > 0) startout = true; //if we're fully inside the plane, then whatever is happening is not relevent for this plane if (d1 <= 0 && d2 <= 0) continue; f = (d1) / (d1-d2); if (d1 > d2) { //entered the brush. favour the furthest fraction to avoid extended edges (yay for convex shapes) if (enterfrac < f) { enterfrac = f; nearfrac = (d1 - (0.03125)) / (d1-d2); enterplane = axis[i]; enterdist = dist; } } else { //left the brush, favour the nearest plane (smallest frac) if (exitfrac > f) { exitfrac = f; } } } } if (!startout) { #if 0//def _DEBUG if (tr->debug) { vecV_t facepoints[256]; unsigned int numpoints; for (i = 0; i < numplanes; i++) { scenetris_t *t; extern shader_t *shader_draw_fill; //generate points now (so we know the correct mins+maxs for the brush, and whether the plane is relevent) numpoints = Terr_GenerateBrushFace(facepoints, countof(facepoints), planes, numplanes, planes[i]); if (cl_numstrisvert+numpoints > cl_maxstrisvert) break; if (cl_numstrisidx+(numpoints-2)*3 > cl_maxstrisidx) break; if (cl_numstris == cl_maxstris) { cl_maxstris+=8; cl_stris = BZ_Realloc(cl_stris, sizeof(*cl_stris)*cl_maxstris); } t = &cl_stris[cl_numstris++]; t->shader = shader_draw_fill; t->flags = 0; t->firstidx = cl_numstrisidx; t->firstvert = cl_numstrisvert; for (j = 2; j < numpoints; j++) { cl_strisidx[cl_numstrisidx++] = 0; cl_strisidx[cl_numstrisidx++] = j-1; cl_strisidx[cl_numstrisidx++] = j; } for (j = 0; j < numpoints; j++) { VectorCopy(facepoints[j], cl_strisvertv[cl_numstrisvert]); cl_strisvertv[cl_numstrisvert][2] += 1; Vector4Set(cl_strisvertc[cl_numstrisvert], 1, 0, 0, 0.2); Vector2Set(cl_strisvertt[cl_numstrisvert], 0, 0); cl_numstrisvert++; } t->numidx = cl_numstrisidx - t->firstidx; t->numvert = cl_numstrisvert-t->firstvert; } } #endif tr->startsolid = true; return false; } if (enterfrac != -1 && enterfrac < exitfrac) { //impact! if (enterfrac < tr->truefrac) { if (nearfrac < 0) nearfrac = 0; tr->nearfrac = nearfrac; tr->truefrac = enterfrac; tr->plane[3] = enterdist; VectorCopy(enterplane, tr->plane); #if 0//def _DEBUG if (tr->debug) { vecV_t facepoints[256]; unsigned int numpoints; for (i = 0; i < numplanes; i++) { scenetris_t *t; extern shader_t *shader_draw_fill; //generate points now (so we know the correct mins+maxs for the brush, and whether the plane is relevent) numpoints = Terr_GenerateBrushFace(facepoints, countof(facepoints), planes, numplanes, planes[i]); if (cl_numstrisvert+numpoints > cl_maxstrisvert) break; if (cl_numstrisidx+(numpoints-2)*3 > cl_maxstrisidx) break; if (cl_numstris == cl_maxstris) { cl_maxstris+=8; cl_stris = BZ_Realloc(cl_stris, sizeof(*cl_stris)*cl_maxstris); } t = &cl_stris[cl_numstris++]; t->shader = shader_draw_fill; t->flags = 0; t->firstidx = cl_numstrisidx; t->firstvert = cl_numstrisvert; for (j = 2; j < numpoints; j++) { cl_strisidx[cl_numstrisidx++] = 0; cl_strisidx[cl_numstrisidx++] = j-1; cl_strisidx[cl_numstrisidx++] = j; } for (j = 0; j < numpoints; j++) { VectorCopy(facepoints[j], cl_strisvertv[cl_numstrisvert]); cl_strisvertv[cl_numstrisvert][2] += 1; Vector4Set(cl_strisvertc[cl_numstrisvert], 0, 1, 0, 0.2); Vector2Set(cl_strisvertt[cl_numstrisvert], 0, 0); cl_numstrisvert++; } t->numidx = cl_numstrisidx - t->firstidx; t->numvert = cl_numstrisvert-t->firstvert; } } #endif return ((vec4_t*)enterplane - planes)+1; } } return false; } //sx,sy are the tile coord //note that tile SECTHEIGHTSIZE-1 does not exist, as the last sample overlaps the first sample of the next section static void Heightmap_Trace_Square(hmtrace_t *tr, int tx, int ty) { vec3_t d[2]; vec3_t p[4]; vec4_t n[6]; int i; #ifndef STRICTEDGES float d1, d2; #endif int sx, sy; hmsection_t *s; unsigned int holerow; unsigned int holebit; sx = tx/(SECTHEIGHTSIZE-1); sy = ty/(SECTHEIGHTSIZE-1); if (sx < tr->hm->firstsegx || sx >= tr->hm->maxsegx || sy < tr->hm->firstsegy || sy >= tr->hm->maxsegy) s = NULL; else s = Terr_GetSection(tr->hm, sx, sy, TGS_TRYLOAD|TGS_WAITLOAD|TGS_ANYSTATE); if (!s || s->loadstate != TSLS_LOADED) { if ((tr->hitcontentsmask & tr->hm->exteriorcontents) || (s && s->loadstate != TSLS_FAILED)) { //you're not allowed to walk into sections that have not loaded. //might as well check the entire section instead of just one tile Vector4Set(n[0], 1, 0, 0, (tx/(SECTHEIGHTSIZE-1) + 1 - CHUNKBIAS)*tr->hm->sectionsize); Vector4Set(n[1], -1, 0, 0, -(tx/(SECTHEIGHTSIZE-1) + 0 - CHUNKBIAS)*tr->hm->sectionsize); Vector4Set(n[2], 0, 1, 0, (ty/(SECTHEIGHTSIZE-1) + 1 - CHUNKBIAS)*tr->hm->sectionsize); Vector4Set(n[3], 0, -1, 0, -(ty/(SECTHEIGHTSIZE-1) + 0 - CHUNKBIAS)*tr->hm->sectionsize); Heightmap_Trace_Brush(tr, n, 4, NULL); } return; } if (s->traceseq != tr->hm->traceseq && s->numents) { s->traceseq = tr->hm->traceseq; Sys_LockMutex(tr->hm->entitylock); for (i = 0; i < s->numents; i++) { vec3_t start_l, end_l; trace_t etr; model_t *model; if (s->ents[i]->traceseq == tr->hm->traceseq) continue; s->ents[i]->traceseq = tr->hm->traceseq; model = s->ents[i]->ent.model; //FIXME: IGNORE the entity if it isn't loaded yet? surely that's bad? if (!model || model->loadstate != MLS_LOADED || !model->funcs.NativeTrace) continue; //figure out where on the submodel the trace is. VectorSubtract (tr->start, s->ents[i]->ent.origin, start_l); VectorSubtract (tr->end, s->ents[i]->ent.origin, end_l); // start_l[2] -= tr->mins[2]; // end_l[2] -= tr->mins[2]; VectorScale(start_l, s->ents[i]->ent.scale, start_l); VectorScale(end_l, s->ents[i]->ent.scale, end_l); //skip if the local trace points are outside the model's bounds /* for (j = 0; j < 3; j++) { if (start_l[j]+tr->mins[j] > model->maxs[j] && end_l[j]+tr->mins[j] > model->maxs[j]) continue; if (start_l[j]+tr->maxs[j] < model->mins[j] && end_l[j]+tr->maxs[j] < model->mins[j]) continue; } */ //do the trace memset(&etr, 0, sizeof(etr)); etr.fraction = 1; model->funcs.NativeTrace (model, 0, &s->ents[i]->ent.framestate, s->ents[i]->ent.axis, start_l, end_l, tr->mins, tr->maxs, tr->shape == iscapsule, tr->hitcontentsmask, &etr); if (etr.startsolid) { //many many bsp objects are not enclosed 'properly' (qbsp strips any surfaces outside the world). //this means that such bsps extend to infinity, resulting in sudden glitchy stuck issues when you enter a section containing such a bsp //so if we started solid, constrain that solidity to the volume of the submodel VectorCopy (s->ents[i]->ent.axis[0], n[0]); VectorNegate(s->ents[i]->ent.axis[0], n[1]); VectorCopy (s->ents[i]->ent.axis[1], n[2]); VectorNegate(s->ents[i]->ent.axis[1], n[3]); VectorCopy (s->ents[i]->ent.axis[2], n[4]); VectorNegate(s->ents[i]->ent.axis[2], n[5]); n[0][3] = DotProduct(n[0], s->ents[i]->ent.origin) + model->maxs[0]; n[1][3] = DotProduct(n[1], s->ents[i]->ent.origin) + -model->mins[0]; n[2][3] = DotProduct(n[2], s->ents[i]->ent.origin) + model->maxs[1]; n[3][3] = DotProduct(n[3], s->ents[i]->ent.origin) + -model->mins[1]; n[4][3] = DotProduct(n[4], s->ents[i]->ent.origin) + model->maxs[2]; n[5][3] = DotProduct(n[5], s->ents[i]->ent.origin) + -model->mins[2]; Heightmap_Trace_Brush(tr, n, 6, NULL); } else { tr->result->startsolid |= etr.startsolid; tr->result->allsolid |= etr.allsolid; if (etr.fraction < tr->nearfrac) { tr->contents = etr.contents; tr->truefrac = etr.truefraction; tr->nearfrac = etr.fraction; tr->plane[3] = etr.plane.dist; tr->plane[0] = etr.plane.normal[0]; tr->plane[1] = etr.plane.normal[1]; tr->plane[2] = etr.plane.normal[2]; } } } Sys_UnlockMutex(tr->hm->entitylock); } sx = tx - CHUNKBIAS*(SECTHEIGHTSIZE-1); sy = ty - CHUNKBIAS*(SECTHEIGHTSIZE-1); tx = tx % (SECTHEIGHTSIZE-1); ty = ty % (SECTHEIGHTSIZE-1); holerow = ((ty<<3)/(SECTHEIGHTSIZE-1)); holebit = 1u<<((tx<<3)/(SECTHEIGHTSIZE-1)); if (s->holes[holerow] & holebit) return; //no collision with holes switch(tr->hm->mode) { case HMM_BLOCKS: //left-most Vector4Set(n[0], -1, 0, 0, -tr->htilesize*(sx+0)); //bottom-most Vector4Set(n[1], 0, 1, 0, tr->htilesize*(sy+1)); //right-most Vector4Set(n[2], 1, 0, 0, tr->htilesize*(sx+1)); //top-most Vector4Set(n[3], 0, -1, 0, -tr->htilesize*(sy+0)); //top Vector4Set(n[4], 0, 0, 1, s->heights[(tx+0)+(ty+0)*SECTHEIGHTSIZE]); Heightmap_Trace_Brush(tr, n, 5, NULL); return; case HMM_TERRAIN: VectorSet(p[0], tr->htilesize*(sx+0), tr->htilesize*(sy+0), s->heights[(tx+0)+(ty+0)*SECTHEIGHTSIZE]); VectorSet(p[1], tr->htilesize*(sx+1), tr->htilesize*(sy+0), s->heights[(tx+1)+(ty+0)*SECTHEIGHTSIZE]); VectorSet(p[2], tr->htilesize*(sx+0), tr->htilesize*(sy+1), s->heights[(tx+0)+(ty+1)*SECTHEIGHTSIZE]); VectorSet(p[3], tr->htilesize*(sx+1), tr->htilesize*(sy+1), s->heights[(tx+1)+(ty+1)*SECTHEIGHTSIZE]); VectorSet(n[5], 0, 0, 1); #ifndef STRICTEDGES d1 = fabs(p[0][2] - p[3][2]); d2 = fabs(p[1][2] - p[2][2]); if (d1 < d2) { /*generate the brush (in world space*/ { VectorSubtract(p[3], p[0], d[0]); VectorSubtract(p[2], p[0], d[1]); //left-most Vector4Set(n[0], -1, 0, 0, -tr->htilesize*(sx+0)); //bottom-most Vector4Set(n[1], 0, 1, 0, tr->htilesize*(sy+1)); //top-right VectorSet(n[2], 0.70710678118654752440084436210485, -0.70710678118654752440084436210485, 0); n[2][3] = DotProduct(n[2], p[0]); //top VectorNormalize(d[0]); VectorNormalize(d[1]); CrossProduct(d[0], d[1], n[3]); VectorNormalize(n[3]); n[3][3] = DotProduct(n[3], p[0]); //down VectorNegate(n[3], n[4]); n[4][3] = DotProduct(n[4], p[0]) - n[4][2]*TERRAINTHICKNESS; n[5][3] = max(p[0][2], p[2][2]); n[5][3] = max(n[5][3], p[3][2]); Heightmap_Trace_Brush(tr, n, 6, NULL); } { VectorSubtract(p[3], p[0], d[0]); VectorSubtract(p[3], p[1], d[1]); //right-most Vector4Set(n[0], 1, 0, 0, tr->htilesize*(sx+1)); //top-most Vector4Set(n[1], 0, -1, 0, -tr->htilesize*(sy+0)); //bottom-left VectorSet(n[2], -0.70710678118654752440084436210485, 0.70710678118654752440084436210485, 0); n[2][3] = DotProduct(n[2], p[0]); //top VectorNormalize(d[0]); VectorNormalize(d[1]); CrossProduct(d[0], d[1], n[3]); VectorNormalize(n[3]); n[3][3] = DotProduct(n[3], p[0]); //down VectorNegate(n[3], n[4]); n[4][3] = DotProduct(n[4], p[0]) - n[4][2]*TERRAINTHICKNESS; n[5][3] = max(p[0][2], p[1][2]); n[5][3] = max(n[5][3], p[3][2]); Heightmap_Trace_Brush(tr, n, 6, NULL); } } else #endif { /*generate the brush (in world space*/ { VectorSubtract(p[1], p[0], d[0]); VectorSubtract(p[2], p[0], d[1]); //left-most Vector4Set(n[0], -1, 0, 0, -tr->htilesize*(sx+0)); //top-most Vector4Set(n[1], 0, -1, 0, -tr->htilesize*(sy+0)); //bottom-right VectorSet(n[2], 0.70710678118654752440084436210485, 0.70710678118654752440084436210485, 0); n[2][3] = DotProduct(n[2], p[1]); //top VectorNormalize(d[0]); VectorNormalize(d[1]); CrossProduct(d[0], d[1], n[3]); VectorNormalize(n[3]); n[3][3] = DotProduct(n[3], p[1]); //down VectorNegate(n[3], n[4]); n[4][3] = DotProduct(n[4], p[1]) - n[4][2]*TERRAINTHICKNESS; n[5][3] = max(p[0][2], p[1][2]); n[5][3] = max(n[5][3], p[2][2]); Heightmap_Trace_Brush(tr, n, 6, NULL); } { VectorSubtract(p[3], p[2], d[0]); VectorSubtract(p[3], p[1], d[1]); //right-most Vector4Set(n[0], 1, 0, 0, tr->htilesize*(sx+1)); //bottom-most Vector4Set(n[1], 0, 1, 0, tr->htilesize*(sy+1)); //top-left VectorSet(n[2], -0.70710678118654752440084436210485, -0.70710678118654752440084436210485, 0); n[2][3] = DotProduct(n[2], p[1]); //top VectorNormalize(d[0]); VectorNormalize(d[1]); CrossProduct(d[0], d[1], n[3]); VectorNormalize(n[3]); n[3][3] = DotProduct(n[3], p[1]); //down VectorNegate(n[3], n[4]); n[4][3] = DotProduct(n[4], p[1]) - n[4][2]*TERRAINTHICKNESS; n[5][3] = max(p[1][2], p[2][2]); n[5][3] = max(n[5][3], p[3][2]); Heightmap_Trace_Brush(tr, n, 6, NULL); } } break; } } #define DIST_EPSILON 0 /* Heightmap_TraceRecurse Traces an arbitary box through a heightmap. (interface with outside) Why is recursion good? 1: it is consistant with bsp models. :) 2: it allows us to use any size model we want 3: we don't have to work out the height of the terrain every X units, but can be more precise. Obviously, we don't care all that much about 1 */ qboolean Heightmap_Trace(struct model_s *model, int hulloverride, framestate_t *framestate, vec3_t mataxis[3], vec3_t start, vec3_t end, vec3_t mins, vec3_t maxs, qboolean capsule, unsigned int against, struct trace_s *trace) { vec2_t pos; vec2_t frac; vec2_t emins; vec2_t emaxs; vec3_t tmp; int ipos[2], npos[2]; int x, y, e; int axis; int breaklimit = 1000; float zbias; hmtrace_t hmtrace; hmtrace.hm = model->terrain; hmtrace.hm->traceseq++; hmtrace.htilesize = hmtrace.hm->sectionsize / (SECTHEIGHTSIZE-1); hmtrace.nearfrac = hmtrace.truefrac = 1; hmtrace.contents = 0; hmtrace.hitcontentsmask = against; hmtrace.plane[0] = 0; hmtrace.plane[1] = 0; hmtrace.plane[2] = 0; hmtrace.plane[3] = 0; if (capsule) { hmtrace.shape = iscapsule; zbias = 0; if (mataxis) VectorSet(hmtrace.up, mataxis[0][2], -mataxis[1][2], mataxis[2][2]); else VectorSet(hmtrace.up, 0, 0, 1); //determine the capsule sizes hmtrace.capsulesize[0] = ((maxs[0]-mins[0]) + (maxs[1]-mins[1]))/4.0; hmtrace.capsulesize[1] = maxs[2]; hmtrace.capsulesize[2] = mins[2]; // zbias = (trace_capsulesize[1] > -hmtrace.capsulesize[2])?hmtrace.capsulesize[1]:-hmtrace.capsulesize[2]; hmtrace.capsulesize[1] -= hmtrace.capsulesize[0]; hmtrace.capsulesize[2] += hmtrace.capsulesize[0]; zbias = 0; } else if (mins[0] || mins[1] || mins[2] || maxs[0] || maxs[1] || maxs[2]) { hmtrace.shape = isbox; zbias = 0; } else { hmtrace.shape = ispoint; zbias = mins[2]; } memset(trace, 0, sizeof(*trace)); hmtrace.result = trace; hmtrace.startsolid = false; //to tile space hmtrace.start[0] = (start[0]); hmtrace.start[1] = (start[1]); hmtrace.start[2] = (start[2] + zbias); hmtrace.end[0] = (end[0]); hmtrace.end[1] = (end[1]); hmtrace.end[2] = (end[2] + zbias); // mins = vec3_origin; // maxs = vec3_origin; VectorCopy(mins, hmtrace.mins); VectorCopy(maxs, hmtrace.maxs); //determine extents VectorAdd(hmtrace.start, hmtrace.mins, hmtrace.absmins); VectorCopy(hmtrace.absmins, hmtrace.absmaxs); VectorAdd(hmtrace.start, hmtrace.maxs, tmp); AddPointToBounds (tmp, hmtrace.absmins, hmtrace.absmaxs); VectorAdd(hmtrace.end, hmtrace.mins, tmp); AddPointToBounds (tmp, hmtrace.absmins, hmtrace.absmaxs); VectorAdd(hmtrace.end, hmtrace.maxs, tmp); AddPointToBounds (tmp, hmtrace.absmins, hmtrace.absmaxs); hmtrace.absmaxs[0] += 1; hmtrace.absmaxs[1] += 1; hmtrace.absmaxs[2] += 1; hmtrace.absmins[0] -= 1; hmtrace.absmins[1] -= 1; hmtrace.absmins[2] -= 1; //figure out where we are in terms of tiles pos[0] = (hmtrace.start[0]+CHUNKBIAS*hmtrace.hm->sectionsize)/hmtrace.htilesize; pos[1] = (hmtrace.start[1]+CHUNKBIAS*hmtrace.hm->sectionsize)/hmtrace.htilesize; emins[0] = (mins[0]-1.5)/hmtrace.htilesize; emins[1] = (mins[1]-1.5)/hmtrace.htilesize; emaxs[0] = (maxs[0]+1.5)/hmtrace.htilesize; emaxs[1] = (maxs[1]+1.5)/hmtrace.htilesize; //Test code if (0) { vec2_t minb, maxb; Vector2Copy(pos, minb); Vector2Copy(pos, maxb); npos[0] = (hmtrace.end[0]+CHUNKBIAS*hmtrace.hm->sectionsize)/hmtrace.htilesize; npos[1] = (hmtrace.end[1]+CHUNKBIAS*hmtrace.hm->sectionsize)/hmtrace.htilesize; if (npos[0] > pos[0]) maxb[0] = pos[0]; else minb[0] = pos[0]; if (npos[1] > pos[1]) maxb[1] = pos[1]; else minb[1] = pos[1]; minb[0] += emins[0]; minb[1] += emins[1]; maxb[0] += emaxs[0]; maxb[1] += emaxs[1]; for (y = floor(minb[1]); y <= ceil(maxb[1]); y++) for (x = floor(minb[0]); x <= ceil(maxb[0]); x++) Heightmap_Trace_Square(&hmtrace, x, y); } //make sure the start tile is valid for (y = floor(pos[1] + emins[1]); y <= ceil(pos[1] + emaxs[1]); y++) for (x = floor(pos[0] + emins[0]); x <= ceil(pos[0] + emaxs[0]); x++) Heightmap_Trace_Square(&hmtrace, x, y); //now walk over the terrain if (hmtrace.end[0] != hmtrace.start[0] || hmtrace.end[1] != hmtrace.start[1]) { vec2_t dir, trstart, trdist; //figure out the leading point for (axis = 0; axis < 2; axis++) { trdist[axis] = hmtrace.end[axis]-hmtrace.start[axis]; dir[axis] = (hmtrace.end[axis] - hmtrace.start[axis])/hmtrace.htilesize; if (dir[axis] > 0) { ipos[axis] = pos[axis] + emins[axis]; trstart[axis] = CHUNKBIAS*hmtrace.hm->sectionsize + (maxs[axis]) + hmtrace.start[axis]; } else { ipos[axis] = pos[axis] + emaxs[axis]; trstart[axis] = CHUNKBIAS*hmtrace.hm->sectionsize + (mins[axis]) + hmtrace.start[axis]; } trstart[axis] /= hmtrace.htilesize; trdist[axis] /= hmtrace.htilesize; } for(;;) { if (breaklimit--< 0) break; for (axis = 0; axis < 2; axis++) { if (dir[axis] > 0) { npos[axis] = ipos[axis]+1; frac[axis] = (npos[axis]-trstart[axis])/trdist[axis]; } else if (dir[axis] < 0) { npos[axis] = ipos[axis]; frac[axis] = (ipos[axis]-trstart[axis])/trdist[axis]; } else frac[axis] = 1000000000000000.0; } //which side are we going down? if (frac[0] < frac[1]) axis = 0; else axis = 1; if (frac[axis] >= 1) break; //progress to the crossed boundary if (dir[axis] < 0) ipos[axis] = ipos[axis]-1; else ipos[axis] = ipos[axis]+1; axis = !axis; if (dir[axis] > 0) { //leading edge is on the right, so start on the left and keep going until we hit the leading edge npos[0] = ipos[0]; npos[1] = ipos[1]; npos[axis] -= ceil(emins[axis]-emaxs[axis]); e = ipos[axis]; npos[axis] -= 1; e++; for (; npos[axis] <= e; npos[axis]++) Heightmap_Trace_Square(&hmtrace, npos[0], npos[1]); } else { //leading edge is on the left npos[0] = ipos[0]; npos[1] = ipos[1]; e = ipos[axis] + ceil(emaxs[axis]-emins[axis]); npos[axis] -= 1; e++; for (; npos[axis] <= e; npos[axis]++) Heightmap_Trace_Square(&hmtrace, npos[0], npos[1]); } // axis = !axis; //and make sure our position on the other axis is correct, for the next time around the loop // if (frac[axis] > hmtrace.truefrac) // break; } } //FIXME: optimise into the section grid { brushes_t *brushes = hmtrace.hm->wbrushes; int count = hmtrace.hm->numbrushes; for (count = hmtrace.hm->numbrushes; count-->0; brushes++) { if (brushes->contents & against) { int face; if (hmtrace.absmaxs[0] < brushes->mins[0] || hmtrace.absmaxs[1] < brushes->mins[1] || hmtrace.absmaxs[2] < brushes->mins[2]) continue; if (hmtrace.absmins[0] > brushes->maxs[0] || hmtrace.absmins[1] > brushes->maxs[1] || hmtrace.absmins[2] > brushes->maxs[2]) continue; face = Heightmap_Trace_Brush(&hmtrace, brushes->planes, brushes->numplanes, brushes); if (face) { trace->brush_id = brushes->id; trace->brush_face = face; } } } } trace->plane.dist = hmtrace.plane[3]; trace->plane.normal[0] = hmtrace.plane[0]; trace->plane.normal[1] = hmtrace.plane[1]; trace->plane.normal[2] = hmtrace.plane[2]; trace->startsolid = trace->allsolid = hmtrace.startsolid; if (hmtrace.nearfrac < 0) hmtrace.nearfrac = 0; trace->fraction = hmtrace.nearfrac; trace->truefraction = hmtrace.truefrac; VectorInterpolate(start, hmtrace.nearfrac, end, trace->endpos); return trace->fraction < 1; } qboolean Heightmap_Trace_Test(struct model_s *model, int hulloverride, framestate_t *framestate, vec3_t mataxis[3], vec3_t start, vec3_t end, vec3_t mins, vec3_t maxs, qboolean capsule, unsigned int against, struct trace_s *trace) { qboolean ret = Heightmap_Trace(model, hulloverride, framestate, mataxis, start, end, mins, maxs, capsule, against, trace); if (!trace->startsolid) { trace_t testtrace; Heightmap_Trace(model, hulloverride, framestate, mataxis, trace->endpos, trace->endpos, mins, maxs, capsule, against, &testtrace); if (testtrace.startsolid) { Con_DPrintf("Trace became solid\n"); trace->fraction = 0; VectorCopy(start, trace->endpos); trace->startsolid = trace->allsolid = true; } } return ret; } typedef struct { int id; int pos[3]; } hmpvs_t; typedef struct { int id; int min[3], max[3]; } hmpvsent_t; unsigned int Heightmap_FatPVS (model_t *mod, vec3_t org, pvsbuffer_t *pvsbuffer, qboolean add) { //embed the org onto the pvs hmpvs_t *hmpvs; if (pvsbuffer->buffersize < sizeof(*hmpvs)) pvsbuffer->buffer = BZ_Realloc(pvsbuffer->buffer, pvsbuffer->buffersize=sizeof(*hmpvs)); hmpvs = (hmpvs_t*)pvsbuffer->buffer; hmpvs->id = 0xdeadbeef; VectorCopy(org, hmpvs->pos); return sizeof(*hmpvs); } #ifndef CLIENTONLY qboolean Heightmap_EdictInFatPVS (model_t *mod, struct pvscache_s *edict, qbyte *pvsdata) { heightmap_t *hm = mod->terrain; int o[3], i; hmpvs_t *hmpvs = (hmpvs_t*)pvsdata; hmpvsent_t *hmed = (hmpvsent_t*)edict; if (!hm->culldistance) return true; //check distance for (i = 0; i < 3; i++) { if (hmpvs->pos[i] < hmed->min[i]) o[i] = hmed->min[i] - hmpvs->pos[i]; else if (hmpvs->pos[i] > hmed->max[i]) o[i] = hmed->max[i] - hmpvs->pos[i]; else o[i] = 0; } return DotProduct(o,o) < hm->culldistance; } void Heightmap_FindTouchedLeafs (model_t *mod, pvscache_t *ent, float *mins, float *maxs) { hmpvsent_t *hmed = (hmpvsent_t*)ent; VectorCopy(mins, hmed->min); VectorCopy(maxs, hmed->max); } #endif void Heightmap_LightPointValues (model_t *mod, vec3_t point, vec3_t res_diffuse, vec3_t res_ambient, vec3_t res_dir) { res_diffuse[0] = 128; res_diffuse[1] = 128; res_diffuse[2] = 128; res_ambient[0] = 64; res_ambient[1] = 64; res_ambient[2] = 64; res_dir[0] = 1;//sin(time); res_dir[1] = 0;//cos(time); res_dir[2] = 0;//sin(time); VectorNormalize(res_dir); } void Heightmap_StainNode (mnode_t *node, float *parms) { } void Heightmap_MarkLights (dlight_t *light, int bit, mnode_t *node) { } qbyte *Heightmap_ClusterPVS (model_t *model, int num, pvsbuffer_t *buffer, pvsmerge_t merge) { return NULL; // static qbyte heightmappvs = 255; // return &heightmappvs; } int Heightmap_ClusterForPoint (model_t *model, vec3_t point) { return -1; } #ifndef SERVERONLY static unsigned char *QDECL Terr_GetLightmap(hmsection_t *s, int idx, qboolean edit) { unsigned char *lm; int x = idx % SECTTEXSIZE, y = idx / SECTTEXSIZE; if (s->lightmap < 0) { Terr_LoadSection(s->hmmod, s, s->sx, s->sy, true); Terr_InitLightmap(s, true); } if (s->lightmap < 0) return NULL; if (edit) { s->flags |= TSF_EDITED; lightmap[s->lightmap]->modified = true; lightmap[s->lightmap]->rectchange.l = 0; lightmap[s->lightmap]->rectchange.t = 0; lightmap[s->lightmap]->rectchange.r = HMLMSTRIDE; lightmap[s->lightmap]->rectchange.b = HMLMSTRIDE; } lm = lightmap[s->lightmap]->lightmaps; lm += ((s->lmy+y) * HMLMSTRIDE + (s->lmx+x)) * lightmap_bytes; return lm; } static void ted_dorelight(heightmap_t *hm) { unsigned char *lm = Terr_GetLightmap(hm->relight, 0, true); int x, y; #define EXPAND 2 vec3_t surfnorms[(SECTTEXSIZE+EXPAND*2)*(SECTTEXSIZE+EXPAND*2)]; // float scaletab[EXPAND*2*EXPAND*2]; vec3_t ldir = {0.4, 0.7, 2}; hmsection_t *s = hm->relight; s->flags &= ~TSF_RELIGHT; hm->relight = NULL; if (s->lightmap < 0) return; for (y = -EXPAND; y < SECTTEXSIZE+EXPAND; y++) for (x = -EXPAND; x < SECTTEXSIZE+EXPAND; x++) { vec3_t pos; pos[0] = hm->relightmin[0] + (x*hm->sectionsize/(SECTTEXSIZE-1)); pos[1] = hm->relightmin[1] + (y*hm->sectionsize/(SECTTEXSIZE-1)); pos[2] = 0; Heightmap_Normal(s->hmmod, pos, surfnorms[x+EXPAND + (y+EXPAND)*(SECTTEXSIZE+EXPAND*2)]); } VectorNormalize(ldir); for (y = 0; y < SECTTEXSIZE; y++, lm += (HMLMSTRIDE-SECTTEXSIZE)*4) for (x = 0; x < SECTTEXSIZE; x++, lm += 4) { vec3_t norm; float d; int sx,sy; VectorClear(norm); for (sy = -EXPAND; sy <= EXPAND; sy++) for (sx = -EXPAND; sx <= EXPAND; sx++) { d = sqrt((EXPAND*2+1)*(EXPAND*2+1) - sx*sx+sy*sy); VectorMA(norm, d, surfnorms[x+sx+EXPAND + (y+sy+EXPAND)*(SECTTEXSIZE+EXPAND*2)], norm); } VectorNormalize(norm); d = DotProduct(ldir, norm); if (d < 0) d = 0; // lm[0] = norm[0]*127 + 128; // lm[1] = norm[1]*127 + 128; // lm[2] = norm[2]*127 + 128; lm[3] = 127 + d*128; } lightmap[s->lightmap]->modified = true; lightmap[s->lightmap]->rectchange.l = 0; lightmap[s->lightmap]->rectchange.t = 0; lightmap[s->lightmap]->rectchange.r = HMLMSTRIDE; lightmap[s->lightmap]->rectchange.b = HMLMSTRIDE; } static void ted_sethole(void *ctx, hmsection_t *s, int idx, float wx, float wy, float w) { unsigned int row = idx/9; unsigned int col = idx%9; unsigned int bit; unsigned int mask; if (row == 8 || col == 8) return; //meh, our painting function is written with an overlap of 1 if (w <= 0) return; mask = 1u<<(col); if (*(float*)ctx > 0) bit = mask; else bit = 0; s->flags |= TSF_NOTIFY|TSF_DIRTY|TSF_EDITED; s->holes[row] = (s->holes[row] & ~mask) | bit; } static void ted_heighttally(void *ctx, hmsection_t *s, int idx, float wx, float wy, float w) { /*raise the terrain*/ ((float*)ctx)[0] += s->heights[idx]*w; ((float*)ctx)[1] += w; } static void ted_heightsmooth(void *ctx, hmsection_t *s, int idx, float wx, float wy, float w) { s->flags |= TSF_NOTIFY|TSF_DIRTY|TSF_EDITED|TSF_RELIGHT; /*interpolate the terrain towards a certain value*/ if (IS_NAN(s->heights[idx])) s->heights[idx] = *(float*)ctx; else s->heights[idx] = s->heights[idx]*(1-w) + w**(float*)ctx; } static void ted_heightdebug(void *ctx, hmsection_t *s, int idx, float wx, float wy, float w) { int tx = idx/SECTHEIGHTSIZE, ty = idx % SECTHEIGHTSIZE; s->flags |= TSF_NOTIFY|TSF_DIRTY|TSF_EDITED|TSF_RELIGHT; /*interpolate the terrain towards a certain value*/ if (tx == 16) tx = 0; if (ty == 16) ty = 0; // if (ty < tx) // tx = ty; s->heights[idx] = (tx>>1) * 32 + (ty>>1) * 32; } static void ted_heightraise(void *ctx, hmsection_t *s, int idx, float wx, float wy, float strength) { s->flags |= TSF_NOTIFY|TSF_DIRTY|TSF_EDITED|TSF_RELIGHT; /*raise the terrain*/ s->heights[idx] += strength; } static void ted_heightset(void *ctx, hmsection_t *s, int idx, float wx, float wy, float strength) { s->flags |= TSF_NOTIFY|TSF_DIRTY|TSF_EDITED|TSF_RELIGHT; /*set the terrain to a specific value*/ s->heights[idx] = *(float*)ctx; } static void ted_waterset(void *ctx, hmsection_t *s, int idx, float wx, float wy, float strength) { struct hmwater_s *w = s->water; if (!w) w = Terr_GenerateWater(s, *(float*)ctx); s->flags |= TSF_NOTIFY|TSF_DIRTY|TSF_EDITED; //FIXME: water doesn't render properly. don't let people make dodgy water regions because they can't see it. //this is temp code. //for (idx = 0; idx < 9*9; idx++) //w->heights[idx] = *(float*)ctx; //end fixme w->heights[idx] = *(float*)ctx; if (w->minheight > w->heights[idx]) w->minheight = w->heights[idx]; if (w->maxheight < w->heights[idx]) w->maxheight = w->heights[idx]; //FIXME: what about holes? } static void ted_texconcentrate(void *ctx, hmsection_t *s, int idx, float wx, float wy, float w) { unsigned char *lm = Terr_GetLightmap(s, idx, true); s->flags |= TSF_NOTIFY|TSF_EDITED; /*concentrate the lightmap values to a single channel*/ if (lm[0] > lm[1] && lm[0] > lm[2] && lm[0] > (255-(lm[0]+lm[1]+lm[2]))) { lm[0] = lm[0]*(1-w) + 255*(w); lm[1] = lm[1]*(1-w) + 0*(w); lm[2] = lm[2]*(1-w) + 0*(w); } else if (lm[1] > lm[2] && lm[1] > (255-(lm[0]+lm[1]+lm[2]))) { lm[0] = lm[0]*(1-w) + 0*(w); lm[1] = lm[1]*(1-w) + 255*(w); lm[2] = lm[2]*(1-w) + 0*(w); } else if (lm[2] > (255-(lm[0]+lm[1]+lm[2]))) { lm[0] = lm[0]*(1-w) + 0*(w); lm[1] = lm[1]*(1-w) + 0*(w); lm[2] = lm[2]*(1-w) + 255*(w); } else { lm[0] = lm[0]*(1-w) + 0*(w); lm[1] = lm[1]*(1-w) + 0*(w); lm[2] = lm[2]*(1-w) + 0*(w); } } static void ted_texnoise(void *ctx, hmsection_t *s, int idx, float wx, float wy, float w) { unsigned char *lm = Terr_GetLightmap(s, idx, true); vec4_t v; float sc; s->flags |= TSF_NOTIFY|TSF_EDITED; /*randomize the lightmap somewhat (you'll probably want to concentrate it a bit after)*/ v[0] = (rand()&255); v[1] = (rand()&255); v[2] = (rand()&255); v[3] = (rand()&255); sc = v[0] + v[1] + v[2] + v[3]; Vector4Scale(v, 255/sc, v); lm[0] = lm[0]*(1-w) + (v[0]*(w)); lm[1] = lm[1]*(1-w) + (v[1]*(w)); lm[2] = lm[2]*(1-w) + (v[2]*(w)); } static void ted_texpaint(void *ctx, hmsection_t *s, int idx, float wx, float wy, float w) { unsigned char *lm = Terr_GetLightmap(s, idx, true); const char *texname = ctx; int t; vec4_t newval; if (w > 1) w = 1; s->flags |= TSF_NOTIFY|TSF_EDITED; for (t = 0; t < 4; t++) { if (!strncmp(s->texname[t], texname, sizeof(s->texname[t])-1)) { int extra; newval[0] = (t == 0); newval[1] = (t == 1); newval[2] = (t == 2); newval[3] = (t == 3); extra = 255 - (lm[0]+lm[1]+lm[2]); lm[2] = lm[2]*(1-w) + (255*newval[0]*(w)); lm[1] = lm[1]*(1-w) + (255*newval[1]*(w)); lm[0] = lm[0]*(1-w) + (255*newval[2]*(w)); extra = extra*(1-w) + (255*newval[3]*(w)); //the extra stuff is to cope with numerical precision. add any lost values to the new texture instead of the implicit one extra = 255 - (extra+lm[0]+lm[1]+lm[2]); if (t != 3) lm[2-t] += extra; return; } } /*special handling to make a section accept the first texture painted on it as a base texture. no more chessboard*/ if (!*s->texname[0] && !*s->texname[1] && !*s->texname[2] && !*s->texname[3]) { Q_strncpyz(s->texname[3], texname, sizeof(s->texname[3])); Terr_LoadSectionTextures(s); for (idx = 0; idx < SECTTEXSIZE*SECTTEXSIZE; idx++) { lm = Terr_GetLightmap(s, idx, true); lm[2] = 0; lm[1] = 0; lm[0] = 0; } return; } for (t = 0; t < 4; t++) { if (!*s->texname[t]) { Q_strncpyz(s->texname[t], texname, sizeof(s->texname[t])); newval[0] = (t == 0); newval[1] = (t == 1); newval[2] = (t == 2); lm[2] = lm[2]*(1-w) + (255*newval[0]*(w)); lm[1] = lm[1]*(1-w) + (255*newval[1]*(w)); lm[0] = lm[0]*(1-w) + (255*newval[2]*(w)); Terr_LoadSectionTextures(s); return; } } } static void ted_texreplace(void *ctx, hmsection_t *s, int idx, float wx, float wy, float w) { if (w > 0) ted_texpaint(ctx, s, idx, wx, wy, 1); } /* static void ted_texlight(void *ctx, hmsection_t *s, int idx, float wx, float wy, float w) { unsigned char *lm = ted_getlightmap(s, idx); vec3_t pos, pos2; vec3_t norm, tnorm; vec3_t ldir = {0.4, 0.7, 2}; float d; int x,y; trace_t tr; VectorClear(norm); for (y = -4; y < 4; y++) for (x = -4; x < 4; x++) { pos[0] = wx - (CHUNKBIAS + x/64.0) * s->hmmod->sectionsize; pos[1] = wy - (CHUNKBIAS + y/64.0) * s->hmmod->sectionsize; #if 0 pos[2] = 10000; pos2[0] = wx - (CHUNKBIAS + x/64.0) * s->hmmod->sectionsize; pos2[1] = wy - (CHUNKBIAS + y/64.0) * s->hmmod->sectionsize; pos2[2] = -10000; Heightmap_Trace(cl.worldmodel, 0, 0, NULL, pos, pos2, vec3_origin, vec3_origin, FTECONTENTS_SOLID, &tr); VectorCopy(tr.plane.normal, tnorm); #else Heightmap_Normal(s->hmmod, pos, tnorm); #endif d = sqrt(32 - x*x+y*y); VectorMA(norm, d, tnorm, norm); } VectorNormalize(ldir); VectorNormalize(norm); d = DotProduct(ldir, norm); if (d < 0) d = 0; lm[3] = d*255; } */ static void ted_texset(void *ctx, hmsection_t *s, int idx, float wx, float wy, float w) { unsigned char *lm = Terr_GetLightmap(s, idx, true); if (w > 1) w = 1; s->flags |= TSF_NOTIFY|TSF_EDITED; lm[2] = lm[2]*(1-w) + (255*((float*)ctx)[0]*(w)); lm[1] = lm[1]*(1-w) + (255*((float*)ctx)[1]*(w)); lm[0] = lm[0]*(1-w) + (255*((float*)ctx)[2]*(w)); } static void ted_textally(void *ctx, hmsection_t *s, int idx, float wx, float wy, float w) { unsigned char *lm = Terr_GetLightmap(s, idx, false); ((float*)ctx)[0] += lm[0]*w; ((float*)ctx)[1] += lm[1]*w; ((float*)ctx)[2] += lm[2]*w; ((float*)ctx)[3] += w; } static void ted_tint(void *ctx, hmsection_t *s, int idx, float wx, float wy, float w) { float *col = s->colours[idx]; float *newval = ctx; if (w > 1) w = 1; s->flags |= TSF_NOTIFY|TSF_DIRTY|TSF_EDITED|TSF_HASCOLOURS; /*dirty because of the vbo*/ col[0] = col[0]*(1-w) + (newval[0]*(w)); col[1] = col[1]*(1-w) + (newval[1]*(w)); col[2] = col[2]*(1-w) + (newval[2]*(w)); col[3] = col[3]*(1-w) + (newval[3]*(w)); } enum { tid_linear, tid_exponential, tid_square_linear, tid_square_exponential, tid_flat }; //calls 'func' for each tile upon the terrain. the 'tile' can be either height or texel static void ted_itterate(heightmap_t *hm, int distribution, float *pos, float radius, float strength, int steps, void(*func)(void *ctx, hmsection_t *s, int idx, float wx, float wy, float strength), void *ctx) { int tx, ty; float wx, wy; float sc[2]; int min[2], max[2]; int sx,sy; hmsection_t *s; float w, xd, yd; if (radius < 0) { radius *= -1; distribution |= 2; } min[0] = floor((pos[0] - radius)/(hm->sectionsize) - 1.5); min[1] = floor((pos[1] - radius)/(hm->sectionsize) - 1.5); max[0] = ceil((pos[0] + radius)/(hm->sectionsize) + 1.5); max[1] = ceil((pos[1] + radius)/(hm->sectionsize) + 1.5); min[0] = bound(hm->firstsegx, min[0], hm->maxsegx); min[1] = bound(hm->firstsegy, min[1], hm->maxsegy); max[0] = bound(hm->firstsegx, max[0], hm->maxsegx); max[1] = bound(hm->firstsegy, max[1], hm->maxsegy); sc[0] = hm->sectionsize/(steps-1); sc[1] = hm->sectionsize/(steps-1); for (sy = min[1]; sy < max[1]; sy++) { for (sx = min[0]; sx < max[0]; sx++) { s = Terr_GetSection(hm, sx, sy, TGS_WAITLOAD|TGS_DEFAULTONFAIL); if (!s) continue; for (ty = 0; ty < steps; ty++) { wy = (sy*(steps-1.0) + ty)*sc[1]; yd = wy - pos[1];// - sc[1]/4; for (tx = 0; tx < steps; tx++) { /*both heights and textures have an overlapping/matching sample at the edge, there's no need for any half-pixels or anything here*/ wx = (sx*(steps-1.0) + tx)*sc[0]; xd = wx - pos[0];// - sc[0]/4; switch(distribution) { case tid_exponential: w = radius*radius - (xd*xd+yd*yd); if (w > 0) func(ctx, s, tx+ty*steps, wx, wy, sqrt(w)*strength/(radius)); break; case tid_linear: w = radius - sqrt(xd*xd+yd*yd); if (w > 0) func(ctx, s, tx+ty*steps, wx, wy, w*strength/(radius)); break; case tid_square_exponential: w = max(fabs(xd), fabs(yd)); w = radius*radius - w*w; if (w > 0) func(ctx, s, tx+ty*steps, wx, wy, sqrt(w)*strength/(radius)); break; case tid_square_linear: w = max(fabs(xd), fabs(yd)); w = radius - w; if (w > 0) func(ctx, s, tx+ty*steps, wx, wy, w*strength/(radius)); break; case tid_flat: w = max(fabs(xd), fabs(yd)); w = radius - w; if (w > 0) func(ctx, s, tx+ty*steps, wx, wy, strength); break; } } } } } } void ted_texkill(hmsection_t *s, const char *killtex) { int x, y, t, to; if (!s) return; for (t = 0; t < 4; t++) { if (!strcmp(s->texname[t], killtex)) { unsigned char *lm = Terr_GetLightmap(s, 0, true); s->flags |= TSF_EDITED; s->texname[t][0] = 0; for (to = 0; to < 4; to++) if (*s->texname[to]) break; if (to == 4) to = 0; if (to == 0 || to == 2) to = 2 - to; if (t == 0 || t == 2) t = 2 - t; for (y = 0; y < SECTTEXSIZE; y++) { for (x = 0; x < SECTTEXSIZE; x++, lm+=4) { if (t == 3) { //to won't be 3 lm[to] = lm[to] + (255 - (lm[0] + lm[1] + lm[2])); } else { if (to != 3) lm[to] = (lm[to]+lm[t])&0xff; lm[t] = 0; } } lm += SECTTEXSIZE*4*(LMCHUNKS-1); } if (t == 0 || t == 2) t = 2 - t; Terr_LoadSectionTextures(s); } } } void QCBUILTIN PF_terrain_edit(pubprogfuncs_t *prinst, struct globalvars_s *pr_globals) { world_t *vmw = prinst->parms->user; int action = G_FLOAT(OFS_PARM0); vec3_t pos;// G_VECTOR(OFS_PARM1); float radius = G_FLOAT(OFS_PARM2); float quant = G_FLOAT(OFS_PARM3); int modelindex = ((wedict_t*)PROG_TO_EDICT(prinst, *vmw->g.self))->v->modelindex; // G_FLOAT(OFS_RETURN) = Heightmap_Edit(w->worldmodel, action, pos, radius, quant); model_t *mod = vmw->Get_CModel(vmw, modelindex); heightmap_t *hm; vec4_t tally; G_FLOAT(OFS_RETURN) = 0; if (!mod) return; if (!mod->terrain) { if (mod->loadstate == MLS_LOADING) COM_WorkerPartialSync(mod, &mod->loadstate, MLS_LOADING); } if (mod->loadstate != MLS_LOADED) return; switch(action) { case ter_ent_get: { int idx = G_INT(OFS_PARM1); if (!mod->numentityinfo) Mod_ParseEntities(mod); if (idx >= mod->numentityinfo || !mod->entityinfo[idx].keyvals) G_INT(OFS_RETURN) = 0; else G_INT(OFS_RETURN) = PR_TempString(prinst, mod->entityinfo[idx].keyvals); } return; case ter_ent_set: { int idx = G_INT(OFS_PARM1); int id; const char *newvals; if (idx >= MAX_EDICTS) //we need some sanity limit... many ents will get removed like lights so this one isn't quite correct, but it'll be in the right sort of ballpark. { G_INT(OFS_RETURN) = 0; return; } //if there's no ents, then that's a problem. make sure that there's at least a worldspawn. if (!mod->numentityinfo) Mod_ParseEntities(mod); //make sure we don't have any cached entities string, by wiping it all. Z_Free((char*)mod->entities_raw); mod->entities_raw = NULL; G_INT(OFS_RETURN) = 0; if (idx < mod->numentityinfo) { if (!G_INT(OFS_PARM2) && !mod->entityinfo[idx].keyvals) return; //no-op Z_Free(mod->entityinfo[idx].keyvals); mod->entityinfo[idx].keyvals = NULL; id = mod->entityinfo[idx].id; } else id = 0; if (G_INT(OFS_PARM2)) { newvals = PR_GetStringOfs(prinst, OFS_PARM2); if (idx >= mod->numentityinfo) Z_ReallocElements((void**)&mod->entityinfo, &mod->numentityinfo, idx+64, sizeof(*mod->entityinfo)); mod->entityinfo[idx].keyvals = Z_StrDup(newvals); if (!id) id = (idx+1) | ((cl.playerview[0].playernum+1)<<24); mod->entityinfo[idx].id = id; } else { newvals = NULL; if (idx < mod->numentityinfo) mod->entityinfo[idx].id = 0; } #ifndef CLIENTONLY if (sv.state && modelindex > 0) { MSG_WriteByte(&sv.multicast, svcfte_brushedit); MSG_WriteShort(&sv.multicast, modelindex); MSG_WriteByte(&sv.multicast, newvals?hmcmd_ent_edit:hmcmd_ent_remove); MSG_WriteLong(&sv.multicast, id); if (newvals) MSG_WriteString(&sv.multicast, newvals); SV_MulticastProtExt(vec3_origin, MULTICAST_ALL_R, ~0, 0, 0); //tell ssqc, csqc will be told by the server SSQC_MapEntityEdited(modelindex, idx, newvals); } else #endif #ifndef SERVERONLY if (cls.state && modelindex > 0) { MSG_WriteByte(&cls.netchan.message, clcfte_brushedit); MSG_WriteShort(&cls.netchan.message, modelindex); MSG_WriteByte(&cls.netchan.message, newvals?hmcmd_ent_edit:hmcmd_ent_remove); MSG_WriteLong(&cls.netchan.message, id); if (newvals) MSG_WriteString(&cls.netchan.message, newvals); #ifdef CSQC_DAT CSQC_MapEntityEdited(modelindex, idx, newvals); #endif } else #endif { #ifdef CSQC_DAT CSQC_MapEntityEdited(modelindex, idx, newvals); #endif } } return; case ter_ent_add: { // int idx = G_INT(OFS_PARM1); // const char *news = PR_GetStringOfs(prinst, OFS_PARM2); G_INT(OFS_RETURN) = mod->numentityinfo; } return; case ter_ent_count: if (!mod->numentityinfo) Mod_ParseEntities(mod); G_INT(OFS_RETURN) = mod->numentityinfo; return; case ter_ents_wipe_deprecated: G_INT(OFS_RETURN) = PR_TempString(prinst, Mod_GetEntitiesString(mod)); Mod_SetEntitiesString(mod, "", true); return; case ter_ents_concat_deprecated: { char *newv; const char *olds = Mod_GetEntitiesString(mod); const char *news = PR_GetStringOfs(prinst, OFS_PARM1); size_t oldlen = strlen(olds); size_t newlen = strlen(news); newv = Z_Malloc(oldlen + newlen + 1); memcpy(newv, olds, oldlen); memcpy(newv+oldlen, news, newlen); newv[oldlen + newlen] = 0; Z_Free((char*)olds); G_FLOAT(OFS_RETURN) = oldlen + newlen; Mod_SetEntitiesString(mod, newv, false); if (mod->terrain) { hm = mod->terrain; hm->entsdirty = true; } } return; case ter_ents_get: G_INT(OFS_RETURN) = PR_TempString(prinst, Mod_GetEntitiesString(mod)); return; case ter_save: if (mod->terrain) { quant = Heightmap_Save(mod->terrain); Con_DPrintf("ter_save: %g sections saved\n", quant); } G_FLOAT(OFS_RETURN) = quant; /* if (mod->type == mod_brush) { Con_Printf("that model isn't a suitable worldmodel\n"); return; } else { FS_CreatePath(fname, FS_GAMEONLY); file = FS_OpenVFS(fname, "wb", FS_GAMEONLY); if (!file) Con_Printf("unable to open %s\n", fname); else { Terr_WriteMapFile(file, mod); VFS_CLOSE(file); } }*/ return; } if (!mod->terrain) { char basename[MAX_QPATH]; COM_FileBase(mod->name, basename, sizeof(basename)); mod->terrain = Mod_LoadTerrainInfo(mod, basename, true); hm = mod->terrain; if (!hm) return; Terr_FinishTerrain(mod); } hm = mod->terrain; pos[0] = G_FLOAT(OFS_PARM1+0) + hm->sectionsize * CHUNKBIAS; pos[1] = G_FLOAT(OFS_PARM1+1) + hm->sectionsize * CHUNKBIAS; pos[2] = G_FLOAT(OFS_PARM1+2); switch(action) { case ter_reload: G_FLOAT(OFS_RETURN) = 1; Terr_PurgeTerrainModel(mod, false, true); break; case ter_sethole: /* { int x, y; hmsection_t *s; x = pos[0]*4 / hm->sectionsize; y = pos[1]*4 / hm->sectionsize; x = bound(hm->firstsegx*4, x, hm->maxsegx*4-1); y = bound(hm->firstsegy*4, y, hm->maxsegy*4-1); s = Terr_GetSection(hm, x/4, y/4, TGS_FORCELOAD); if (!s) return; ted_sethole(&quant, s, (x&3) + (y&3)*4, x/4, y/4, 0); } */ pos[0] -= 0.5 * hm->sectionsize / 8; pos[1] -= 0.5 * hm->sectionsize / 8; ted_itterate(hm, tid_linear, pos, radius, 1, 9, ted_sethole, &quant); break; case ter_height_set: ted_itterate(hm, tid_linear, pos, radius, 1, SECTHEIGHTSIZE, ted_heightset, &quant); break; case ter_height_flatten: tally[0] = 0; tally[1] = 0; ted_itterate(hm, tid_exponential, pos, radius, 1, SECTHEIGHTSIZE, ted_heighttally, &tally); tally[0] /= tally[1]; if (IS_NAN(tally[0])) tally[0] = 0; ted_itterate(hm, tid_exponential, pos, radius, quant, SECTHEIGHTSIZE, ted_heightsmooth, &tally); ted_itterate(hm, tid_exponential, pos, radius, quant, SECTHEIGHTSIZE, ted_heightdebug, &tally); break; case ter_height_smooth: tally[0] = 0; tally[1] = 0; ted_itterate(hm, tid_linear, pos, radius, 1, SECTHEIGHTSIZE, ted_heighttally, &tally); tally[0] /= tally[1]; if (IS_NAN(tally[0])) tally[0] = 0; ted_itterate(hm, tid_linear, pos, radius, quant, SECTHEIGHTSIZE, ted_heightsmooth, &tally); break; case ter_height_spread: tally[0] = 0; tally[1] = 0; ted_itterate(hm, tid_exponential, pos, radius/2, 1, SECTHEIGHTSIZE, ted_heighttally, &tally); tally[0] /= tally[1]; if (IS_NAN(tally[0])) tally[0] = 0; ted_itterate(hm, tid_exponential, pos, radius, 1, SECTHEIGHTSIZE, ted_heightsmooth, &tally); break; case ter_water_set: ted_itterate(hm, tid_linear, pos, radius, 1, 9, ted_waterset, &quant); break; case ter_lower: quant *= -1; case ter_raise: ted_itterate(hm, tid_exponential, pos, radius, quant, SECTHEIGHTSIZE, ted_heightraise, &quant); break; case ter_tint: ted_itterate(hm, tid_exponential, pos, radius, quant, SECTHEIGHTSIZE, ted_tint, G_VECTOR(OFS_PARM4)); //and parm5 too break; // case ter_mixset: // ted_itterate(hm, tid_exponential, pos, radius, 1, SECTTEXSIZE, ted_mixset, G_VECTOR(OFS_PARM4)); // break; case ter_tex_blend: ted_itterate(hm, tid_exponential, pos, radius, quant/10, SECTTEXSIZE, ted_texpaint, (void*)PR_GetStringOfs(prinst, OFS_PARM4)); break; case ter_tex_replace: ted_itterate(hm, tid_exponential, pos, radius, 1, SECTTEXSIZE, ted_texreplace, (void*)PR_GetStringOfs(prinst, OFS_PARM3)); break; case ter_tex_concentrate: ted_itterate(hm, tid_exponential, pos, radius, 1, SECTTEXSIZE, ted_texconcentrate, NULL); break; case ter_tex_noise: ted_itterate(hm, tid_exponential, pos, radius, 1, SECTTEXSIZE, ted_texnoise, NULL); break; case ter_tex_blur: Vector4Set(tally, 0, 0, 0, 0); ted_itterate(hm, tid_exponential, pos, radius, 1, SECTTEXSIZE, ted_textally, &tally); VectorScale(tally, 1/(tally[3]*255), tally); ted_itterate(hm, tid_exponential, pos, radius, quant, SECTTEXSIZE, ted_texset, &tally); break; case ter_tex_get: { int x, y; hmsection_t *s; x = pos[0] / hm->sectionsize; y = pos[1] / hm->sectionsize; x = bound(hm->firstsegx, x, hm->maxsegx-1); y = bound(hm->firstsegy, y, hm->maxsegy-1); s = Terr_GetSection(hm, x, y, TGS_WAITLOAD|TGS_DEFAULTONFAIL); if (!s) return; x = bound(0, quant, 3); G_INT(OFS_RETURN) = PR_TempString(prinst, s->texname[x]); } break; case ter_tex_mask: Z_Free(hm->texmask); hm->texmask = NULL; if (G_INT(OFS_PARM1) == 0) hm->texmask = NULL; else hm->texmask = Z_StrDup(PR_GetStringOfs(prinst, OFS_PARM1)); break; case ter_tex_kill: { int x, y; x = pos[0] / hm->sectionsize; y = pos[1] / hm->sectionsize; x = bound(hm->firstsegx, x, hm->maxsegx-1); y = bound(hm->firstsegy, y, hm->maxsegy-1); ted_texkill(Terr_GetSection(hm, x, y, TGS_WAITLOAD|TGS_DEFAULTONFAIL), PR_GetStringOfs(prinst, OFS_PARM4)); } break; case ter_reset: { int x, y; hmsection_t *s; x = pos[0] / hm->sectionsize; y = pos[1] / hm->sectionsize; x = bound(hm->firstsegx, x, hm->maxsegx-1); y = bound(hm->firstsegy, y, hm->maxsegy-1); s = Terr_GetSection(hm, x, y, TGS_WAITLOAD|TGS_DEFAULTONFAIL); if (s) { s->flags = (s->flags & ~TSF_EDITED); Terr_ClearSection(s); Terr_GenerateDefault(hm, s); } } break; case ter_mesh_add: { vec3_t axis[3]; wedict_t *ed = G_WEDICT(prinst, OFS_PARM1); //FIXME: modeltype pitch inversion AngleVectorsFLU(ed->v->angles, axis[0], axis[1], axis[2]); Terr_AddMesh(hm, TGS_WAITLOAD|TGS_DEFAULTONFAIL, vmw->Get_CModel(vmw, ed->v->modelindex), NULL, ed->v->origin, axis, ed->xv->scale); } break; case ter_mesh_kill: { int i; // entity_t *e; int x, y; // float r; hmsection_t *s; x = pos[0] / hm->sectionsize; y = pos[1] / hm->sectionsize; x = bound(hm->firstsegx, x, hm->maxsegx-1); y = bound(hm->firstsegy, y, hm->maxsegy-1); s = Terr_GetSection(hm, x, y, TGS_WAITLOAD|TGS_DEFAULTONFAIL); if (!s) return; Sys_LockMutex(hm->entitylock); //FIXME: this doesn't work properly. if (s->numents) { for (i = 0; i < s->numents; i++) s->ents[i]->refs -= 1; s->flags |= TSF_EDITED; s->numents = 0; } Sys_UnlockMutex(hm->entitylock); } break; } } #else static unsigned char *QDECL Terr_GetLightmap(hmsection_t *s, int idx, qboolean edit) { return NULL; } void QCBUILTIN PF_terrain_edit(pubprogfuncs_t *prinst, struct globalvars_s *pr_globals) { G_FLOAT(OFS_RETURN) = 0; } #endif void Terr_ParseEntityLump(model_t *mod, heightmap_t *heightmap) { char key[128]; char value[2048]; const char *data = Mod_GetEntitiesString(mod); heightmap->sectionsize = 1024; heightmap->mode = HMM_TERRAIN; heightmap->culldistance = 4096*4096; heightmap->forcedefault = false; heightmap->defaultgroundheight = 0; heightmap->defaultwaterheight = 0; Q_snprintfz(heightmap->defaultwatershader, sizeof(heightmap->defaultwatershader), "water/%s", heightmap->path); Q_strncpyz(heightmap->defaultgroundtexture, "", sizeof(heightmap->defaultgroundtexture)); if (data) if ((data=COM_ParseOut(data, key, sizeof(key)))) //read the map info. if (key[0] == '{') while (1) { if (!(data=COM_ParseOut(data, key, sizeof(key)))) break; // error if (key[0] == '}') break; // end of worldspawn if (key[0] == '_') memmove(key, key+1, strlen(key)); //_ vars are for comments/utility stuff that arn't visible to progs and for compat. We want to support these stealth things. if (!((data=COM_ParseOut(data, value, sizeof(value))))) break; // error if (!strcmp("segmentsize", key)) heightmap->sectionsize = atof(value); else if (!strcmp("minxsegment", key)) heightmap->firstsegx = atoi(value); else if (!strcmp("minysegment", key)) heightmap->firstsegy = atoi(value); else if (!strcmp("maxxsegment", key)) heightmap->maxsegx = atoi(value); else if (!strcmp("maxysegment", key)) heightmap->maxsegy = atoi(value); else if (!strcmp("forcedefault", key)) heightmap->forcedefault = !!atoi(value); else if (!strcmp("defaultwaterheight", key)) heightmap->defaultwaterheight = atof(value); else if (!strcmp("defaultgroundheight", key)) heightmap->defaultgroundheight = atof(value); else if (!strcmp("defaultgroundtexture", key)) Q_strncpyz(heightmap->defaultgroundtexture, value, sizeof(heightmap->defaultgroundtexture)); else if (!strcmp("defaultwatertexture", key)) Q_strncpyz(heightmap->defaultwatershader, value, sizeof(heightmap->defaultwatershader)); else if (!strcmp("culldistance", key)) { heightmap->culldistance = atof(value); heightmap->culldistance *= heightmap->culldistance; } else if (!strcmp("drawdist", key)) heightmap->maxdrawdist = atof(value); else if (!strcmp("seed", key)) { Z_Free(heightmap->seed); heightmap->seed = Z_StrDup(value); } else if (!strcmp("exterior", key)) { heightmap->legacyterrain = false; if (!strcmp(value, "empty") || !strcmp(value, "")) heightmap->exteriorcontents = FTECONTENTS_EMPTY; else if (!strcmp(value, "sky")) heightmap->exteriorcontents = FTECONTENTS_SKY; else if (!strcmp(value, "lava")) heightmap->exteriorcontents = FTECONTENTS_LAVA; else //if (!strcmp(value, "solid")) heightmap->exteriorcontents = FTECONTENTS_SOLID; } else if (!strcmp("skybox", key)) Q_strncpyz(heightmap->skyname, value, sizeof(heightmap->skyname)); else if (!strcmp("tiles", key)) { char *d; heightmap->mode = HMM_BLOCKS; d = value; d = COM_ParseOut(d, key, sizeof(key)); heightmap->tilepixcount[0] = atoi(key); d = COM_ParseOut(d, key, sizeof(key)); heightmap->tilepixcount[1] = atoi(key); d = COM_ParseOut(d, key, sizeof(key)); heightmap->tilecount[0] = atoi(key); d = COM_ParseOut(d, key, sizeof(key)); heightmap->tilecount[1] = atoi(key); } } /*bias and bound it*/ heightmap->firstsegx += CHUNKBIAS; heightmap->firstsegy += CHUNKBIAS; heightmap->maxsegx += CHUNKBIAS; heightmap->maxsegy += CHUNKBIAS; if (heightmap->firstsegx < 0) heightmap->firstsegx = 0; if (heightmap->firstsegy < 0) heightmap->firstsegy = 0; if (heightmap->maxsegx > CHUNKLIMIT) heightmap->maxsegx = CHUNKLIMIT; if (heightmap->maxsegy > CHUNKLIMIT) heightmap->maxsegy = CHUNKLIMIT; } void Terr_FinishTerrain(model_t *mod) { #ifndef SERVERONLY heightmap_t *hm = mod->terrain; if (qrenderer != QR_NONE) { if (*hm->skyname) { hm->skyshader = R_RegisterCustom(va("skybox_%s", hm->skyname), SUF_NONE, Shader_DefaultSkybox, NULL); if (!hm->skyshader->skydome) hm->skyshader = NULL; } else hm->skyshader = NULL; switch (hm->mode) { case HMM_BLOCKS: hm->shader = R_RegisterShader("terraintileshader", SUF_NONE, "{\n" "{\n" "map $diffuse\n" "}\n" "}\n" ); break; case HMM_TERRAIN: hm->shader = R_RegisterShader(hm->groundshadername, SUF_LIGHTMAP, "{\n" "bemode rtlight\n" "{\n" "{\n" "map $diffuse\n" "blendfunc add\n" "}\n" //FIXME: these maps are a legacy thing, and could be removed if third-party glsl properly contains s_diffuse "{\n" "map $upperoverlay\n" "}\n" "{\n" "map $loweroverlay\n" "}\n" "{\n" "map $fullbright\n" "}\n" "{\n" "map $lightmap\n" "}\n" "{\n" "map $shadowmap\n" "}\n" "{\n" "map $lightcubemap\n" "}\n" //woo, one glsl to rule them all "program terrain#RTLIGHT\n" "}\n" "bemode depthdark\n" "{\n" "program depthonly\n" "{\n" "depthwrite\n" "}\n" "}\n" "bemode depthonly\n" "{\n" "program depthonly\n" "{\n" "depthwrite\n" "maskcolor\n" "}\n" "}\n" //FIXME: these maps are a legacy thing, and could be removed if third-party glsl properly contains s_diffuse "{\n" "map $diffuse\n" "rgbgen vertex\n" "alphagen vertex\n" "}\n" "{\n" "map $upperoverlay\n" "}\n" "{\n" "map $loweroverlay\n" "}\n" "{\n" "map $fullbright\n" "}\n" "{\n" "map $lightmap\n" "}\n" "program terrain\n" "if r_terraindebug\n" "program terraindebug\n" "endif\n" "}\n" ); break; } } #endif } #ifndef SERVERONLY void Terr_Brush_Draw(heightmap_t *hm, batch_t **batches, entity_t *e) { batch_t *b; size_t i, j; vbobctx_t ctx; brushbatch_t *bb; brushtex_t *bt; brushes_t *br; struct { vecV_t coord[65536]; vec2_t texcoord[65536]; vec2_t lmcoord[65536]; vec3_t normal[65536]; vec3_t svector[65536]; vec3_t tvector[65536]; index_t index[65535]; } *arrays = NULL; size_t numverts = 0; size_t numindicies = 0; int w, h, lmnum; float scale[2]; lightmapinfo_t *lm; qboolean dorelight = true; #ifdef RUNTIMELIGHTING //FIXME: lightmaps //if we're enabling lightmaps, make sure all surfaces have known sizes first. //allocate lightmap space for all surfaces, and then rebuild all textures. //if a surface is modified, clear its lightmap to -1 and when its batches are rebuilt, it'll unlight naturally. if (hm->entsdirty) { model_t *mod = e->model; if (mod->submodelof) mod = mod->submodelof; hm->entsdirty = false; LightReloadEntities(hm->relightcontext, Mod_GetEntitiesString(mod), true); //FIXME: figure out some way to hint this without having to relight the entire frigging world. for (bt = hm->brushtextures; bt; bt = bt->next) for (i = 0, br = hm->wbrushes; i < hm->numbrushes; i++, br++) for (j = 0; j < br->numplanes; j++) br->faces[j].relight = true; } if (hm->recalculatebrushlighting && !r_fullbright.ival) { unsigned int lmcount; unsigned int lmblocksize = 512;//LMBLOCK_SIZE_MAX hm->recalculatebrushlighting = false; if (!hm->relightcontext) { for (numverts = 0, numindicies = 0, i = 0, br = hm->wbrushes; i < hm->numbrushes; i++, br++) { for (j = 0; j < br->numplanes; j++) { br->faces[j].lightmap = -1; br->faces[j].lmbase[0] = 0; br->faces[j].lmbase[1] = 0; } } for (bt = hm->brushtextures; bt; bt = bt->next) { bt->rebuild = true; bt->firstlm = 0; bt->lmcount = 0; } BZ_Free(hm->brushlmremaps); hm->brushlmremaps = NULL; hm->brushmaxlms = 0; } else { Mod_LightmapAllocInit(&hm->brushlmalloc, false, lmblocksize, lmblocksize, 0); hm->brushlmscale = 1.0/lmblocksize; //textures is to try to ensure that they are allocated consecutively. for (bt = hm->brushtextures; bt; bt = bt->next) { bt->firstlm = hm->brushlmalloc.lmnum; for (numverts = 0, numindicies = 0, i = 0, br = hm->wbrushes; i < hm->numbrushes; i++, br++) { for (j = 0; j < br->numplanes; j++) { if (br->faces[j].tex == bt) { if (br->faces[j].lightdata) { Mod_LightmapAllocBlock(&hm->brushlmalloc, br->faces[j].lmextents[0], br->faces[j].lmextents[1], &br->faces[j].lmbase[0], &br->faces[j].lmbase[1], &br->faces[j].lightmap); br->faces[j].relit = true; } else { //this surface has no lightmap info or something. br->faces[j].lightmap = -1; br->faces[j].lmbase[0] = 0; br->faces[j].lmbase[1] = 0; } } } } bt->rebuild = true; bt->lmcount = hm->brushlmalloc.lmnum - bt->firstlm; if (hm->brushlmalloc.allocated[0]) bt->lmcount++; if (hm->brushlmalloc.deluxe) { bt->firstlm *= 2; bt->lmcount *= 2; } } lmcount = hm->brushlmalloc.lmnum; if (hm->brushlmalloc.allocated[0]) lmcount++; if (hm->brushlmalloc.deluxe) lmcount *= 2; if (lmcount > hm->brushmaxlms) { int first; hm->brushlmremaps = BZ_Realloc(hm->brushlmremaps, sizeof(*hm->brushlmremaps) * lmcount); first = Surf_NewLightmaps(lmcount - hm->brushmaxlms, hm->brushlmalloc.width, hm->brushlmalloc.height, hm->brushlmalloc.deluxe); while(hm->brushmaxlms < lmcount) hm->brushlmremaps[hm->brushmaxlms++] = first++; } } } if (hm->relightcontext && !r_fullbright.ival) for (i = 0, br = hm->wbrushes; i < hm->numbrushes; i++, br++) { for (j = 0; j < br->numplanes; j++) { if (br->faces[j].relight && dorelight) { qbyte styles[4] = {0,255,255,255}; int texsize[2] = {br->faces[j].lmextents[0]-1, br->faces[j].lmextents[1]-1}; vec2_t exactmins, exactmaxs; int m, k; vec2_t lm; for (m = 0; m < br->faces[j].numpoints; m++) { for (k = 0; k < 2; k++) { lm[k] = DotProduct(br->faces[j].points[m], br->faces[j].stdir[k]) + br->faces[j].stdir[k][3]; if (m == 0) exactmins[k] = exactmaxs[k] = lm[k]; else if (lm[k] > exactmaxs[k]) exactmaxs[k] = lm[k]; else if (lm[k] < exactmins[k]) exactmins[k] = lm[k]; } } dorelight = false; br->faces[j].relight = false; LightPlane (hm->relightcontext, hm->lightthreadmem, styles, br->faces[j].lightdata, NULL, br->planes[j], br->faces[j].stdir, exactmins, exactmaxs, br->faces[j].lmbias, texsize, br->faces[j].lmscale); //special version that doesn't know what a face is or anything. br->faces[j].relit = true; } if (br->faces[j].relit && br->faces[j].lightmap >= 0) { int s,t; qbyte *out, *in; lm = lightmap[hm->brushlmremaps[br->faces[j].lightmap]]; br->faces[j].relit = false; lm->modified = true; lm->rectchange.l = 0; lm->rectchange.t = 0; lm->rectchange.r = lm->width; lm->rectchange.b = lm->height; in = br->faces[j].lightdata; out = lm->lightmaps + (br->faces[j].lmbase[1] * lm->width + br->faces[j].lmbase[0]) * lightmap_bytes; switch(lightmap_fmt) { default: Sys_Error("Bad lightmap_fmt\n"); break; case TF_BGRA32: for (t = 0; t < br->faces[j].lmextents[1]; t++) { for (s = 0; s < br->faces[j].lmextents[0]; s++) { *out++ = in[2]; *out++ = in[1]; *out++ = in[0]; *out++ = 0xff; in+=3; } out += (lm->width - br->faces[j].lmextents[0]) * 4; } break; /*case TF_RGBA32: for (t = 0; t < br->faces[j].lmextents[1]; t++) { for (s = 0; s < br->faces[j].lmextents[0]; s++) { *out++ = in[0]; *out++ = in[1]; *out++ = in[2]; *out++ = 0xff; in+=3; } out += (lm->width - br->faces[j].lmextents[0]) * 4; } break;*/ /*case TF_BGR24: for (t = 0; t < br->faces[j].lmextents[1]; t++) { for (s = 0; s < br->faces[j].lmextents[0]; s++) { *out++ = in[2]; *out++ = in[1]; *out++ = in[0]; in+=3; } out += (lm->width - br->faces[j].lmextents[0]) * 3; } break;*/ case TF_RGB24: for (t = 0; t < br->faces[j].lmextents[1]; t++) { for (s = 0; s < br->faces[j].lmextents[0]; s++) { *out++ = in[0]; *out++ = in[1]; *out++ = in[2]; in+=3; } out += (lm->width - br->faces[j].lmextents[0]) * 3; } break; } } } } #endif for (bt = hm->brushtextures; bt; bt = bt->next) { if (!bt->shader) { #ifdef PACKAGE_TEXWAD miptex_t *tx = W_GetMipTex(bt->shadername); #else const miptex_t *tx = NULL; #endif if (!Q_strcasecmp(bt->shadername, "clip")) bt->shader = R_RegisterShader(bt->shadername, SUF_LIGHTMAP, "{\nsurfaceparm nodraw\n}"); else bt->shader = R_RegisterCustom (bt->shadername, SUF_LIGHTMAP, Shader_DefaultBSPQ1, NULL); // bt->shader = R_RegisterShader_Lightmap(bt->shadername); if (!Q_strncasecmp(bt->shadername, "sky", 3) && tx) R_InitSky (bt->shader, bt->shadername, (qbyte*)tx + tx->offsets[0], tx->width, tx->height); else if (tx) { qbyte *mips[4] = {(qbyte*)tx + tx->offsets[0], (qbyte*)tx + tx->offsets[1], (qbyte*)tx + tx->offsets[2], (qbyte*)tx + tx->offsets[3]}; unsigned int mapflags = SHADER_HASPALETTED | SHADER_HASDIFFUSE | SHADER_HASFULLBRIGHT | SHADER_HASNORMALMAP | SHADER_HASGLOSS; R_BuildLegacyTexnums(bt->shader, tx->name, NULL, mapflags, 0, TF_MIP4_SOLID8, tx->width, tx->height, mips, NULL); } else R_BuildDefaultTexnums(NULL, bt->shader); if (tx) { if (!bt->shader->width) bt->shader->width = tx->width; if (!bt->shader->height) bt->shader->height = tx->height; BZ_Free(tx); } } if (bt->rebuild) { //FIXME: don't block. if (R_GetShaderSizes(bt->shader, &w, &h, false) < 0) continue; bt->rebuild = false; if (w<1) w = 64; if (h<1) h = 64; scale[0] = 1.0/w; //I hate needing this. scale[1] = 1.0/h; while(bt->batches) { bb = bt->batches; bt->batches = bb->next; BE_VBO_Destroy(&bb->vbo.coord, bb->vbo.vbomem); BE_VBO_Destroy(&bb->vbo.indicies, bb->vbo.ebomem); BZ_Free(bb); } if (!arrays) arrays = BZ_Malloc(sizeof(*arrays)); for (lmnum = -1; lmnum < bt->firstlm+bt->lmcount; ((lmnum==-1)?(lmnum=bt->firstlm):(lmnum=lmnum+1))) { i = 0; br = hm->wbrushes; for (;i < hm->numbrushes;) { for (numverts = 0, numindicies = 0; i < hm->numbrushes; i++, br++) { //if a single batch has too many verts, cut it off before it overflows our maximum batch size, and hope we don't get a really really complex brush. if (numverts > 0xf000 || numindicies > 0xf000) break; for (j = 0; j < br->numplanes; j++) { if (br->faces[j].tex == bt && !br->selected && br->faces[j].lightmap == lmnum) { size_t k, o; float s,t; for (k = 0, o = numverts; k < br->faces[j].numpoints; k++, o++) { VectorCopy(br->faces[j].points[k], arrays->coord[o]); VectorCopy(br->planes[j], arrays->normal[o]); VectorCopy(br->faces[j].stdir[0], arrays->svector[o]); VectorCopy(br->faces[j].stdir[1], arrays->tvector[o]); //compute the texcoord planes s = (DotProduct(arrays->svector[o], arrays->coord[o]) + br->faces[j].stdir[0][3]); t = (DotProduct(arrays->tvector[o], arrays->coord[o]) + br->faces[j].stdir[1][3]); arrays->texcoord[o][0] = s * scale[0]; arrays->texcoord[o][1] = t * scale[1]; //maths, maths, and more maths. arrays->lmcoord[o][0] = (br->faces[j].lmbase[0]+0.5 + s/br->faces[j].lmscale-br->faces[j].lmbias[0]) * hm->brushlmscale; arrays->lmcoord[o][1] = (br->faces[j].lmbase[1]+0.5 + t/br->faces[j].lmscale-br->faces[j].lmbias[1]) * hm->brushlmscale; } for (k = 2; k < br->faces[j].numpoints; k++) { //triangle fans arrays->index[numindicies++] = numverts + 0; arrays->index[numindicies++] = numverts + k-1; arrays->index[numindicies++] = numverts + k-0; } numverts += br->faces[j].numpoints; } } } if (numverts || numindicies) { bb = Z_Malloc(sizeof(*bb) + (sizeof(bb->mesh.xyz_array[0])+sizeof(arrays->texcoord[0])+sizeof(arrays->lmcoord[0])+sizeof(arrays->normal[0])+sizeof(arrays->svector[0])+sizeof(arrays->tvector[0])) * numverts); bb->next = bt->batches; bt->batches = bb; bb->lightmap = lmnum; BE_VBO_Begin(&ctx, (sizeof(arrays->coord[0])+sizeof(arrays->texcoord[0])+sizeof(arrays->lmcoord[0])+sizeof(arrays->normal[0])+sizeof(arrays->svector[0])+sizeof(arrays->tvector[0])) * numverts); BE_VBO_Data(&ctx, arrays->coord, sizeof(arrays->coord [0])*numverts, &bb->vbo.coord); BE_VBO_Data(&ctx, arrays->texcoord, sizeof(arrays->texcoord [0])*numverts, &bb->vbo.texcoord); BE_VBO_Data(&ctx, arrays->lmcoord, sizeof(arrays->lmcoord [0])*numverts, &bb->vbo.lmcoord[0]); BE_VBO_Data(&ctx, arrays->normal, sizeof(arrays->normal [0])*numverts, &bb->vbo.normals); BE_VBO_Data(&ctx, arrays->svector, sizeof(arrays->svector [0])*numverts, &bb->vbo.svector); BE_VBO_Data(&ctx, arrays->tvector, sizeof(arrays->tvector [0])*numverts, &bb->vbo.tvector); BE_VBO_Finish(&ctx, arrays->index, sizeof(arrays->index [0])*numindicies, &bb->vbo.indicies, &bb->vbo.vbomem, &bb->vbo.ebomem); bb->mesh.xyz_array = (vecV_t*)(bb+1); memcpy(bb->mesh.xyz_array, arrays->coord, sizeof(*bb->mesh.xyz_array) * numverts); bb->mesh.st_array = (vec2_t*)(bb->mesh.xyz_array+numverts); memcpy(bb->mesh.st_array, arrays->texcoord, sizeof(*bb->mesh.st_array) * numverts); bb->mesh.lmst_array[0] = (vec2_t*)(bb->mesh.st_array+numverts); memcpy(bb->mesh.lmst_array[0], arrays->lmcoord, sizeof(*bb->mesh.lmst_array) * numverts); bb->mesh.normals_array = (vec3_t*)(bb->mesh.lmst_array[0]+numverts); memcpy(bb->mesh.normals_array, arrays->normal, sizeof(*bb->mesh.normals_array) * numverts); bb->mesh.snormals_array = (vec3_t*)(bb->mesh.normals_array+numverts); memcpy(bb->mesh.snormals_array, arrays->svector, sizeof(*bb->mesh.snormals_array) * numverts); bb->mesh.tnormals_array = (vec3_t*)(bb->mesh.snormals_array+numverts); memcpy(bb->mesh.tnormals_array, arrays->tvector, sizeof(*bb->mesh.tnormals_array) * numverts); bb->pmesh = &bb->mesh; bb->mesh.numindexes = numindicies; bb->mesh.numvertexes = numverts; numverts = 0; numindicies = 0; } } } } for(bb = bt->batches; bb; bb = bb->next) { b = BE_GetTempBatch(); if (b) { j = 0; if (bb->lightmap >= 0) b->lightmap[j++] = r_fullbright.ival?-1:hm->brushlmremaps[bb->lightmap]; for (; j < MAXRLIGHTMAPS; j++) b->lightmap[j] = -1; b->ent = e; b->shader = bt->shader; b->flags = 0; b->mesh = &bb->pmesh; b->meshes = 1; b->buildmeshes = NULL; b->skin = NULL; b->texture = NULL; b->vbo = &bb->vbo; b->next = batches[b->shader->sort]; batches[b->shader->sort] = b; } } } if (arrays) BZ_Free(arrays); } #endif static brushtex_t *Terr_Brush_FindTexture(heightmap_t *hm, const char *texname) { brushtex_t *bt; if (!hm) return NULL; for (bt = hm->brushtextures; bt; bt = bt->next) { if (!strcmp(bt->shadername, texname)) return bt; } bt = Z_Malloc(sizeof(*bt)); bt->next = hm->brushtextures; hm->brushtextures = bt; Q_strncpyz(bt->shadername, texname, sizeof(bt->shadername)); return bt; } static brushes_t *Terr_Brush_Insert(model_t *model, heightmap_t *hm, brushes_t *brush) { vecV_t facepoints[64]; unsigned int iface, oface, j, k; unsigned int numpoints; brushes_t *out; vec2_t mins, maxs; vec2_t lm; if (!hm) { if (model && model->loadstate == MLS_LOADING) COM_WorkerPartialSync(model, &model->loadstate, MLS_LOADING); if (model && model->loadstate == MLS_LOADED) { char basename[MAX_QPATH]; COM_FileBase(model->name, basename, sizeof(basename)); model->terrain = Mod_LoadTerrainInfo(model, basename, true); hm = model->terrain; if (!hm) return NULL; Terr_FinishTerrain(model); } else return NULL; } hm->wbrushes = BZ_Realloc(hm->wbrushes, sizeof(*hm->wbrushes) * (hm->numbrushes+1)); out = &hm->wbrushes[hm->numbrushes]; out->selected = false; out->contents = brush->contents; out->axialplanes = 0; out->planes = BZ_Malloc((sizeof(*out->planes)+sizeof(*out->faces)) * brush->numplanes); out->faces = (void*)(out->planes+brush->numplanes); ClearBounds(out->mins, out->maxs); for (iface = 0, oface = 0; iface < brush->numplanes; iface++) { for (j = 0; j < oface; j++) { if (out->planes[j][0] == brush->planes[iface][0] && out->planes[j][1] == brush->planes[iface][1] && out->planes[j][2] == brush->planes[iface][2] && out->planes[j][3] == brush->planes[iface][3]) break; } if (j < oface) { Con_DPrintf("duplicate plane\n"); continue; } //generate points now (so we know the correct mins+maxs for the brush, and whether the plane is relevent) numpoints = Fragment_ClipPlaneToBrush(facepoints, sizeof(facepoints)/sizeof(facepoints[0]), brush->planes, sizeof(*brush->planes), brush->numplanes, brush->planes[iface]); if (!numpoints) { Con_DPrintf("redundant face\n"); continue; //this surface was chopped away entirely, and isn't relevant. } //copy the basic face info out so we can save/restore/query/edit it later. Vector4Copy(brush->planes[iface], out->planes[oface]); out->faces[oface].tex = brush->faces[iface].tex; Vector4Copy(brush->faces[iface].stdir[0], out->faces[oface].stdir[0]); Vector4Copy(brush->faces[iface].stdir[1], out->faces[oface].stdir[1]); if (out->planes[oface][0] == 1) out->axialplanes |= 1u<<0; else if (out->planes[oface][1] == 1) out->axialplanes |= 1u<<1; else if (out->planes[oface][2] == 1) out->axialplanes |= 1u<<2; else if (out->planes[oface][0] == -1) out->axialplanes |= 1u<<3; else if (out->planes[oface][1] == -1) out->axialplanes |= 1u<<4; else if (out->planes[oface][2] == -1) out->axialplanes |= 1u<<5; //make sure this stuff is rebuilt properly. out->faces[oface].tex->rebuild = true; //keep this stuff cached+reused, so everything is consistant. also work out min/max lightmap texture coords out->faces[oface].points = BZ_Malloc(numpoints * sizeof(*out->faces[oface].points)); Vector2Set(mins, 0, 0); Vector2Set(maxs, 0, 0); for (j = 0; j < numpoints; j++) { AddPointToBounds(facepoints[j], out->mins, out->maxs); VectorCopy(facepoints[j], out->faces[oface].points[j]); for (k = 0; k < 2; k++) { lm[k] = DotProduct(out->faces[oface].points[j], out->faces[oface].stdir[k]) + out->faces[oface].stdir[k][3]; if (j == 0) mins[k] = maxs[k] = lm[k]; else if (lm[k] > maxs[k]) maxs[k] = lm[k]; else if (lm[k] < mins[k]) mins[k] = lm[k]; } } out->faces[oface].numpoints = numpoints; //determine lightmap scale, and extents. rescale the lightmap if it ought to have been subdivided. out->faces[oface].relight = true; out->faces[oface].lmscale = 16; for (k = 0; k < 2; ) { out->faces[oface].lmbias[k] = floor(mins[k]/out->faces[oface].lmscale); out->faces[oface].lmextents[k] = ceil((maxs[k])/out->faces[oface].lmscale)-out->faces[oface].lmbias[k]+1; if (out->faces[oface].lmextents[k] > 128) { //surface is too large for lightmap data. just drop its resolution, because splitting the face in plane-defined geometry is a bad idea. if (out->faces[oface].lmscale > 256) { out->faces[oface].relight = false; k++; } else { out->faces[oface].lmscale *= 2; k = 0; } } else k++; } out->faces[oface].lightmap = -1; out->faces[oface].lmbase[0] = 0; out->faces[oface].lmbase[1] = 0; if (out->faces[oface].relight) { out->faces[oface].lightdata = BZ_Malloc(out->faces[oface].lmextents[0] * out->faces[oface].lmextents[1] * 3); memset(out->faces[oface].lightdata, 0x3f, out->faces[oface].lmextents[0]*out->faces[oface].lmextents[1]*3); } else out->faces[oface].lightdata = NULL; // Con_Printf("lm extents: %u %u (%i points)\n", out->faces[oface].lmextents[0], out->faces[oface].lmextents[1], numpoints); oface++; } if (oface < 4) { //a brush with less than 4 planes cannot be a valid convex area (but can happen when certain redundant planes are chopped out). don't accept creation //(we often get 2-plane brushes if the sides are sucked in) for (j = 0; j < oface; j++) { BZ_Free(out->faces[j].lightdata); BZ_Free(out->faces[j].points); } BZ_Free(out->planes); return NULL; } out->numplanes = oface; if (brush->id) out->id = brush->id; else { unsigned int i; //loop to avoid creating two brushes with the same id do { out->id = (++hm->brushidseq)&0x00ffffff; #ifndef SERVERONLY if (cls.state) //avoid networking conflicts by having each node generating its own private ids out->id |= (cl.playerview[0].playernum+1)<<24; #endif for (i = 0; i < hm->numbrushes; i++) { if (hm->wbrushes[i].id == out->id) break; } } while (i != hm->numbrushes); } // Con_Printf("brush %u (%i faces)\n", out->id, oface); hm->numbrushes+=1; hm->brushesedited = true; hm->recalculatebrushlighting = true; //lightmaps need to be reallocated //make sure the brush's bounds are added to the containing model. AddPointToBounds(out->mins, model->mins, model->maxs); AddPointToBounds(out->maxs, model->mins, model->maxs); return out; } static void Terr_Brush_DeleteIdx(heightmap_t *hm, size_t idx) { int i; brushes_t *br = &hm->wbrushes[idx]; if (!hm) return; for (i = 0; i < br->numplanes; i++) { BZ_Free(br->faces[i].lightdata); BZ_Free(br->faces[i].points); br->faces[i].tex->rebuild = true; } BZ_Free(br->planes); hm->numbrushes--; hm->brushesedited = true; //plug the hole with some other brush. if (idx < hm->numbrushes) hm->wbrushes[idx] = hm->wbrushes[hm->numbrushes]; } static qboolean Terr_Brush_DeleteId(heightmap_t *hm, unsigned int brushid) { size_t i; brushes_t *br; if (!hm) return false; for (i = 0; i < hm->numbrushes; i++) { br = &hm->wbrushes[i]; if (br->id == brushid) { Terr_Brush_DeleteIdx(hm, i); return true; } } return false; } static void Brush_Serialise(sizebuf_t *sb, brushes_t *br) { unsigned int i; MSG_WriteLong(sb, br->id); MSG_WriteLong(sb, br->contents); MSG_WriteLong(sb, br->numplanes); for (i = 0; i < br->numplanes; i++) { MSG_WriteString(sb, br->faces[i].tex->shadername); MSG_WriteFloat(sb, br->planes[i][0]); MSG_WriteFloat(sb, br->planes[i][1]); MSG_WriteFloat(sb, br->planes[i][2]); MSG_WriteFloat(sb, br->planes[i][3]); MSG_WriteFloat(sb, br->faces[i].stdir[0][0]); MSG_WriteFloat(sb, br->faces[i].stdir[0][1]); MSG_WriteFloat(sb, br->faces[i].stdir[0][2]); MSG_WriteFloat(sb, br->faces[i].stdir[0][3]); MSG_WriteFloat(sb, br->faces[i].stdir[1][0]); MSG_WriteFloat(sb, br->faces[i].stdir[1][1]); MSG_WriteFloat(sb, br->faces[i].stdir[1][2]); MSG_WriteFloat(sb, br->faces[i].stdir[1][3]); } } static qboolean Brush_Deserialise(heightmap_t *hm, brushes_t *br) { unsigned int i; unsigned int maxplanes = br->numplanes; br->id = MSG_ReadLong(); br->contents = MSG_ReadLong(); br->numplanes = MSG_ReadLong(); if (br->numplanes > maxplanes) return false; for (i = 0; i < br->numplanes; i++) { //FIXME: as a server, we probably want to reject the brush if we exceed some texnum/memory limitation, so clients can't just spam new textures endlessly. br->faces[i].tex = Terr_Brush_FindTexture(hm, MSG_ReadString()); br->planes[i][0] = MSG_ReadFloat(); br->planes[i][1] = MSG_ReadFloat(); br->planes[i][2] = MSG_ReadFloat(); br->planes[i][3] = MSG_ReadFloat(); br->faces[i].stdir[0][0] = MSG_ReadFloat(); br->faces[i].stdir[0][1] = MSG_ReadFloat(); br->faces[i].stdir[0][2] = MSG_ReadFloat(); br->faces[i].stdir[0][3] = MSG_ReadFloat(); br->faces[i].stdir[1][0] = MSG_ReadFloat(); br->faces[i].stdir[1][1] = MSG_ReadFloat(); br->faces[i].stdir[1][2] = MSG_ReadFloat(); br->faces[i].stdir[1][3] = MSG_ReadFloat(); } return true; } #ifndef SERVERONLY heightmap_t *CL_BrushEdit_ForceContext(model_t *mod) { heightmap_t *hm = mod?mod->terrain:NULL; if (!hm) { if (mod && mod->loadstate == MLS_LOADING) COM_WorkerPartialSync(mod, &mod->loadstate, MLS_LOADING); if (mod && mod->loadstate == MLS_LOADED) { char basename[MAX_QPATH]; COM_FileBase(mod->name, basename, sizeof(basename)); mod->terrain = Mod_LoadTerrainInfo(mod, basename, true); hm = mod->terrain; if (!hm) return NULL; Terr_FinishTerrain(mod); } else return NULL; } return hm; } void CL_Parse_BrushEdit(void) { unsigned int modelindex = MSG_ReadShort(); int cmd = MSG_ReadByte(); model_t *mod = (modelindexterrain:NULL; #ifdef CLIENTONLY const qboolean ignore = false; #else const qboolean ignore = (sv.state>=ss_loading); //if we're the server then we already have this info. don't break anything (this info is present for demos). #endif if (cmd == hmcmd_brush_delete) { int id = MSG_ReadLong(); if (ignore) return; //ignore if we're the server, we should already have it anyway. Terr_Brush_DeleteId(hm, id); } else if (cmd == hmcmd_brush_insert) //1=create/replace { brushes_t brush; hm = CL_BrushEdit_ForceContext(mod); //do this early, to ensure that the textures are correct memset(&brush, 0, sizeof(brush)); brush.numplanes = 128; brush.planes = alloca(sizeof(*brush.planes) * brush.numplanes); brush.faces = alloca(sizeof(*brush.faces) * brush.numplanes); if (!Brush_Deserialise(hm, &brush)) Host_EndGame("CL_Parse_BrushEdit: unparsable brush\n"); if (ignore) return; //ignore if we're the server, we should already have it anyway (but might need it for demos, hence why its still sent). if (brush.id) { int i; if (cls.demoplayback) Terr_Brush_DeleteId(hm, brush.id); else { for (i = 0; i < hm->numbrushes; i++) { brushes_t *br = &hm->wbrushes[i]; if (br->id == brush.id) return; //we already have it. assume we just edited it. } } } Terr_Brush_Insert(mod, hm, &brush); } else if (cmd == hmcmd_prespawning) { //delete all if (ignore) return; //ignore if we're the server, we should already have it anyway. hm = CL_BrushEdit_ForceContext(mod); //make sure we don't end up with any loaded brushes. if (hm) { while(hm->numbrushes) Terr_Brush_DeleteIdx(hm, hm->numbrushes-1); } } else if (cmd == hmcmd_prespawned) { } else if (cmd == hmcmd_ent_edit || cmd == hmcmd_ent_remove) { //ent edit int id = MSG_ReadLong(); const char *data; int idx = mod->numentityinfo, i; if (cmd == hmcmd_ent_edit) data = MSG_ReadString(); else data = NULL; //convert id to idx for (i = 0; i < mod->numentityinfo; i++) { if (mod->entityinfo[i].id == id) { idx = i; break; } if (!mod->entityinfo[i].keyvals) idx = i; } //FIXME: cap the maximum data sizes (both count and storage, to prevent DOS attacks). if (idx == mod->numentityinfo && data) Z_ReallocElements((void**)&mod->entityinfo, &mod->numentityinfo, mod->numentityinfo+64, sizeof(*mod->entityinfo)); if (idx < mod->numentityinfo) { if (!ignore) { mod->entityinfo[idx].id = id; Z_Free(mod->entityinfo[idx].keyvals); if (data) mod->entityinfo[idx].keyvals = Z_StrDup(data); else mod->entityinfo[idx].keyvals = NULL; CSQC_MapEntityEdited(modelindex, idx, data); } } } else Host_EndGame("CL_Parse_BrushEdit: unknown command %i\n", cmd); } #endif #ifndef CLIENTONLY qboolean SV_Prespawn_Brushes(sizebuf_t *msg, unsigned int *modelindex, unsigned int *lastid) { //lastid starts at 0 unsigned int bestid, i; brushes_t *best; model_t *mod; heightmap_t *hm; while(1) { if (*modelindex < MAX_PRECACHE_MODELS) mod = sv.models[*modelindex]; else mod = NULL; if (!mod) { if (!(*modelindex)++) continue; return false; } hm = mod->terrain; if (!hm || !hm->brushesedited) { *modelindex+=1; *lastid = 0; continue; } if (!*lastid) { //make sure the client starts with a clean slate. MSG_WriteByte(msg, svcfte_brushedit); MSG_WriteShort(msg, *modelindex); MSG_WriteByte(msg, hmcmd_prespawning); } //weird loop to try to ensure we never miss any brushes. //get the lowest index that is 1 higher than our previous. for (best = NULL, bestid = ~0u, i = 0; i < hm->numbrushes; i++) { unsigned int bid = hm->wbrushes[i].id; if (bid > *lastid && bid <= bestid) { best = &hm->wbrushes[i]; bestid = best->id; if (bestid == *lastid+1) break; } } if (best) { MSG_WriteByte(msg, svcfte_brushedit); MSG_WriteShort(msg, *modelindex); MSG_WriteByte(msg, hmcmd_brush_insert); Brush_Serialise(msg, best); *lastid = bestid; return true; } *modelindex+=1; *lastid = 0; } } qboolean SV_Parse_BrushEdit(void) { qboolean authorise = (host_client->penalties & BAN_MAPPER) || (host_client->netchan.remote_address.type == NA_LOOPBACK); unsigned int modelindex = MSG_ReadShort(); int cmd = MSG_ReadByte(); model_t *mod = (modelindexterrain:NULL; if (cmd == hmcmd_brush_delete) { //delete unsigned int brushid = MSG_ReadLong(); if (!authorise) { SV_PrintToClient(host_client, PRINT_MEDIUM, "Brush editing ignored: you are not a mapper\n"); return true; } Terr_Brush_DeleteId(hm, brushid); MSG_WriteByte(&sv.multicast, svcfte_brushedit); MSG_WriteShort(&sv.multicast, modelindex); MSG_WriteByte(&sv.multicast, hmcmd_brush_delete); MSG_WriteLong(&sv.multicast, brushid); SV_MulticastProtExt(vec3_origin, MULTICAST_ALL_R, ~0, 0, 0); return true; } else if (cmd == hmcmd_brush_insert) { brushes_t brush; memset(&brush, 0, sizeof(brush)); brush.numplanes = 128; brush.planes = alloca(sizeof(*brush.planes) * brush.numplanes); brush.faces = alloca(sizeof(*brush.faces) * brush.numplanes); if (!Brush_Deserialise(hm, &brush)) { Con_Printf("SV_Parse_BrushEdit: %s sent an unparsable brush\n", host_client->name); return false; } if (!authorise) { SV_PrintToClient(host_client, PRINT_MEDIUM, "Brush editing ignored: you are not a mapper\n"); return true; } Terr_Brush_DeleteId(hm, brush.id); if (!Terr_Brush_Insert(mod, hm, &brush)) return true; //looks mostly valid, but something was degenerate. fpu precision... //FIXME: expand the world entity's sizes if needed? MSG_WriteByte(&sv.multicast, svcfte_brushedit); MSG_WriteShort(&sv.multicast, modelindex); MSG_WriteByte(&sv.multicast, hmcmd_brush_insert); Brush_Serialise(&sv.multicast, &brush); SV_MulticastProtExt(vec3_origin, MULTICAST_ALL_R, ~0, 0, 0); return true; } else if (cmd == hmcmd_ent_edit || cmd == hmcmd_ent_remove) { unsigned int entid = MSG_ReadLong(); char *keyvals = (cmd == hmcmd_ent_edit)?MSG_ReadString():NULL; if (mod->submodelof != mod) return true; if (!authorise) { SV_PrintToClient(host_client, PRINT_MEDIUM, "Entity editing ignored: you are not a mapper\n"); //FIXME: undo the client's edit? or is that rude? return true; } //FIXME: need to update the server's entity list //SSQC_MapEntityEdited(idx, newvals); MSG_WriteByte(&sv.multicast, svcfte_brushedit); MSG_WriteShort(&sv.multicast, modelindex); MSG_WriteByte(&sv.multicast, keyvals?hmcmd_ent_edit:hmcmd_ent_remove); MSG_WriteLong(&sv.multicast, entid); if (keyvals) MSG_WriteString(&sv.multicast, keyvals); SV_MulticastProtExt(vec3_origin, MULTICAST_ALL_R, ~0, 0, 0); } else { Con_Printf("SV_Parse_BrushEdit: %s sent an unknown command: %i\n", host_client->name, cmd); return false; } return true; } #endif typedef struct { int shadername; vec3_t planenormal; float planedist; vec3_t sdir; float sbias; vec3_t tdir; float tbias; } qcbrushface_t; static void *validateqcpointer(pubprogfuncs_t *prinst, size_t qcptr, size_t elementsize, size_t elementcount, qboolean allownull) { //make sure that the sizes can't overflow if (elementcount > 0x10000) { PR_BIError(prinst, "brush: elementcount %u is too large\n", (unsigned int)elementcount); return NULL; } if (qcptr < 0 || qcptr+(elementsize*elementcount) > prinst->stringtablesize) { PR_BIError(prinst, "brush: invalid qc pointer\n"); return NULL; } if (!qcptr) { if (!allownull) PR_BIError(prinst, "brush: null qc pointer\n"); return NULL; } return prinst->stringtable + qcptr; } // {"brush_get", PF_brush_get, 0, 0, 0, 0, D(qcbrushface "int(float modelidx, int brushid, brushface_t *out_faces, int maxfaces, int *out_contents)", "Queries a brush's information. You must pre-allocate the face array for the builtin to write to. Return value is the number of faces retrieved, 0 on error.")}, void QCBUILTIN PF_brush_get(pubprogfuncs_t *prinst, struct globalvars_s *pr_globals) { world_t *vmw = prinst->parms->user; model_t *mod = vmw->Get_CModel(vmw, G_FLOAT(OFS_PARM0)); heightmap_t *hm = mod?mod->terrain:NULL; unsigned int brushid = G_INT(OFS_PARM1); unsigned int maxfaces = G_INT(OFS_PARM3); qcbrushface_t *out_faces = validateqcpointer(prinst, G_INT(OFS_PARM2), sizeof(*out_faces), maxfaces, true); unsigned int *out_contents = validateqcpointer(prinst, G_INT(OFS_PARM4), sizeof(*out_contents), 1, true); unsigned int fa, i; brushes_t *br; //assume the worst. G_INT(OFS_RETURN) = 0; if (out_contents) *out_contents = 0; if (!hm) return; for (i = 0; i < hm->numbrushes; i++) { br = &hm->wbrushes[i]; if (br->id == brushid) { if (out_contents) *out_contents = br->contents; if (!out_faces) G_INT(OFS_RETURN) = br->numplanes; else { maxfaces = min(br->numplanes, maxfaces); for (fa = 0; fa < maxfaces; fa++) { out_faces->shadername = PR_TempString(prinst, br->faces[fa].tex->shadername); VectorCopy(br->planes[fa], out_faces->planenormal); out_faces->planedist = br->planes[fa][3]; VectorCopy(br->faces[fa].stdir[0], out_faces->sdir); out_faces->sbias = br->faces[fa].stdir[0][3]; VectorCopy(br->faces[fa].stdir[1], out_faces->tdir); out_faces->tbias = br->faces[fa].stdir[1][3]; out_faces++; } G_INT(OFS_RETURN) = fa; } return; } } } // {"brush_create", PF_brush_create, 0, 0, 0, 0, D("int(float modelidx, brushface_t *in_faces, int numfaces, int contents)", "Inserts a new brush into the model. Return value is the new brush's id.")}, void QCBUILTIN PF_brush_create(pubprogfuncs_t *prinst, struct globalvars_s *pr_globals) { world_t *vmw = prinst->parms->user; int modelindex = G_FLOAT(OFS_PARM0); model_t *mod = vmw->Get_CModel(vmw, modelindex); heightmap_t *hm = mod?mod->terrain:NULL; unsigned int numfaces = G_INT(OFS_PARM2); qcbrushface_t *in_faces = validateqcpointer(prinst, G_INT(OFS_PARM1), sizeof(*in_faces), numfaces, numfaces==0); unsigned int contents = G_INT(OFS_PARM3); unsigned int brushid = (prinst->callargc > 4)?G_INT(OFS_PARM4):0; //to simplify edits unsigned int i; brushes_t brush, *nb; vec4_t *planes; struct brushface_s *faces; G_INT(OFS_RETURN) = 0; if (!hm) { if (mod && mod->loadstate == MLS_LOADING) COM_WorkerPartialSync(mod, &mod->loadstate, MLS_LOADING); if (mod && mod->loadstate == MLS_LOADED) { char basename[MAX_QPATH]; COM_FileBase(mod->name, basename, sizeof(basename)); mod->terrain = Mod_LoadTerrainInfo(mod, basename, true); hm = mod->terrain; if (!hm) return; Terr_FinishTerrain(mod); } else return; } //if we're creating one that already exists, then assume that its a move. if (brushid && Terr_Brush_DeleteId(hm, brushid)) { #ifndef CLIENTONLY if (sv.state && modelindex > 0) { MSG_WriteByte(&sv.multicast, svcfte_brushedit); MSG_WriteShort(&sv.multicast, modelindex); MSG_WriteByte(&sv.multicast, hmcmd_brush_delete); MSG_WriteLong(&sv.multicast, brushid); SV_MulticastProtExt(vec3_origin, MULTICAST_ALL_R, ~0, 0, 0); } else #endif #ifndef SERVERONLY if (cls.state && modelindex > 0) { MSG_WriteByte(&cls.netchan.message, clcfte_brushedit); MSG_WriteShort(&cls.netchan.message, modelindex); MSG_WriteByte(&cls.netchan.message, hmcmd_brush_delete); MSG_WriteLong(&cls.netchan.message, brushid); } #else { } #endif } planes = alloca(sizeof(*planes) * numfaces); faces = alloca(sizeof(*faces) * numfaces); for (i = 0; i < numfaces; i++) { VectorCopy(in_faces[i].planenormal, planes[i]); planes[i][3] = in_faces[i].planedist; faces[i].tex = Terr_Brush_FindTexture(hm, PR_GetString(prinst, in_faces[i].shadername)); VectorCopy(in_faces[i].sdir, faces[i].stdir[0]); faces[i].stdir[0][3] = in_faces[i].sbias; VectorCopy(in_faces[i].tdir, faces[i].stdir[1]); faces[i].stdir[1][3] = in_faces[i].tbias; } //now emit it brush.id = 0; brush.contents = contents; brush.numplanes = numfaces; brush.planes = planes; brush.faces = faces; if (numfaces) { nb = Terr_Brush_Insert(mod, hm, &brush); if (nb) { G_INT(OFS_RETURN) = nb->id; #ifndef CLIENTONLY if (sv.state && modelindex > 0) { MSG_WriteByte(&sv.multicast, svcfte_brushedit); MSG_WriteShort(&sv.multicast, modelindex); MSG_WriteByte(&sv.multicast, hmcmd_brush_insert); Brush_Serialise(&sv.multicast, nb); SV_MulticastProtExt(vec3_origin, MULTICAST_ALL_R, ~0, 0, 0); return; } #endif #ifndef SERVERONLY if (cls.state && modelindex > 0) { MSG_WriteByte(&cls.netchan.message, clcfte_brushedit); MSG_WriteShort(&cls.netchan.message, modelindex); MSG_WriteByte(&cls.netchan.message, hmcmd_brush_insert); Brush_Serialise(&cls.netchan.message, nb); return; } #endif } } } // {"brush_delete", PF_brush_delete, 0, 0, 0, 0, D("void(float modelidx, int brushid)", "Destroys the specified brush.")}, void QCBUILTIN PF_brush_delete(pubprogfuncs_t *prinst, struct globalvars_s *pr_globals) { world_t *vmw = prinst->parms->user; int modelindex = G_FLOAT(OFS_PARM0); model_t *mod = vmw->Get_CModel(vmw, modelindex); heightmap_t *hm = mod?mod->terrain:NULL; unsigned int brushid = G_INT(OFS_PARM1); if (!hm) return; Terr_Brush_DeleteId(hm, brushid); #ifndef CLIENTONLY if (sv.state && modelindex > 0) { MSG_WriteByte(&sv.multicast, svcfte_brushedit); MSG_WriteShort(&sv.multicast, modelindex); MSG_WriteByte(&sv.multicast, hmcmd_brush_delete); MSG_WriteLong(&sv.multicast, brushid); SV_MulticastProtExt(vec3_origin, MULTICAST_ALL_R, ~0, 0, 0); return; } #endif #ifndef SERVERONLY if (cls.state && modelindex > 0) { MSG_WriteByte(&cls.netchan.message, clcfte_brushedit); MSG_WriteShort(&cls.netchan.message, modelindex); MSG_WriteByte(&cls.netchan.message, hmcmd_brush_delete); MSG_WriteLong(&cls.netchan.message, brushid); return; } #endif } // {"brush_selected", PF_brush_selected, 0, 0, 0, 0, D("float(float modelid, int brushid, int faceid, float selectedstate)", "Allows you to easily set transient visual properties of a brush. If brush/face is -1, applies to all. returns old value. selectedstate=-1 changes nothing (called for its return value).")}, void QCBUILTIN PF_brush_selected(pubprogfuncs_t *prinst, struct globalvars_s *pr_globals) { world_t *vmw = prinst->parms->user; model_t *mod = vmw->Get_CModel(vmw, G_FLOAT(OFS_PARM0)); heightmap_t *hm = mod?mod->terrain:NULL; unsigned int brushid = G_INT(OFS_PARM1); // unsigned int faceid = G_INT(OFS_PARM2); unsigned int state = G_FLOAT(OFS_PARM3); unsigned int i; brushes_t *br; G_FLOAT(OFS_RETURN) = 0; if (!hm) return; // hm->recalculatebrushlighting = true; for (i = 0; i < hm->numbrushes; i++) { br = &hm->wbrushes[i]; if (br->id == brushid) { G_FLOAT(OFS_RETURN) = br->selected; if (state >= 0) { if (br->selected != state) { for (i = 0; i < br->numplanes; i++) { br->faces[i].tex->rebuild = true; br->faces[i].relight = true; } br->selected = state; } } // return; } } } // {"brush_calcfacepoints",PF_brush_calcfacepoints,0,0, 0, 0, D("int(int faceid, brushface_t *in_faces, int numfaces, vector *points, int maxpoints)", "Determines the points of the specified face, if the specified brush were to actually be created.")}, void QCBUILTIN PF_brush_calcfacepoints(pubprogfuncs_t *prinst, struct globalvars_s *pr_globals) { unsigned int faceid = G_INT(OFS_PARM0); unsigned int numfaces = G_INT(OFS_PARM2); qcbrushface_t *in_faces = validateqcpointer(prinst, G_INT(OFS_PARM1), sizeof(*in_faces), numfaces, false); unsigned int maxpoints = G_INT(OFS_PARM4); vec3_t *out_verts = validateqcpointer(prinst, G_INT(OFS_PARM3), sizeof(*out_verts), maxpoints, false); vecV_t facepoints[256]; vec4_t planes[256]; unsigned int j, numpoints; faceid--; if (faceid < 0 || faceid >= numfaces) { G_INT(OFS_RETURN) = 0; return; } //make sure this isn't a dupe face for (j = 0; j < faceid; j++) { if (in_faces[j].planenormal[0] == in_faces[faceid].planenormal[0] && in_faces[j].planenormal[1] == in_faces[faceid].planenormal[1] && in_faces[j].planenormal[2] == in_faces[faceid].planenormal[2] && in_faces[j].planedist == in_faces[faceid].planedist) { G_INT(OFS_RETURN) = 0; return; } } //generate a list that Terr_GenerateBrushFace can actually use, silly, but lets hope this isn't needed to be nippy for (j = 0; j < numfaces; j++) { VectorCopy(in_faces[j].planenormal, planes[j]); planes[j][3] = in_faces[j].planedist; } //generate points now (so we know the correct mins+maxs for the brush, and whether the plane is relevent) numpoints = Fragment_ClipPlaneToBrush(facepoints, countof(facepoints), planes, sizeof(*planes), numfaces, planes[faceid]); G_INT(OFS_RETURN) = numpoints; if (numpoints > maxpoints) numpoints = maxpoints; //... and copy them out without padding. yeah, silly. for (j = 0; j < numpoints; j++) { VectorCopy(facepoints[j], out_verts[j]); } } // {"brush_getfacepoints",PF_brush_getfacepoints,0,0, 0, 0, D("int(float modelid, int brushid, int faceid, vector *points, int maxpoints)", "Allows you to easily set transient visual properties of a brush. If brush/face is -1, applies to all. returns old value. selectedstate=-1 changes nothing (called for its return value).")}, void QCBUILTIN PF_brush_getfacepoints(pubprogfuncs_t *prinst, struct globalvars_s *pr_globals) { world_t *vmw = prinst->parms->user; model_t *mod = vmw->Get_CModel(vmw, G_FLOAT(OFS_PARM0)); heightmap_t *hm = mod?mod->terrain:NULL; unsigned int brushid = G_INT(OFS_PARM1); unsigned int faceid = G_INT(OFS_PARM2); unsigned int maxpoints = G_INT(OFS_PARM4), p; vec3_t *out_verts = validateqcpointer(prinst, G_INT(OFS_PARM3), sizeof(*out_verts), maxpoints, false); size_t i; brushes_t *br; G_INT(OFS_RETURN) = 0; if (!hm) return; for (i = 0; i < hm->numbrushes; i++) { br = &hm->wbrushes[i]; if (br->id == brushid) { if (!faceid) { if (maxpoints >= 2) { VectorCopy(br->mins, out_verts[0]); VectorCopy(br->maxs, out_verts[1]); G_INT(OFS_RETURN) = 2; } else if (maxpoints == 1) { VectorInterpolate(br->mins, 0.5, br->maxs, out_verts[0]); G_INT(OFS_RETURN) = 1; } } else { faceid--; if (faceid >= br->numplanes) break; maxpoints = min(maxpoints, br->faces[faceid].numpoints); for (p = 0; p < maxpoints; p++) VectorCopy(br->faces[faceid].points[p], out_verts[p]); G_INT(OFS_RETURN) = p; } break; } } } // {"brush_findinvolume",PF_brush_findinvolume,0,0, 0, 0, D("int(float modelid, vector *planes, float *dists, int numplanes, int *out_brushes, int *out_faces, int maxresults)", "Allows you to easily obtain a list of brushes+faces within the given bounding region. If out_faces is not null, the same brush might be listed twice.")}, void QCBUILTIN PF_brush_findinvolume(pubprogfuncs_t *prinst, struct globalvars_s *pr_globals) { world_t *vmw = prinst->parms->user; model_t *mod = vmw->Get_CModel(vmw, G_FLOAT(OFS_PARM0)); heightmap_t *hm = mod?mod->terrain:NULL; int in_numplanes = G_INT(OFS_PARM3); vec3_t *in_normals = validateqcpointer(prinst, G_INT(OFS_PARM1), sizeof(*in_normals), in_numplanes, false); float *in_distances = validateqcpointer(prinst, G_INT(OFS_PARM2), sizeof(*in_distances), in_numplanes, false); unsigned int maxresults = G_INT(OFS_PARM6); unsigned int *out_brushids = validateqcpointer(prinst, G_INT(OFS_PARM4), sizeof(*out_brushids), maxresults, false); unsigned int *out_faceids = G_INT(OFS_PARM5)?validateqcpointer(prinst, G_INT(OFS_PARM5), sizeof(*out_faceids), maxresults, false):NULL; unsigned int i, j, k, r = 0; brushes_t *br; vec3_t best; float dist; //find all brushes/faces with a vetex within the region //the brush is inside if any every plane has at least one vertex on the inner side if (hm) for (i = 0; i < hm->numbrushes; i++) { br = &hm->wbrushes[i]; for (j = 0; j < in_numplanes; j++) { for (k=0 ; k<3 ; k++) { if (in_normals[j][k] < 0) best[k] = br->maxs[k]; else best[k] = br->mins[k]; } dist = DotProduct (best, in_normals[j]); dist = in_distances[j] - dist; if (dist <= 0) //don't find coplanar brushes. add an epsilon if you need this. break; } if (j == in_numplanes) { //the box had some point on the near side of every single plane, and thus must contain at least part of the box if (r == maxresults) break; //ran out out_brushids[r] = br->id; if (out_faceids) //FIXME: handle this properly. out_faceids[r] = 0; r++; } } G_INT(OFS_RETURN) = r; } void Terr_WriteBrushInfo(vfsfile_t *file, brushes_t *br) { //( -0 -0 16 ) ( -0 -0 32 ) ( 64 -0 16 ) texname [x y z d] [x y z d] rotation sscale tscale float *point[3]; int i; VFS_PRINTF(file, "\n{"); for (i = 0; i < br->numplanes; i++) { point[0] = br->faces[i].points[0]; point[1] = br->faces[i].points[1]; point[2] = br->faces[i].points[2]; //%.9g is 'meant' to be lossless for a standard ieee single-precision float. (%.17g for a double) VFS_PRINTF(file, "\n( %.9g %.9g %.9g ) ( %.9g %.9g %.9g ) ( %.9g %.9g %.9g ) \"%s\" [ %.9g %.9g %.9g %.9g ] [ %.9g %.9g %.9g %.9g ] 0 1 1", point[0][0], point[0][1], point[0][2], point[1][0], point[1][1], point[1][2], point[2][0], point[2][1], point[2][2], br->faces[i].tex?br->faces[i].tex->shadername:"", br->faces[i].stdir[0][0], br->faces[i].stdir[0][1], br->faces[i].stdir[0][2], br->faces[i].stdir[0][3], br->faces[i].stdir[1][0], br->faces[i].stdir[1][1], br->faces[i].stdir[1][2], br->faces[i].stdir[1][3] ); } VFS_PRINTF(file, "\n}"); } void Terr_WriteMapFile(vfsfile_t *file, model_t *mod) { char token[8192]; int nest = 0; const char *start, *entities = Mod_GetEntitiesString(mod); int i; unsigned int entnum = 0; heightmap_t *hm; hm = mod->terrain; if (hm && hm->legacyterrain) VFS_WRITE(file, "terrain\n", 8); start = entities; while(entities) { entities = COM_ParseOut(entities, token, sizeof(token)); if (token[0] == '}' && token[1] == 0) { nest--; if (!nest) { if (!entnum) { // VFS_PRINTF(file, "\n//Worldspawn brushes go here"); hm = mod->terrain; if (hm) for (i = 0; i < hm->numbrushes; i++) Terr_WriteBrushInfo(file, &hm->wbrushes[i]); } entnum++; } } else if (token[0] == '{' && token[1] == 0) { nest++; } else { if (!strcmp(token, "model")) { int submodelnum; entities = COM_ParseOut(entities, token, sizeof(token)); if (*token == '*') submodelnum = atoi(token+1); else submodelnum = 0; if (submodelnum) { model_t *submod; Q_snprintfz(token, sizeof(token), "*%i:%s", submodelnum, mod->name); submod = Mod_FindName (token); // VFS_PRINTF(file, "\nBrushes for %s go here", token); hm = submod->terrain; if (hm) { for (i = 0; i < hm->numbrushes; i++) Terr_WriteBrushInfo(file, &hm->wbrushes[i]); start = entities; } } } else entities = COM_ParseOut(entities, token, sizeof(token)); } VFS_WRITE(file, start, entities - start); start = entities; } } void Mod_Terrain_Save_f(void) { vfsfile_t *file; model_t *mod; const char *mapname = Cmd_Argv(1); char fname[MAX_QPATH]; if (Cmd_IsInsecure()) { Con_Printf("Please use this command via the console\n"); return; } if (*mapname) mod = Mod_FindName(va("maps/%s", mapname)); #ifndef SERVERONLY else if (cls.state) mod = cl.worldmodel; #endif else mod = NULL; if (!mod) { Con_Printf("no model loaded by that name\n"); return; } if (mod->loadstate != MLS_LOADED) { Con_Printf("that model isn't fully loaded\n"); return; } if (*Cmd_Argv(2)) Q_snprintfz(fname, sizeof(fname), "maps/%s.map", Cmd_Argv(2)); else Q_snprintfz(fname, sizeof(fname), "%s", mod->name); if (mod->type != mod_heightmap) { //warning: brushes are not saved unless its a .map COM_StripExtension(mod->name, fname, sizeof(fname)); Q_strncatz(fname, ".ent", sizeof(fname)); FS_CreatePath(fname, FS_GAMEONLY); file = FS_OpenVFS(fname, "wb", FS_GAMEONLY); if (!file) Con_Printf("unable to open %s\n", fname); else { const char *s = Mod_GetEntitiesString(mod); VFS_WRITE(file, s, strlen(s)); VFS_CLOSE(file); FS_FlushFSHashWritten(fname); } } else { FS_CreatePath(fname, FS_GAMEONLY); file = FS_OpenVFS(fname, "wb", FS_GAMEONLY); if (!file) Con_Printf("unable to open %s\n", fname); else { Terr_WriteMapFile(file, mod); VFS_CLOSE(file); FS_FlushFSHashWritten(fname); } } } qboolean Terr_ReformEntitiesLump(model_t *mod, heightmap_t *hm, char *entities) { char token[8192]; int nest = 0; int buflen = strlen(entities); char *out, *outstart, *start; int i; int submodelnum = 0; qboolean isdetail = false; qboolean foundsubmodel = false; qboolean inbrush = false; int numplanes = 0; vec4_t planes[64]; struct brushface_s faces[64]; int brushcontents = FTECONTENTS_SOLID; heightmap_t *subhm = NULL; model_t *submod = NULL; const char *brushpunct = "(){}[]"; //use an empty string for better compat with vanilla qbsp... #ifdef RUNTIMELIGHTING hm->entsdirty = true; hm->relightcontext = LightStartup(NULL, mod, false, false); hm->lightthreadmem = BZ_Malloc(lightthreadctxsize); hm->inheritedlightthreadmem = false; #endif /*FIXME: we need to re-form the entities lump to insert model fields as appropriate*/ outstart = out = Z_Malloc(buflen+1); while(entities) { start = entities; entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); if (token[0] == '}' && token[1] == 0) { nest--; if (inbrush) { brushes_t brush; //finish the brush brush.contents = brushcontents; brush.numplanes = numplanes; brush.planes = planes; brush.faces = faces; brush.id = 0; if (numplanes && subhm) { qboolean oe = subhm->brushesedited; Terr_Brush_Insert(submod, subhm, &brush); subhm->brushesedited = oe; } numplanes = 0; inbrush = false; continue; } } else if (token[0] == '{' && token[1] == 0) { nest++; if (nest == 1) { //entering a new entity foundsubmodel = false; isdetail = false; } if (nest == 2) { if (isdetail) //func_detail injects its brushes into the world model for some reason. { submod = mod; subhm = hm; } else if (!foundsubmodel) { foundsubmodel = true; if (submodelnum) { Q_snprintfz(token, sizeof(token), "*%i", submodelnum); *out++ = 'm'; *out++ = 'o'; *out++ = 'd'; *out++ = 'e'; *out++ = 'l'; *out++ = ' '; *out++ = '\"'; for (i = 0; token[i]; i++) *out++ = token[i]; *out++ = '\"'; *out++ = ' '; Q_snprintfz(token, sizeof(token), "*%i:%s", submodelnum, mod->name); submod = Mod_FindName (token); if (submod->loadstate == MLS_NOTLOADED) { submod->type = mod_heightmap; Mod_SetEntitiesString(submod, "", true); subhm = submod->terrain = Mod_LoadTerrainInfo(submod, submod->name, true); subhm->exteriorcontents = FTECONTENTS_EMPTY; ClearBounds(submod->mins, submod->maxs); submod->funcs.NativeTrace = Heightmap_Trace_Test; submod->funcs.PointContents = Heightmap_PointContents; submod->funcs.NativeContents = Heightmap_NativeBoxContents; submod->funcs.LightPointValues = Heightmap_LightPointValues; submod->funcs.StainNode = Heightmap_StainNode; submod->funcs.MarkLights = Heightmap_MarkLights; submod->funcs.ClusterForPoint = Heightmap_ClusterForPoint; submod->funcs.ClusterPVS = Heightmap_ClusterPVS; #ifndef CLIENTONLY submod->funcs.FindTouchedLeafs = Heightmap_FindTouchedLeafs; submod->funcs.EdictInFatPVS = Heightmap_EdictInFatPVS; submod->funcs.FatPVS = Heightmap_FatPVS; #endif submod->loadstate = MLS_LOADED; submod->pvsbytes = sizeof(hmpvs_t); #ifdef RUNTIMELIGHTING subhm->relightcontext = LightStartup(hm->relightcontext, submod, false, false); subhm->lightthreadmem = hm->lightthreadmem; subhm->inheritedlightthreadmem = true; #endif } else subhm = NULL; } else { submod = mod; subhm = hm; } submodelnum++; } inbrush = true; continue; } } else if (inbrush) { //parse a plane //Quake: ( -0 -0 16 ) ( -0 -0 32 ) ( 64 -0 16 ) texname 0 -32 rotation sscale tscale //hexen2: ( -0 -0 16 ) ( -0 -0 32 ) ( 64 -0 16 ) texname 0 -32 rotation sscale tscale utterlypointless //Valve: ( -0 -0 16 ) ( -0 -0 32 ) ( 64 -0 16 ) texname [x y z d] [x y z d] rotation sscale tscale //fte : ( px py pz pd ) texname [sx sy sz sd] [tx ty tz td] 0 1 1 brushtex_t *bt; vec3_t d1,d2; vec3_t points[3]; vec4_t texplane[2]; float scale[2], rot; int p; qboolean hlstyle = false; memset(points, 0, sizeof(points)); for (p = 0; p < 3; p++) { if (token[0] != '(' || token[1] != 0) break; entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); points[p][0] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); points[p][1] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); points[p][2] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); if (token[0] != ')' || token[1] != 0) { // VectorClear(points[1]); // VectorClear(points[2]); points[1][0] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); if (p == 0 && !strcmp(token, ")")) p = 4; //we just managed to read an entire plane instead of 3 points. break; } entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); } if (p < 3) { Con_Printf(CON_ERROR "%s: malformed brush\n", mod->name); return false; } if (numplanes == sizeof(planes)/sizeof(planes[0])) { Con_Printf(CON_ERROR "%s: too many planes in brush\n", mod->name); return false; } bt = Terr_Brush_FindTexture(subhm, token); if (*token == '*') { if (!Q_strncasecmp(token, "*lava", 5)) brushcontents = FTECONTENTS_LAVA; else if (!Q_strncasecmp(token, "*slime", 5)) brushcontents = FTECONTENTS_SLIME; else brushcontents = FTECONTENTS_WATER; } else if (!Q_strncasecmp(token, "*sky", 4)) brushcontents = FTECONTENTS_SKY; else if (!Q_strcasecmp(token, "clip")) brushcontents = FTECONTENTS_PLAYERCLIP; else brushcontents = FTECONTENTS_SOLID; //FIXME: halflife format has the entire [x y z dist] plane specified. entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); if (*token == '[') { hlstyle = true; entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); texplane[0][0] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); texplane[0][1] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); texplane[0][2] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); texplane[0][3] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); //] entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); //[ entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); texplane[1][0] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); texplane[1][1] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); texplane[1][2] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); texplane[1][3] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); //] } else { VectorClear(texplane[0]); VectorClear(texplane[1]); texplane[0][3] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); texplane[1][3] = atof(token); } entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); rot = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); scale[0] = atof(token); entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); scale[1] = atof(token); //hexen2 has some extra junk that is useless - some 'light' value, but its never used and should normally be -1. while (*entities == ' ' || *entities == '\t') entities++; if (*entities == '-' || (*entities >= '0' && *entities <= '9')) entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); //okay, that's all the actual parsing, now try to make sense of this plane. if (p == 4) { //parsed an actual plane VectorCopy(points[0], planes[numplanes]); planes[numplanes][3] = points[1][0]; } else { //parsed 3 points. VectorSubtract(points[0], points[1], d1); VectorSubtract(points[2], points[1], d2); CrossProduct(d1, d2, planes[numplanes]); VectorNormalize(planes[numplanes]); planes[numplanes][3] = DotProduct(points[1], planes[numplanes]); } faces[numplanes].tex = bt; //quake's .maps use the normal to decide which texture directions to use in some lame axially-aligned way. if (!hlstyle) { float a=fabs(planes[numplanes][0]),b=fabs(planes[numplanes][1]),c=fabs(planes[numplanes][2]); if (a>=b&&a>=c) texplane[0][1] = 1; else texplane[0][0] = 1; if (c>a&&c>b) texplane[1][1] = -1; else texplane[1][2] = -1; } if (rot) { int mas, mat; float s,t; float a = rot*(M_PI/180); float cosa = cos(a), sina=sin(a); for (mas=0; mas<2&&!texplane[0][mas]; mas++); for (mat=0; mat<2&&!texplane[1][mat]; mat++); for (i = 0; i < 2; i++) { s = cosa*texplane[i][mas] - sina*texplane[i][mat]; t = sina*texplane[i][mas] + cosa*texplane[i][mat]; texplane[i][mas] = s; texplane[i][mat] = t; } } if (!scale[0]) scale[0] = 1; if (!scale[1]) scale[1] = 1; VectorScale(texplane[0], 1.0/scale[0], faces[numplanes].stdir[0]); VectorScale(texplane[1], 1.0/scale[1], faces[numplanes].stdir[1]); faces[numplanes].stdir[0][3] = texplane[0][3]; faces[numplanes].stdir[1][3] = texplane[1][3]; numplanes++; continue; } else { if (!strcmp(token, "classname")) { entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); if (!strcmp(token, "func_detail")) isdetail = true; } else entities = COM_ParseTokenOut(entities, brushpunct, token, sizeof(token), NULL); } while(start < entities) *out++ = *start++; } *out = 0; Mod_SetEntitiesString(mod, outstart, false); mod->numsubmodels = submodelnum; return true; } qboolean QDECL Terr_LoadTerrainModel (model_t *mod, void *buffer, size_t bufsize) { int legacyterrain; heightmap_t *hm; char token[MAX_QPATH]; int sectsize = 0; char *src; src = COM_ParseOut(buffer, token, sizeof(token)); if (!strcmp(token, "terrain")) { legacyterrain = true; buffer = src; } else if (!strcmp(token, "{")) legacyterrain = false; else { Con_Printf(CON_ERROR "%s wasn't terrain map\n", mod->name); //shouldn't happen return false; } mod->type = mod_heightmap; ClearBounds(mod->mins, mod->maxs); hm = Z_Malloc(sizeof(*hm)); ClearLink(&hm->recycle); // ClearLink(&hm->collected); COM_FileBase(mod->name, hm->path, sizeof(hm->path)); if (!Terr_ReformEntitiesLump(mod, hm, buffer)) return false; strcpy(hm->groundshadername, "terrainshader"); strcpy(hm->skyname, "sky1"); hm->entitylock = Sys_CreateMutex(); hm->sectionsize = sectsize; if (legacyterrain) { hm->firstsegx = -1; hm->firstsegy = -1; hm->maxsegx = +1; hm->maxsegy = +1; } else { hm->firstsegx = 0; hm->firstsegy = 0; hm->maxsegx = 0; hm->maxsegy = 0; } hm->legacyterrain = legacyterrain; if (legacyterrain) hm->exteriorcontents = FTECONTENTS_SOLID; //sky outside the map Terr_ParseEntityLump(mod, hm); if (hm->firstsegx != hm->maxsegx) { vec3_t point; point[0] = (hm->firstsegx - CHUNKBIAS) * hm->sectionsize; point[1] = (hm->firstsegy - CHUNKBIAS) * hm->sectionsize; point[2] = -999999999999999999999999.f; AddPointToBounds(point, mod->mins, mod->maxs); point[0] = (hm->maxsegx - CHUNKBIAS) * hm->sectionsize; point[1] = (hm->maxsegy - CHUNKBIAS) * hm->sectionsize; point[2] = 999999999999999999999999.f; AddPointToBounds(point, mod->mins, mod->maxs); } mod->funcs.NativeTrace = Heightmap_Trace_Test; mod->funcs.PointContents = Heightmap_PointContents; mod->funcs.NativeContents = Heightmap_NativeBoxContents; mod->funcs.LightPointValues = Heightmap_LightPointValues; mod->funcs.StainNode = Heightmap_StainNode; mod->funcs.MarkLights = Heightmap_MarkLights; mod->funcs.ClusterForPoint = Heightmap_ClusterForPoint; mod->funcs.ClusterPVS = Heightmap_ClusterPVS; #ifndef CLIENTONLY mod->funcs.FindTouchedLeafs = Heightmap_FindTouchedLeafs; mod->funcs.EdictInFatPVS = Heightmap_EdictInFatPVS; mod->funcs.FatPVS = Heightmap_FatPVS; #endif /* mod->hulls[0].funcs.HullPointContents = Heightmap_PointContents; mod->hulls[1].funcs.HullPointContents = Heightmap_PointContents; mod->hulls[2].funcs.HullPointContents = Heightmap_PointContents; mod->hulls[3].funcs.HullPointContents = Heightmap_PointContents; */ mod->pvsbytes = sizeof(hmpvs_t); mod->terrain = hm; #ifdef RUNTIMELIGHTING if (hm->relightcontext) { LightReloadEntities(hm->relightcontext, Mod_GetEntitiesString(mod), true); hm->entsdirty = false; } #endif validatelinks(&hm->recycle); return true; } void *Mod_LoadTerrainInfo(model_t *mod, char *loadname, qboolean force) { heightmap_t *hm; heightmap_t potential; if (!Mod_GetEntitiesString(mod)) return NULL; memset(&potential, 0, sizeof(potential)); Terr_ParseEntityLump(mod, &potential); if (potential.firstsegx >= potential.maxsegx || potential.firstsegy >= potential.maxsegy) { //figure out the size such that it encompases the entire bsp. potential.firstsegx = floor(mod->mins[0] / potential.sectionsize) + CHUNKBIAS; potential.firstsegy = floor(mod->mins[1] / potential.sectionsize) + CHUNKBIAS; potential.maxsegx = ceil(mod->maxs[0] / potential.sectionsize) + CHUNKBIAS; potential.maxsegy = ceil(mod->maxs[1] / potential.sectionsize) + CHUNKBIAS; if (*loadname=='*') { potential.firstsegx = bound(0, potential.firstsegx, CHUNKLIMIT); potential.firstsegy = bound(0, potential.firstsegy, CHUNKLIMIT); potential.maxsegx = bound(potential.firstsegx, potential.maxsegx, CHUNKLIMIT); potential.maxsegy = bound(potential.firstsegx, potential.maxsegy, CHUNKLIMIT); } else {//bound it, such that 0 0 will always be loaded. potential.firstsegx = bound(0, potential.firstsegx, CHUNKBIAS); potential.firstsegy = bound(0, potential.firstsegy, CHUNKBIAS); potential.maxsegx = bound(CHUNKBIAS+1, potential.maxsegx, CHUNKLIMIT); potential.maxsegy = bound(CHUNKBIAS+1, potential.maxsegy, CHUNKLIMIT); } if (!force) { char sect[MAX_QPATH]; Q_snprintfz(sect, sizeof(sect), "maps/%s/sect_%03x_%03x.hms", loadname, potential.firstsegx + (potential.maxsegx-potential.firstsegx)/2, potential.firstsegy + (potential.maxsegy-potential.firstsegy)/2); if (!COM_FCheckExists(sect)) { Q_snprintfz(sect, sizeof(sect), "maps/%s/block_00_00.hms", loadname); if (!COM_FCheckExists(sect)) return NULL; } } } hm = Z_Malloc(sizeof(*hm)); *hm = potential; hm->entitylock = Sys_CreateMutex(); ClearLink(&hm->recycle); Q_strncpyz(hm->path, loadname, sizeof(hm->path)); Q_strncpyz(hm->groundshadername, "terrainshader", sizeof(hm->groundshadername)); hm->exteriorcontents = FTECONTENTS_EMPTY; //bsp geometry outside the heightmap return hm; } #ifndef SERVERONLY void Mod_Terrain_Create_f(void) { int x,y; hmsection_t *s; heightmap_t *hm; char *mname; char *mapdesc; char *skyname; char *groundname; char *watername; char *groundheight; char *waterheight; char *seed; vfsfile_t *file; model_t mod; memset(&mod, 0, sizeof(mod)); if (Cmd_Argc() < 2) { Con_Printf("%s: NAME \"DESCRIPTION\" SKYNAME DEFAULTGROUNDTEX DEFAULTHEIGHT DEFAULTWATER DEFAULTWATERHEIGHT seed\nGenerates a fresh maps/foo.hmp file. You may wish to edit it with notepad later to customise it. You will need csaddon.dat in order to edit the actual terrain.\n", Cmd_Argv(0)); return; } mname = va("maps/%s.hmp", Cmd_Argv(1)); mapdesc = Cmd_Argv(2); if (!*mapdesc) mapdesc = Cmd_Argv(1); skyname = Cmd_Argv(3); groundname = Cmd_Argv(4); groundheight = Cmd_Argv(5); watername = Cmd_Argv(6); waterheight = Cmd_Argv(7); seed = Cmd_Argv(7); Mod_SetEntitiesString(&mod, va( "{\n" "classname \"worldspawn\"\n" "message \"%s\"\n" "_sky \"%s\"\n" "_fog 0.02\n" "_maxdrawdist 0 /*overrides fog distance (if greater)*/\n" "_segmentsize 1024 /*how big each section is. this affects texturing and resolutions*/\n" "_minxsegment -2048\n" "_minysegment -2048\n" "_maxxsegment 2048\n" "_maxysegment 2048\n" "_seed \"%s\" /*for auto-gen plugins*/\n" "_exterior solid\n" "_defaultgroundtexture \"%s\"\n" "_defaultgroundheight \"%s\"\n" "_defaultwatertexture \"%s\"\n" "_defaultwaterheight \"%s\"\n" //hurrah, sea level. // "_tiles 64 64 8 8\n" "}\n" "{\n" "classname info_player_start\n" "origin \"0 0 1024\" /*EDITME*/\n" "}\n" "/*ADD EXTRA ENTITIES!*/\n" , mapdesc ,*skyname?skyname:"terrsky1", seed ,*groundname?groundname:"ground1_1" ,*groundheight?groundheight:"-1024" ,*watername?watername:"*water2" ,*waterheight?waterheight:"0" ), true); mod.type = mod_heightmap; mod.terrain = hm = Z_Malloc(sizeof(*hm)); Terr_ParseEntityLump(&mod, hm); hm->entitylock = Sys_CreateMutex(); ClearLink(&hm->recycle); Q_strncpyz(hm->path, Cmd_Argv(1), sizeof(hm->path)); Q_strncpyz(hm->groundshadername, "terrainshader", sizeof(hm->groundshadername)); hm->exteriorcontents = FTECONTENTS_SOLID; for (x = CHUNKBIAS-1; x < CHUNKBIAS+1; x++) for (y = CHUNKBIAS-1; y < CHUNKBIAS+1; y++) Terr_GetSection(hm, x, y, TGS_TRYLOAD|TGS_DEFAULTONFAIL); for (x = CHUNKBIAS-1; x < CHUNKBIAS+1; x++) for (y = CHUNKBIAS-1; y < CHUNKBIAS+1; y++) { s = Terr_GetSection(hm, x, y, TGS_WAITLOAD|TGS_DEFAULTONFAIL); if (s && (s->flags & (TSF_EDITED|TSF_DIRTY))) { Terr_InitLightmap(s, false); Terr_SaveSection(hm, s, x, y, true); } } if (COM_FCheckExists(mname)) { Con_Printf("%s: already exists, not overwriting.\n", mname); return; } FS_CreatePath(mname, FS_GAMEONLY); file = FS_OpenVFS(mname, "wb", FS_GAMEONLY); if (!file) Con_Printf("unable to open %s\n", mname); else { Terr_WriteMapFile(file, &mod); VFS_CLOSE(file); Con_Printf("Wrote %s\n", mname); FS_FlushFSHashWritten(mname); } Mod_SetEntitiesString(&mod, NULL, false); Terr_FreeModel(&mod); } #endif //reads in the terrain a tile at a time, and writes it out again. //the new version will match our current format version. //this is mostly so I can strip out old format revisions... #ifndef SERVERONLY void Mod_Terrain_Convert_f(void) { model_t *mod; heightmap_t *hm; if (Cmd_FromGamecode()) return; if (Cmd_Argc() >= 2) mod = Mod_FindName(va("maps/%s.hmp", Cmd_Argv(1))); else if (cls.state) mod = cl.worldmodel; else mod = NULL; if (!mod || mod->type == mod_dummy) return; hm = mod->terrain; if (!hm) return; { char *texkill = Cmd_Argv(2); hmsection_t *s; int x, sx; int y, sy; while(Terr_Collect(hm)) //collect as many as we can now, so when we collect later, the one that's collected is fresh. ; for (y = hm->firstsegy; y < hm->maxsegy; y+=SECTIONSPERBLOCK) { Sys_Printf("%g%% complete\n", 100 * (y-hm->firstsegy)/(float)(hm->maxsegy-hm->firstsegy)); for (x = hm->firstsegx; x < hm->maxsegx; x+=SECTIONSPERBLOCK) { for (sy = y; sy < y+SECTIONSPERBLOCK && sy < hm->maxsegy; sy++) { for (sx = x; sx < x+SECTIONSPERBLOCK && sx < hm->maxsegx; sx++) { s = Terr_GetSection(hm, sx, sy, TGS_WAITLOAD|TGS_NODOWNLOAD|TGS_NORENDER); if (s) { if (*texkill) ted_texkill(s, texkill); s->flags |= TSF_EDITED; } } } for (sy = y; sy < y+SECTIONSPERBLOCK && sy < hm->maxsegy; sy++) { for (sx = x; sx < x+SECTIONSPERBLOCK && sx < hm->maxsegx; sx++) { s = Terr_GetSection(hm, sx, sy, TGS_WAITLOAD|TGS_NODOWNLOAD|TGS_NORENDER); if (s) { if (s->flags & TSF_EDITED) { if (Terr_SaveSection(hm, s, sx, sy, true)) { s->flags &= ~TSF_EDITED; } } } } } while(Terr_Collect(hm)) ; } } Sys_Printf("%g%% complete\n", 100.0f); } } #endif void Mod_Terrain_Reload_f(void) { model_t *mod; heightmap_t *hm; if (Cmd_Argc() >= 2) mod = Mod_FindName(va("maps/%s.hmp", Cmd_Argv(1))); #ifndef SERVERONLY else if (cls.state) mod = cl.worldmodel; #endif else mod = NULL; if (!mod || mod->type == mod_dummy) return; hm = mod->terrain; if (!hm) return; if (Cmd_Argc() >= 4) { hmsection_t *s; int sx = atoi(Cmd_Argv(2)) + CHUNKBIAS; int sy = atoi(Cmd_Argv(3)) + CHUNKBIAS; if (hm) { s = Terr_GetSection(hm, sx, sy, TGS_NOLOAD); if (s) { s->flags |= TSF_NOTIFY; } } } else Terr_PurgeTerrainModel(mod, false, true); } terrainfuncs_t *QDECL Terr_GetTerrainFuncs(void) { #ifdef SERVERONLY return NULL; //dedicated server builds have all the visual stuff stripped, which makes APIs too inconsistent. Generate then save. Or fix up the API... #else return &terrainfuncs; #endif } void Terr_Init(void) { terrainfuncs.GenerateWater = Terr_GenerateWater; terrainfuncs.InitLightmap = Terr_InitLightmap; terrainfuncs.AddMesh = Terr_AddMesh; terrainfuncs.GetLightmap = Terr_GetLightmap; terrainfuncs.GetSection = Terr_GetSection; terrainfuncs.GenerateSections = Terr_GenerateSections; terrainfuncs.FinishedSection = Terr_FinishedSection; Cvar_Register(&mod_terrain_networked, "Terrain"); Cvar_Register(&mod_terrain_defaulttexture, "Terrain"); Cvar_Register(&mod_terrain_savever, "Terrain"); Cmd_AddCommand("mod_terrain_save", Mod_Terrain_Save_f); Cmd_AddCommand("mod_terrain_reload", Mod_Terrain_Reload_f); #ifndef SERVERONLY Cmd_AddCommand("mod_terrain_create", Mod_Terrain_Create_f); Cmd_AddCommandD("mod_terrain_convert", Mod_Terrain_Convert_f, "mod_terrain_convert [mapname] [texkill]\nConvert a terrain to the current format. If texkill is specified, only tiles with the named texture will be converted, and tiles with that texture will be stripped. This is a slow operation."); #endif Mod_RegisterModelFormatText(NULL, "FTE Heightmap Map (hmp)", "terrain", Terr_LoadTerrainModel); Mod_RegisterModelFormatText(NULL, "Quake Map Format (map)", "{", Terr_LoadTerrainModel); } #endif