Read more...Summary
Earning a strong 4.25 out of 5 on my completely arbitrary rating system (see below), I found Dragon Age: Veilguard to be a worthy addition to the Dragon Age series. Given the concerns about the future of Bioware, I am personally breathing a sigh of relief, as this gives me some confidence that they will be able to continue delivering better and better installments of Dragon Age and Mass Effect, two of my favorite RPG universes in all of gaming.
Representation
I think the first thing to talk about is representation, since it seems to be the elephant in the room in many of the reviews I am reading, which seem to include a lot of coded language around being “strong-armed” or “force-fed” parts of the story. While there were a few story arcs that put aspects of identity front and center, the player is never forced to engage with them, and they felt respectfully written and true to the story the writers were trying to tell. During character creation, I was pleased to see the choice of gender, gender identity, gender presentation, and voice were all independent. It also includes a variety of customizations that I have not seen before, including vitiligo and top-surgery scars. The player can opt-in to a transgender identity early on in the story through a unique moment of reflection for the character, and taking that path opens up a small number of additional dialogue opportunities between the character and the companions as the story unfolds. I absolutely enjoyed this aspect of the game, and felt that it added depth to the characters and narrative.
Story
The story overall was excellent, and the pacing was particularly noticeable for me. A lot of RPGs can make it feel awkward or “non-canonical” for the character to spend time engaging in side quests and advancing companion storylines, but the writing in Veilguard managed to tie it all together very nicely. The character is encouraged to (and rewarded for) going deep and broad, and completionists will be rewarded for their efforts. There are story beats where it feels “right” to rush ahead through a sequence of story quests, and times where it feels “right” to spend some time on yourself and your allies. The quests were deep and varied and tied into the overall plot (or one of your character’s relationships or allies), and I never felt like I was being sent on an errand, which made the 75 hours I spent completing the game fly by. There were plenty of major set piece battles and quests throughout the adventure, and they all feel consequential, emotional, and atmospheric… and, true to its name, it has no shortage of dragon encounters! The final battle was a satisfying capstone to the game, with plenty of opportunities to see the result of your actions (and inaction) come full circle. It was long enough, without dragging or feeling repetitive, and the endings feel well-done.
Combat
The core combat system is a significant highlight for me, delivering a punchy and rewarding experience where every victory feels hard-earned, even though I was playing on the default difficulty and was never really in danger (I only ended up using two revives total in the entire playthrough). Higher difficulties look like they ratchet the difficulty up in ways that I can definitely see being worthwhile for subsequent play-throughs or for players seeking more of a challenge: timings will be tighter, party composition will be more important, and optimizing for weak points, enemy weaknesses, and player resistances will become more important. Sound and audio cues are plentiful, though on the settings I was using the combat graphics can get pretty busy and make it hard to see some of the “tells.”
Gameplay
This satisfying gameplay is complemented by stunning graphics and in-engine cinematics that I often took time to enjoy (and/or screenshot) just on their own. Each of the primary locations has its own visual style, and each one is cinematic and atmospheric and offers plenty of opportunities to take in the scenery and enjoy the complaints of your graphics card. Once I resolved some instability with my graphics card itself that required tuning the clock down a bit, the Nvidia optimized settings for Veilguard ended up being almost perfect for me, with the game running at 60fps for a huge majority of my play-time.
Exploration
Exploration in Veilguard is rewarding, with things to find (and the accompanying small hit of dopamine) around every corner. The maps are unique and littered with puzzles and secrets that make venturing off the beaten path consistently feel rewarding. The puzzles were generally pretty easy, with a few head-scratchers thrown in for good measure, but some did border on the tedious. I suspect that even the ones that were fun to figure out on the first play-through might end up feeling like a bit of a drag on a replay. Another thing to note for explorers and completionists: the game will frequently take you back to a place somewhat after it opens up, so you might find yourself retreading your steps and walking past treasure chests that you stumbled across in your explorations as part of some later quest. It’s also worth noting that the first time you visit a number of the “permanent” locations, you will be more on-rails and will have the rest of the area open for exploration later, so there will be times where you see something and can’t explore it just yet. I generally didn’t find this to be a problem, but it is a bit unfortunate that there are some quests that you can’t return to, and so any items or upgrades that you miss along the way will be inaccessible to you as you progress and eventually complete the quest. (I’m not 100% sure, but on reflection it seems like you may be able to tell whether you can return to a setting by whether there is an Eluvian on the map when you enter.)
Another charming detail worth noting: You can pet the cats! And there are lots of them. And, if you have a controller, it will treat you to a nice rumble along with their purring. (There are also some dogs, but not nearly as many. Also, the dogs don’t purr.)
Companions
Companions and squad-mates are a staple of Bioware games, and Veilguard is no exception. I found this crop to have a lot of depth and a staggering amount of banter, to the point where it seems like you might never hear it all, even after repeated play-throughs. One thing I appreciated was that a lot of the inter-companion banter during quests would pick up where it left off (with some suitable “… so where was I …”) if it is interrupted by combat. The individual characters’ story arcs were really fleshed-out and well-integrated into the world, and aren’t shy about making you feel things either. These arcs make the companions feel alive and make the risks you are taking as part of the story feel more weighty. You get to see the impact your character has on the companions as they wrestle with their past or their desires or their insecurities, and there are meaningful choices to make along the way. No two companion arcs are the same, and they have a mix of “conversation” and “combat” sections throughout. Funnily enough, the richness of these companion arcs is so compelling that it made me realize that the player’s character (at least with the Veilguard background) actually doesn’t have nearly the same kind of development, beyond the central plot at least. I felt like it picked up a bit at the end, but “I” never really felt as well-developed as my companions did, though this may well be desirable from a game design standpoint to allow the player more freedom to choose motives and internal desires for the PC. The voice acting is predominantly good for both Rook and his companions, with a few excellent moments and a few awkward ones here and there.
Progression
The rate at which you acquire talent points, unlock new talent trees, and upgrade your weapons feel good, ensuring a constant feeling of advancement. The talent tree I had for the Warrior felt pretty fun, and I never felt like I had to go through “bad” nodes to get to the ones I wanted. By the end, I definitely felt like I had gotten to all of the nodes that I wanted and had found some fun ones that I didn’t know I needed. I was initially not sure which of the “champion” trees I was going to enjoy, but I was pleasantly surprised to find that the early-game and foundational skills were very fun and ended up leading me naturally toward the champion tree that worked well with them. You can also freely change around your talent points, though after a certain point doing so would be very daunting due to the sheer number of points you accrue. I will say that if you are thinking of hitting the “reset all talents” button, you might want to screenshot your build, since it could be very difficult to remember where you put everything if you end up wanting to go back. There is an arena that unlocks partway through that provides a useful space to practice builds and get a feel for different companion synergies, though it quickly gets to the point where it is too “easy,” which might make it useless for optimizing late-game builds on higher difficulties. It’s also worth noting that player power (and survivability) can be earned through exploration, not just from completing quests and slaying monsters. I found that the default difficulty offered a balanced challenge, and my character’s journey from a “squishy” warrior who can’t time a parry to save his life into an indestructible tank wielding flaming weapons as he ricochets around the battlefield was enjoyable.
Technicals
On the technical side, while the graphics are visually stunning, I was not a fan of the facial animation when simultaneously smiling and talking, though the other animations were fine for me. I definitely picked more of the stoic, angry, emotional, etc dialogue options as a result. The lighting, particularly on the hair, is much more cinematic and atmospheric than prior installments of the game (or, at least, with the settings I’ve been able to play on).
Wrap-up
As a long-time player of Bioware titles, I think Veilguard is clearly cut from the same cloth and makes good use of the “style” that we’ve come to know and love. The companion system feels like an evolution of the one in Mass Effect 2, for example, doubling down on the story arcs and the player’s involvement with them. They even took another pass at what feels like the dream sequences from Mass Effect 3, learning from fans’ criticisms and making it feel great. The dialogue wheel, companion banter, player base, faction reputation system, exploration progress tracking, map systems, fast travel, etc all feel like they build on successful foundations, and occasionally even improve upon them.
If you have played Dragon Age or other BioWare titles or you are interested in trying out an excellent RPG and the 50-75 hour investment doesn’t scare you off, I highly recommend picking up Veilguard!
Final Rating: 4.25/5
- Narrative + Presentation (50%)
- Story + Narrative: 4.5/5
- Characters + Companions: 4/5
- Companion quests: 5/5
- Memorability: 4/5
- Voice Acting: 4/5
- Worldbuilding + Lore: 4/5
- Core Gameplay + Mechanics (25%)
- Combat System: 5/5
- Depth: 4/5
- Variety of encounters: 5/5
- Exploration + World Design (10%)
- Engaging: 5/5
- Level Design: 4/5
- Atmosphere: 5/5
- Puzzles: 3/5
- RPG Aspects (10%)
- Replayability: 3/5
- Character progression: 4.5/5
- RP: 3/5
- Technical (5%)
- Graphics + Visuals: 4.5/5
- Sound design: 3/5
- Performance + Stability: 3/5
This is a collection of my thoughts as I examine the latest Go generics proposal, announced in this blog post.
I don’t come across many times where I think that generics in Go would solve a problem, but there are a few times where I’ve wanted something like it on multiple occasions:
Chantex, which is my head-canon name for channels that are taking the place of mutexes, are a thing that I have started doing a lot after spending some time mulling over Bryan C. Mills’ awesome Rethinking Classical Concurrency Patterns talk.
I won’t dive too much in detail, but here is what it can look like:
type Server struct {
state chan *state // chantex
}
func (s *Server) Login(ctx context.Context, u *User) error {
var state *state
select {
case state = <-s.state:
defer func() { s.state <- state }()
case <-ctx.Done():
return ctx.Err()
}
return state.addUser(u)
}
type state struct{
activeUsers map[userID]*User
}
func (s *state) addUser(u *User) error {
if _, ok := s.activeUsers[u.id]; ok {
return fmt.Errorf("user %q is already logged in", u.name)
}
s.activeUsers[u.id] = u
return nil
}
There are a number of benefits here over the standard mutex pattern, but here are my favorite three:
So, I won’t try to convince you to start using this pattern if you’re not already inclined to do so, but I wanted to try to see if it could be improved with generics. Here’s what I came up with:
type Chantex(type T) chan T
func New(type T)(initialValue *T) Chantex(T) {
ch := make(Chantex(T), 1)
ch <- initialValue
return ch
}
func (c Chantex(T)) Lock() *T {
return <-c
}
func (c Chantex(T)) Unlock(t *T) {
c <- t
}
func (c Chantex(T)) TryLock(ctx context.Context) (t *T, err error) {
select {
case t = <-c:
return t, nil
case <-ctx.Done():
return t, ctx.Err()
}
}
Using this with the example above, it turns into this:
type Server struct {
state Chantex(state)
}
func (s *Server) Login(ctx context.Context, u *User) error {
state, err := s.state.TryLock(ctx)
if err != nil {
return fmt.Errorf("state lock: %s", err)
}
defer s.state.Unlock(state)
return state.addUser(u)
}
type state struct {
activeUsers map[userID]*User
}
func (s *state) addUser(u *User) error {
if _, ok := s.activeUsers[u.id]; ok {
return fmt.Errorf("user %q is already logged in", u.name)
}
s.activeUsers[u.id] = u
return nil
}
It’s only two lines shorter, so you’d have to use it over a dozen times before it makes up for its line count delta. Aside from that, though, it is going to look a lot more familiar to readers who are familiar with the mutex, and it doesn’t have the somewhat-unusual-looking anonymous-defer-in-a-select bit. Luckily, however, with this approach you don’t actually lose the flexibility to use it in a select if you need something more custom:
func (s *Server) ActiveUsers(ctx context.Context) ([]*User, error) {
var state *state
select {
case state = <-s.state:
defer func() { s.state <- state }()
case <-time.After(1*time.Second):
panic("state: possible deadlock")
case <-ctx.Done():
return nil, fmt.Errorf("state lock: %w", ctx.Err())
}
var users []*User
for _, user := range state.activeUsers {
users = append(users, user)
}
return users, nil
}
So, you can actually use this with effectively same code as from before the Chantex(T)
refactor if it makes sense in context.
Overall I am pretty happy with how this one came out. Check out the full code onthe playground if you’re interested.
I think it would be even more useful for some of the other types discussed in the Rethinking Classical Concurrency Patterns talk,
in particular the Future
and Pool
types.
The last thing that took me a bit to figure out: how to make a Locker
method.
This required me to swap over to requring a pointer type explicitly for Chantex
, when I had originally just made it type Chantex(type T) chan T
, but this lines up with how I normally use it:
type Chantex(type T) chan *T
func (c Chantex(T)) Locker(ptr *T) sync.Locker {
return locker(T){ptr, c}
}
func (l locker(P)) Lock() {
*l.ptr = *l.mu.Lock()
}
func (l locker(P)) Unlock() {
l.mu.Unlock(l.ptr)
}
This seems like a worthwhile change, since it potentially avoids any confusion about copying values if the underlying implementation is not well-understood, and it had effectively no change on the caller side of the API.
To track data at scale, we have a metrics pipeline that collects data from running servers. It is similar to prometheus. Each metric can be one of a fixed number of types (basically numbers, floats, and strings) and can have a variable number (fixed on a per-metric basis) of dimensions, which can also be of a (slightly smaller) set of fixed types (strings and ints basically).
Yes, I realize that this example is actually in the generics proposal, but I wanted to play around with other approaches too.
Code could look something like this:
package mine
import (
"log"
"example.com/monitoring/metrics"
"example.com/monitoring/fields"
)
var (
serversOnline = metric.NewStoredIntCounter("servers_online", field.Int("zone"))
)
func init() {
zone, err := currentZone()
if err != nil {
log.Panicf("Failed to determine local zone: %s", err)
}
scrape := time.Tick(30*time.Second)
go func() {
for {
<-scrape
servers, err := countServers()
if err != nil {
log.Errorf("Failed to count servers: %s", err)
continue
}
serversOnline.Set(servers, zone)
}
}()
}
There are a number of downsides of this approach:
Set
method is defined as (m *intMetric) Set(value int, dimensions ...interface{})
int
, float64
, string
, etc) needs its own typeint
, string
, etc) also requires its own constructor and typeIn other words, this approach is getting the worst of all worlds: it is not performant, not type-safe, and it requires lots of copy/pasted code.
I would like to think that generics could solve this problem, and while I think the current proposal does help, it still doesn’t leave us in what might be the optimal place. I don’t think it precludes it in the future, however, but let’s get to it.
So far, this is the only approach that will actually work under the generics proposal. As you will see below though, it is not entirely satisfying.
The “generic” version of the (relevant pieces of) the code could look like this under the current proposal:
serversOnline := metric.NewStoredCounter(int)("servers_online", field.New(string)("zone"))
serversOnline.Set(servers, zone)
Unfortuantely, this doesn’t get us type-safety: the Set
method must still be defined as (m *Metric(T)) Set(value T, dimensions ...interface{})
. So, instead, we could change the API to look more like this:
https://go2goplay.golang.org/p/UPsvxoDw9m6
package metric
type Sample1(type V, D1) struct {
Timestamp time.Time
Value V
Dimension1 D1
}
type Metric1(type V, D1) struct {
Name string
Values []Sample1(V, D1)
}
func NewMetric1(type V, D1)(name string) *Metric1(V, D1) {
return &Metric1(V, D1){Name: name}
}
func (m *Metric1(V, D1)) Set(value V, dim1 D1) {
m.Values = append(m.Values, Sample1(V, D1){time.Now(), value, dim1})
}
With this approach, we do manage to get type-safety for our Set
function. Note the major downside here though:
instead of having to define one top-level type for each value and dimension type (generics gives us that),
we have to define a new top-level type (two, actually) for each number of arguments… you would also need this:
https://go2goplay.golang.org/p/zpMDuqd9s-F
package metric
type Sample2(type V, D1, D2) struct {
Timestamp time.Time
Value V
Dimension1 D1
Dimension2 D2
}
type Metric2(type V, D1, D2) struct {
Name string
Values []Sample2(V, D1, D2)
}
func NewMetric2(type V, D1, D2)(name string) *Metric2(V, D1, D2) {
return &Metric2(V, D1, D2){Name: name}
}
func (m *Metric2(V, D1, D2)) Set(value V, dim1 D1, dim2 D2) {
m.Values = append(m.Values, Sample2(V, D1, D2){time.Now(), value, dim1, dim2})
}
… and so on and so forth.
Hand-crafting this would likely be onerous, so it would likely require code generation for the generic implementations at each number of dimensions up to some maximum number, which seems like it almost defeats the purpose, because you could then just generate the code for the metric directly.
There are a few kinds of API that I could envision that might work with some other kind of generic:
m := field.Add(int)("age", field.Add(string)("name", metric.New(float64)("height")))
m.Set(age, name, height)
This seems like it should be somewhat possible at first glance, particularly if you expand the Set method to be something like
m.With(age).With(name).Set(height)
However, this requires the With method to include the proper type for the sub-function in its type parameters, which is not practical recursively.
m := metric.New(float64, string, int)("height", "name", "age")
m.Set(height, name, age)
This pattern is simimlar to the Set
problem above: there is no way to write the Set method,
which would require some form of variadic type parameters:
// Set with [] type parameters defines the recursive component of the type.
//
// This is only what a variadic type parameter *could* look like.
func (m *Metric(T,[D0,DN...])) Set(value T, dim0 D0, dims ...DN) *Sample(T, DN...) {
return m.Set(value, dims...).addDimension(m.d0.name, dim0)
}
// Set without the [] type parameters defines the "base case" for the recursion.
func (m *Metric(T)) Set(value T) *Sample(T) {
return &Sample(T){Value: value}
}
m := metric.New(float64)("height").WithField(string)("name").WithField(int)("age")
m.Create().With(name).With(age).Set(height)
The Set
is not possible to write for the same reasons as above, and the WithField
method is not possible to write because methods may only include the type parameters
of the type itself.
Over the course of experimenting with this, I have a few overall impressions:
zero
builtin or allowing nil
to be used for the zero value of type parameters would be useful.(type *T)
x := F(T1, []T1, T2(T1))(y, z)
or something)), it is probably too complex.Also, I have to say, I really appreciate that the team got this up and running on the playground, it enables a ton of fun experimentation. And double kudos for fixing the bug I found so fast!
Every few years I decide that my current hosting solution isn’t ideal and go looking for a new one.
This year, I’ve decided to reduce costs (because my website receives so little traffic) by moving my static content over to GitHub Pages.
Feel free to check out the source if you want to see the content of my old blog’s posts, but realistically I probably won’t spend the required amount of time to update them to Jekyll beyond what I’ve done so far (which was to dump them from the Ghost sqlite database and hackishly template that out to files).
If you’re looking for my CS1372 resources, you can find them here.
The most popular references are these:
Here are just a few examples of leap second woes:
Read more...Building Blocks
Using Kubernetes or Google Container Engine there are a few building blocks that you need to understand. There are great docs on their sites, but I’ll give an overview of them here.
The big trick with using a static IP is to specify in your service config
portalIPs:
- put.your.ip.here
and, in particular, you must not have
createExternalLoadBalancer: true
because that will attempt to create one for you.
Read more...The Container Registry allows you to easily push your docker images to Cloud Storage.
Nominally, the registry entry for an image will be
gcr.io/projectname/imagename
, whereprojectname
is the name of the project on the Developer Console andcontainername
is whatever id you want. At this point, however, both of these only reliably seem to supportA-Za-z_-
.TL;DR: If you're using my container script:
echo REMOTE=gcr.io/projectname/imagename >> container.cfg
Or, for Google Apps:
echo REMOTE=b.gcr.io/bucketname/imagename >> container.cfg
Then, you can simply
./container.sh push
Read more...This page is primarily for my benefit, but hopefully somebody else will find it useful as well!
General Notes
Open Sourceable Dockerfiles
I always like sharing what I do with other people. That’s part of what I love about the internet. Thus, I try to make my Dockerfiles reusable by anyone. I also don’t want to include anything in my Dockerfiles that I would consider sensitive or overly site-specific, like paths to my data volumes. Here are some tips on making release-ready Dockerfiles.
Read more...There has been a lot of discussion on the Go Nuts mailing list about how to manage versioning in the nascent Go package ecosystem. We muttered about it at first, and then muttered about it some more when
goinstall
came about, and there has been a pretty significant uptick in discussion since thego
tool began to take shape and the Go 1 release date approached. In talking with other Gophers at the GoSF meet-up recently, there doesn’t seem to be anyone who really has a good solution.TL;DR: kylelemons.net/go/rx
Oddly appropriate, don’t you think?
Read more...As 2011 makes its way out and 2012 takes its place, it’s time for a bit of reflection and a bit of looking forward. I haven’t been writing software professionally for particularly long, but I have written software in a number of languages (everything from Pascal to Python), and I think we can all agree that none of the usual suspects are particularly ideal. If you’re like me, you hunker down with your favorite set of language features and write your code in as much isolation as possible, so that you can work around or ignore whatever problems your language and/or environment cause you. You probably have some tools lying around to help you do various things for which the language or your editor/environment aren’t well-equipped. You probably don’t even realize all of the things that bother you about the language after awhile, because it’s the norm: every programmer has to deal with them, it just comes with the territory. Please note: I will be comparing Go to other languages extensively in this blog post; do not take it to be an indictment of them or even, really, as reasons to not use them. I’m simply giving my opinion on their differences and why I personally find that Go is a more suitable language for my development. I have used all of the languages that I discuss and will continue to use them when their particular strengths are required.