debug = $debug; $this->layout = new Window(1,1,static::FRAME_WIDTH,static::FRAME_HEIGHT+1,'LAYOUT',NULL,$debug); $this->header = new Window(1,1,static::FRAME_WIDTH,1,'HEADER',$this->layout,$debug); //dump(['this'=>get_class($this),'header_from'=>$this->header->x,'header_to'=>$this->header->bx,'width'=>$this->header->width]); // Provider can use all its space $this->provider = new Window(1,1,static::FRAME_PROVIDER_LENGTH,1,'PROVIDER',$this->header,$debug); //dump(['this'=>get_class($this),'provider_from'=>$this->provider->x,'provider_to'=>$this->provider->bx,'width'=>$this->provider->width]); // Page number is prefixed with a color change (if required, otherwise a space) $this->pagenum = new Window($this->provider->bx+1,1,static::FRAME_PAGE_LENGTH,1,'#',$this->header,$debug); //dump(['this'=>get_class($this),'pagenum_from'=>$this->pagenum->x,'pagenum_to'=>$this->pagenum->bx,'width'=>$this->pagenum->width]); // Unit is prefixed with a color change (required, since a different color to page) $this->unit = new Window($this->pagenum->bx+1,1,static::FRAME_COST_LENGTH,1,'$',$this->header,$debug); //dump(['this'=>get_class($this),'unit_from'=>$this->unit->x,'unit_to'=>$this->unit->bx,'width'=>$this->unit->width]); $this->content = new Window(1,2,static::FRAME_WIDTH,static::FRAME_HEIGHT,'CONTENT',$this->layout,$debug); $this->resetHistory(); $this->clear(); $this->goto($frame,$index); } public function __get(string $key): mixed { switch ($key) { case 'access' : case 'id' : case 'cls': case 'cost': case 'created_at': case 'public' : case 'type' : return $this->fo?->{$key}; case 'cug': return $this->fo?->cug_id; case 'frame': case 'index': return $this->{$key}; case 'next': return ($this->index < 'z') ? chr(ord($this->index)+1) : $this->index; case 'prev': return ($this->index > 'a') ? chr(ord($this->index)-1) : $this->index; case 'page': return sprintf('%d%s',$this->frame,$this->index); case 'height': return $this->layout->height; case 'width': return $this->layout->width; case 'fields_input': return $this->fields_input; case 'field_current': return (! is_null($this->field_active)) ? $this->fields_input->get($this->field_active): NULL; default: throw new \Exception('Unknown key: '.$key); } } public function __set(string $key,mixed $value): void { switch ($key) { case 'showheader': $this->{$key} = $value; break; default: throw new \Exception('Unknown key: '.$key); } } public function __toString(): string { return $this->display()->join(""); } /* METHODS */ /** * Return a list of alternative versions of this frame. * * @todo: Need to adjust to not include access=0 frames unless owner */ public function alts(): Collection { return Frame::where('frame',$this->frame) ->where('index',$this->index) ->where('id','<>',$this->fo->id) ->where('mode_id',$this->id) ->where('access',1) ->limit(9) ->get(); } private function atcode(string $name,int $length,mixed $pad=' '): string { switch ($name) { case 'NODE': $result = '00010001'; break; case 'DATETIME': $result = Carbon::now()->toRfc822String(); break; case 'DATE': $result = Carbon::now()->format('Y-m-d'); break; case 'TIME': $result = Carbon::now()->format('H:ia'); break; default: $result = $name; } if (strlen($result) < abs($length) && $pad) $result = ($length < 0) ? Str::padLeft($result,abs($length),$pad) : Str::padRight($result,abs($length),$pad); return $result; } /** * History go back to previous page * * @return bool */ public function back(): bool { if ($this->history->count() > 1) { $this->history->pop(); $this->fo = $this->history->last(); return TRUE; } return FALSE; } /** * Parse a page, extracting fields and formatting into our Window objects * * @param bool $force * @return array * @throws \Exception */ public function build(bool $force=FALSE): array { if ($this->build && ! $force) throw new \Exception('Refusing to build without force.'); $this->load(); $test = FALSE; $this->provider->content = $this->parse(($test ? chr(0x02).'T'.chr(0x03).'B'.chr(0x04) : 'TB').'A'.($test ? ' - 12345678901234567890123456789012345678901234567890123456' : ''),static::FRAME_PROVIDER_LENGTH,$this->provider->y,$this->provider->x); $this->pagenum->content = $this->parse($this->color_page.($test ? '123456789012345a' : $this->page),static::FRAME_SPACE+static::FRAME_PAGE_LENGTH,$this->pagenum->y,$this->pagenum->x); $this->unit->content = $this->parse($this->color_unit.Str::padLeft(($this->cost+($test ? 1234 : 0)).'c',static::FRAME_COST_LENGTH-1,' '),static::FRAME_SPACE+static::FRAME_COST_LENGTH,$this->unit->y,$this->unit->x); $this->content->content = $this->parse($this->fo->content,static::FRAME_WIDTH,$this->content->y,$this->content->x); $this->header->visible = ($this->showheader || $test); $this->build_system_fields(); $this->build = $this->layout->build(1,1,$this->debug); // Add our dynamic values $fields = $this->fields_dynamic->filter(function($item) { return $item->value; }); Log::channel('bbs')->debug(sprintf('There are [%d] dynamic fields to populate',$fields->count())); if ($fields->count()) $this->fields_insert($fields); // Add our input fields $fields = $this->fields_input->filter(function($item) { return is_null($item->value); }); Log::channel('bbs')->debug(sprintf('There are [%d] input fields to setup',$fields->count())); if ($fields->count()) $this->fields_insert($fields); return $this->build; } // @todo To complete - some of these came from SBBS and are not valid here private function build_system_fields(): void { // Fields we can process automatically $auto = ['NODE','DATETIME','DATE','TIME','REALNAME','BBS']; $df = $this->fields_dynamic->filter(function($item) { return is_null($item->value); }); if (! $df->count()) return; foreach ($df as $field) { if (in_array($field->name,$auto)) $this->field_dynamic($field->name,$this->atcode($field->name,$field->size,$field->pad)); } } private function clear(): void { $this->build = []; $this->fields_dynamic = collect(); $this->fields_input = collect(); $this->fieldReset(); } // Insert our *_field data (if it is set) public function display(): Collection { if (! $this->build) throw new \Exception('Page not ready'); // build $display = $this->build; // populate dynamic fields - refresh dynamic fields if 09, otherwise show previous compiled with 00 // check if there are any dynamic fields with no values switch ($this->mo->name) { case 'ansi': $new_line = NULL; $shownullchars = TRUE; break; case 'viewdata': $new_line = static::BG_BLACK|static::WHITE; $shownullchars = FALSE; break; default: throw new \Exception(sprintf('Dont know how to display a [%s] page',$this->mo->name)); } $result = collect(); $last = $new_line; if ($this->debug) dump(['page-width'=>$this->width,'page-height'=>$this->height]); // render for ($y=1;$y<=$this->height;$y++) { $line = ''; if ($new_line) $last = $new_line; if ($this->debug) dump('============== ['.$y.'] ==============='); $x = 1; while ($x <= $this->width) { if ($this->debug) dump('* CELL : y:'.$y.', x:'.$x); // The current char value $char = (isset($display[$y]) && isset($display[$y][$x])) ? $display[$y][$x] : NULL; if ($this->debug) dump(' - CHAR : '.(! is_null($char) ? $char->ch : 'undefined').', ATTR:'.(! is_null($char) ? $char->attr : 'undefined').', LAST:'.$last); if ($this->debug) { dump('-------- ['.$x.'] ------'); dump('y:'.$y.',x:'.$x.', attr:'.(! is_null($char) ? $char->attr : 'undefined')); } // Only write a new attribute if it has changed (and not Videotex) if ($last !== $char->attr) { // The current attribute for this character $attr = is_null($char) ? NULL : $char->attr($this->mo,$last,$this->debug); switch ($this->mo->name) { case 'ansi': // If the attribute is null, we'll write our default attribute if (is_null($attr)) $line .= ''; #static::BG_BLACK|static::LIGHTGRAY; else $line .= (! is_null($attr)) ? $attr : ''; break; case 'viewdata': // If the attribute is null, we'll ignore it since we are drawing a character if (! is_null($attr)) { if ($this->debug) dump(sprintf('= SEND attr:%02x, last: %02x [%s] (%s)',ord($attr),$last,$char->ch,serialize($attr))); $line .= "\x1b".$attr; //$x++; } break; default: throw new \Exception(sprintf('[%s] has not been implemented',$this->mo->name)); } } if (! is_null($char->ch)) { if ($this->debug) dump(' = SEND CHAR :'.$char->ch.', attr:'.$char->attr.', last:'.$last); $line .= $char->ch; } else if ($shownullchars || ((is_null($char->ch) && is_null($char->attr)))) { if ($this->debug) dump(' = CHAR UNDEFINED'); $line .= ' '; } $last = $char->attr; $x++; } if ($this->debug) dump(['line'=>$line]); $result->push($line); if ($this->debug && ($y > $this->debug)) exit(1); } return $result; } /** * Update a dynamic field with a value * * @param $name * @param $value * @return void * @throws \Exception */ private function field_dynamic($name,$value): void { if (($x=$this->fields_dynamic->search(function($item) use ($name) { return $item->name === $name; })) !== FALSE) { $field = $this->fields_dynamic->get($x); // Store our value $field->value = $value; } else { throw new \Exception(sprintf('Dynamic field: [%s], doesnt exist?',$name)); } } private function fields_insert($fields) { foreach ($fields as $field) { if (is_null($field->value)) continue; $content = str_split($field->value); $y = $field->y; $x = $field->x; for ($x;$x < $field->x+abs($field->size);$x++) { $index = $x-$field->x; if (isset($content[$index])) $this->build[$y][$x]->ch = ($field->type !== 'p') ? $content[$index] : '*'; else $this->build[$y][$x]->ch = $field->pad; } } } public function fieldReset(): void { $this->field_active = NULL; foreach ($this->fields_input as $field) $field->value = NULL; } public function fieldNext(): Field|NULL { if ($this->fields_input->count()) { if (is_null($this->field_active)) $this->field_active = 0; else $this->field_active++; return $this->fields_input->get($this->field_active); } else return NULL; } /** * Load a frame by it's ID. * * @param int $id * @return void */ public function get(int $id): void { $this->po->findOrFail($id); $this->frame = $this->po->frame; $this->index = $this->po->index; } /** * Go to a specific frame * * @param int $frame * @param string $index * @return void * @throws \Exception */ public function goto(int $frame,string $index='a'): void { if (strlen($index) !== 1) throw new \Exception('Invalid index:'.$index); $this->frame = $frame; $this->index = $index; $this->fo = NULL; } public function haveNext(): bool { return $this->fo ? Frame::where('frame',$this->frame) ->where('index',$this->next) ->where('mode_id',$this->fo->mode_id) ->exists() : FALSE; } public function isCug(int $cug): bool { return $this->cug === $cug; } // @todo To implement public function isOwner(User $o): bool { return FALSE; } public function isRoute(int $route): bool { return is_numeric($this->fo->{sprintf('r%d',$route)}); } /** * Load a frame, throw a model not found exception if it doesnt exist * * @return void */ public function load(): void { $this->fo = Frame::where('mode_id',$this->mo->id) ->where('frame',$this->frame) ->where('index',$this->index) ->orderBy('created_at','DESC') ->firstOrFail(); $this->history->push($this->fo); $this->clear(); } public function method(int $route): ?Action { if (($x=($this->fo->{sprintf('r%d',$route)})) && (! $this->isRoute($route))) return Action::factory($x); return NULL; } public function new(int $frame,string $index='a'): void { $this->frame = $frame; $this->index = $index; $this->fo = new Frame; // Make sure parent frame exists if (($this->index !== 'a') && (! Frame::where('frame',$this->frame)->where('index',$this->prev)->where('mode',$this->mo->id)->exists())) throw new ParentNotFoundException(sprintf('Parent %d%s doesnt exist',$frame,$index)); } public function next(): void { $this->index = $this->next; $this->fo = NULL; } /** * Clear a user's history * * @return void */ public function resetHistory(): void { $this->history = collect(); } public function route(int $route): void { if ($this->isRoute($route)) { $this->frame = (int)$this->fo->{sprintf('r%d',$route)}; $this->index = 'a'; $this->fo = NULL; } else { throw new NoRouteException('Invalid route '.$route); } } public function prev(): void { $this->index = $this->prev; $this->fo = NULL; } }